use bh_jws_utils::{ec_public_affine_coords_to_jwk, SigningAlgorithm};
use bherror::traits::ForeignError as _;
use coset::{
iana::{Algorithm, Ec2KeyParameter, EllipticCurve},
AsCborValue, KeyType, Label,
};
use serde::{Deserialize, Deserializer, Serialize, Serializer};
use super::base64::base64_url_decode;
use crate::error::{MdocError, Result};
const DEFAULT_DEVICE_KID: &str = "device_kid";
pub(crate) fn serialize_coset<T, S>(
cose_value: &T,
serializer: S,
) -> std::result::Result<S::Ok, S::Error>
where
T: AsCborValue + Clone,
S: Serializer,
{
let cbor_value = cose_value
.clone()
.to_cbor_value()
.map_err(serde::ser::Error::custom)?;
cbor_value.serialize(serializer)
}
pub(crate) fn deserialize_coset<'de, T, D>(deserializer: D) -> std::result::Result<T, D::Error>
where
T: AsCborValue,
D: Deserializer<'de>,
{
let cbor_value = ciborium::Value::deserialize(deserializer)?;
T::from_cbor_value(cbor_value).map_err(serde::de::Error::custom)
}
pub fn cose_key_from_jwk(
jwk: &serde_json::Map<String, serde_json::Value>,
) -> Result<coset::CoseKey> {
matches(jwk, "kty", "EC")?;
matches(jwk, "crv", "P-256")?;
let x = extract_coord(jwk, "x")?;
let y = extract_coord(jwk, "y")?;
Ok(coset::CoseKeyBuilder::new_ec2_pub_key(EllipticCurve::P_256, x, y).build())
}
fn matches(
jwk: &serde_json::Map<String, serde_json::Value>,
key: &'static str,
expected_value: &'static str,
) -> Result<()> {
if jwk.get(key).is_some_and(|value| value != expected_value) {
return Err(bherror::Error::root(MdocError::JwkToCoseKey(format!(
"Expected key {key} with value {expected_value}"
))));
}
Ok(())
}
fn extract_coord(
jwk: &serde_json::Map<String, serde_json::Value>,
coord_key: &'static str,
) -> Result<Vec<u8>> {
let coord = jwk
.get(coord_key)
.and_then(|coord| coord.as_str())
.ok_or_else(|| {
bherror::Error::root(MdocError::JwkToCoseKey(format!(
"Missing coordinate {coord_key} of type String"
)))
})?;
base64_url_decode(coord)
.foreign_err(|| MdocError::JwkToCoseKey(format!("Failed to decode coordinate {coord_key}")))
}
pub fn cose_key_to_jwk(
cose_key: &coset::CoseKey,
) -> Result<serde_json::Map<String, serde_json::Value>> {
if cose_key.kty != KeyType::Assigned(coset::iana::KeyType::EC2) {
return Err(bherror::Error::root(MdocError::CoseKeyToJwk(
"only EC keys are supported".to_owned(),
)));
}
let curve = get_cose_key_param(cose_key, &Label::Int(Ec2KeyParameter::Crv as i64))?;
if curve != &ciborium::Value::from(EllipticCurve::P_256 as u64) {
return Err(bherror::Error::root(MdocError::CoseKeyToJwk(
"only P-256 curve is supported".to_owned(),
)));
}
let x = get_ec_key_param(cose_key, Ec2KeyParameter::X)?;
let y = get_ec_key_param(cose_key, Ec2KeyParameter::Y)?;
Ok(ec_public_affine_coords_to_jwk(
x,
y,
Some(DEFAULT_DEVICE_KID),
))
}
fn get_ec_key_param(cose_key: &coset::CoseKey, param: Ec2KeyParameter) -> Result<&[u8; 32]> {
let ciborium::Value::Bytes(value) = get_cose_key_param(cose_key, &Label::Int(param as i64))?
else {
return Err(bherror::Error::root(MdocError::CoseKeyToJwk(format!(
"{:?} parameter MUST BE bytes",
param
))));
};
value
.as_slice()
.try_into()
.foreign_err(|| MdocError::CoseKeyToJwk(format!("{:?} MUST HAVE 32 bytes", param)))
}
fn get_cose_key_param<'a>(
cose_key: &'a coset::CoseKey,
label: &Label,
) -> Result<&'a ciborium::Value> {
cose_key
.params
.iter()
.find_map(|(l, v)| (l == label).then_some(v))
.ok_or_else(|| {
bherror::Error::root(MdocError::CoseKeyToJwk(format!(
"key param {:?} not found",
label
)))
})
}
pub(crate) fn coset_alg_to_jws_alg(alg: &Algorithm) -> Option<SigningAlgorithm> {
Some(match alg {
Algorithm::ES256 => SigningAlgorithm::Es256,
Algorithm::ES384 => SigningAlgorithm::Es384,
Algorithm::ES512 => SigningAlgorithm::Es512,
Algorithm::PS256 => SigningAlgorithm::Ps256,
Algorithm::PS384 => SigningAlgorithm::Ps384,
Algorithm::PS512 => SigningAlgorithm::Ps512,
_ => return None,
})
}
pub(crate) fn jws_alg_to_coset_alg(alg: &SigningAlgorithm) -> Algorithm {
match alg {
SigningAlgorithm::Es256 => Algorithm::ES256,
SigningAlgorithm::Es384 => Algorithm::ES384,
SigningAlgorithm::Es512 => Algorithm::ES512,
SigningAlgorithm::Ps256 => Algorithm::PS256,
SigningAlgorithm::Ps384 => Algorithm::PS384,
SigningAlgorithm::Ps512 => Algorithm::PS512,
}
}
#[cfg(test)]
mod tests {
use assert_matches::assert_matches;
use super::cose_key_from_jwk;
use crate::MdocError;
#[test]
fn cose_key_from_ec_jwk() {
let jwk = serde_json::json!({
"kty":"EC",
"crv":"P-256",
"x":"MKBCTNIcKUSDii11ySs3526iDZ8AiTo7Tu6KPAqv7D4",
"y":"4Etl6SRW2YiLUrN5vfvVHuhp7x8PxltmWWlbbM4IFyM",
"use":"enc",
"kid":"1",
})
.as_object()
.cloned()
.unwrap();
assert_matches!(cose_key_from_jwk(&jwk), Ok(_));
}
#[test]
fn cose_key_from_rsa_jwk() {
let jwk = serde_json::json!({
"kty":"RSA",
"n": "0vx7agoebGcQSuuPiLJXZptN9nndrQmbXEps2aiAFbWhM78LhWx
4cbbfAAtVT86zwu1RK7aPFFxuhDR1L6tSoc_BJECPebWKRXjBZCiFV4n3oknjhMs
tn64tZ_2W-5JsGY4Hc5n9yBXArwl93lqt7_RN5w6Cf0h4QyQ5v-65YGjQR0_FDW2
QvzqY368QQMicAtaSqzs8KJZgnYb9c7d0zgdAZHzu6qMQvRL5hajrn1n91CbOpbI
SD08qNLyrdkt-bFTWhAI4vMQFh6WeZu0fM4lFd2NcRwr3XPksINHaQ-G_xBniIqb
w0Ls1jF44-csFCur-kEgU8awapJzKnqDKgw",
"e":"AQAB",
"alg":"RS256",
"kid":"2011-04-29"
})
.as_object()
.cloned()
.unwrap();
assert!(
matches!(cose_key_from_jwk(&jwk).unwrap_err().error, MdocError::JwkToCoseKey(msg) if msg == "Expected key kty with value EC"),
"We currently only support EC keys"
);
}
}