baseid_mdl/
cose.rs

1//! COSE_Sign1 signing and verification for Mobile Security Objects.
2//!
3//! Wraps the `coset` crate to provide MSO-specific COSE_Sign1 operations.
4//! The signer/verifier abstraction from `baseid-crypto` is used for
5//! cryptographic agility.
6
7use baseid_core::error::{CryptoError, SerializationError};
8use baseid_core::types::SignatureAlgorithm;
9use baseid_crypto::signer::{Signer, Verifier};
10use coset::iana::Algorithm;
11use coset::{CborSerializable, CoseSign1, CoseSign1Builder, HeaderBuilder};
12
13use crate::mso::MobileSecurityObject;
14
15/// Map a `baseid-crypto` signature algorithm to the COSE IANA algorithm identifier.
16fn to_cose_algorithm(alg: SignatureAlgorithm) -> Algorithm {
17    match alg {
18        SignatureAlgorithm::EdDsa => Algorithm::EdDSA,
19        SignatureAlgorithm::Es256 => Algorithm::ES256,
20        SignatureAlgorithm::Es384 => Algorithm::ES384,
21        SignatureAlgorithm::Es256k => Algorithm::ES256K,
22        SignatureAlgorithm::BbsPlus => Algorithm::ES256, // unused for mdoc
23    }
24}
25
26/// Sign an MSO with COSE_Sign1.
27///
28/// Serializes the MSO to CBOR, wraps it in a COSE_Sign1 envelope signed
29/// by the provided signer. Returns the COSE_Sign1 CBOR bytes.
30pub fn sign_mso(mso: &MobileSecurityObject, signer: &dyn Signer) -> baseid_core::Result<Vec<u8>> {
31    // 1. Serialize MSO to CBOR bytes
32    let mut mso_cbor = Vec::new();
33    ciborium::into_writer(mso, &mut mso_cbor).map_err(|_| SerializationError::Cbor)?;
34
35    // 2. Build protected header with algorithm
36    let alg = to_cose_algorithm(signer.algorithm());
37    let protected = HeaderBuilder::new().algorithm(alg).build();
38
39    // 3. Build CoseSign1 without signature to compute tbs_data
40    let unsigned = CoseSign1Builder::new()
41        .protected(protected)
42        .payload(mso_cbor)
43        .build();
44
45    // 4. Compute the to-be-signed data and sign it
46    let tbs = unsigned.tbs_data(b"");
47    let signature = signer.sign(&tbs)?;
48
49    // 5. Construct the signed CoseSign1
50    let signed = CoseSign1 {
51        signature,
52        ..unsigned
53    };
54
55    // 6. Serialize to CBOR bytes
56    signed.to_vec().map_err(|_| SerializationError::Cbor.into())
57}
58
59/// Verify a COSE_Sign1-signed MSO.
60///
61/// Decodes the COSE_Sign1 envelope, verifies the signature, extracts
62/// and returns the MSO.
63pub fn verify_signed_mso(
64    cose_bytes: &[u8],
65    verifier: &dyn Verifier,
66) -> baseid_core::Result<MobileSecurityObject> {
67    // 1. Deserialize COSE_Sign1 from CBOR bytes
68    let sign1 = CoseSign1::from_slice(cose_bytes).map_err(|_| CryptoError::VerificationFailed)?;
69
70    // 2. Verify the signature using tbs_data
71    let tbs = sign1.tbs_data(b"");
72    let valid = verifier.verify(&tbs, &sign1.signature)?;
73    if !valid {
74        return Err(CryptoError::VerificationFailed.into());
75    }
76
77    // 3. Extract and deserialize the MSO payload
78    let payload = sign1
79        .payload
80        .as_ref()
81        .ok_or(CryptoError::VerificationFailed)?;
82
83    let mso: MobileSecurityObject =
84        ciborium::from_reader(payload.as_slice()).map_err(|_| SerializationError::Cbor)?;
85
86    Ok(mso)
87}
88
89#[cfg(test)]
90mod tests {
91    use super::*;
92    use crate::mdoc::{DataElement, MobileDocument};
93    use crate::mso::ValidityInfo;
94    use baseid_core::types::KeyType;
95    use baseid_crypto::KeyPair;
96    use std::collections::BTreeMap;
97
98    fn sample_mso() -> MobileSecurityObject {
99        let mut namespaces = BTreeMap::new();
100        namespaces.insert(
101            "org.iso.18013.5.1".to_string(),
102            vec![
103                DataElement {
104                    identifier: "family_name".to_string(),
105                    value: serde_json::json!("Doe"),
106                },
107                DataElement {
108                    identifier: "given_name".to_string(),
109                    value: serde_json::json!("John"),
110                },
111            ],
112        );
113        let doc = MobileDocument {
114            doc_type: "org.iso.18013.5.1.mDL".to_string(),
115            namespaces,
116        };
117        let validity = ValidityInfo {
118            signed: "2024-01-01T00:00:00Z".to_string(),
119            valid_from: "2024-01-01T00:00:00Z".to_string(),
120            valid_until: "2025-01-01T00:00:00Z".to_string(),
121        };
122        MobileSecurityObject::create(&doc, validity).unwrap()
123    }
124
125    #[test]
126    fn sign_and_verify_roundtrip() {
127        let kp = KeyPair::generate(KeyType::Ed25519).unwrap();
128        let mso = sample_mso();
129
130        let cose_bytes = sign_mso(&mso, &kp).unwrap();
131        let recovered = verify_signed_mso(&cose_bytes, &kp.public).unwrap();
132
133        assert_eq!(recovered.doc_type, mso.doc_type);
134        assert_eq!(recovered.digest_algorithm, mso.digest_algorithm);
135        assert_eq!(recovered.value_digests.len(), mso.value_digests.len());
136        assert_eq!(
137            recovered.validity_info.valid_from,
138            mso.validity_info.valid_from
139        );
140    }
141
142    #[test]
143    fn tampered_signature_rejected() {
144        let kp = KeyPair::generate(KeyType::Ed25519).unwrap();
145        let mso = sample_mso();
146
147        let mut cose_bytes = sign_mso(&mso, &kp).unwrap();
148
149        // Tamper with the last byte (part of the signature)
150        let len = cose_bytes.len();
151        cose_bytes[len - 1] ^= 0xFF;
152
153        let result = verify_signed_mso(&cose_bytes, &kp.public);
154        assert!(result.is_err());
155    }
156
157    fn sign_verify_with_key_type(key_type: KeyType) {
158        let kp = KeyPair::generate(key_type).unwrap();
159        let mso = sample_mso();
160
161        let cose_bytes = sign_mso(&mso, &kp).unwrap();
162        let recovered = verify_signed_mso(&cose_bytes, &kp.public).unwrap();
163
164        assert_eq!(recovered.doc_type, mso.doc_type);
165    }
166
167    #[test]
168    fn each_key_type_ed25519() {
169        sign_verify_with_key_type(KeyType::Ed25519);
170    }
171
172    #[test]
173    fn each_key_type_p256() {
174        sign_verify_with_key_type(KeyType::P256);
175    }
176
177    #[test]
178    fn each_key_type_p384() {
179        sign_verify_with_key_type(KeyType::P384);
180    }
181
182    #[test]
183    fn each_key_type_secp256k1() {
184        sign_verify_with_key_type(KeyType::Secp256k1);
185    }
186
187    #[test]
188    fn wrong_key_rejected() {
189        let kp1 = KeyPair::generate(KeyType::Ed25519).unwrap();
190        let kp2 = KeyPair::generate(KeyType::Ed25519).unwrap();
191        let mso = sample_mso();
192
193        let cose_bytes = sign_mso(&mso, &kp1).unwrap();
194        let result = verify_signed_mso(&cose_bytes, &kp2.public);
195        assert!(result.is_err());
196    }
197
198    #[test]
199    fn invalid_cbor_rejected() {
200        let kp = KeyPair::generate(KeyType::Ed25519).unwrap();
201        assert!(verify_signed_mso(&[0xFF, 0xFF], &kp.public).is_err());
202    }
203}