use bh_jws_utils::{jwk_sha256_thumbprint_bytes, JwkPublic, SignatureVerifier, SigningAlgorithm};
use bherror::traits::{
ErrorContext as _, ForeignBoxed as _, ForeignError as _, PropagateError as _,
};
use ciborium::{into_writer, Value};
use coset::{CoseMac0, CoseSign1, CoseSign1Builder, RegisteredLabelWithPrivate};
use serde::{ser::SerializeSeq as _, Deserialize, Serialize};
use super::response::DeviceNameSpacesBytes;
use crate::{
models::{data_retrieval::common::DocType, Bytes, BytesCbor},
utils::{
coset::{coset_alg_to_jws_alg, deserialize_coset, jws_alg_to_coset_alg, serialize_coset},
digest::sha256,
},
DeviceKey, MdocError, Result,
};
const DEVICE_AUTHENTICATION_IDENTIFIER: &str = "DeviceAuthentication";
const DEVICE_HANDOVER_IDENTIFIER: &str = "OpenID4VPHandover";
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub enum DeviceAuth {
DeviceSignature(DeviceSignature),
DeviceMac(DeviceMac),
}
impl DeviceAuth {
pub(crate) fn new_signature(
device_authentication: DeviceAuthentication,
signer: &impl bh_jws_utils::Signer,
) -> Result<Self> {
Ok(Self::DeviceSignature(DeviceSignature::new(
device_authentication,
signer,
)?))
}
pub(crate) fn verify_signature<'a>(
&self,
device_authentication: DeviceAuthentication,
get_signature_verifier: impl Fn(SigningAlgorithm) -> Option<&'a dyn SignatureVerifier>,
device_key: &DeviceKey,
) -> Result<()> {
match self {
Self::DeviceSignature(device_signature) => device_signature.verify_signature(
device_authentication,
get_signature_verifier,
device_key,
),
Self::DeviceMac(_device_mac) => Err(bherror::Error::root(MdocError::DeviceMac)),
}
}
#[cfg(test)]
pub(crate) fn device_signature_inner_mut(&mut self) -> &mut CoseSign1 {
match self {
DeviceAuth::DeviceSignature(signature) => &mut signature.0,
_ => unimplemented!(),
}
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct DeviceSignature(
#[serde(
serialize_with = "serialize_coset",
deserialize_with = "deserialize_coset"
)]
pub(crate) CoseSign1,
);
impl From<CoseSign1> for DeviceSignature {
fn from(value: coset::CoseSign1) -> Self {
Self(value)
}
}
impl DeviceSignature {
fn new(
device_authentication: DeviceAuthentication,
signer: &impl bh_jws_utils::Signer,
) -> Result<Self> {
let protected = coset::Header {
alg: Some(RegisteredLabelWithPrivate::Assigned(jws_alg_to_coset_alg(
&signer.algorithm(),
))),
..Default::default()
};
let payload = device_signature_and_mac_payload(device_authentication)?;
let cose_sign1 = CoseSign1Builder::new()
.protected(protected)
.try_create_detached_signature(&payload, &[], |data| signer.sign(data))
.foreign_boxed_err(|| MdocError::Signing)?
.build();
Ok(Self(cose_sign1))
}
fn verify_signature<'a>(
&self,
device_authentication: DeviceAuthentication,
get_signature_verifier: impl Fn(SigningAlgorithm) -> Option<&'a dyn SignatureVerifier>,
device_key: &DeviceKey,
) -> Result<()> {
let alg = self
.signing_algorithm()
.ok_or_else(|| bherror::Error::root(MdocError::MissingSigningAlgorithm))
.ctx(|| "device authentication")?;
let jwk = device_key.as_jwk()?;
let signature_verifier = get_signature_verifier(alg)
.ok_or_else(|| bherror::Error::root(MdocError::MissingSignatureVerifier(alg)))?;
let payload = device_signature_and_mac_payload(device_authentication)?;
self.0
.verify_detached_signature(&payload, &[], |sig, data| {
let verified = signature_verifier
.verify(data, sig, &jwk)
.foreign_boxed_err(|| MdocError::InvalidSignature)
.ctx(|| "error while verifying signature")?;
if !verified {
return Err(bherror::Error::root(MdocError::InvalidSignature)
.ctx("the signature is not valid"));
};
Ok(())
})
}
pub fn signing_algorithm(&self) -> Option<SigningAlgorithm> {
let alg = self.0.protected.header.alg.as_ref()?;
let RegisteredLabelWithPrivate::Assigned(alg) = alg else {
return None;
};
coset_alg_to_jws_alg(alg)
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct DeviceMac(
#[serde(
serialize_with = "serialize_coset",
deserialize_with = "deserialize_coset"
)]
pub(crate) CoseMac0,
);
#[derive(Debug, Serialize)]
struct DeviceAuthenticationBytes<'a>(BytesCbor<DeviceAuthentication<'a>>);
impl<'a> From<DeviceAuthentication<'a>> for DeviceAuthenticationBytes<'a> {
fn from(value: DeviceAuthentication<'a>) -> Self {
Self(value.into())
}
}
#[derive(Debug)]
pub struct DeviceAuthentication<'a> {
session_transcript: SessionTranscript<'a>,
doc_type: &'a DocType,
name_spaces: &'a DeviceNameSpacesBytes,
}
impl<'a> DeviceAuthentication<'a> {
pub fn new(
client_id: &'a str,
response_uri: &'a str,
nonce: &'a str,
jwk_public: Option<&JwkPublic>,
doc_type: &'a DocType,
name_spaces: &'a DeviceNameSpacesBytes,
) -> Result<Self> {
let jwk_thumbprint = jwk_public.map(JwkThumbprint::new).transpose()?;
Ok(Self {
session_transcript: SessionTranscript {
handover: OID4VPHandover(OpenID4VPHandoverInfoHash(OpenID4VPHandoverInfo {
client_id,
nonce,
jwk_thumbprint,
response_uri,
})),
},
doc_type,
name_spaces,
})
}
}
impl Serialize for DeviceAuthentication<'_> {
fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
let mut seq = serializer.serialize_seq(Some(4))?;
seq.serialize_element(DEVICE_AUTHENTICATION_IDENTIFIER)?;
seq.serialize_element(&self.session_transcript)?;
seq.serialize_element(self.doc_type)?;
seq.serialize_element(self.name_spaces)?;
seq.end()
}
}
#[derive(Debug)]
struct SessionTranscript<'a> {
handover: OID4VPHandover<'a>,
}
impl Serialize for SessionTranscript<'_> {
fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
let mut seq = serializer.serialize_seq(Some(3))?;
seq.serialize_element(&Value::Null)?;
seq.serialize_element(&Value::Null)?;
seq.serialize_element(&self.handover)?;
seq.end()
}
}
#[derive(Debug)]
struct OID4VPHandover<'a>(OpenID4VPHandoverInfoHash<'a>);
impl Serialize for OID4VPHandover<'_> {
fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
let mut seq = serializer.serialize_seq(Some(2))?;
seq.serialize_element(DEVICE_HANDOVER_IDENTIFIER)?;
seq.serialize_element(&self.0)?;
seq.end()
}
}
#[derive(Debug)]
struct OpenID4VPHandoverInfoHash<'a>(OpenID4VPHandoverInfo<'a>);
impl Serialize for OpenID4VPHandoverInfoHash<'_> {
fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
let mut payload = Vec::new();
into_writer(&self.0, &mut payload).map_err(serde::ser::Error::custom)?;
let bytes: Bytes = sha256(payload).to_vec().into();
bytes.serialize(serializer)
}
}
#[derive(Debug)]
struct OpenID4VPHandoverInfo<'a> {
client_id: &'a str,
nonce: &'a str,
jwk_thumbprint: Option<JwkThumbprint>,
response_uri: &'a str,
}
impl Serialize for OpenID4VPHandoverInfo<'_> {
fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
let mut seq = serializer.serialize_seq(Some(4))?;
seq.serialize_element(self.client_id)?;
seq.serialize_element(self.nonce)?;
if let Some(jwk_thumbprint) = &self.jwk_thumbprint {
seq.serialize_element(jwk_thumbprint)?;
} else {
seq.serialize_element(&Value::Null)?;
}
seq.serialize_element(self.response_uri)?;
seq.end()
}
}
#[derive(Debug, Serialize)]
struct JwkThumbprint(Bytes);
impl JwkThumbprint {
fn new(jwk: &JwkPublic) -> Result<Self> {
let jwk_thumbprint = jwk_sha256_thumbprint_bytes(jwk.to_owned())
.with_err(|| MdocError::InvalidJwkPublic)?
.into();
Ok(Self(jwk_thumbprint))
}
}
fn device_signature_and_mac_payload(
device_authentication: DeviceAuthentication,
) -> Result<Vec<u8>> {
let mut payload = Vec::new();
into_writer(
&DeviceAuthenticationBytes::from(device_authentication),
&mut payload,
)
.foreign_err(|| MdocError::DeviceAuthentication)?;
Ok(payload)
}
#[cfg(test)]
mod tests {
use std::sync::LazyLock;
use assert_matches::assert_matches;
use super::*;
const CLIENT_ID: &str = "x509_san_dns:example.com";
const NONCE: &str = "exc7gBkxjx1rdc9udRrveKvSsJIq80avlXeLHhGwqtA";
static JWK_PUBLIC: LazyLock<serde_json::Value> = LazyLock::new(|| {
serde_json::json!({
"kty": "EC",
"crv": "P-256",
"x": "DxiH5Q4Yx3UrukE2lWCErq8N8bqC9CHLLrAwLz5BmE0",
"y": "XtLM4-3h5o3HUH0MHVJV0kyq0iBlrBwlh8qEDMZ4-Pc",
"use": "enc",
"alg": "ECDH-ES",
"kid": "1",
})
});
const RESPONSE_URI: &str = "https://example.com/response";
fn example_session_transcript() -> SessionTranscript<'static> {
SessionTranscript {
handover: OID4VPHandover(OpenID4VPHandoverInfoHash(OpenID4VPHandoverInfo {
client_id: CLIENT_ID,
nonce: NONCE,
jwk_thumbprint: Some(JwkThumbprint::new(JWK_PUBLIC.as_object().unwrap()).unwrap()),
response_uri: RESPONSE_URI,
})),
}
}
#[test]
fn test_oid4vp_handover_info_serialization() {
let session_transcript = example_session_transcript();
let handover_info = session_transcript.handover.0 .0;
let expected_hex = "847818783530395f73616e5f646e733a6578616d706c652e636f6d782b6578633767\
426b786a7831726463397564527276654b7653734a4971383061766c58654c486847\
7771744158204283ec927ae0f208daaa2d026a814f2b22dca52cf85ffa8f3f8626c6\
bd669047781c68747470733a2f2f6578616d706c652e636f6d2f726573706f6e7365";
let mut encoded = Vec::new();
into_writer(&handover_info, &mut encoded).unwrap();
let encoded_hex = hex::encode(encoded);
assert_eq!(expected_hex, encoded_hex);
}
#[test]
fn test_oid4vp_handover_serialization() {
let session_transcript = example_session_transcript();
let handover = session_transcript.handover;
let expected_hex = "82714f70656e494434565048616e646f7665725820048bc053c00442af9b8eed494c\
efdd9d95240d254b046b11b68013722aad38ac";
let mut encoded = Vec::new();
into_writer(&handover, &mut encoded).unwrap();
let encoded_hex = hex::encode(encoded);
assert_eq!(expected_hex, encoded_hex);
}
#[test]
fn test_session_transcript_serialization() {
let session_transcript = example_session_transcript();
let expected_hex = "83f6f682714f70656e494434565048616e646f7665725820048bc053c00442af9b8e\
ed494cefdd9d95240d254b046b11b68013722aad38ac";
let mut encoded = Vec::new();
into_writer(&session_transcript, &mut encoded).unwrap();
let encoded_hex = hex::encode(encoded);
assert_eq!(expected_hex, encoded_hex);
}
#[test]
fn test_jwk_thumbprint_from_jwk_success() {
let jwk_public = serde_json::json!({
"kty": "EC",
"crv": "P-256",
"x": "f83OJ3D2xF4p6uZ8l9QWmK7rT2cVbYq1H0sLzX9aBcE",
"y": "x_FEzRu9y7kQ8Lm2pV4nC6tWq1sJd3Hg5KaZ0bN7cYQ",
});
let _thumbprint = JwkThumbprint::new(jwk_public.as_object().unwrap()).unwrap();
}
#[test]
fn test_jwk_thumbprint_from_invalid_jwk_fails() {
let jwk_public = serde_json::json!({
"name": "John",
});
let err = JwkThumbprint::new(jwk_public.as_object().unwrap()).unwrap_err();
assert_matches!(err.error, MdocError::InvalidJwkPublic);
}
}