From 498ebc21340db4776f4a755232b50361ff19288f Mon Sep 17 00:00:00 2001 From: Radu Marias Date: Tue, 23 Jul 2024 00:14:41 +0300 Subject: [PATCH 01/24] Integrate coset crate. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # Description - Integrate `coset` crate. - Crate module `cose` where we have `Cose` trait with some basic methods and implemented by `CoseSign1` and `CoseMac0` which are wrappers over coset analogous. - Implement `SignatureAlgorithm` trait for `CoseSign1` and `CoseMac0`. # Ref https://linear.app/spruceid/issue/SKIT-448/iso-mdl-investigate-coset # Changes - Add `CoseSign1` and `CoseMac0` wrappers over coset analogous. - Add `Cose` trait with some basic methods. - Implement `SignatureAlgorithm` for `CoseSign1` and `CoseMac0`. - Add a global map to hold signature payloads as our flow returns a reference to the payload and later that is signed externally. - Serialize/deserialize `CoseSign1` and `CoseMac0`. - Change signature flow to use the coset signature. - Use `iana::Algorithm`. ## Other changes None # Reviewers, please pay special attention to… The way `CoseSign1` is used. Signature flow and global map that hold signature payload, so we can return reference. # Tested Tested locally, running all tests. Build with these targets: ``` aarch64-linux-android armv7-linux-androideabi i686-linux-android wasm32-unknown-unknown wasm32-wasi x86_64-unknown-linux-gnu i686-unknown-linux-gnu x86_64-unknown-linux-musl x86_64-pc-windows-gnu i686-pc-windows-gnu ``` # Checklist - [x] [isomdl] Integrate `CoseSign1` - [ ] [isomdl] Integrate `COSEMac0` - [ ] [isomdl] Use coset `CoseKey` - [ ] [isomdl] Test building with `macosx` targets --- .gitignore | 3 +- Cargo.toml | 9 +- src/definitions/device_key/cose_key.rs | 14 +- src/definitions/device_request.rs | 2 +- src/definitions/device_response.rs | 9 +- src/definitions/device_signed.rs | 5 +- src/definitions/issuer_signed.rs | 2 +- src/definitions/mso.rs | 4 +- src/definitions/namespaces/latin1.rs | 1 + src/issuance/mdoc.rs | 72 +++++---- src/lib.rs | 3 +- src/presentation/device.rs | 140 ++++++++++++++---- .../cose/mac0/secret_key_aes_cbc_mac | 1 + test/definitions/cose/mac0/secret_key_hmac | 1 + .../cose/mac0/serialized_aes_cbc_mac.cbor | 1 + .../cose/mac0/serialized_hmac.cbor | 1 + test/definitions/cose/sign1/secret_key | 1 + test/definitions/cose/sign1/serialized.cbor | 1 + 18 files changed, 186 insertions(+), 84 deletions(-) create mode 100644 test/definitions/cose/mac0/secret_key_aes_cbc_mac create mode 100644 test/definitions/cose/mac0/secret_key_hmac create mode 100644 test/definitions/cose/mac0/serialized_aes_cbc_mac.cbor create mode 100644 test/definitions/cose/mac0/serialized_hmac.cbor create mode 100644 test/definitions/cose/sign1/secret_key create mode 100644 test/definitions/cose/sign1/serialized.cbor diff --git a/.gitignore b/.gitignore index 88d95f30..a9df2f3d 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ **/Cargo.lock **/target -*~ \ No newline at end of file +*~ +.idea/ diff --git a/Cargo.toml b/Cargo.toml index 43582fa4..399e8696 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -36,15 +36,14 @@ async-signature = "0.3.0" base64 = "0.13" pem-rfc7468 = "0.7.0" x509-cert = { version = "0.1.1", features = ["pem"] } +coset = { version = "0.3.7", features = ["std"] } +cbc-mac = "0.1.0" ssi-jwk = { version = "0.1" } isomdl-macros = { version = "0.1.0", path = "macros" } clap = { version = "4", features = ["derive"] } -clap-stdin = "0.2.1" - -[dependencies.cose-rs] -git = "https://github.com/spruceid/cose-rs" -rev = "4104505" +clap-stdin = "0.5.1" +digest = "0.10.7" [dev-dependencies] hex = "0.4.3" diff --git a/src/definitions/device_key/cose_key.rs b/src/definitions/device_key/cose_key.rs index 4fe1a62e..d2cf0576 100644 --- a/src/definitions/device_key/cose_key.rs +++ b/src/definitions/device_key/cose_key.rs @@ -1,5 +1,5 @@ use aes::cipher::generic_array::{typenum::U8, GenericArray}; -use cose_rs::algorithm::Algorithm; +use coset::iana; use p256::EncodedPoint; use serde::{Deserialize, Serialize}; use serde_cbor::Value as CborValue; @@ -64,28 +64,28 @@ pub enum Error { } impl CoseKey { - pub fn signature_algorithm(&self) -> Option { + pub fn signature_algorithm(&self) -> Option { match self { CoseKey::EC2 { crv: EC2Curve::P256, .. - } => Some(Algorithm::ES256), + } => Some(iana::Algorithm::ES256), CoseKey::EC2 { crv: EC2Curve::P384, .. - } => Some(Algorithm::ES384), + } => Some(iana::Algorithm::ES384), CoseKey::EC2 { crv: EC2Curve::P521, .. - } => Some(Algorithm::ES512), + } => Some(iana::Algorithm::ES512), CoseKey::OKP { crv: OKPCurve::Ed448, .. - } => Some(Algorithm::EdDSA), + } => Some(iana::Algorithm::EdDSA), CoseKey::OKP { crv: OKPCurve::Ed25519, .. - } => Some(Algorithm::EdDSA), + } => Some(iana::Algorithm::EdDSA), _ => None, } } diff --git a/src/definitions/device_request.rs b/src/definitions/device_request.rs index ced1cbe7..c6eecccf 100644 --- a/src/definitions/device_request.rs +++ b/src/definitions/device_request.rs @@ -1,5 +1,5 @@ +use crate::cose::sign1::CoseSign1; use crate::definitions::helpers::{NonEmptyMap, NonEmptyVec, Tag24}; -use cose_rs::CoseSign1; use serde::{Deserialize, Serialize}; use std::collections::BTreeMap; diff --git a/src/definitions/device_response.rs b/src/definitions/device_response.rs index 036059d0..abadca0b 100644 --- a/src/definitions/device_response.rs +++ b/src/definitions/device_response.rs @@ -1,9 +1,11 @@ +use std::collections::BTreeMap; + +use serde::{Deserialize, Serialize}; + use crate::definitions::{ helpers::{NonEmptyMap, NonEmptyVec}, DeviceSigned, IssuerSigned, }; -use serde::{Deserialize, Serialize}; -use std::collections::BTreeMap; #[derive(Clone, Debug, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] @@ -100,9 +102,10 @@ impl TryFrom for Status { #[cfg(test)] mod test { - use super::DeviceResponse; use hex::FromHex; + use super::DeviceResponse; + static DEVICE_RESPONSE_CBOR: &str = include_str!("../../test/definitions/device_response.cbor"); #[test] diff --git a/src/definitions/device_signed.rs b/src/definitions/device_signed.rs index 05a4716b..e1831409 100644 --- a/src/definitions/device_signed.rs +++ b/src/definitions/device_signed.rs @@ -1,8 +1,9 @@ +use crate::cose::mac0::CoseMac0; +use crate::cose::sign1::CoseSign1; use crate::definitions::{ helpers::{NonEmptyMap, Tag24}, session::SessionTranscript, }; -use cose_rs::sign1::CoseSign1; use serde::{Deserialize, Serialize}; use serde_cbor::{Error as CborError, Value as CborValue}; use std::collections::BTreeMap; @@ -25,7 +26,7 @@ pub enum DeviceAuth { #[serde(rename_all = "camelCase")] Signature { device_signature: CoseSign1 }, #[serde(rename_all = "camelCase")] - Mac { device_mac: CborValue }, + Mac { device_mac: CoseMac0 }, } pub type DeviceAuthenticationBytes = Tag24>; diff --git a/src/definitions/issuer_signed.rs b/src/definitions/issuer_signed.rs index d9848bf7..05a880de 100644 --- a/src/definitions/issuer_signed.rs +++ b/src/definitions/issuer_signed.rs @@ -1,8 +1,8 @@ +use crate::cose::sign1::CoseSign1; use crate::definitions::{ helpers::{ByteStr, NonEmptyMap, NonEmptyVec, Tag24}, DigestId, }; -use cose_rs::sign1::CoseSign1; use serde::{Deserialize, Serialize}; use serde_cbor::Value as CborValue; diff --git a/src/definitions/mso.rs b/src/definitions/mso.rs index d3a91a36..311526ab 100644 --- a/src/definitions/mso.rs +++ b/src/definitions/mso.rs @@ -51,7 +51,9 @@ mod test { serde_cbor::from_slice(&cbor_bytes).expect("unable to decode cbor as an IssuerSigned"); let mso_bytes = signed .issuer_auth - .payload() + .0 + .payload + .as_ref() .expect("expected a COSE_Sign1 with attached payload, found detached payload"); let mso: Tag24 = serde_cbor::from_slice(mso_bytes).expect("unable to parse payload as Mso"); diff --git a/src/definitions/namespaces/latin1.rs b/src/definitions/namespaces/latin1.rs index 07067d6e..a4b40400 100644 --- a/src/definitions/namespaces/latin1.rs +++ b/src/definitions/namespaces/latin1.rs @@ -94,6 +94,7 @@ mod test { #[test] fn upper_latin() { + #[allow(clippy::invisible_characters)] let upper_latin_chars = vec![ ' ', '¡', '¢', '£', '¤', '¥', '¦', '§', '¨', '©', 'ª', '«', '¬', '­', '®', '¯', '°', '±', '²', '³', '´', 'µ', '¶', '·', '¸', '¹', 'º', '»', '¼', '½', '¾', '¿', 'À', 'Á', diff --git a/src/issuance/mdoc.rs b/src/issuance/mdoc.rs index dceecc1b..a0e4cfe5 100644 --- a/src/issuance/mdoc.rs +++ b/src/issuance/mdoc.rs @@ -1,3 +1,17 @@ +use std::collections::{BTreeMap, HashSet}; + +use anyhow::{anyhow, Result}; +use async_signature::AsyncSigner; +use coset::cbor::Value; +use coset::{iana, CborSerializable, Header, Label, RegisteredLabelWithPrivate}; +use rand::Rng; +use serde::{Deserialize, Serialize}; +use serde_cbor::Value as CborValue; +use sha2::{Digest, Sha256, Sha384, Sha512}; +use signature::{SignatureEncoding, Signer}; + +use crate::cose::sign1::CoseSign1; +use crate::cose::{Cose, SignatureAlgorithm}; use crate::{ definitions::{ helpers::{NonEmptyMap, NonEmptyVec, Tag24}, @@ -6,18 +20,6 @@ use crate::{ }, issuance::x5chain::{X5Chain, X5CHAIN_HEADER_LABEL}, }; -use anyhow::{anyhow, Result}; -use async_signature::AsyncSigner; -use cose_rs::{ - algorithm::{Algorithm, SignatureAlgorithm}, - sign1::{CoseSign1, PreparedCoseSign1}, -}; -use rand::Rng; -use serde::{Deserialize, Serialize}; -use serde_cbor::Value as CborValue; -use sha2::{Digest, Sha256, Sha384, Sha512}; -use signature::{SignatureEncoding, Signer}; -use std::collections::{BTreeMap, HashSet}; pub type Namespaces = BTreeMap>; @@ -37,7 +39,7 @@ pub struct PreparedMdoc { doc_type: String, mso: Mso, namespaces: IssuerNamespaces, - prepared_sig: PreparedCoseSign1, + prepared_sig: CoseSign1, } #[derive(Debug, Clone, Default)] @@ -62,7 +64,7 @@ impl Mdoc { validity_info: ValidityInfo, digest_algorithm: DigestAlgorithm, device_key_info: DeviceKeyInfo, - signature_algorithm: Algorithm, + signature_algorithm: iana::Algorithm, enable_decoy_digests: bool, ) -> Result { if let Some(authorizations) = &device_key_info.key_authorizations { @@ -84,11 +86,14 @@ impl Mdoc { let mso_bytes = serde_cbor::to_vec(&Tag24::new(&mso)?)?; - let prepared_sig = CoseSign1::builder() - .payload(mso_bytes) - .signature_algorithm(signature_algorithm) - .prepare() - .map_err(|e| anyhow!("error preparing cosesign1: {}", e))?; + let protected = Header { + alg: Some(RegisteredLabelWithPrivate::Assigned(signature_algorithm)), + ..Header::default() + }; + let builder = coset::CoseSign1Builder::new() + .protected(protected) + .payload(mso_bytes); + let prepared_sig = CoseSign1::new(builder.build()); let preparation_mdoc = PreparedMdoc { doc_type, @@ -185,13 +190,17 @@ impl PreparedMdoc { doc_type, namespaces, mso, - prepared_sig, + mut prepared_sig, } = self; - let mut issuer_auth = prepared_sig.finalize(signature); + prepared_sig.0.signature = signature; + let mut issuer_auth = prepared_sig; + let val = Value::from_slice(&serde_cbor::to_vec(&x5chain.into_cbor()).unwrap()).unwrap(); issuer_auth - .unprotected_mut() - .insert_i(X5CHAIN_HEADER_LABEL, x5chain.into_cbor()); + .0 + .unprotected + .rest + .push((Label::Int(X5CHAIN_HEADER_LABEL as i64), val)); Mdoc { doc_type, @@ -243,7 +252,7 @@ impl Builder { /// /// The signature algorithm which the mdoc will be signed with must be known ahead of time as /// it is a required field in the signature headers. - pub fn prepare(self, signature_algorithm: Algorithm) -> Result { + pub fn prepare(self, signature_algorithm: iana::Algorithm) -> Result { let doc_type = self .doc_type .ok_or_else(|| anyhow!("missing parameter: 'doc_type'"))?; @@ -452,19 +461,20 @@ fn generate_digest_id(used_ids: &mut HashSet) -> DigestId { #[cfg(test)] pub mod test { - use super::*; - use crate::definitions::device_key::cose_key::{CoseKey, EC2Curve, EC2Y}; - use crate::definitions::namespaces::{ - org_iso_18013_5_1::OrgIso1801351, org_iso_18013_5_1_aamva::OrgIso1801351Aamva, - }; - - use crate::definitions::traits::{FromJson, ToNamespaceMap}; use elliptic_curve::sec1::ToEncodedPoint; use p256::ecdsa::{Signature, SigningKey}; use p256::pkcs8::DecodePrivateKey; use p256::SecretKey; use time::OffsetDateTime; + use crate::definitions::device_key::cose_key::{CoseKey, EC2Curve, EC2Y}; + use crate::definitions::namespaces::{ + org_iso_18013_5_1::OrgIso1801351, org_iso_18013_5_1_aamva::OrgIso1801351Aamva, + }; + use crate::definitions::traits::{FromJson, ToNamespaceMap}; + + use super::*; + static ISSUER_CERT: &[u8] = include_bytes!("../../test/issuance/issuer-cert.pem"); static ISSUER_KEY: &str = include_str!("../../test/issuance/issuer-key.pem"); diff --git a/src/lib.rs b/src/lib.rs index 372eecbe..10482749 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,5 +1,4 @@ -pub use cose_rs; - +pub mod cose; pub mod definitions; pub mod issuance; pub mod presentation; diff --git a/src/presentation/device.rs b/src/presentation/device.rs index 82711a2b..75810ccb 100644 --- a/src/presentation/device.rs +++ b/src/presentation/device.rs @@ -1,3 +1,16 @@ +use std::collections::BTreeMap; +use std::num::ParseIntError; + +use coset::{CoseSign1Builder, Header, RegisteredLabelWithPrivate}; +use p256::FieldBytes; +use serde::{Deserialize, Serialize}; +use serde_cbor::Value as CborValue; +use uuid::Uuid; + +use session::SessionTranscript180135; + +use crate::cose::sign1::CoseSign1; +use crate::cose::Cose; use crate::definitions::IssuerSignedItem; use crate::{ definitions::{ @@ -17,14 +30,6 @@ use crate::{ }, issuance::Mdoc, }; -use cose_rs::sign1::{CoseSign1, PreparedCoseSign1}; -use p256::FieldBytes; -use serde::{Deserialize, Serialize}; -use serde_cbor::Value as CborValue; -use session::SessionTranscript180135; -use std::collections::BTreeMap; -use std::num::ParseIntError; -use uuid::Uuid; #[derive(Serialize, Deserialize)] pub struct SessionManagerInit { @@ -104,7 +109,7 @@ struct PreparedDocument { doc_type: String, issuer_signed: IssuerSigned, device_namespaces: DeviceNamespacesBytes, - prepared_cose_sign1: PreparedCoseSign1, + prepared_cose_sign1: CoseSign1, errors: Option, } @@ -273,7 +278,7 @@ impl SessionManager { self.handle_decoded_request(session_data) } - /// Get next payload for signing. + /// Get the next payload for signing. pub fn get_next_signature_payload(&self) -> Option<(Uuid, &[u8])> { match &self.state { State::Signing(p) => p.get_next_signature_payload(), @@ -396,16 +401,17 @@ impl PreparedDocument { let Self { issuer_signed, device_namespaces, - prepared_cose_sign1, + mut prepared_cose_sign1, errors, doc_type, .. } = self; - let cose_sign1 = prepared_cose_sign1.finalize(signature); + prepared_cose_sign1.set_signature(signature); let device_signed = DeviceSigned { namespaces: device_namespaces, + // todo: support for CoseMac0 device_auth: DeviceAuth::Signature { - device_signature: cose_sign1, + device_signature: prepared_cose_sign1, }, }; DeviceResponseDoc { @@ -542,22 +548,15 @@ pub trait DeviceSession { continue; } }; - let prepared_cose_sign1 = match CoseSign1::builder() - .detached() - .payload(device_auth_bytes) - .signature_algorithm(signature_algorithm) - .prepare() - { - Ok(prepared) => prepared, - Err(_e) => { - let error: DocumentError = [(doc_type, DocumentErrorCode::DataNotReturned)] - .into_iter() - .collect(); - document_errors.push(error); - continue; - } + let protected = Header { + alg: Some(RegisteredLabelWithPrivate::Assigned(signature_algorithm)), + ..Header::default() }; - + // todo: support for CoseMac0 + let builder = CoseSign1Builder::new() + .protected(protected) + .payload(device_auth_bytes); + let prepared_cose_sign1 = CoseSign1::new(builder.build()); let prepared_document = PreparedDocument { id: document.id, doc_type, @@ -566,6 +565,7 @@ pub trait DeviceSession { issuer_auth: document.issuer_auth.clone(), }, device_namespaces, + // todo: support for CoseMac0 prepared_cose_sign1, errors: errors.try_into().ok(), }; @@ -731,11 +731,18 @@ impl TryFrom for AgeOver { #[cfg(test)] mod test { + use coset::{iana, CborSerializable, CoseSign1Builder}; + use ecdsa::Signature; + use hex::FromHex; + use p256::ecdsa::{SigningKey, VerifyingKey}; + use p256::{NistP256, SecretKey}; + use serde_json::json; + use signature::{Signer, Verifier}; + use crate::definitions::helpers::ByteStr; + use crate::definitions::mso::DigestId; use super::*; - use crate::definitions::mso::DigestId; - use serde_json::json; #[test] fn filter_permitted() { @@ -849,4 +856,77 @@ mod test { println!("{:?}", x); } + + // static COSE_SIGN1: &str = include_str!("../../test/definitions/cose/sign1/serialized.cbor"); + static COSE_KEY: &str = include_str!("../../test/definitions/cose/sign1/secret_key"); + + fn sign(payload: &[u8]) -> Vec { + let key = Vec::::from_hex(COSE_KEY).unwrap(); + let signer: SigningKey = SecretKey::from_slice(&key).unwrap().into(); + + let sig: Signature = signer.try_sign(payload).unwrap(); + sig.to_vec() + } + + fn verify(sig: &[u8], payload: &[u8]) -> coset::Result<(), String> { + let key = Vec::::from_hex(COSE_KEY).unwrap(); + let signer: SigningKey = SecretKey::from_slice(&key).unwrap().into(); + let verifier: VerifyingKey = (&signer).into(); + let signature: Signature = + Signature::from_slice(sig).map_err(|err| err.to_string())?; + verifier + .verify(payload, &signature) + .map_err(|err| err.to_string()) + } + + #[test] + fn test_coset() { + // Inputs. + let pt = b"This is the content"; + let aad = b"this is additional data"; + + // Build a `CoseSign1` object. + let protected = coset::HeaderBuilder::new() + .algorithm(iana::Algorithm::ES256) + .key_id(b"11".to_vec()) + .build(); + let sign1 = CoseSign1Builder::new() + .protected(protected) + .payload(pt.to_vec()) + .create_signature(aad, sign) // closure to do sign operation + .build(); + + // Serialize to bytes. + let sign1_data = sign1.to_vec().unwrap(); + println!( + "'{}' + '{}' => {}", + String::from_utf8_lossy(pt), + String::from_utf8_lossy(aad), + hex::encode(&sign1_data) + ); + + // At the receiving end, deserialize the bytes back to a `CoseSign1` object. + let mut sign1 = coset::CoseSign1::from_slice(&sign1_data).unwrap(); + + // At this point, real code would validate the protected headers. + + // Check the signature, which needs to have the same `aad` provided, by + // providing a closure that can do the verify operation. + let result = sign1.verify_signature(aad, verify); + println!("Signature verified: {:?}.", result); + assert!(result.is_ok()); + + // Changing an unprotected header leaves the signature valid. + sign1.unprotected.content_type = Some(coset::ContentType::Text("text/plain".to_owned())); + assert!(sign1.verify_signature(aad, verify).is_ok()); + + // Providing a different `aad` means the signature won't validate. + assert!(sign1.verify_signature(b"not aad", verify).is_err()); + + // Changing a protected header invalidates the signature. + sign1.protected.original_data = None; + sign1.protected.header.content_type = + Some(coset::ContentType::Text("text/plain".to_owned())); + assert!(sign1.verify_signature(aad, verify).is_err()); + } } diff --git a/test/definitions/cose/mac0/secret_key_aes_cbc_mac b/test/definitions/cose/mac0/secret_key_aes_cbc_mac new file mode 100644 index 00000000..ba0d038a --- /dev/null +++ b/test/definitions/cose/mac0/secret_key_aes_cbc_mac @@ -0,0 +1 @@ +a361316953796d6d65747269636133181a622d31982018a4183318f218df184a1888184218fe183c1887183a17189a18311821186f18ab18d60718eb188e183016181f18ee0b18a318310e183c186718d8 \ No newline at end of file diff --git a/test/definitions/cose/mac0/secret_key_hmac b/test/definitions/cose/mac0/secret_key_hmac new file mode 100644 index 00000000..9fb6a674 --- /dev/null +++ b/test/definitions/cose/mac0/secret_key_hmac @@ -0,0 +1 @@ +a361316953796d6d6574726963613305622d318f187418681869187318201869187318201874186818651820186b18651879 \ No newline at end of file diff --git a/test/definitions/cose/mac0/serialized_aes_cbc_mac.cbor b/test/definitions/cose/mac0/serialized_aes_cbc_mac.cbor new file mode 100644 index 00000000..d2b38a57 --- /dev/null +++ b/test/definitions/cose/mac0/serialized_aes_cbc_mac.cbor @@ -0,0 +1 @@ +d28444a101181aa10442313154546869732069732074686520636f6e74656e742e5063aa5b6c455a9b305621a5f51ca5e245 \ No newline at end of file diff --git a/test/definitions/cose/mac0/serialized_hmac.cbor b/test/definitions/cose/mac0/serialized_hmac.cbor new file mode 100644 index 00000000..e82c7f9d --- /dev/null +++ b/test/definitions/cose/mac0/serialized_hmac.cbor @@ -0,0 +1 @@ +d28443a10105a10442313154546869732069732074686520636f6e74656e742e582009de60921747849a72f4e57df91ffeb4ce51b27636eddc610d3fc0ae848daeb7 \ No newline at end of file diff --git a/test/definitions/cose/sign1/secret_key b/test/definitions/cose/sign1/secret_key new file mode 100644 index 00000000..408d0692 --- /dev/null +++ b/test/definitions/cose/sign1/secret_key @@ -0,0 +1 @@ +57c92077664146e876760c9520d054aa93c3afb04e306705db6090308507b4d3 \ No newline at end of file diff --git a/test/definitions/cose/sign1/serialized.cbor b/test/definitions/cose/sign1/serialized.cbor new file mode 100644 index 00000000..3e3da0da --- /dev/null +++ b/test/definitions/cose/sign1/serialized.cbor @@ -0,0 +1 @@ +D28443A10126A10442313154546869732069732074686520636F6E74656E742E58408EB33E4CA31D1C465AB05AAC34CC6B23D58FEF5C083106C4D25A91AEF0B0117E2AF9A291AA32E14AB834DC56ED2A223444547E01F11D3B0916E5A4C345CACB36 \ No newline at end of file From 82ff8fd2c4e24c039e5a7adc488d0cc06b64f25f Mon Sep 17 00:00:00 2001 From: Radu Marias Date: Tue, 23 Jul 2024 00:17:06 +0300 Subject: [PATCH 02/24] Integrate coset crate. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # Description - Integrate `coset` crate. - Crate module `cose` where we have `Cose` trait with some basic methods and implemented by `CoseSign1` and `CoseMac0` which are wrappers over coset analogous. - Implement `SignatureAlgorithm` trait for `CoseSign1` and `CoseMac0`. # Ref https://linear.app/spruceid/issue/SKIT-448/iso-mdl-investigate-coset # Changes - Add `CoseSign1` and `CoseMac0` wrappers over coset analogous. - Add `Cose` trait with some basic methods. - Implement `SignatureAlgorithm` for `CoseSign1` and `CoseMac0`. - Add a global map to hold signature payloads as our flow returns a reference to the payload and later that is signed externally. - Serialize/deserialize `CoseSign1` and `CoseMac0`. - Change signature flow to use the coset signature. - Use `iana::Algorithm`. ## Other changes None # Reviewers, please pay special attention to… The way `CoseSign1` is used. Signature flow and global map that hold signature payload, so we can return reference. # Tested Tested locally, running all tests. Build with these targets: ``` aarch64-linux-android armv7-linux-androideabi i686-linux-android wasm32-unknown-unknown wasm32-wasi x86_64-unknown-linux-gnu i686-unknown-linux-gnu x86_64-unknown-linux-musl x86_64-pc-windows-gnu i686-pc-windows-gnu ``` # Checklist - [x] [isomdl] Integrate `CoseSign1` - [ ] [isomdl] Integrate `COSEMac0` - [ ] [isomdl] Use coset `CoseKey` - [ ] [isomdl] Test building with `macosx` targets --- src/cose.rs | 14 +++++ src/cose/mac0.rs | 135 ++++++++++++++++++++++++++++++++++++++++++++++ src/cose/sign1.rs | 120 +++++++++++++++++++++++++++++++++++++++++ 3 files changed, 269 insertions(+) create mode 100644 src/cose.rs create mode 100644 src/cose/mac0.rs create mode 100644 src/cose/sign1.rs diff --git a/src/cose.rs b/src/cose.rs new file mode 100644 index 00000000..0f8f172c --- /dev/null +++ b/src/cose.rs @@ -0,0 +1,14 @@ +pub mod mac0; +pub mod sign1; + +use coset::iana; + +pub trait Cose { + fn signature_payload(&self) -> &[u8]; + fn set_signature(&mut self, signature: Vec); +} + +/// Trait to represent the signature algorithm of a signer or verifier. +pub trait SignatureAlgorithm { + fn algorithm(&self) -> iana::Algorithm; +} diff --git a/src/cose/mac0.rs b/src/cose/mac0.rs new file mode 100644 index 00000000..fdac9e15 --- /dev/null +++ b/src/cose/mac0.rs @@ -0,0 +1,135 @@ +use crate::cose::Cose; +use coset::cbor::Value; +use coset::{mac_structure_data, AsCborValue, MacContext}; +use serde::{Deserialize, Deserializer, Serialize, Serializer}; +use std::collections::HashMap; +use std::sync::atomic::{AtomicI64, Ordering}; +use std::sync::{Arc, Mutex, OnceLock}; + +#[derive(Clone, Debug, Default)] +pub struct CoseMac0(pub(crate) coset::CoseMac0, Arc>>); + +impl CoseMac0 { + pub fn new(cose_mac0: coset::CoseMac0) -> Self { + Self(cose_mac0, Arc::new(Mutex::new(None))) + } +} + +impl Serialize for CoseMac0 { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + // todo: map_err + self.0 + .clone() + .to_cbor_value() + .unwrap() + .serialize(serializer) + } +} + +impl<'de> Deserialize<'de> for CoseMac0 { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + // Deserialize the input to a CBOR Value + let value = Value::deserialize(deserializer)?; + + // Convert the CBOR Value to CoseMac0 + let inner = coset::CoseMac0::from_cbor_value(value).map_err(serde::de::Error::custom)?; + + Ok(CoseMac0(inner, Arc::new(Mutex::new(None)))) + } +} + +static mut SIGNATURE_PAYLOAD: OnceLock>> = OnceLock::new(); +static ATOMIC_CTR: AtomicI64 = AtomicI64::new(0); + +impl Cose for CoseMac0 { + fn signature_payload(&self) -> &[u8] { + // We need to return a reference to a value that is build in-place, + // so we need to keep it somewhere. We use a global map for this. + let payload = mac_structure_data( + MacContext::CoseMac0, + self.0.protected.clone(), + &[], + self.0.payload.as_ref().expect("payload missing"), // safe: documented + ); + let mut guard = self.1.lock().unwrap(); + let id = guard.get_or_insert(ATOMIC_CTR.fetch_add(1, Ordering::SeqCst)); + unsafe { + let map = SIGNATURE_PAYLOAD.get_or_init(HashMap::new); + SIGNATURE_PAYLOAD.get_mut().unwrap().insert(*id, payload); + map.get(id).unwrap() + } + } + + fn set_signature(&mut self, signature: Vec) { + self.0.tag = signature; + } +} + +impl Drop for CoseMac0 { + fn drop(&mut self) { + // Remove the signature payload from the global map + if let Some(id) = self.1.lock().unwrap().take() { + unsafe { + SIGNATURE_PAYLOAD.get_mut().unwrap().remove(&id); + } + } + } +} + +mod hmac { + use coset::iana; + use hmac::Hmac; + use sha2::{Sha256, Sha384, Sha512}; + + use super::super::SignatureAlgorithm; + + /// Implement [`SignatureAlgorithm`] for each `HMAC` variant. + + impl SignatureAlgorithm for Hmac { + fn algorithm(&self) -> iana::Algorithm { + iana::Algorithm::HMAC_256_256 + } + } + + impl SignatureAlgorithm for Hmac { + fn algorithm(&self) -> iana::Algorithm { + iana::Algorithm::HMAC_384_384 + } + } + + impl SignatureAlgorithm for Hmac { + fn algorithm(&self) -> iana::Algorithm { + iana::Algorithm::HMAC_512_512 + } + } +} + +mod aes_cbc_mac { + use super::super::SignatureAlgorithm; + use aes::{Aes128, Aes256}; + use cbc_mac::CbcMac; + use coset::iana; + + type Aes128CbcMac = CbcMac; + type Aes256CbcMac = CbcMac; + + /// Implement [`SignatureAlgorithm`] for each `AES-CBC-MAC` variant. + + impl SignatureAlgorithm for Aes128CbcMac { + fn algorithm(&self) -> iana::Algorithm { + iana::Algorithm::AES_MAC_128_128 + } + } + + impl SignatureAlgorithm for Aes256CbcMac { + fn algorithm(&self) -> iana::Algorithm { + iana::Algorithm::AES_MAC_256_128 + } + } +} diff --git a/src/cose/sign1.rs b/src/cose/sign1.rs new file mode 100644 index 00000000..201eaa29 --- /dev/null +++ b/src/cose/sign1.rs @@ -0,0 +1,120 @@ +use crate::cose::Cose; +use coset::cbor::Value; +use coset::{sig_structure_data, AsCborValue, SignatureContext}; +use serde::{Deserialize, Deserializer, Serialize, Serializer}; +use std::collections::HashMap; +use std::sync::atomic::{AtomicI64, Ordering}; +use std::sync::{Arc, Mutex, OnceLock}; + +#[derive(Clone, Debug, Default)] +pub struct CoseSign1(pub(crate) coset::CoseSign1, Arc>>); + +impl CoseSign1 { + pub fn new(cose_sign1: coset::CoseSign1) -> Self { + Self(cose_sign1, Arc::new(Mutex::new(None))) + } +} + +impl Serialize for CoseSign1 { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + // todo: map_err + self.0 + .clone() + .to_cbor_value() + .map_err(serde::ser::Error::custom)? + .serialize(serializer) + } +} + +impl<'de> Deserialize<'de> for CoseSign1 { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + // Deserialize the input to a CBOR Value + let value = Value::deserialize(deserializer)?; + + // Convert the CBOR Value to CoseSign1 + let inner = coset::CoseSign1::from_cbor_value(value).map_err(serde::de::Error::custom)?; + + Ok(CoseSign1(inner, Arc::new(Mutex::new(None)))) + } +} + +static mut SIGNATURE_PAYLOAD: OnceLock>> = OnceLock::new(); +static ATOMIC_CTR: AtomicI64 = AtomicI64::new(0); + +impl Cose for CoseSign1 { + fn signature_payload(&self) -> &[u8] { + // We need to return a reference to a value that is build in-place, + // so we need to keep it somewhere. We use a global map for this. + let payload = sig_structure_data( + SignatureContext::CoseSign1, + self.0.protected.clone(), + None, + &[], + self.0.payload.as_ref().unwrap_or(&vec![]), + ); + let mut guard = self.1.lock().unwrap(); + let id = guard.get_or_insert(ATOMIC_CTR.fetch_add(1, Ordering::SeqCst)); + unsafe { + let map = SIGNATURE_PAYLOAD.get_or_init(HashMap::new); + SIGNATURE_PAYLOAD.get_mut().unwrap().insert(*id, payload); + map.get(id).unwrap() + } + } + + fn set_signature(&mut self, signature: Vec) { + self.0.signature = signature; + } +} + +impl Drop for CoseSign1 { + fn drop(&mut self) { + // Remove the signature payload from the global map + if let Some(id) = self.1.lock().unwrap().take() { + unsafe { + SIGNATURE_PAYLOAD.get_mut().unwrap().remove(&id); + } + } + } +} + +mod p256 { + use crate::cose::SignatureAlgorithm; + use coset::iana; + use p256::ecdsa::{SigningKey, VerifyingKey}; + + impl SignatureAlgorithm for SigningKey { + fn algorithm(&self) -> iana::Algorithm { + iana::Algorithm::ES256 + } + } + + impl SignatureAlgorithm for VerifyingKey { + fn algorithm(&self) -> iana::Algorithm { + iana::Algorithm::ES256 + } + } +} + +mod p384 { + use crate::cose::SignatureAlgorithm; + use coset::iana; + use p384::ecdsa::{SigningKey, VerifyingKey}; + + impl SignatureAlgorithm for SigningKey { + fn algorithm(&self) -> iana::Algorithm { + iana::Algorithm::ES384 + } + } + + impl SignatureAlgorithm for VerifyingKey { + fn algorithm(&self) -> iana::Algorithm { + iana::Algorithm::ES384 + } + } +} From 9a6bf23981fcb8e4e79875d55fc03fa3eb80e484 Mon Sep 17 00:00:00 2001 From: Radu Marias Date: Tue, 23 Jul 2024 15:20:30 +0300 Subject: [PATCH 03/24] Integrate coset crate. # Description Remove some unneeded implementations, refactor some code and other small changes. # Ref https://linear.app/spruceid/issue/SKIT-448/iso-mdl-investigate-coset # Changes - Remove `cbc-mac`. - Remove `HMAC_384_384` and `HMAC_512_512`. - Use `set_signature`. - Have `X5Chain` return `ciborium Value`. # Tested Tested locally, running all tests and build on several platforms. # Checklist - [x] [isomdl] Integrate `CoseSign1` - [ ] [isomdl] Integrate `COSEMac0` - [ ] [isomdl] Use coset `CoseKey` - [x] [isomdl] Test building with `macosx` targets --- Cargo.toml | 2 +- src/cose/mac0.rs | 50 +++++++---------------------------------- src/issuance/mdoc.rs | 8 +++---- src/issuance/x5chain.rs | 2 +- 4 files changed, 13 insertions(+), 49 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 399e8696..cf98e114 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -37,7 +37,7 @@ base64 = "0.13" pem-rfc7468 = "0.7.0" x509-cert = { version = "0.1.1", features = ["pem"] } coset = { version = "0.3.7", features = ["std"] } -cbc-mac = "0.1.0" +ciborium = "0.2.2" ssi-jwk = { version = "0.1" } isomdl-macros = { version = "0.1.0", path = "macros" } diff --git a/src/cose/mac0.rs b/src/cose/mac0.rs index fdac9e15..adfe0e44 100644 --- a/src/cose/mac0.rs +++ b/src/cose/mac0.rs @@ -1,11 +1,13 @@ -use crate::cose::Cose; -use coset::cbor::Value; -use coset::{mac_structure_data, AsCborValue, MacContext}; -use serde::{Deserialize, Deserializer, Serialize, Serializer}; use std::collections::HashMap; use std::sync::atomic::{AtomicI64, Ordering}; use std::sync::{Arc, Mutex, OnceLock}; +use coset::cbor::Value; +use coset::{mac_structure_data, AsCborValue, MacContext}; +use serde::{Deserialize, Deserializer, Serialize, Serializer}; + +use crate::cose::Cose; + #[derive(Clone, Debug, Default)] pub struct CoseMac0(pub(crate) coset::CoseMac0, Arc>>); @@ -85,51 +87,15 @@ impl Drop for CoseMac0 { mod hmac { use coset::iana; use hmac::Hmac; - use sha2::{Sha256, Sha384, Sha512}; + use sha2::Sha256; use super::super::SignatureAlgorithm; - /// Implement [`SignatureAlgorithm`] for each `HMAC` variant. + /// Implement [`SignatureAlgorithm`]. impl SignatureAlgorithm for Hmac { fn algorithm(&self) -> iana::Algorithm { iana::Algorithm::HMAC_256_256 } } - - impl SignatureAlgorithm for Hmac { - fn algorithm(&self) -> iana::Algorithm { - iana::Algorithm::HMAC_384_384 - } - } - - impl SignatureAlgorithm for Hmac { - fn algorithm(&self) -> iana::Algorithm { - iana::Algorithm::HMAC_512_512 - } - } -} - -mod aes_cbc_mac { - use super::super::SignatureAlgorithm; - use aes::{Aes128, Aes256}; - use cbc_mac::CbcMac; - use coset::iana; - - type Aes128CbcMac = CbcMac; - type Aes256CbcMac = CbcMac; - - /// Implement [`SignatureAlgorithm`] for each `AES-CBC-MAC` variant. - - impl SignatureAlgorithm for Aes128CbcMac { - fn algorithm(&self) -> iana::Algorithm { - iana::Algorithm::AES_MAC_128_128 - } - } - - impl SignatureAlgorithm for Aes256CbcMac { - fn algorithm(&self) -> iana::Algorithm { - iana::Algorithm::AES_MAC_256_128 - } - } } diff --git a/src/issuance/mdoc.rs b/src/issuance/mdoc.rs index a0e4cfe5..1924655f 100644 --- a/src/issuance/mdoc.rs +++ b/src/issuance/mdoc.rs @@ -2,8 +2,7 @@ use std::collections::{BTreeMap, HashSet}; use anyhow::{anyhow, Result}; use async_signature::AsyncSigner; -use coset::cbor::Value; -use coset::{iana, CborSerializable, Header, Label, RegisteredLabelWithPrivate}; +use coset::{iana, Header, Label, RegisteredLabelWithPrivate}; use rand::Rng; use serde::{Deserialize, Serialize}; use serde_cbor::Value as CborValue; @@ -193,14 +192,13 @@ impl PreparedMdoc { mut prepared_sig, } = self; - prepared_sig.0.signature = signature; + prepared_sig.set_signature(signature); let mut issuer_auth = prepared_sig; - let val = Value::from_slice(&serde_cbor::to_vec(&x5chain.into_cbor()).unwrap()).unwrap(); issuer_auth .0 .unprotected .rest - .push((Label::Int(X5CHAIN_HEADER_LABEL as i64), val)); + .push((Label::Int(X5CHAIN_HEADER_LABEL as i64), x5chain.into_cbor())); Mdoc { doc_type, diff --git a/src/issuance/x5chain.rs b/src/issuance/x5chain.rs index 898ba765..a1a538a6 100644 --- a/src/issuance/x5chain.rs +++ b/src/issuance/x5chain.rs @@ -1,6 +1,6 @@ use crate::definitions::helpers::NonEmptyVec; use anyhow::{anyhow, Result}; -use serde_cbor::Value as CborValue; +use ciborium::Value as CborValue; use std::{fs::File, io::Read}; use x509_cert::{ certificate::Certificate, From 87d75c27723c511a2de37e6cfe3f22ee92cd4378 Mon Sep 17 00:00:00 2001 From: Radu Marias Date: Mon, 5 Aug 2024 01:04:12 +0300 Subject: [PATCH 04/24] Add CoseSign1 and CoseMac0 and migrate parts of the serilization to ciborium. - Add concept of CoseSign1Builder and CoseMac0Builder - Add signature prepare and verify methods - Add tests. --- Cargo.toml | 1 + src/cose.rs | 5 - src/cose/mac0.rs | 504 +++++++++++++++-- src/cose/sign1.rs | 521 ++++++++++++++++-- src/definitions/mso.rs | 2 +- src/definitions/traits/to_cbor.rs | 4 +- src/issuance/mdoc.rs | 24 +- src/presentation/device.rs | 41 +- .../cose/mac0/{secret_key_hmac => secret_key} | 0 .../cose/mac0/secret_key_aes_cbc_mac | 1 - .../{serialized_hmac.cbor => serialized.cbor} | 0 .../cose/mac0/serialized_aes_cbc_mac.cbor | 1 - 12 files changed, 948 insertions(+), 156 deletions(-) rename test/definitions/cose/mac0/{secret_key_hmac => secret_key} (100%) delete mode 100644 test/definitions/cose/mac0/secret_key_aes_cbc_mac rename test/definitions/cose/mac0/{serialized_hmac.cbor => serialized.cbor} (100%) delete mode 100644 test/definitions/cose/mac0/serialized_aes_cbc_mac.cbor diff --git a/Cargo.toml b/Cargo.toml index cf98e114..964c6e5d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -44,6 +44,7 @@ isomdl-macros = { version = "0.1.0", path = "macros" } clap = { version = "4", features = ["derive"] } clap-stdin = "0.5.1" digest = "0.10.7" +serde_bytes = "0.11.15" [dev-dependencies] hex = "0.4.3" diff --git a/src/cose.rs b/src/cose.rs index 0f8f172c..5aca68c1 100644 --- a/src/cose.rs +++ b/src/cose.rs @@ -3,11 +3,6 @@ pub mod sign1; use coset::iana; -pub trait Cose { - fn signature_payload(&self) -> &[u8]; - fn set_signature(&mut self, signature: Vec); -} - /// Trait to represent the signature algorithm of a signer or verifier. pub trait SignatureAlgorithm { fn algorithm(&self) -> iana::Algorithm; diff --git a/src/cose/mac0.rs b/src/cose/mac0.rs index adfe0e44..c39af098 100644 --- a/src/cose/mac0.rs +++ b/src/cose/mac0.rs @@ -1,89 +1,301 @@ -use std::collections::HashMap; -use std::sync::atomic::{AtomicI64, Ordering}; -use std::sync::{Arc, Mutex, OnceLock}; - +use ::hmac::Hmac; use coset::cbor::Value; -use coset::{mac_structure_data, AsCborValue, MacContext}; -use serde::{Deserialize, Deserializer, Serialize, Serializer}; +use coset::cwt::ClaimsSet; +use coset::{ + iana, mac_structure_data, AsCborValue, CborSerializable, CoseError, MacContext, + RegisteredLabelWithPrivate, +}; +use digest::{Mac, MacError}; +use serde::ser::{SerializeMap, SerializeSeq}; +use serde::{ser, Deserialize, Deserializer, Serialize}; +use serde_cbor::tags::Tagged; +use sha2::Sha256; -use crate::cose::Cose; +use crate::cose::SignatureAlgorithm; -#[derive(Clone, Debug, Default)] -pub struct CoseMac0(pub(crate) coset::CoseMac0, Arc>>); +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct PreparedCoseMac0 { + tagged: bool, + cose_mac0: CoseMac0, + tag_payload: Vec, +} -impl CoseMac0 { - pub fn new(cose_mac0: coset::CoseMac0) -> Self { - Self(cose_mac0, Arc::new(Mutex::new(None))) - } +#[derive(Clone, Debug)] +pub struct CoseMac0 { + tagged: bool, + pub inner: coset::CoseMac0, } -impl Serialize for CoseMac0 { - fn serialize(&self, serializer: S) -> Result - where - S: Serializer, - { - // todo: map_err - self.0 - .clone() - .to_cbor_value() - .unwrap() - .serialize(serializer) +/// Errors that can occur when building, signing or verifying a COSE_Mac0. +#[derive(Debug, thiserror::Error)] +pub enum Error { + #[error("the COSE_Mac0 has an attached payload but an detached payload was provided")] + DoublePayload, + #[error("the COSE_Mac0 has a detached payload which was not provided")] + NoPayload, + #[error("tag did not match the structure expected by the verifier: {0}")] + MalformedTag(MacError), + #[error("tag is already present")] + AlreadyTagged, + #[error("error occurred when tagging COSE_Mac0: {0}")] + Tagging(MacError), + #[error("unable to set ClaimsSet: {0}")] + UnableToDeserializeIntoClaimsSet(CoseError), +} + +/// Result with error type: [`Error`]. +pub type Result = std::result::Result; + +/// Result for verification of a COSE_Mac0. +#[derive(Debug)] +pub enum VerificationResult { + Success, + Failure(String), + Error(Error), +} + +impl VerificationResult { + /// Result of verification. + /// + /// False implies the signature is inauthentic or the verification algorithm encountered an + /// error. + pub fn success(&self) -> bool { + matches!(self, VerificationResult::Success) + } + + /// Translate to a std::result::Result. + /// + /// Converts failure reasons and errors into a String. + pub fn to_result(self) -> Result<(), String> { + match self { + VerificationResult::Success => Ok(()), + VerificationResult::Failure(reason) => Err(reason), + VerificationResult::Error(e) => Err(format!("{}", e)), + } + } + + /// Retrieve the error if the verification algorithm encountered an error. + pub fn to_error(self) -> Option { + match self { + VerificationResult::Error(e) => Some(e), + _ => None, + } } } -impl<'de> Deserialize<'de> for CoseMac0 { - fn deserialize(deserializer: D) -> Result - where - D: Deserializer<'de>, - { - // Deserialize the input to a CBOR Value - let value = Value::deserialize(deserializer)?; +impl PreparedCoseMac0 { + pub fn new( + builder: coset::CoseMac0Builder, + detached_payload: Option>, + aad: Option>, + tagged: bool, + ) -> Result { + let cose_mac0 = builder.build(); - // Convert the CBOR Value to CoseMac0 - let inner = coset::CoseMac0::from_cbor_value(value).map_err(serde::de::Error::custom)?; + // Check if the signature is already present. + match (&cose_mac0.tag, detached_payload.as_ref()) { + (v, Some(_)) if !v.is_empty() => return Err(Error::AlreadyTagged), + _ => {} + } + + // Check if the payload is present and if it is attached or detached. + // Needs to be exclusively attached or detached. + let payload = match (cose_mac0.payload.as_ref(), detached_payload) { + (Some(_), Some(_)) => return Err(Error::DoublePayload), + (None, None) => return Err(Error::NoPayload), + (Some(payload), None) => Some(payload.clone()), + (None, Some(payload)) => Some(payload), + }; + let payload = payload + // Payload is mandatory + .as_ref() + .expect("payload missing"); + // Create the signature payload ot be used later on signing. + let tag_payload = mac_structure_data( + MacContext::CoseMac0, + cose_mac0.protected.clone(), + aad.unwrap_or_default().as_ref(), + payload, + ); + + Ok(Self { + tagged, + cose_mac0: CoseMac0 { + tagged, + inner: cose_mac0, + }, + tag_payload, + }) + } + + pub fn signature_payload(&self) -> &[u8] { + &self.tag_payload + } - Ok(CoseMac0(inner, Arc::new(Mutex::new(None)))) + pub fn finalize(self, tag: Vec) -> CoseMac0 { + let mut cose_mac0 = self.cose_mac0; + cose_mac0.inner.tag = tag; + cose_mac0 } } -static mut SIGNATURE_PAYLOAD: OnceLock>> = OnceLock::new(); -static ATOMIC_CTR: AtomicI64 = AtomicI64::new(0); +impl CoseMac0 { + /// Verify that the tag of a `COSE_Mac0` is authentic. + pub fn verify( + &self, + verifier: &Hmac, + detached_payload: Option>, + external_aad: Option>, + ) -> VerificationResult { + if let Some(RegisteredLabelWithPrivate::Assigned(alg)) = + self.inner.protected.header.alg.as_ref() + { + if verifier.algorithm() != *alg { + return VerificationResult::Failure( + "algorithm in protected headers did not match verifier's algorithm".into(), + ); + } + } -impl Cose for CoseMac0 { - fn signature_payload(&self) -> &[u8] { - // We need to return a reference to a value that is build in-place, - // so we need to keep it somewhere. We use a global map for this. - let payload = mac_structure_data( + let payload = match (self.inner.payload.as_ref(), detached_payload.as_ref()) { + (None, None) => return VerificationResult::Error(Error::NoPayload), + (Some(attached), None) => attached, + (None, Some(detached)) => detached, + _ => return VerificationResult::Error(Error::DoublePayload), + }; + + let tag = &self.inner.tag; + + // Create the signature payload ot be used later on signing. + let tag_payload = mac_structure_data( MacContext::CoseMac0, - self.0.protected.clone(), - &[], - self.0.payload.as_ref().expect("payload missing"), // safe: documented + self.inner.protected.clone(), + external_aad.unwrap_or_default().as_ref(), + payload, ); - let mut guard = self.1.lock().unwrap(); - let id = guard.get_or_insert(ATOMIC_CTR.fetch_add(1, Ordering::SeqCst)); - unsafe { - let map = SIGNATURE_PAYLOAD.get_or_init(HashMap::new); - SIGNATURE_PAYLOAD.get_mut().unwrap().insert(*id, payload); - map.get(id).unwrap() + + let mut mac = verifier.clone(); + mac.reset(); + mac.update(&tag_payload); + match mac.verify_slice(tag) { + Ok(()) => VerificationResult::Success, + Err(e) => VerificationResult::Failure(format!("tag is not authentic: {}", e)), } } - fn set_signature(&mut self, signature: Vec) { - self.0.tag = signature; + pub fn claims_set(&self) -> Result> { + match self.inner.payload.as_ref() { + None => Ok(None), + Some(payload) => ClaimsSet::from_slice(payload).map_or_else( + |e| Err(Error::UnableToDeserializeIntoClaimsSet(e)), + |c| Ok(Some(c)), + ), + } + } + + pub fn tagged(&self) -> bool { + self.tagged + } + + pub fn set_tagged(&mut self) { + self.tagged = true; } } -impl Drop for CoseMac0 { - fn drop(&mut self) { - // Remove the signature payload from the global map - if let Some(id) = self.1.lock().unwrap().take() { - unsafe { - SIGNATURE_PAYLOAD.get_mut().unwrap().remove(&id); +impl ser::Serialize for CoseMac0 { + #[inline] + fn serialize(&self, serializer: S) -> Result { + let value = self + .inner + .clone() + .to_cbor_value() + .map_err(ser::Error::custom)?; // Convert the inner CoseMac0 object to a tagged CBOR vector + if self.tagged { + return Tagged::new(Some(iana::CborTag::CoseMac0 as u64), value).serialize(serializer); + } + match value { + Value::Bytes(x) => serializer.serialize_bytes(&x), + Value::Bool(x) => serializer.serialize_bool(x), + Value::Text(x) => serializer.serialize_str(x.as_str()), + Value::Null => serializer.serialize_unit(), + + Value::Tag(tag, ref v) => Tagged::new(Some(tag), v).serialize(serializer), + + Value::Float(x) => { + let y = x as f32; + if (y as f64).to_bits() == x.to_bits() { + serializer.serialize_f32(y) + } else { + serializer.serialize_f64(x) + } + } + + #[allow(clippy::unnecessary_fallible_conversions)] + Value::Integer(x) => { + if let Ok(x) = u8::try_from(x) { + serializer.serialize_u8(x) + } else if let Ok(x) = i8::try_from(x) { + serializer.serialize_i8(x) + } else if let Ok(x) = u16::try_from(x) { + serializer.serialize_u16(x) + } else if let Ok(x) = i16::try_from(x) { + serializer.serialize_i16(x) + } else if let Ok(x) = u32::try_from(x) { + serializer.serialize_u32(x) + } else if let Ok(x) = i32::try_from(x) { + serializer.serialize_i32(x) + } else if let Ok(x) = u64::try_from(x) { + serializer.serialize_u64(x) + } else if let Ok(x) = i64::try_from(x) { + serializer.serialize_i64(x) + } else if let Ok(x) = u128::try_from(x) { + serializer.serialize_u128(x) + } else if let Ok(x) = i128::try_from(x) { + serializer.serialize_i128(x) + } else { + unreachable!() + } + } + + Value::Array(x) => { + let mut map = serializer.serialize_seq(Some(x.len()))?; + + for v in x { + map.serialize_element(&v)?; + } + + map.end() + } + + Value::Map(x) => { + let mut map = serializer.serialize_map(Some(x.len()))?; + + for (k, v) in x { + map.serialize_entry(&k, &v)?; + } + + map.end() } + _ => unimplemented!(), } } } +impl<'de> Deserialize<'de> for CoseMac0 { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + // Deserialize the input to a CBOR Value + let value = Value::deserialize(deserializer)?; + // Convert the CBOR Value to CoseMac0 + Ok(CoseMac0 { + tagged: false, + inner: coset::CoseMac0::from_cbor_value(value).map_err(serde::de::Error::custom)?, + }) + } +} + mod hmac { use coset::iana; use hmac::Hmac; @@ -99,3 +311,179 @@ mod hmac { } } } + +#[cfg(test)] +mod tests { + use crate::cose::mac0::{CoseMac0, PreparedCoseMac0}; + use coset::cwt::{ClaimsSet, Timestamp}; + use coset::{iana, CborSerializable, Header}; + use digest::Mac; + use hex::FromHex; + use hmac::Hmac; + use sha2::Sha256; + + static COSE_MAC0: &str = include_str!("../../test/definitions/cose/mac0/serialized.cbor"); + static COSE_KEY: &str = include_str!("../../test/definitions/cose/mac0/secret_key"); + + const RFC8392_KEY: &str = "6c1382765aec5358f117733d281c1c7bdc39884d04a45a1e6c67c858bc206c19"; + const RFC8392_COSE_MAC0: &str = "d18443a10126a104524173796d6d657472696345434453413235365850a70175636f61703a2f2f61732e6578616d706c652e636f6d02656572696b77037818636f61703a2f2f6c696768742e6578616d706c652e636f6d041a5612aeb0051a5610d9f0061a5610d9f007420b715820a377dfe17a3c3c3bdb363c426f85d3c1a1f11007765965017602f207700071b0"; + + #[test] + fn roundtrip() { + let bytes = Vec::::from_hex(COSE_MAC0).unwrap(); + let mut parsed: CoseMac0 = + serde_cbor::from_slice(&bytes).expect("failed to parse COSE_MAC0 from bytes"); + parsed.set_tagged(); + let roundtripped = + serde_cbor::to_vec(&parsed).expect("failed to serialize COSE_MAC0 to bytes"); + assert_eq!( + bytes, roundtripped, + "original bytes and roundtripped bytes do not match" + ); + } + + #[test] + fn signing() { + let key = Vec::::from_hex(COSE_KEY).unwrap(); + let signer = Hmac::::new_from_slice(&key).expect("failed to create HMAC signer"); + let protected = coset::HeaderBuilder::new() + .algorithm(iana::Algorithm::HMAC_256_256) + .build(); + let unprotected = coset::HeaderBuilder::new().key_id(b"11".to_vec()).build(); + let builder = coset::CoseMac0Builder::new() + .protected(protected) + .unprotected(unprotected) + .payload(b"This is the content.".to_vec()); + let prepared = PreparedCoseMac0::new(builder, None, None, true).unwrap(); + let signature_payload = prepared.signature_payload(); + let signature = sign(signature_payload, &signer).unwrap(); + let cose_mac0 = prepared.finalize(signature); + let serialized = + serde_cbor::to_vec(&cose_mac0).expect("failed to serialize COSE_MAC0 to bytes"); + + let expected = Vec::::from_hex(COSE_MAC0).unwrap(); + assert_eq!( + expected, serialized, + "expected COSE_MAC0 and signed data do not match" + ); + } + + fn sign(signature_payload: &[u8], s: &Hmac) -> anyhow::Result> { + let mut mac = s.clone(); + mac.reset(); + mac.update(signature_payload); + Ok(mac.finalize().into_bytes().to_vec()) + } + + #[test] + fn verifying() { + let key = Vec::::from_hex(COSE_KEY).unwrap(); + let verifier = + Hmac::::new_from_slice(&key).expect("failed to create HMAC verifier"); + + let cose_mac0_bytes = Vec::::from_hex(COSE_MAC0).unwrap(); + let cose_mac0: CoseMac0 = + serde_cbor::from_slice(&cose_mac0_bytes).expect("failed to parse COSE_MAC0 from bytes"); + + cose_mac0 + .verify(&verifier, None, None) + .to_result() + .expect("COSE_MAC0 could not be verified") + } + + #[test] + fn remote_signed() { + let key = Vec::::from_hex(COSE_KEY).unwrap(); + let signer = Hmac::::new_from_slice(&key).expect("failed to create HMAC signer"); + let protected = coset::HeaderBuilder::new() + .algorithm(iana::Algorithm::HMAC_256_256) + .build(); + let unprotected = coset::HeaderBuilder::new().key_id(b"11".to_vec()).build(); + let builder = coset::CoseMac0Builder::new() + .protected(protected) + .unprotected(unprotected) + .payload(b"This is the content.".to_vec()); + let prepared = PreparedCoseMac0::new(builder, None, None, true).unwrap(); + let signature_payload = prepared.signature_payload(); + let signature = sign(signature_payload, &signer).unwrap(); + let cose_mac0 = prepared.finalize(signature); + + let serialized = + serde_cbor::to_vec(&cose_mac0).expect("failed to serialize COSE_MAC0 to bytes"); + + let expected = Vec::::from_hex(COSE_MAC0).unwrap(); + assert_eq!( + expected, serialized, + "expected COSE_MAC0 and signed data do not match" + ); + + let verifier = + Hmac::::new_from_slice(&key).expect("failed to create HMAC verifier"); + cose_mac0 + .verify(&verifier, None, None) + .to_result() + .expect("COSE_MAC0 could not be verified") + } + + fn rfc8392_example_inputs() -> (Header, Header, ClaimsSet) { + let protected = coset::HeaderBuilder::new() + .algorithm(iana::Algorithm::ES256) + .build(); + + let unprotected = coset::HeaderBuilder::new() + .key_id( + hex::decode("4173796d6d65747269634543445341323536").expect("error decoding key id"), + ) + .build(); + + let claims_set = ClaimsSet { + issuer: Some("coap://as.example.com".to_string()), + subject: Some("erikw".to_string()), + audience: Some("coap://light.example.com".to_string()), + expiration_time: Some(Timestamp::WholeSeconds(1444064944)), + not_before: Some(Timestamp::WholeSeconds(1443944944)), + issued_at: Some(Timestamp::WholeSeconds(1443944944)), + cwt_id: Some(hex::decode("0b71").unwrap()), + rest: vec![], + }; + + (protected, unprotected, claims_set) + } + + #[test] + fn signing_cwt() { + // Using key from RFC8392 example + let key = hex::decode(RFC8392_KEY).unwrap(); + let signer = Hmac::::new_from_slice(&key).expect("failed to create HMAC signer"); + let (protected, unprotected, claims_set) = rfc8392_example_inputs(); + let builder = coset::CoseMac0Builder::new() + .protected(protected) + .unprotected(unprotected) + .payload(claims_set.to_vec().expect("failed to set claims set")); + let prepared = PreparedCoseMac0::new(builder, None, None, true).unwrap(); + let signature_payload = prepared.signature_payload(); + let signature = sign(signature_payload, &signer).expect("failed to sign CWT"); + let cose_mac0 = prepared.finalize(signature); + let serialized = + serde_cbor::to_vec(&cose_mac0).expect("failed to serialize COSE_MAC0 to bytes"); + let expected = hex::decode(RFC8392_COSE_MAC0).unwrap(); + + assert_eq!( + expected, serialized, + "expected COSE_MAC0 and signed CWT do not match" + ); + } + + #[test] + fn deserializing_signed_cwt() { + let cose_mac0_bytes = hex::decode(RFC8392_COSE_MAC0).unwrap(); + let cose_mac0: CoseMac0 = + serde_cbor::from_slice(&cose_mac0_bytes).expect("failed to parse COSE_MAC0 from bytes"); + let parsed_claims_set = cose_mac0 + .claims_set() + .expect("failed to parse claims set from payload") + .expect("retrieved empty claims set"); + let (_, _, expected_claims_set) = rfc8392_example_inputs(); + assert_eq!(parsed_claims_set, expected_claims_set); + } +} diff --git a/src/cose/sign1.rs b/src/cose/sign1.rs index 201eaa29..aa3e60d4 100644 --- a/src/cose/sign1.rs +++ b/src/cose/sign1.rs @@ -1,93 +1,313 @@ -use crate::cose::Cose; use coset::cbor::Value; -use coset::{sig_structure_data, AsCborValue, SignatureContext}; -use serde::{Deserialize, Deserializer, Serialize, Serializer}; -use std::collections::HashMap; -use std::sync::atomic::{AtomicI64, Ordering}; -use std::sync::{Arc, Mutex, OnceLock}; +use coset::cwt::ClaimsSet; +use coset::{ + iana, sig_structure_data, AsCborValue, CborSerializable, CoseError, RegisteredLabelWithPrivate, + SignatureContext, +}; +use serde::ser::{SerializeMap, SerializeSeq}; +use serde::{ser, Deserialize, Deserializer, Serialize}; +use serde_cbor::tags::Tagged; +use signature::Verifier; -#[derive(Clone, Debug, Default)] -pub struct CoseSign1(pub(crate) coset::CoseSign1, Arc>>); +use crate::cose::SignatureAlgorithm; -impl CoseSign1 { - pub fn new(cose_sign1: coset::CoseSign1) -> Self { - Self(cose_sign1, Arc::new(Mutex::new(None))) - } +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct PreparedCoseSign1 { + tagged: bool, + cose_sign1: CoseSign1, + signature_payload: Vec, } -impl Serialize for CoseSign1 { - fn serialize(&self, serializer: S) -> Result - where - S: Serializer, - { - // todo: map_err - self.0 - .clone() - .to_cbor_value() - .map_err(serde::ser::Error::custom)? - .serialize(serializer) +#[derive(Clone, Debug)] +pub struct CoseSign1 { + tagged: bool, + pub inner: coset::CoseSign1, +} + +/// Errors that can occur when building, signing or verifying a COSE_Sign1. +#[derive(Debug, thiserror::Error)] +pub enum Error { + #[error("the COSE_Sign1 has an attached payload but an detached payload was provided")] + DoublePayload, + #[error("the COSE_Sign1 has a detached payload which was not provided")] + NoPayload, + #[error("signature did not match the structure expected by the verifier: {0}")] + MalformedSignature(signature::Error), + #[error("signature is already present")] + AlreadySigned, + #[error("error occurred when signing COSE_Sign1: {0}")] + Signing(signature::Error), + #[error("unable to set ClaimsSet: {0}")] + UnableToDeserializeIntoClaimsSet(CoseError), +} + +/// Result with error type: [`Error`]. +pub type Result = std::result::Result; + +/// Result for verification of a COSE_Sign1. +#[derive(Debug)] +pub enum VerificationResult { + Success, + Failure(String), + Error(Error), +} + +impl VerificationResult { + /// Result of verification. + /// + /// False implies the signature is inauthentic or the verification algorithm encountered an + /// error. + pub fn success(&self) -> bool { + matches!(self, VerificationResult::Success) + } + + /// Translate to a std::result::Result. + /// + /// Converts failure reasons and errors into a String. + pub fn to_result(self) -> Result<(), String> { + match self { + VerificationResult::Success => Ok(()), + VerificationResult::Failure(reason) => Err(reason), + VerificationResult::Error(e) => Err(format!("{}", e)), + } + } + + /// Retrieve the error if the verification algorithm encountered an error. + pub fn to_error(self) -> Option { + match self { + VerificationResult::Error(e) => Some(e), + _ => None, + } } } -impl<'de> Deserialize<'de> for CoseSign1 { - fn deserialize(deserializer: D) -> Result - where - D: Deserializer<'de>, - { - // Deserialize the input to a CBOR Value - let value = Value::deserialize(deserializer)?; +impl PreparedCoseSign1 { + pub fn new( + builder: coset::CoseSign1Builder, + detached_payload: Option>, + aad: Option>, + tagged: bool, + ) -> Result { + let cose_sign1 = builder.build(); - // Convert the CBOR Value to CoseSign1 - let inner = coset::CoseSign1::from_cbor_value(value).map_err(serde::de::Error::custom)?; + // Check if the signature is already present. + match (&cose_sign1.signature, detached_payload.as_ref()) { + (v, Some(_)) if !v.is_empty() => return Err(Error::AlreadySigned), + _ => {} + } - Ok(CoseSign1(inner, Arc::new(Mutex::new(None)))) + // Check if the payload is present and if it is attached or detached. + // Needs to be exclusively attached or detached. + let payload = match (cose_sign1.payload.as_ref(), detached_payload) { + (Some(_), Some(_)) => return Err(Error::DoublePayload), + (None, None) => return Err(Error::NoPayload), + (Some(payload), None) => Some(payload.clone()), + (None, Some(payload)) => Some(payload), + }; + let payload = payload + // If payload is None, use cbor null as payload. + .unwrap_or_else(|| vec![246u8]); + // Create the signature payload ot be used later on signing. + let signature_payload = sig_structure_data( + SignatureContext::CoseSign1, + cose_sign1.protected.clone(), + None, + aad.unwrap_or_default().as_ref(), + &payload, + ); + + Ok(Self { + tagged, + cose_sign1: CoseSign1 { + tagged, + inner: cose_sign1, + }, + signature_payload, + }) + } + + pub fn signature_payload(&self) -> &[u8] { + &self.signature_payload + } + + pub fn finalize(self, signature: Vec) -> CoseSign1 { + let mut cose_sign1 = self.cose_sign1; + cose_sign1.inner.signature = signature; + cose_sign1 } } -static mut SIGNATURE_PAYLOAD: OnceLock>> = OnceLock::new(); -static ATOMIC_CTR: AtomicI64 = AtomicI64::new(0); +impl CoseSign1 { + /// Verify that the signature of a COSE_Sign1 is authentic. + pub fn verify<'a, V, S>( + &'a self, + verifier: &V, + detached_payload: Option>, + external_aad: Option>, + ) -> VerificationResult + where + V: Verifier + SignatureAlgorithm, + S: TryFrom<&'a [u8]>, + S::Error: Into, + { + if let Some(RegisteredLabelWithPrivate::Assigned(alg)) = + self.inner.protected.header.alg.as_ref() + { + if verifier.algorithm() != *alg { + return VerificationResult::Failure( + "algorithm in protected headers did not match verifier's algorithm".into(), + ); + } + } + + let payload = match (self.inner.payload.as_ref(), detached_payload.as_ref()) { + (None, None) => return VerificationResult::Error(Error::NoPayload), + (Some(attached), None) => attached, + (None, Some(detached)) => detached, + _ => return VerificationResult::Error(Error::DoublePayload), + }; -impl Cose for CoseSign1 { - fn signature_payload(&self) -> &[u8] { - // We need to return a reference to a value that is build in-place, - // so we need to keep it somewhere. We use a global map for this. - let payload = sig_structure_data( + let signature = match S::try_from(self.inner.signature.as_ref()) + .map_err(Into::into) + .map_err(Error::MalformedSignature) + { + Ok(sig) => sig, + Err(e) => return VerificationResult::Error(e), + }; + + let signature_payload = sig_structure_data( SignatureContext::CoseSign1, - self.0.protected.clone(), + self.inner.protected.clone(), None, - &[], - self.0.payload.as_ref().unwrap_or(&vec![]), + external_aad.unwrap_or_default().as_ref(), + payload, ); - let mut guard = self.1.lock().unwrap(); - let id = guard.get_or_insert(ATOMIC_CTR.fetch_add(1, Ordering::SeqCst)); - unsafe { - let map = SIGNATURE_PAYLOAD.get_or_init(HashMap::new); - SIGNATURE_PAYLOAD.get_mut().unwrap().insert(*id, payload); - map.get(id).unwrap() + + match verifier.verify(&signature_payload, &signature) { + Ok(()) => VerificationResult::Success, + Err(e) => VerificationResult::Failure(format!("signature is not authentic: {}", e)), } } - fn set_signature(&mut self, signature: Vec) { - self.0.signature = signature; + pub fn claims_set(&self) -> Result> { + match self.inner.payload.as_ref() { + None => Ok(None), + Some(payload) => ClaimsSet::from_slice(payload).map_or_else( + |e| Err(Error::UnableToDeserializeIntoClaimsSet(e)), + |c| Ok(Some(c)), + ), + } + } + + pub fn tagged(&self) -> bool { + self.tagged + } + + pub fn set_tagged(&mut self) { + self.tagged = true; } } -impl Drop for CoseSign1 { - fn drop(&mut self) { - // Remove the signature payload from the global map - if let Some(id) = self.1.lock().unwrap().take() { - unsafe { - SIGNATURE_PAYLOAD.get_mut().unwrap().remove(&id); +impl ser::Serialize for CoseSign1 { + #[inline] + fn serialize(&self, serializer: S) -> Result { + let value = self + .inner + .clone() + .to_cbor_value() + .map_err(ser::Error::custom)?; // Convert the inner CoseSign1 object to a tagged CBOR vector + if self.tagged { + return Tagged::new(Some(iana::CborTag::CoseSign1 as u64), value).serialize(serializer); + } + match value { + Value::Bytes(x) => serializer.serialize_bytes(&x), + Value::Bool(x) => serializer.serialize_bool(x), + Value::Text(x) => serializer.serialize_str(x.as_str()), + Value::Null => serializer.serialize_unit(), + + Value::Tag(tag, ref v) => Tagged::new(Some(tag), v).serialize(serializer), + + Value::Float(x) => { + let y = x as f32; + if (y as f64).to_bits() == x.to_bits() { + serializer.serialize_f32(y) + } else { + serializer.serialize_f64(x) + } + } + + #[allow(clippy::unnecessary_fallible_conversions)] + Value::Integer(x) => { + if let Ok(x) = u8::try_from(x) { + serializer.serialize_u8(x) + } else if let Ok(x) = i8::try_from(x) { + serializer.serialize_i8(x) + } else if let Ok(x) = u16::try_from(x) { + serializer.serialize_u16(x) + } else if let Ok(x) = i16::try_from(x) { + serializer.serialize_i16(x) + } else if let Ok(x) = u32::try_from(x) { + serializer.serialize_u32(x) + } else if let Ok(x) = i32::try_from(x) { + serializer.serialize_i32(x) + } else if let Ok(x) = u64::try_from(x) { + serializer.serialize_u64(x) + } else if let Ok(x) = i64::try_from(x) { + serializer.serialize_i64(x) + } else if let Ok(x) = u128::try_from(x) { + serializer.serialize_u128(x) + } else if let Ok(x) = i128::try_from(x) { + serializer.serialize_i128(x) + } else { + unreachable!() + } + } + + Value::Array(x) => { + let mut map = serializer.serialize_seq(Some(x.len()))?; + + for v in x { + map.serialize_element(&v)?; + } + + map.end() + } + + Value::Map(x) => { + let mut map = serializer.serialize_map(Some(x.len()))?; + + for (k, v) in x { + map.serialize_entry(&k, &v)?; + } + + map.end() } + _ => unimplemented!(), } } } +impl<'de> Deserialize<'de> for CoseSign1 { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + // Deserialize the input to a CBOR Value + let value = Value::deserialize(deserializer)?; + // Convert the CBOR Value to CoseSign1 + Ok(CoseSign1 { + tagged: false, + inner: coset::CoseSign1::from_cbor_value(value).map_err(serde::de::Error::custom)?, + }) + } +} + mod p256 { - use crate::cose::SignatureAlgorithm; use coset::iana; use p256::ecdsa::{SigningKey, VerifyingKey}; + use crate::cose::SignatureAlgorithm; + impl SignatureAlgorithm for SigningKey { fn algorithm(&self) -> iana::Algorithm { iana::Algorithm::ES256 @@ -102,10 +322,11 @@ mod p256 { } mod p384 { - use crate::cose::SignatureAlgorithm; use coset::iana; use p384::ecdsa::{SigningKey, VerifyingKey}; + use crate::cose::SignatureAlgorithm; + impl SignatureAlgorithm for SigningKey { fn algorithm(&self) -> iana::Algorithm { iana::Algorithm::ES384 @@ -118,3 +339,185 @@ mod p384 { } } } + +#[cfg(test)] +mod tests { + use coset::cwt::{ClaimsSet, Timestamp}; + use coset::{iana, CborSerializable, Header}; + use hex::FromHex; + use p256::ecdsa::{Signature, SigningKey, VerifyingKey}; + use p256::SecretKey; + use signature::{SignatureEncoding, Signer}; + + use crate::cose::sign1::{CoseSign1, Error, PreparedCoseSign1}; + use crate::cose::SignatureAlgorithm; + + static COSE_SIGN1: &str = include_str!("../../test/definitions/cose/sign1/serialized.cbor"); + static COSE_KEY: &str = include_str!("../../test/definitions/cose/sign1/secret_key"); + + const RFC8392_KEY: &str = "6c1382765aec5358f117733d281c1c7bdc39884d04a45a1e6c67c858bc206c19"; + const RFC8392_COSE_SIGN1: &str = "d28443a10126a104524173796d6d657472696345434453413235365850a70175636f61703a2f2f61732e6578616d706c652e636f6d02656572696b77037818636f61703a2f2f6c696768742e6578616d706c652e636f6d041a5612aeb0051a5610d9f0061a5610d9f007420b7158405427c1ff28d23fbad1f29c4c7c6a555e601d6fa29f9179bc3d7438bacaca5acd08c8d4d4f96131680c429a01f85951ecee743a52b9b63632c57209120e1c9e30"; + + #[test] + fn roundtrip() { + let bytes = Vec::::from_hex(COSE_SIGN1).unwrap(); + let mut parsed: CoseSign1 = + serde_cbor::from_slice(&bytes).expect("failed to parse COSE_Sign1 from bytes"); + parsed.set_tagged(); + let roundtripped = + serde_cbor::to_vec(&parsed).expect("failed to serialize COSE_Sign1 to bytes"); + println!("bytes: {:?}", hex::encode(&bytes)); + println!("roundtripped: {:?}", hex::encode(&roundtripped)); + assert_eq!( + bytes, roundtripped, + "original bytes and roundtripped bytes do not match" + ); + } + + #[test] + fn signing() { + let key = Vec::::from_hex(COSE_KEY).unwrap(); + let signer: SigningKey = SecretKey::from_slice(&key).unwrap().into(); + let protected = coset::HeaderBuilder::new() + .algorithm(iana::Algorithm::ES256) + .build(); + let unprotected = coset::HeaderBuilder::new().key_id(b"11".to_vec()).build(); + let builder = coset::CoseSign1Builder::new() + .protected(protected) + .unprotected(unprotected) + .payload(b"This is the content.".to_vec()); + let prepared = PreparedCoseSign1::new(builder, None, None, true).unwrap(); + let signature_payload = prepared.signature_payload(); + let signature = sign::(signature_payload, &signer).unwrap(); + let cose_sign1 = prepared.finalize(signature); + let serialized = + serde_cbor::to_vec(&cose_sign1).expect("failed to serialize COSE_Sign1 to bytes"); + + let expected = Vec::::from_hex(COSE_SIGN1).unwrap(); + assert_eq!( + expected, serialized, + "expected COSE_Sign1 and signed data do not match" + ); + } + + fn sign(signature_payload: &[u8], s: &S) -> anyhow::Result> + where + S: Signer + SignatureAlgorithm, + Sig: SignatureEncoding, + { + Ok(s.try_sign(signature_payload) + .map_err(Error::Signing)? + .to_vec()) + } + + #[test] + fn verifying() { + let key = Vec::::from_hex(COSE_KEY).unwrap(); + let signer: SigningKey = SecretKey::from_slice(&key).unwrap().into(); + let verifier: VerifyingKey = (&signer).into(); + + let cose_sign1_bytes = Vec::::from_hex(COSE_SIGN1).unwrap(); + let cose_sign1: CoseSign1 = serde_cbor::from_slice(&cose_sign1_bytes) + .expect("failed to parse COSE_Sign1 from bytes"); + + cose_sign1 + .verify::(&verifier, None, None) + .to_result() + .expect("COSE_Sign1 could not be verified") + } + + #[test] + fn remote_signed() { + let key = Vec::::from_hex(COSE_KEY).unwrap(); + let signer: SigningKey = SecretKey::from_slice(&key).unwrap().into(); + let protected = coset::HeaderBuilder::new() + .algorithm(iana::Algorithm::ES256) + .build(); + let unprotected = coset::HeaderBuilder::new().key_id(b"11".to_vec()).build(); + let builder = coset::CoseSign1Builder::new() + .protected(protected) + .unprotected(unprotected) + .payload(b"This is the content.".to_vec()); + let prepared = PreparedCoseSign1::new(builder, None, None, true).unwrap(); + let signature_payload = prepared.signature_payload(); + let signature = sign::(signature_payload, &signer).unwrap(); + let cose_sign1 = prepared.finalize(signature); + + let serialized = + serde_cbor::to_vec(&cose_sign1).expect("failed to serialize COSE_Sign1 to bytes"); + + let expected = Vec::::from_hex(COSE_SIGN1).unwrap(); + assert_eq!( + expected, serialized, + "expected COSE_Sign1 and signed data do not match" + ); + + let verifier: VerifyingKey = (&signer).into(); + cose_sign1 + .verify::(&verifier, None, None) + .to_result() + .expect("COSE_Sign1 could not be verified") + } + + fn rfc8392_example_inputs() -> (Header, Header, ClaimsSet) { + let protected = coset::HeaderBuilder::new() + .algorithm(iana::Algorithm::ES256) + .build(); + + let unprotected = coset::HeaderBuilder::new() + .key_id( + hex::decode("4173796d6d65747269634543445341323536").expect("error decoding key id"), + ) + .build(); + + let claims_set = ClaimsSet { + issuer: Some("coap://as.example.com".to_string()), + subject: Some("erikw".to_string()), + audience: Some("coap://light.example.com".to_string()), + expiration_time: Some(Timestamp::WholeSeconds(1444064944)), + not_before: Some(Timestamp::WholeSeconds(1443944944)), + issued_at: Some(Timestamp::WholeSeconds(1443944944)), + cwt_id: Some(hex::decode("0b71").unwrap()), + rest: vec![], + }; + + (protected, unprotected, claims_set) + } + + #[test] + fn signing_cwt() { + // Using key from RFC8392 example + let key = hex::decode(RFC8392_KEY).unwrap(); + let signer: SigningKey = SecretKey::from_slice(&key).unwrap().into(); + let (protected, unprotected, claims_set) = rfc8392_example_inputs(); + let builder = coset::CoseSign1Builder::new() + .protected(protected) + .unprotected(unprotected) + .payload(claims_set.to_vec().expect("failed to set claims set")); + let prepared = PreparedCoseSign1::new(builder, None, None, true).unwrap(); + let signature_payload = prepared.signature_payload(); + let signature = + sign::(signature_payload, &signer).expect("failed to sign CWT"); + let cose_sign1 = prepared.finalize(signature); + let serialized = + serde_cbor::to_vec(&cose_sign1).expect("failed to serialize COSE_Sign1 to bytes"); + let expected = hex::decode(RFC8392_COSE_SIGN1).unwrap(); + assert_eq!( + expected, serialized, + "expected COSE_Sign1 and signed CWT do not match" + ); + } + + #[test] + fn deserializing_signed_cwt() { + let cose_sign1_bytes = hex::decode(RFC8392_COSE_SIGN1).unwrap(); + let cose_sign1: CoseSign1 = serde_cbor::from_slice(&cose_sign1_bytes) + .expect("failed to parse COSE_Sign1 from bytes"); + let parsed_claims_set = cose_sign1 + .claims_set() + .expect("failed to parse claims set from payload") + .expect("retrieved empty claims set"); + let (_, _, expected_claims_set) = rfc8392_example_inputs(); + assert_eq!(parsed_claims_set, expected_claims_set); + } +} diff --git a/src/definitions/mso.rs b/src/definitions/mso.rs index 311526ab..40768c64 100644 --- a/src/definitions/mso.rs +++ b/src/definitions/mso.rs @@ -51,7 +51,7 @@ mod test { serde_cbor::from_slice(&cbor_bytes).expect("unable to decode cbor as an IssuerSigned"); let mso_bytes = signed .issuer_auth - .0 + .inner .payload .as_ref() .expect("expected a COSE_Sign1 with attached payload, found detached payload"); diff --git a/src/definitions/traits/to_cbor.rs b/src/definitions/traits/to_cbor.rs index 22e690d5..3a88c469 100644 --- a/src/definitions/traits/to_cbor.rs +++ b/src/definitions/traits/to_cbor.rs @@ -1,5 +1,5 @@ -//! ToCbor is specifically NOT implemented for Vec where T: ToCbor, as Vec likely should be -//! represented as a bytestr instead of an array in cbor. +//! ToCbor is specifically NOT implemented for `Vec` where `T: ToCbor`, as `Vec` likely should be +//! represented as a `bytestr` instead of an array in cbor. use serde_cbor::Value; use std::collections::BTreeMap; diff --git a/src/issuance/mdoc.rs b/src/issuance/mdoc.rs index 1924655f..c0b6e9b4 100644 --- a/src/issuance/mdoc.rs +++ b/src/issuance/mdoc.rs @@ -2,15 +2,15 @@ use std::collections::{BTreeMap, HashSet}; use anyhow::{anyhow, Result}; use async_signature::AsyncSigner; -use coset::{iana, Header, Label, RegisteredLabelWithPrivate}; +use coset::{iana, Label}; use rand::Rng; use serde::{Deserialize, Serialize}; use serde_cbor::Value as CborValue; use sha2::{Digest, Sha256, Sha384, Sha512}; use signature::{SignatureEncoding, Signer}; -use crate::cose::sign1::CoseSign1; -use crate::cose::{Cose, SignatureAlgorithm}; +use crate::cose::sign1::{CoseSign1, PreparedCoseSign1}; +use crate::cose::SignatureAlgorithm; use crate::{ definitions::{ helpers::{NonEmptyMap, NonEmptyVec, Tag24}, @@ -38,7 +38,7 @@ pub struct PreparedMdoc { doc_type: String, mso: Mso, namespaces: IssuerNamespaces, - prepared_sig: CoseSign1, + prepared_sig: PreparedCoseSign1, } #[derive(Debug, Clone, Default)] @@ -85,14 +85,13 @@ impl Mdoc { let mso_bytes = serde_cbor::to_vec(&Tag24::new(&mso)?)?; - let protected = Header { - alg: Some(RegisteredLabelWithPrivate::Assigned(signature_algorithm)), - ..Header::default() - }; + let protected = coset::HeaderBuilder::new() + .algorithm(signature_algorithm) + .build(); let builder = coset::CoseSign1Builder::new() .protected(protected) .payload(mso_bytes); - let prepared_sig = CoseSign1::new(builder.build()); + let prepared_sig = PreparedCoseSign1::new(builder, None, None, true)?; let preparation_mdoc = PreparedMdoc { doc_type, @@ -189,13 +188,12 @@ impl PreparedMdoc { doc_type, namespaces, mso, - mut prepared_sig, + prepared_sig, } = self; - prepared_sig.set_signature(signature); - let mut issuer_auth = prepared_sig; + let mut issuer_auth = prepared_sig.finalize(signature); issuer_auth - .0 + .inner .unprotected .rest .push((Label::Int(X5CHAIN_HEADER_LABEL as i64), x5chain.into_cbor())); diff --git a/src/presentation/device.rs b/src/presentation/device.rs index 75810ccb..d5d851a6 100644 --- a/src/presentation/device.rs +++ b/src/presentation/device.rs @@ -1,7 +1,7 @@ use std::collections::BTreeMap; use std::num::ParseIntError; -use coset::{CoseSign1Builder, Header, RegisteredLabelWithPrivate}; +use coset::CoseSign1Builder; use p256::FieldBytes; use serde::{Deserialize, Serialize}; use serde_cbor::Value as CborValue; @@ -9,8 +9,7 @@ use uuid::Uuid; use session::SessionTranscript180135; -use crate::cose::sign1::CoseSign1; -use crate::cose::Cose; +use crate::cose::sign1::{CoseSign1, PreparedCoseSign1}; use crate::definitions::IssuerSignedItem; use crate::{ definitions::{ @@ -109,7 +108,7 @@ struct PreparedDocument { doc_type: String, issuer_signed: IssuerSigned, device_namespaces: DeviceNamespacesBytes, - prepared_cose_sign1: CoseSign1, + prepared_cose_sign1: PreparedCoseSign1, errors: Option, } @@ -291,6 +290,7 @@ impl SessionManager { if matches!(self.state, State::Signing(_)) { match std::mem::take(&mut self.state) { State::Signing(mut p) => { + // todo: support for CoseMac0 p.submit_next_signature(signature); if p.is_complete() { let response = p.finalize_response(); @@ -370,6 +370,7 @@ impl PreparedDeviceResponse { pub fn submit_next_signature(&mut self, signature: Vec) { let signed_doc = match self.prepared_documents.pop() { + // todo: support for CoseMac0 Some(doc) => doc.finalize(signature), None => { //tracing::error!( @@ -401,17 +402,18 @@ impl PreparedDocument { let Self { issuer_signed, device_namespaces, - mut prepared_cose_sign1, + prepared_cose_sign1, errors, doc_type, .. } = self; - prepared_cose_sign1.set_signature(signature); + // todo: support for CoseMac0 + let cose_sign1 = prepared_cose_sign1.finalize(signature); let device_signed = DeviceSigned { namespaces: device_namespaces, // todo: support for CoseMac0 device_auth: DeviceAuth::Signature { - device_signature: prepared_cose_sign1, + device_signature: cose_sign1, }, }; DeviceResponseDoc { @@ -548,15 +550,23 @@ pub trait DeviceSession { continue; } }; - let protected = Header { - alg: Some(RegisteredLabelWithPrivate::Assigned(signature_algorithm)), - ..Header::default() - }; + let header = coset::HeaderBuilder::new() + .algorithm(signature_algorithm) + .build(); // todo: support for CoseMac0 - let builder = CoseSign1Builder::new() - .protected(protected) - .payload(device_auth_bytes); - let prepared_cose_sign1 = CoseSign1::new(builder.build()); + let cose1_builder = CoseSign1Builder::new().protected(header); + let prepared_cose_sign1 = + match PreparedCoseSign1::new(cose1_builder, Some(device_auth_bytes), None, true) { + Ok(prepared) => prepared, + Err(_e) => { + let error: DocumentError = [(doc_type, DocumentErrorCode::DataNotReturned)] + .into_iter() + .collect(); + document_errors.push(error); + continue; + } + }; + let prepared_document = PreparedDocument { id: document.id, doc_type, @@ -565,7 +575,6 @@ pub trait DeviceSession { issuer_auth: document.issuer_auth.clone(), }, device_namespaces, - // todo: support for CoseMac0 prepared_cose_sign1, errors: errors.try_into().ok(), }; diff --git a/test/definitions/cose/mac0/secret_key_hmac b/test/definitions/cose/mac0/secret_key similarity index 100% rename from test/definitions/cose/mac0/secret_key_hmac rename to test/definitions/cose/mac0/secret_key diff --git a/test/definitions/cose/mac0/secret_key_aes_cbc_mac b/test/definitions/cose/mac0/secret_key_aes_cbc_mac deleted file mode 100644 index ba0d038a..00000000 --- a/test/definitions/cose/mac0/secret_key_aes_cbc_mac +++ /dev/null @@ -1 +0,0 @@ -a361316953796d6d65747269636133181a622d31982018a4183318f218df184a1888184218fe183c1887183a17189a18311821186f18ab18d60718eb188e183016181f18ee0b18a318310e183c186718d8 \ No newline at end of file diff --git a/test/definitions/cose/mac0/serialized_hmac.cbor b/test/definitions/cose/mac0/serialized.cbor similarity index 100% rename from test/definitions/cose/mac0/serialized_hmac.cbor rename to test/definitions/cose/mac0/serialized.cbor diff --git a/test/definitions/cose/mac0/serialized_aes_cbc_mac.cbor b/test/definitions/cose/mac0/serialized_aes_cbc_mac.cbor deleted file mode 100644 index d2b38a57..00000000 --- a/test/definitions/cose/mac0/serialized_aes_cbc_mac.cbor +++ /dev/null @@ -1 +0,0 @@ -d28444a101181aa10442313154546869732069732074686520636f6e74656e742e5063aa5b6c455a9b305621a5f51ca5e245 \ No newline at end of file From abaf78a6241cd672b9654e8227c859ea0230b2b6 Mon Sep 17 00:00:00 2001 From: Radu Marias Date: Mon, 5 Aug 2024 01:04:40 +0300 Subject: [PATCH 05/24] Change serialized value for tests of CoseMac0 --- test/definitions/cose/mac0/serialized.cbor | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/definitions/cose/mac0/serialized.cbor b/test/definitions/cose/mac0/serialized.cbor index e82c7f9d..0efde9a1 100644 --- a/test/definitions/cose/mac0/serialized.cbor +++ b/test/definitions/cose/mac0/serialized.cbor @@ -1 +1 @@ -d28443a10105a10442313154546869732069732074686520636f6e74656e742e582009de60921747849a72f4e57df91ffeb4ce51b27636eddc610d3fc0ae848daeb7 \ No newline at end of file +d18443a10105a10442313154546869732069732074686520636f6e74656e742e58203f30c4a2c740c3a0d90310b48cd282bcdb29ab8073a32e287fa07e188d317e8a \ No newline at end of file From 0943283e00cf46ae872748dfd4add5c9047136a2 Mon Sep 17 00:00:00 2001 From: Radu Marias Date: Mon, 5 Aug 2024 02:12:58 +0300 Subject: [PATCH 06/24] Add `DeviceAuthType` to specify what kind of signature to use for device responses. --- src/definitions/device_signed.rs | 9 +++ src/presentation/device.rs | 129 +++++++++++++++++++++++-------- 2 files changed, 105 insertions(+), 33 deletions(-) diff --git a/src/definitions/device_signed.rs b/src/definitions/device_signed.rs index e1831409..d809a88b 100644 --- a/src/definitions/device_signed.rs +++ b/src/definitions/device_signed.rs @@ -29,6 +29,15 @@ pub enum DeviceAuth { Mac { device_mac: CoseMac0 }, } +#[derive(Debug, Clone, Copy, Serialize, Deserialize)] +#[serde(untagged)] +pub enum DeviceAuthType { + #[serde(rename_all = "camelCase")] + Sign1, + #[serde(rename_all = "camelCase")] + Mac0, +} + pub type DeviceAuthenticationBytes = Tag24>; #[derive(Clone, Debug, Deserialize, Serialize)] diff --git a/src/presentation/device.rs b/src/presentation/device.rs index d5d851a6..0eb811e9 100644 --- a/src/presentation/device.rs +++ b/src/presentation/device.rs @@ -1,7 +1,7 @@ use std::collections::BTreeMap; use std::num::ParseIntError; -use coset::CoseSign1Builder; +use coset::{CoseMac0Builder, CoseSign1Builder}; use p256::FieldBytes; use serde::{Deserialize, Serialize}; use serde_cbor::Value as CborValue; @@ -9,7 +9,9 @@ use uuid::Uuid; use session::SessionTranscript180135; +use crate::cose::mac0::PreparedCoseMac0; use crate::cose::sign1::{CoseSign1, PreparedCoseSign1}; +use crate::definitions::device_signed::DeviceAuthType; use crate::definitions::IssuerSignedItem; use crate::{ definitions::{ @@ -54,6 +56,7 @@ pub struct SessionManager { sk_reader: [u8; 32], reader_message_counter: u32, state: State, + device_auth_type: DeviceAuthType, } #[derive(Clone, Debug, Default, Serialize, Deserialize)] @@ -100,6 +103,22 @@ pub struct PreparedDeviceResponse { signed_documents: Vec, document_errors: Option, status: Status, + device_auth_type: DeviceAuthType, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +enum PreparedCose { + Sign1(PreparedCoseSign1), + Mac0(PreparedCoseMac0), +} + +impl PreparedCose { + fn signature_payload(&self) -> &[u8] { + match self { + PreparedCose::Sign1(inner) => inner.signature_payload(), + PreparedCose::Mac0(inner) => inner.signature_payload(), + } + } } #[derive(Debug, Clone, Serialize, Deserialize)] @@ -108,7 +127,7 @@ struct PreparedDocument { doc_type: String, issuer_signed: IssuerSigned, device_namespaces: DeviceNamespacesBytes, - prepared_cose_sign1: PreparedCoseSign1, + prepared_cose: PreparedCose, errors: Option, } @@ -195,6 +214,7 @@ impl SessionManagerEngaged { sk_reader, reader_message_counter: 0, state: State::AwaitingRequest, + device_auth_type: DeviceAuthType::Sign1, }; let requested_data = sm.handle_decoded_request(SessionData { @@ -210,12 +230,12 @@ impl SessionManager { fn parse_request(&self, request: &[u8]) -> Result { let request: CborValue = serde_cbor::from_slice(request).map_err(|_| { // tracing::error!("unable to decode DeviceRequest bytes as cbor: {}", error); - PreparedDeviceResponse::empty(Status::CborDecodingError) + PreparedDeviceResponse::empty(Status::CborDecodingError, self.device_auth_type) })?; serde_cbor::value::from_value(request).map_err(|_| { // tracing::error!("unable to validate DeviceRequest cbor: {}", error); - PreparedDeviceResponse::empty(Status::CborValidationError) + PreparedDeviceResponse::empty(Status::CborValidationError, self.device_auth_type) }) } @@ -229,7 +249,10 @@ impl SessionManager { // request.version, // DeviceRequest::VERSION // ); - return Err(PreparedDeviceResponse::empty(Status::GeneralError)); + return Err(PreparedDeviceResponse::empty( + Status::GeneralError, + self.device_auth_type, + )); } Ok(request .doc_requests @@ -290,7 +313,6 @@ impl SessionManager { if matches!(self.state, State::Signing(_)) { match std::mem::take(&mut self.state) { State::Signing(mut p) => { - // todo: support for CoseMac0 p.submit_next_signature(signature); if p.is_complete() { let response = p.finalize_response(); @@ -332,7 +354,7 @@ impl SessionManager { /// Retrieve the completed response. pub fn retrieve_response(&mut self) -> Option> { if self.response_ready() { - // Replace state with AwaitingRequest. + // Replace the state with AwaitingRequest. let state = std::mem::take(&mut self.state); match state { State::ReadyToRespond(r) => Some(r), @@ -346,12 +368,13 @@ impl SessionManager { } impl PreparedDeviceResponse { - fn empty(status: Status) -> Self { + fn empty(status: Status, device_auth_type: DeviceAuthType) -> Self { PreparedDeviceResponse { status, prepared_documents: Vec::new(), document_errors: None, signed_documents: Vec::new(), + device_auth_type, } } @@ -365,16 +388,15 @@ impl PreparedDeviceResponse { pub fn get_next_signature_payload(&self) -> Option<(Uuid, &[u8])> { self.prepared_documents .last() - .map(|doc| (doc.id, doc.prepared_cose_sign1.signature_payload())) + .map(|doc| (doc.id, doc.prepared_cose.signature_payload())) } pub fn submit_next_signature(&mut self, signature: Vec) { let signed_doc = match self.prepared_documents.pop() { - // todo: support for CoseMac0 Some(doc) => doc.finalize(signature), None => { //tracing::error!( - // "received a signature for finalising when there are no more prepared docs" + // "received a signature for finalizing when there are no more prepared docs" //); return; } @@ -385,7 +407,8 @@ impl PreparedDeviceResponse { pub fn finalize_response(self) -> DeviceResponse { if !self.is_complete() { //tracing::warn!("attempt to finalize PreparedDeviceResponse before all prepared documents had been authorized"); - return PreparedDeviceResponse::empty(Status::GeneralError).finalize_response(); + return PreparedDeviceResponse::empty(Status::GeneralError, self.device_auth_type) + .finalize_response(); } DeviceResponse { @@ -402,19 +425,23 @@ impl PreparedDocument { let Self { issuer_signed, device_namespaces, - prepared_cose_sign1, + prepared_cose, errors, doc_type, .. } = self; - // todo: support for CoseMac0 - let cose_sign1 = prepared_cose_sign1.finalize(signature); + let device_auth = match prepared_cose { + PreparedCose::Sign1(inner) => DeviceAuth::Signature { + device_signature: inner.finalize(signature), + }, + PreparedCose::Mac0(inner) => DeviceAuth::Mac { + device_mac: inner.finalize(signature), + }, + }; + let device_signed = DeviceSigned { namespaces: device_namespaces, - // todo: support for CoseMac0 - device_auth: DeviceAuth::Signature { - device_signature: cose_sign1, - }, + device_auth, }; DeviceResponseDoc { doc_type, @@ -430,6 +457,7 @@ pub trait DeviceSession { fn documents(&self) -> &Documents; fn session_transcript(&self) -> Self::ST; + fn device_auth_type(&self) -> DeviceAuthType; fn prepare_response( &self, requests: &RequestedItems, @@ -553,19 +581,49 @@ pub trait DeviceSession { let header = coset::HeaderBuilder::new() .algorithm(signature_algorithm) .build(); - // todo: support for CoseMac0 - let cose1_builder = CoseSign1Builder::new().protected(header); - let prepared_cose_sign1 = - match PreparedCoseSign1::new(cose1_builder, Some(device_auth_bytes), None, true) { - Ok(prepared) => prepared, - Err(_e) => { - let error: DocumentError = [(doc_type, DocumentErrorCode::DataNotReturned)] - .into_iter() - .collect(); - document_errors.push(error); - continue; - } - }; + + let prepared_cose = match self.device_auth_type() { + DeviceAuthType::Sign1 => { + let cose_sign1_builder = CoseSign1Builder::new().protected(header); + let prepared_cose_sign1 = match PreparedCoseSign1::new( + cose_sign1_builder, + Some(device_auth_bytes), + None, + true, + ) { + Ok(prepared) => prepared, + Err(_e) => { + let error: DocumentError = + [(doc_type, DocumentErrorCode::DataNotReturned)] + .into_iter() + .collect(); + document_errors.push(error); + continue; + } + }; + PreparedCose::Sign1(prepared_cose_sign1) + } + DeviceAuthType::Mac0 => { + let cose_mac0_builder = CoseMac0Builder::new().protected(header); + let prepared_cose_mac0 = match PreparedCoseMac0::new( + cose_mac0_builder, + Some(device_auth_bytes), + None, + true, + ) { + Ok(prepared) => prepared, + Err(_e) => { + let error: DocumentError = + [(doc_type, DocumentErrorCode::DataNotReturned)] + .into_iter() + .collect(); + document_errors.push(error); + continue; + } + }; + PreparedCose::Mac0(prepared_cose_mac0) + } + }; let prepared_document = PreparedDocument { id: document.id, @@ -575,7 +633,7 @@ pub trait DeviceSession { issuer_auth: document.issuer_auth.clone(), }, device_namespaces, - prepared_cose_sign1, + prepared_cose, errors: errors.try_into().ok(), }; prepared_documents.push(prepared_document); @@ -585,6 +643,7 @@ pub trait DeviceSession { document_errors: document_errors.try_into().ok(), status: Status::OK, signed_documents: Vec::new(), + device_auth_type: self.device_auth_type(), } } } @@ -599,6 +658,10 @@ impl DeviceSession for SessionManager { fn session_transcript(&self) -> SessionTranscript180135 { self.session_transcript.clone() } + + fn device_auth_type(&self) -> DeviceAuthType { + self.device_auth_type + } } impl From for Document { From 10b96da85314e540f638c1bdabf29506ad85521d Mon Sep 17 00:00:00 2001 From: Radu Marias Date: Mon, 5 Aug 2024 02:40:40 +0300 Subject: [PATCH 07/24] Document `sign1` and `mac0` modules. --- src/cose/mac0.rs | 65 +++++++++++++++++++++++++++++++++++++++-------- src/cose/sign1.rs | 57 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 112 insertions(+), 10 deletions(-) diff --git a/src/cose/mac0.rs b/src/cose/mac0.rs index c39af098..95424767 100644 --- a/src/cose/mac0.rs +++ b/src/cose/mac0.rs @@ -1,3 +1,4 @@ +use crate::cose::SignatureAlgorithm; use ::hmac::Hmac; use coset::cbor::Value; use coset::cwt::ClaimsSet; @@ -11,8 +12,44 @@ use serde::{ser, Deserialize, Deserializer, Serialize}; use serde_cbor::tags::Tagged; use sha2::Sha256; -use crate::cose::SignatureAlgorithm; - +/// Prepared `COSE_Mac0` for remote signing. +/// +/// To produce a `COSE_Mac0` do the following: +/// +/// 1. Set the signature algorithm with [coset::HeaderBuilder::algorithm]. +/// 2. Produce a signature remotely, according to the chosen signature algorithm, +/// using the [PreparedCoseMac0::signature_payload] as the payload. +/// 3. Generate the `COSE_Mac0` by passing the produced signature into +/// [PreparedCoseMac0::finalize]. +/// +/// Example: +/// ``` +/// use coset::iana; +/// use digest::Mac; +/// use hex::FromHex;use hmac::Hmac; +/// use sha2::Sha256; +/// use isomdl::cose::mac0::PreparedCoseMac0; +/// +/// let key = Vec::::from_hex("a361316953796d6d6574726963613305622d318f187418681869187318201869187318201874186818651820186b18651879").unwrap(); +/// let signer = Hmac::::new_from_slice(&key).expect("failed to create HMAC signer"); +/// let protected = coset::HeaderBuilder::new() +/// .algorithm(iana::Algorithm::HMAC_256_256) +/// .build(); +/// let unprotected = coset::HeaderBuilder::new().key_id(b"11".to_vec()).build(); +/// let builder = coset::CoseMac0Builder::new() +/// .protected(protected) +/// .unprotected(unprotected) +/// .payload(b"This is the content.".to_vec()); +/// let prepared = PreparedCoseMac0::new(builder, None, None, true).unwrap(); +/// let signature_payload = prepared.signature_payload(); +/// let signature = tag(signature_payload, &signer).unwrap(); +/// let cose_mac0 = prepared.finalize(signature); +/// fn tag(signature_payload: &[u8], s: &Hmac) -> anyhow::Result> { +/// let mut mac = s.clone(); +/// mac.reset(); +/// mac.update(signature_payload); +/// Ok(mac.finalize().into_bytes().to_vec()) +/// } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct PreparedCoseMac0 { tagged: bool, @@ -128,10 +165,12 @@ impl PreparedCoseMac0 { }) } + /// Returns the signature payload that needs to be used to tag it. pub fn signature_payload(&self) -> &[u8] { &self.tag_payload } + /// Finalize the COSE_Mac0 by adding the tag. pub fn finalize(self, tag: Vec) -> CoseMac0 { let mut cose_mac0 = self.cose_mac0; cose_mac0.inner.tag = tag; @@ -183,6 +222,7 @@ impl CoseMac0 { } } + /// Retrieve the CWT claims set. pub fn claims_set(&self) -> Result> { match self.inner.payload.as_ref() { None => Ok(None), @@ -193,15 +233,19 @@ impl CoseMac0 { } } + /// If we are serialized as tagged. pub fn tagged(&self) -> bool { self.tagged } + /// Set serialization to tagged. pub fn set_tagged(&mut self) { self.tagged = true; } } +/// Serialize [CoseMac0] by serializing the [Value]. +/// If marked as tagged, then serialize as [Value::Tag] impl ser::Serialize for CoseMac0 { #[inline] fn serialize(&self, serializer: S) -> Result { @@ -281,6 +325,7 @@ impl ser::Serialize for CoseMac0 { } } +/// Deserialize [CoseMac0] by first deserializing the [Value] and then using [coset::CoseMac0::from_cbor_value]. impl<'de> Deserialize<'de> for CoseMac0 { fn deserialize(deserializer: D) -> Result where @@ -343,7 +388,7 @@ mod tests { } #[test] - fn signing() { + fn tagging() { let key = Vec::::from_hex(COSE_KEY).unwrap(); let signer = Hmac::::new_from_slice(&key).expect("failed to create HMAC signer"); let protected = coset::HeaderBuilder::new() @@ -356,7 +401,7 @@ mod tests { .payload(b"This is the content.".to_vec()); let prepared = PreparedCoseMac0::new(builder, None, None, true).unwrap(); let signature_payload = prepared.signature_payload(); - let signature = sign(signature_payload, &signer).unwrap(); + let signature = tag(signature_payload, &signer).unwrap(); let cose_mac0 = prepared.finalize(signature); let serialized = serde_cbor::to_vec(&cose_mac0).expect("failed to serialize COSE_MAC0 to bytes"); @@ -368,7 +413,7 @@ mod tests { ); } - fn sign(signature_payload: &[u8], s: &Hmac) -> anyhow::Result> { + fn tag(signature_payload: &[u8], s: &Hmac) -> anyhow::Result> { let mut mac = s.clone(); mac.reset(); mac.update(signature_payload); @@ -392,7 +437,7 @@ mod tests { } #[test] - fn remote_signed() { + fn remote_tagging() { let key = Vec::::from_hex(COSE_KEY).unwrap(); let signer = Hmac::::new_from_slice(&key).expect("failed to create HMAC signer"); let protected = coset::HeaderBuilder::new() @@ -405,7 +450,7 @@ mod tests { .payload(b"This is the content.".to_vec()); let prepared = PreparedCoseMac0::new(builder, None, None, true).unwrap(); let signature_payload = prepared.signature_payload(); - let signature = sign(signature_payload, &signer).unwrap(); + let signature = tag(signature_payload, &signer).unwrap(); let cose_mac0 = prepared.finalize(signature); let serialized = @@ -451,7 +496,7 @@ mod tests { } #[test] - fn signing_cwt() { + fn tagging_cwt() { // Using key from RFC8392 example let key = hex::decode(RFC8392_KEY).unwrap(); let signer = Hmac::::new_from_slice(&key).expect("failed to create HMAC signer"); @@ -462,7 +507,7 @@ mod tests { .payload(claims_set.to_vec().expect("failed to set claims set")); let prepared = PreparedCoseMac0::new(builder, None, None, true).unwrap(); let signature_payload = prepared.signature_payload(); - let signature = sign(signature_payload, &signer).expect("failed to sign CWT"); + let signature = tag(signature_payload, &signer).expect("failed to sign CWT"); let cose_mac0 = prepared.finalize(signature); let serialized = serde_cbor::to_vec(&cose_mac0).expect("failed to serialize COSE_MAC0 to bytes"); @@ -475,7 +520,7 @@ mod tests { } #[test] - fn deserializing_signed_cwt() { + fn deserializing_tdeserializing_signed_cwtagged_cwt() { let cose_mac0_bytes = hex::decode(RFC8392_COSE_MAC0).unwrap(); let cose_mac0: CoseMac0 = serde_cbor::from_slice(&cose_mac0_bytes).expect("failed to parse COSE_MAC0 from bytes"); diff --git a/src/cose/sign1.rs b/src/cose/sign1.rs index aa3e60d4..fd0086bb 100644 --- a/src/cose/sign1.rs +++ b/src/cose/sign1.rs @@ -11,6 +11,50 @@ use signature::Verifier; use crate::cose::SignatureAlgorithm; +/// Prepared `COSE_Sign1` for remote signing. +/// +/// To produce a `COSE_Sign1` do the following: +/// +/// 1. Set the signature algorithm with [coset::HeaderBuilder::algorithm]. +/// 2. Produce a signature remotely, according to the chosen signature algorithm, +/// using the [PreparedCoseSign1::signature_payload] as the payload. +/// 3. Generate the `COSE_Sign1` by passing the produced signature into +/// [PreparedCoseSign1::finalize]. +/// +/// Example: +/// ``` +/// use coset::iana; +/// use hex::FromHex; +/// use p256::ecdsa::{Signature, SigningKey}; +/// use p256::SecretKey;/// +/// use signature::{SignatureEncoding, Signer, SignerMut}; +/// use isomdl::cose::sign1::{Error, PreparedCoseSign1}; +/// use isomdl::cose::SignatureAlgorithm; +/// +/// let key = Vec::::from_hex("57c92077664146e876760c9520d054aa93c3afb04e306705db6090308507b4d3").unwrap(); +/// let signer: SigningKey = SecretKey::from_slice(&key).unwrap().into(); +/// let protected = coset::HeaderBuilder::new() +/// .algorithm(iana::Algorithm::ES256) +/// .build(); +/// let unprotected = coset::HeaderBuilder::new().key_id(b"11".to_vec()).build(); +/// let builder = coset::CoseSign1Builder::new() +/// .protected(protected) +/// .unprotected(unprotected) +/// .payload(b"This is the content.".to_vec()); +/// let prepared = PreparedCoseSign1::new(builder, None, None, true).unwrap(); +/// let signature_payload = prepared.signature_payload(); +/// let signature = sign::(signature_payload, &signer).unwrap(); +/// let cose_sign1 = prepared.finalize(signature); +/// +/// fn sign(signature_payload: &[u8], s: &S) -> anyhow::Result> +/// where +/// S: Signer + SignatureAlgorithm, +/// Sig: SignatureEncoding, +/// { +/// Ok(s.try_sign(signature_payload) +/// .map_err(Error::Signing)? +/// .to_vec()) +/// } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct PreparedCoseSign1 { tagged: bool, @@ -18,6 +62,7 @@ pub struct PreparedCoseSign1 { signature_payload: Vec, } +/// COSE_Sign1 implementation. #[derive(Clone, Debug)] pub struct CoseSign1 { tagged: bool, @@ -126,10 +171,12 @@ impl PreparedCoseSign1 { }) } + /// Returns the signature payload that needs to be used to sign. pub fn signature_payload(&self) -> &[u8] { &self.signature_payload } + /// Finalize the COSE_Sign1 by adding the signature. pub fn finalize(self, signature: Vec) -> CoseSign1 { let mut cose_sign1 = self.cose_sign1; cose_sign1.inner.signature = signature; @@ -189,6 +236,7 @@ impl CoseSign1 { } } + /// Retrieve the CWT claims set. pub fn claims_set(&self) -> Result> { match self.inner.payload.as_ref() { None => Ok(None), @@ -199,15 +247,19 @@ impl CoseSign1 { } } + /// If we are serialized as tagged. pub fn tagged(&self) -> bool { self.tagged } + /// Set serialization to tagged. pub fn set_tagged(&mut self) { self.tagged = true; } } +/// Serialize [CoseSign1] by serializing the [Value]. +/// If marked as tagged, then serialize as [Value::Tag] impl ser::Serialize for CoseSign1 { #[inline] fn serialize(&self, serializer: S) -> Result { @@ -287,6 +339,7 @@ impl ser::Serialize for CoseSign1 { } } +/// Deserialize [CoseSign1] by first deserializing the [Value] and then using [coset::CoseSign1::from_cbor_value]. impl<'de> Deserialize<'de> for CoseSign1 { fn deserialize(deserializer: D) -> Result where @@ -308,6 +361,8 @@ mod p256 { use crate::cose::SignatureAlgorithm; + /// Implement [`SignatureAlgorithm`]. + impl SignatureAlgorithm for SigningKey { fn algorithm(&self) -> iana::Algorithm { iana::Algorithm::ES256 @@ -327,6 +382,8 @@ mod p384 { use crate::cose::SignatureAlgorithm; + /// Implement [`SignatureAlgorithm`]. + impl SignatureAlgorithm for SigningKey { fn algorithm(&self) -> iana::Algorithm { iana::Algorithm::ES384 From f3981aeca115782d12c051f900f7388d439cec64 Mon Sep 17 00:00:00 2001 From: Radu Marias Date: Mon, 5 Aug 2024 05:20:10 +0300 Subject: [PATCH 08/24] Use CoseKey from coset --- src/cose/mac0.rs | 2 +- src/cose/sign1.rs | 2 +- src/definitions/device_engagement/error.rs | 7 - src/definitions/device_key/cose_key.rs | 504 ++++++--------------- src/definitions/device_key/mod.rs | 1 - src/definitions/mod.rs | 2 +- src/definitions/session.rs | 16 +- src/issuance/mdoc.rs | 26 +- 8 files changed, 165 insertions(+), 395 deletions(-) diff --git a/src/cose/mac0.rs b/src/cose/mac0.rs index 95424767..b865a0c3 100644 --- a/src/cose/mac0.rs +++ b/src/cose/mac0.rs @@ -253,7 +253,7 @@ impl ser::Serialize for CoseMac0 { .inner .clone() .to_cbor_value() - .map_err(ser::Error::custom)?; // Convert the inner CoseMac0 object to a tagged CBOR vector + .map_err(ser::Error::custom)?; if self.tagged { return Tagged::new(Some(iana::CborTag::CoseMac0 as u64), value).serialize(serializer); } diff --git a/src/cose/sign1.rs b/src/cose/sign1.rs index fd0086bb..3490b0bc 100644 --- a/src/cose/sign1.rs +++ b/src/cose/sign1.rs @@ -267,7 +267,7 @@ impl ser::Serialize for CoseSign1 { .inner .clone() .to_cbor_value() - .map_err(ser::Error::custom)?; // Convert the inner CoseSign1 object to a tagged CBOR vector + .map_err(ser::Error::custom)?; if self.tagged { return Tagged::new(Some(iana::CborTag::CoseSign1 as u64), value).serialize(serializer); } diff --git a/src/definitions/device_engagement/error.rs b/src/definitions/device_engagement/error.rs index 593cb6dc..eda5eaec 100644 --- a/src/definitions/device_engagement/error.rs +++ b/src/definitions/device_engagement/error.rs @@ -1,4 +1,3 @@ -use crate::definitions::device_key::cose_key::Error as CoseKeyError; use crate::definitions::helpers::tag24::Error as Tag24Error; use serde_cbor::Error as SerdeCborError; @@ -31,12 +30,6 @@ pub enum Error { InvalidNfcResponseDataLengthError, } -impl From for Error { - fn from(_: CoseKeyError) -> Self { - Error::CoseKeyError - } -} - impl From for Error { fn from(_: Tag24Error) -> Self { Error::Tag24Error diff --git a/src/definitions/device_key/cose_key.rs b/src/definitions/device_key/cose_key.rs index d2cf0576..0df3e36f 100644 --- a/src/definitions/device_key/cose_key.rs +++ b/src/definitions/device_key/cose_key.rs @@ -1,46 +1,19 @@ -use aes::cipher::generic_array::{typenum::U8, GenericArray}; -use coset::iana; +use aes::cipher::consts::U8; +use aes::cipher::generic_array::GenericArray; +use ciborium::Value; +use coset::iana::Algorithm; +use coset::{iana, AsCborValue, KeyType, Label}; use p256::EncodedPoint; -use serde::{Deserialize, Serialize}; -use serde_cbor::Value as CborValue; -use ssi_jwk::JWK; -use std::collections::BTreeMap; +use serde::ser; +use serde::ser::{SerializeMap, SerializeSeq}; +use serde::Deserialize; +use serde::Deserializer; +use serde_cbor::tags::Tagged; -/// An implementation of RFC-8152 [COSE_Key](https://datatracker.ietf.org/doc/html/rfc8152#section-13) -/// restricted to the requirements of ISO/IEC 18013-5:2021. -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] -#[serde(try_from = "CborValue", into = "CborValue")] -pub enum CoseKey { - EC2 { crv: EC2Curve, x: Vec, y: EC2Y }, - OKP { crv: OKPCurve, x: Vec }, -} - -/// The sign bit or value of the y-coordinate for the EC point. -#[derive(Debug, Clone, PartialEq, Eq)] -pub enum EC2Y { - Value(Vec), - SignBit(bool), -} - -/// The RFC-8152 identifier of the curve, for EC2 key type. -#[derive(Debug, Clone, PartialEq, Eq)] -pub enum EC2Curve { - P256, - P384, - P521, - P256K, -} - -/// The RFC-8152 identifier of the curve, for OKP key type. -#[derive(Debug, Clone, PartialEq, Eq)] -pub enum OKPCurve { - X25519, - X448, - Ed25519, - Ed448, -} +#[derive(Clone, Debug, PartialEq)] +pub struct CoseKey(coset::CoseKey); -/// Errors that can occur when deserialising a COSE_Key. +/// Errors that can occur when deserializing a COSE_Key. #[derive(Debug, Clone, thiserror::Error)] pub enum Error { #[error("COSE_Key of kty 'EC2' missing x coordinate")] @@ -48,9 +21,9 @@ pub enum Error { #[error("COSE_Key of kty 'EC2' missing y coordinate")] EC2MissingY, #[error("Expected to parse a CBOR bool or bstr for y-coordinate, received: '{0:?}'")] - InvalidTypeY(CborValue), + InvalidTypeY(Value), #[error("Expected to parse a CBOR map, received: '{0:?}'")] - NotAMap(CborValue), + NotAMap(Value), #[error("Unable to discern the elliptic curve")] UnknownCurve, #[error("This implementation of COSE_Key only supports P-256, P-384, P-521, Ed25519 and Ed448 elliptic curves")] @@ -63,355 +36,160 @@ pub enum Error { UnsupportedFormat, } +impl Eq for CoseKey {} + impl CoseKey { - pub fn signature_algorithm(&self) -> Option { - match self { - CoseKey::EC2 { - crv: EC2Curve::P256, - .. - } => Some(iana::Algorithm::ES256), - CoseKey::EC2 { - crv: EC2Curve::P384, - .. - } => Some(iana::Algorithm::ES384), - CoseKey::EC2 { - crv: EC2Curve::P521, - .. - } => Some(iana::Algorithm::ES512), - CoseKey::OKP { - crv: OKPCurve::Ed448, - .. - } => Some(iana::Algorithm::EdDSA), - CoseKey::OKP { - crv: OKPCurve::Ed25519, - .. - } => Some(iana::Algorithm::EdDSA), - _ => None, - } + pub fn new(key: coset::CoseKey) -> Self { + Self(key) } -} -impl From for CborValue { - fn from(key: CoseKey) -> CborValue { - let mut map = BTreeMap::new(); - match key { - CoseKey::EC2 { crv, x, y } => { - // kty: 1, EC2: 2 - map.insert(CborValue::Integer(1), CborValue::Integer(2)); - // crv: -1 - map.insert(CborValue::Integer(-1), crv.into()); - // x: -2 - map.insert(CborValue::Integer(-2), CborValue::Bytes(x)); - // y: -3 - map.insert(CborValue::Integer(-3), y.into()); - } - CoseKey::OKP { crv, x } => { - // kty: 1, OKP: 1 - map.insert(CborValue::Integer(1), CborValue::Integer(1)); - // crv: -1 - map.insert(CborValue::Integer(-1), crv.into()); - // x: -2 - map.insert(CborValue::Integer(-2), CborValue::Bytes(x)); - } + pub fn signature_algorithm(&self) -> Option { + match self.0.kty { + KeyType::Assigned(kty) => match kty { + iana::KeyType::EC2 => match self.0.params[1].0 { + Label::Int(crv) if crv == iana::EllipticCurve::Ed448 as i64 => { + Some(Algorithm::EdDSA) + } + Label::Int(crv) if crv == iana::EllipticCurve::Ed25519 as i64 => { + Some(Algorithm::EdDSA) + } + _ => None, + }, + iana::KeyType::OKP => match self.0.params[1].0 { + Label::Int(crv) if crv == iana::EllipticCurve::P_256 as i64 => { + Some(Algorithm::ES256) + } + Label::Int(crv) if crv == iana::EllipticCurve::P_384 as i64 => { + Some(Algorithm::ES384) + } + Label::Int(crv) if crv == iana::EllipticCurve::P_521 as i64 => { + Some(Algorithm::ES512) + } + Label::Text(_) => None, + _ => None, + }, + _ => None, + }, + _ => None, } - CborValue::Map(map) } } -impl TryFrom for CoseKey { - type Error = Error; - - fn try_from(v: CborValue) -> Result { - if let CborValue::Map(mut map) = v { - match ( - map.remove(&CborValue::Integer(1)), - map.remove(&CborValue::Integer(-1)), - map.remove(&CborValue::Integer(-2)), - ) { - ( - Some(CborValue::Integer(2)), - Some(CborValue::Integer(crv_id)), - Some(CborValue::Bytes(x)), - ) => { - let crv = crv_id.try_into()?; - let y = map - .remove(&CborValue::Integer(-3)) - .ok_or(Error::EC2MissingY)? - .try_into()?; - Ok(Self::EC2 { crv, x, y }) - } - ( - Some(CborValue::Integer(1)), - Some(CborValue::Integer(crv_id)), - Some(CborValue::Bytes(x)), - ) => { - let crv = crv_id.try_into()?; - Ok(Self::OKP { crv, x }) +/// Serialize [CoseKey] by serializing the [Value]. +impl ser::Serialize for CoseKey { + #[inline] + fn serialize(&self, serializer: S) -> Result { + let value = self.0.clone().to_cbor_value().map_err(ser::Error::custom)?; + match value { + Value::Bytes(x) => serializer.serialize_bytes(&x), + Value::Bool(x) => serializer.serialize_bool(x), + Value::Text(x) => serializer.serialize_str(x.as_str()), + Value::Null => serializer.serialize_unit(), + + Value::Tag(tag, ref v) => Tagged::new(Some(tag), v).serialize(serializer), + + Value::Float(x) => { + let y = x as f32; + if (y as f64).to_bits() == x.to_bits() { + serializer.serialize_f32(y) + } else { + serializer.serialize_f64(x) } - _ => Err(Error::UnsupportedKeyType), } - } else { - Err(Error::NotAMap(v)) - } - } -} - -impl TryFrom for EncodedPoint { - type Error = Error; - fn try_from(value: CoseKey) -> Result { - match value { - CoseKey::EC2 { - crv: EC2Curve::P256, - x, - y, - } => { - let x_generic_array = GenericArray::from_slice(x.as_ref()); - match y { - EC2Y::Value(y) => { - let y_generic_array = GenericArray::from_slice(y.as_ref()); - - Ok(EncodedPoint::from_affine_coordinates( - x_generic_array, - y_generic_array, - false, - )) - } - EC2Y::SignBit(y) => { - let mut bytes = x.clone(); - if y { - bytes.insert(0, 3) - } else { - bytes.insert(0, 2) - } - let encoded = - EncodedPoint::from_bytes(bytes).map_err(|_e| Error::InvalidCoseKey)?; - Ok(encoded) - } + #[allow(clippy::unnecessary_fallible_conversions)] + Value::Integer(x) => { + if let Ok(x) = u8::try_from(x) { + serializer.serialize_u8(x) + } else if let Ok(x) = i8::try_from(x) { + serializer.serialize_i8(x) + } else if let Ok(x) = u16::try_from(x) { + serializer.serialize_u16(x) + } else if let Ok(x) = i16::try_from(x) { + serializer.serialize_i16(x) + } else if let Ok(x) = u32::try_from(x) { + serializer.serialize_u32(x) + } else if let Ok(x) = i32::try_from(x) { + serializer.serialize_i32(x) + } else if let Ok(x) = u64::try_from(x) { + serializer.serialize_u64(x) + } else if let Ok(x) = i64::try_from(x) { + serializer.serialize_i64(x) + } else if let Ok(x) = u128::try_from(x) { + serializer.serialize_u128(x) + } else if let Ok(x) = i128::try_from(x) { + serializer.serialize_i128(x) + } else { + unreachable!() } } - CoseKey::OKP { crv: _, x } => { - let x_generic_array: GenericArray<_, U8> = - GenericArray::clone_from_slice(&x[0..42]); - let encoded = EncodedPoint::from_bytes(x_generic_array) - .map_err(|_e| Error::InvalidCoseKey)?; - Ok(encoded) - } - _ => Err(Error::InvalidCoseKey), - } - } -} - -impl From for CborValue { - fn from(y: EC2Y) -> CborValue { - match y { - EC2Y::Value(s) => CborValue::Bytes(s), - EC2Y::SignBit(b) => CborValue::Bool(b), - } - } -} - -impl TryFrom for EC2Y { - type Error = Error; - fn try_from(v: CborValue) -> Result { - match v { - CborValue::Bytes(s) => Ok(EC2Y::Value(s)), - CborValue::Bool(b) => Ok(EC2Y::SignBit(b)), - _ => Err(Error::InvalidTypeY(v)), - } - } -} + Value::Array(x) => { + let mut map = serializer.serialize_seq(Some(x.len()))?; -impl From for CborValue { - fn from(crv: EC2Curve) -> CborValue { - match crv { - EC2Curve::P256 => CborValue::Integer(1), - EC2Curve::P384 => CborValue::Integer(2), - EC2Curve::P521 => CborValue::Integer(3), - EC2Curve::P256K => CborValue::Integer(8), - } - } -} - -impl TryFrom for EC2Curve { - type Error = Error; - - fn try_from(crv_id: i128) -> Result { - match crv_id { - 1 => Ok(EC2Curve::P256), - 2 => Ok(EC2Curve::P384), - 3 => Ok(EC2Curve::P521), - 8 => Ok(EC2Curve::P256K), - _ => Err(Error::UnsupportedCurve), - } - } -} - -impl From for CborValue { - fn from(crv: OKPCurve) -> CborValue { - match crv { - OKPCurve::X25519 => CborValue::Integer(4), - OKPCurve::X448 => CborValue::Integer(5), - OKPCurve::Ed25519 => CborValue::Integer(6), - OKPCurve::Ed448 => CborValue::Integer(7), - } - } -} + for v in x { + map.serialize_element(&v)?; + } -impl TryFrom for OKPCurve { - type Error = Error; + map.end() + } - fn try_from(crv_id: i128) -> Result { - match crv_id { - 4 => Ok(OKPCurve::X25519), - 5 => Ok(OKPCurve::X448), - 6 => Ok(OKPCurve::Ed25519), - 7 => Ok(OKPCurve::Ed448), - _ => Err(Error::UnsupportedCurve), - } - } -} + Value::Map(x) => { + let mut map = serializer.serialize_map(Some(x.len()))?; -impl TryFrom for CoseKey { - type Error = Error; + for (k, v) in x { + map.serialize_entry(&k, &v)?; + } - fn try_from(jwk: JWK) -> Result { - match jwk.params { - ssi_jwk::Params::EC(params) => { - let x = params - .x_coordinate - .as_ref() - .ok_or(Error::EC2MissingX)? - .0 - .clone(); - Ok(CoseKey::EC2 { - crv: (¶ms).try_into()?, - x, - y: params.try_into()?, - }) + map.end() } - ssi_jwk::Params::OKP(params) => Ok(CoseKey::OKP { - crv: (¶ms).try_into()?, - x: params.public_key.0.clone(), - }), - _ => Err(Error::UnsupportedKeyType), - } - } -} - -impl TryFrom<&ssi_jwk::ECParams> for EC2Curve { - type Error = Error; - - fn try_from(params: &ssi_jwk::ECParams) -> Result { - match params.curve.as_ref() { - Some(crv) if crv == "P-256" => Ok(Self::P256), - Some(crv) if crv == "P-384" => Ok(Self::P384), - Some(crv) if crv == "P-521" => Ok(Self::P521), - Some(crv) if crv == "secp256k1" => Ok(Self::P256K), - Some(_) => Err(Error::UnsupportedCurve), - None => Err(Error::UnknownCurve), + _ => unimplemented!(), } } } -impl TryFrom for EC2Y { - type Error = Error; - - fn try_from(params: ssi_jwk::ECParams) -> Result { - if let Some(y) = params.y_coordinate.as_ref() { - Ok(Self::Value(y.0.clone())) - } else { - Err(Error::EC2MissingY) - } +/// Deserialize [CoseKey] by first deserializing the [Value] and then using [coset::CoseSign1::from_cbor_value]. +impl<'de> Deserialize<'de> for CoseKey { + fn deserialize(deserializer: D) -> crate::cose::sign1::Result + where + D: Deserializer<'de>, + { + // Deserialize the input to a CBOR Value + let value = Value::deserialize(deserializer)?; + // Convert the CBOR Value to CoseKey + Ok(CoseKey( + coset::CoseKey::from_cbor_value(value).map_err(serde::de::Error::custom)?, + )) } } -impl TryFrom for JWK { +impl TryFrom for EncodedPoint { type Error = Error; - fn try_from(cose: CoseKey) -> Result { - Ok(match cose { - CoseKey::EC2 { crv, x, y } => JWK { - params: ssi_jwk::Params::EC(ssi_jwk::ECParams { - curve: Some(match crv { - EC2Curve::P256 => "P-256".to_string(), - EC2Curve::P384 => "P-384".to_string(), - EC2Curve::P521 => "P-521".to_string(), - EC2Curve::P256K => "secp256k1".to_string(), - }), - x_coordinate: Some(ssi_jwk::Base64urlUInt(x)), - y_coordinate: match y { - EC2Y::Value(vec) => Some(ssi_jwk::Base64urlUInt(vec)), - EC2Y::SignBit(_) => return Err(Error::UnsupportedFormat), - }, - ecc_private_key: None, - }), - public_key_use: None, - key_operations: None, - algorithm: None, - key_id: None, - x509_url: None, - x509_certificate_chain: None, - x509_thumbprint_sha1: None, - x509_thumbprint_sha256: None, - }, - CoseKey::OKP { crv, x } => JWK { - params: ssi_jwk::Params::OKP(ssi_jwk::OctetParams { - curve: match crv { - OKPCurve::X25519 => "X25519".to_string(), - OKPCurve::X448 => "X448".to_string(), - OKPCurve::Ed25519 => "Ed25519".to_string(), - OKPCurve::Ed448 => "Ed448".to_string(), - }, - public_key: ssi_jwk::Base64urlUInt(x), - private_key: None, - }), - public_key_use: None, - key_operations: None, - algorithm: None, - key_id: None, - x509_url: None, - x509_certificate_chain: None, - x509_thumbprint_sha1: None, - x509_thumbprint_sha256: None, + fn try_from(value: CoseKey) -> Result { + let x = value.0.params[1].1.as_bytes().ok_or(Error::EC2MissingX)?; + let y = value.0.params[2].1.as_bytes().ok_or(Error::EC2MissingY)?; + match value.0.kty { + KeyType::Assigned(kty) => match kty { + iana::KeyType::EC2 => { + // todo: EC2Y::SignBit(y) + let x_generic_array = GenericArray::from_slice(x.as_ref()); + let y_generic_array = GenericArray::from_slice(y.as_ref()); + Ok(EncodedPoint::from_affine_coordinates( + x_generic_array, + y_generic_array, + false, + )) + } + iana::KeyType::OKP => { + let x_generic_array: GenericArray<_, U8> = + GenericArray::clone_from_slice(&x[0..42]); + let encoded = EncodedPoint::from_bytes(x_generic_array) + .map_err(|_e| Error::InvalidCoseKey)?; + Ok(encoded) + } + _ => Err(Error::UnsupportedKeyType), }, - }) - } -} - -impl TryFrom<&ssi_jwk::OctetParams> for OKPCurve { - type Error = Error; - - fn try_from(params: &ssi_jwk::OctetParams) -> Result { - match params.curve.as_str() { - "Ed25519" => Ok(Self::Ed25519), - "Ed448" => Ok(Self::Ed448), - "X25519" => Ok(Self::X25519), - "X448" => Ok(Self::X448), - _ => Err(Error::UnsupportedCurve), + _ => Err(Error::UnsupportedKeyType), } } } - -#[cfg(test)] -mod test { - use super::*; - use hex::FromHex; - - static EC_P256: &str = include_str!("../../../test/definitions/cose_key/ec_p256.cbor"); - - #[test] - fn ec_p256() { - let key_bytes = >::from_hex(EC_P256).expect("unable to convert cbor hex to bytes"); - let key = serde_cbor::from_slice(&key_bytes).unwrap(); - match &key { - CoseKey::EC2 { crv, .. } => assert_eq!(crv, &EC2Curve::P256), - _ => panic!("expected an EC2 cose key"), - }; - assert_eq!( - serde_cbor::to_vec(&key).unwrap(), - key_bytes, - "cbor encoding roundtrip failed" - ); - } -} diff --git a/src/definitions/device_key/mod.rs b/src/definitions/device_key/mod.rs index 750615c0..826adddf 100644 --- a/src/definitions/device_key/mod.rs +++ b/src/definitions/device_key/mod.rs @@ -5,7 +5,6 @@ use std::collections::BTreeMap; pub mod cose_key; pub use cose_key::CoseKey; -pub use cose_key::EC2Curve; #[derive(Clone, Debug, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] diff --git a/src/definitions/mod.rs b/src/definitions/mod.rs index f146dccf..0168cb79 100644 --- a/src/definitions/mod.rs +++ b/src/definitions/mod.rs @@ -14,7 +14,7 @@ pub mod validity_info; pub use device_engagement::{ BleOptions, DeviceEngagement, DeviceRetrievalMethod, NfcOptions, Security, WifiOptions, }; -pub use device_key::cose_key::{EC2Curve, Error, EC2Y}; +pub use device_key::cose_key::Error; pub use device_key::{CoseKey, DeviceKeyInfo, KeyAuthorizations}; pub use device_request::DocRequest; pub use device_response::{DeviceResponse, Document}; diff --git a/src/definitions/session.rs b/src/definitions/session.rs index 7502826f..fa74a385 100644 --- a/src/definitions/session.rs +++ b/src/definitions/session.rs @@ -1,9 +1,7 @@ use super::helpers::Tag24; use super::DeviceEngagement; use crate::definitions::device_engagement::EReaderKeyBytes; -use crate::definitions::device_key::cose_key::EC2Y; use crate::definitions::device_key::CoseKey; -use crate::definitions::device_key::EC2Curve; use crate::definitions::helpers::bytestr::ByteStr; use crate::definitions::session::EncodedPoints::{Ep256, Ep384}; @@ -14,6 +12,7 @@ use aes_gcm::{ Nonce, // Or `Aes128Gcm` }; use anyhow::Result; +use coset::iana::EllipticCurve; use ecdsa::EncodedPoint; use elliptic_curve::{ ecdh::EphemeralSecret, ecdh::SharedSecret, generic_array::sequence::Concat, @@ -140,12 +139,13 @@ pub fn create_p256_ephemeral_keys() -> Result<(p256::SecretKey, CoseKey), Error> let x_coordinate = encoded_point.x().ok_or(Error::EphemeralKeyError)?; let y_coordinate = encoded_point.y().ok_or(Error::EphemeralKeyError)?; - let crv = EC2Curve::try_from(1).map_err(|_e| Error::EphemeralKeyError)?; - let public_key = CoseKey::EC2 { - crv, - x: x_coordinate.to_vec(), - y: EC2Y::Value(y_coordinate.to_vec()), - }; + let public_key = coset::CoseKeyBuilder::new_ec2_pub_key( + EllipticCurve::P_256, + x_coordinate.to_vec(), + y_coordinate.to_vec(), + ) + .build(); + let public_key = CoseKey::new(public_key); Ok((private_key, public_key)) } diff --git a/src/issuance/mdoc.rs b/src/issuance/mdoc.rs index c0b6e9b4..a5c6922e 100644 --- a/src/issuance/mdoc.rs +++ b/src/issuance/mdoc.rs @@ -457,18 +457,18 @@ fn generate_digest_id(used_ids: &mut HashSet) -> DigestId { #[cfg(test)] pub mod test { + use crate::definitions::namespaces::{ + org_iso_18013_5_1::OrgIso1801351, org_iso_18013_5_1_aamva::OrgIso1801351Aamva, + }; + use crate::definitions::traits::{FromJson, ToNamespaceMap}; + use crate::definitions::CoseKey; + use coset::iana::EllipticCurve; use elliptic_curve::sec1::ToEncodedPoint; use p256::ecdsa::{Signature, SigningKey}; use p256::pkcs8::DecodePrivateKey; use p256::SecretKey; use time::OffsetDateTime; - use crate::definitions::device_key::cose_key::{CoseKey, EC2Curve, EC2Y}; - use crate::definitions::namespaces::{ - org_iso_18013_5_1::OrgIso1801351, org_iso_18013_5_1_aamva::OrgIso1801351Aamva, - }; - use crate::definitions::traits::{FromJson, ToNamespaceMap}; - use super::*; static ISSUER_CERT: &[u8] = include_bytes!("../../test/issuance/issuer-cert.pem"); @@ -604,13 +604,13 @@ pub mod test { let key = p256::SecretKey::from_sec1_der(&der_bytes).unwrap(); let pub_key = key.public_key(); let ec = pub_key.to_encoded_point(false); - let x = ec.x().unwrap().to_vec(); - let y = EC2Y::Value(ec.y().unwrap().to_vec()); - let device_key = CoseKey::EC2 { - crv: EC2Curve::P256, - x, - y, - }; + let device_key = coset::CoseKeyBuilder::new_ec2_pub_key( + EllipticCurve::P_256, + ec.x().unwrap().to_vec(), + ec.y().unwrap().to_vec(), + ) + .build(); + let device_key = CoseKey::new(device_key); let device_key_info = DeviceKeyInfo { device_key, From a73e3e46417eed15a75a85417ba51b8e6167275d Mon Sep 17 00:00:00 2001 From: Radu Marias Date: Mon, 5 Aug 2024 05:27:23 +0300 Subject: [PATCH 09/24] Move to `key` module. --- src/cose.rs | 1 + src/{definitions/device_key/cose_key.rs => cose/key.rs} | 0 src/definitions/device_key/mod.rs | 3 +-- src/definitions/mod.rs | 2 +- src/presentation/mod.rs | 3 ++- 5 files changed, 5 insertions(+), 4 deletions(-) rename src/{definitions/device_key/cose_key.rs => cose/key.rs} (100%) diff --git a/src/cose.rs b/src/cose.rs index 5aca68c1..da9d9852 100644 --- a/src/cose.rs +++ b/src/cose.rs @@ -1,3 +1,4 @@ +pub mod key; pub mod mac0; pub mod sign1; diff --git a/src/definitions/device_key/cose_key.rs b/src/cose/key.rs similarity index 100% rename from src/definitions/device_key/cose_key.rs rename to src/cose/key.rs diff --git a/src/definitions/device_key/mod.rs b/src/definitions/device_key/mod.rs index 826adddf..cf808e74 100644 --- a/src/definitions/device_key/mod.rs +++ b/src/definitions/device_key/mod.rs @@ -3,8 +3,7 @@ use serde::{Deserialize, Serialize}; use serde_cbor::Value as CborValue; use std::collections::BTreeMap; -pub mod cose_key; -pub use cose_key::CoseKey; +pub use crate::cose::key::CoseKey; #[derive(Clone, Debug, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] diff --git a/src/definitions/mod.rs b/src/definitions/mod.rs index 0168cb79..f3c72d79 100644 --- a/src/definitions/mod.rs +++ b/src/definitions/mod.rs @@ -11,10 +11,10 @@ pub mod session; pub mod traits; pub mod validity_info; +pub use crate::cose::key::Error; pub use device_engagement::{ BleOptions, DeviceEngagement, DeviceRetrievalMethod, NfcOptions, Security, WifiOptions, }; -pub use device_key::cose_key::Error; pub use device_key::{CoseKey, DeviceKeyInfo, KeyAuthorizations}; pub use device_request::DocRequest; pub use device_response::{DeviceResponse, Document}; diff --git a/src/presentation/mod.rs b/src/presentation/mod.rs index e38a2dd9..67cc3248 100644 --- a/src/presentation/mod.rs +++ b/src/presentation/mod.rs @@ -25,7 +25,8 @@ impl Stringify for device::SessionManagerEngaged {} impl Stringify for device::SessionManager {} impl Stringify for reader::SessionManager {} -use crate::definitions::{device_key::cose_key::CoseKey, helpers::Tag24}; +use crate::cose::key::CoseKey; +use crate::definitions::helpers::Tag24; use hkdf::Hkdf; use sha2::Sha256; From a9fdf0fa6407115241703445984fc0c4b1107b12 Mon Sep 17 00:00:00 2001 From: Radu Marias Date: Mon, 5 Aug 2024 07:46:11 +0300 Subject: [PATCH 10/24] Add serialize and convert for CoseKey --- src/cose/key.rs | 265 ++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 235 insertions(+), 30 deletions(-) diff --git a/src/cose/key.rs b/src/cose/key.rs index 0df3e36f..9767d3eb 100644 --- a/src/cose/key.rs +++ b/src/cose/key.rs @@ -1,7 +1,7 @@ use aes::cipher::consts::U8; use aes::cipher::generic_array::GenericArray; use ciborium::Value; -use coset::iana::Algorithm; +use coset::iana::{Algorithm, EllipticCurve, EnumI64}; use coset::{iana, AsCborValue, KeyType, Label}; use p256::EncodedPoint; use serde::ser; @@ -9,6 +9,9 @@ use serde::ser::{SerializeMap, SerializeSeq}; use serde::Deserialize; use serde::Deserializer; use serde_cbor::tags::Tagged; +use serde_cbor::Value as CborValue; +use ssi_jwk::JWK; +use std::collections::BTreeMap; #[derive(Clone, Debug, PartialEq)] pub struct CoseKey(coset::CoseKey); @@ -21,9 +24,11 @@ pub enum Error { #[error("COSE_Key of kty 'EC2' missing y coordinate")] EC2MissingY, #[error("Expected to parse a CBOR bool or bstr for y-coordinate, received: '{0:?}'")] - InvalidTypeY(Value), + InvalidTypeY(serde_cbor::Value), + #[error("Expected to parse a CBOR bool for y-coordinate, received: '{0:?}'")] + InvalidTypeYSign(Value), #[error("Expected to parse a CBOR map, received: '{0:?}'")] - NotAMap(Value), + NotAMap(serde_cbor::Value), #[error("Unable to discern the elliptic curve")] UnknownCurve, #[error("This implementation of COSE_Key only supports P-256, P-384, P-521, Ed25519 and Ed448 elliptic curves")] @@ -46,26 +51,15 @@ impl CoseKey { pub fn signature_algorithm(&self) -> Option { match self.0.kty { KeyType::Assigned(kty) => match kty { - iana::KeyType::EC2 => match self.0.params[1].0 { - Label::Int(crv) if crv == iana::EllipticCurve::Ed448 as i64 => { - Some(Algorithm::EdDSA) - } - Label::Int(crv) if crv == iana::EllipticCurve::Ed25519 as i64 => { - Some(Algorithm::EdDSA) - } + iana::KeyType::EC2 => match get_crv(self).unwrap() { + EllipticCurve::Ed448 => Some(Algorithm::EdDSA), + EllipticCurve::Ed25519 => Some(Algorithm::EdDSA), _ => None, }, - iana::KeyType::OKP => match self.0.params[1].0 { - Label::Int(crv) if crv == iana::EllipticCurve::P_256 as i64 => { - Some(Algorithm::ES256) - } - Label::Int(crv) if crv == iana::EllipticCurve::P_384 as i64 => { - Some(Algorithm::ES384) - } - Label::Int(crv) if crv == iana::EllipticCurve::P_521 as i64 => { - Some(Algorithm::ES512) - } - Label::Text(_) => None, + iana::KeyType::OKP => match get_crv(self).unwrap() { + EllipticCurve::P_256 => Some(Algorithm::ES256), + EllipticCurve::P_384 => Some(Algorithm::ES384), + EllipticCurve::P_521 => Some(Algorithm::ES512), _ => None, }, _ => None, @@ -166,19 +160,33 @@ impl<'de> Deserialize<'de> for CoseKey { impl TryFrom for EncodedPoint { type Error = Error; fn try_from(value: CoseKey) -> Result { - let x = value.0.params[1].1.as_bytes().ok_or(Error::EC2MissingX)?; - let y = value.0.params[2].1.as_bytes().ok_or(Error::EC2MissingY)?; + let x = get_x(&value)?.as_bytes().ok_or(Error::EC2MissingX)?; match value.0.kty { KeyType::Assigned(kty) => match kty { iana::KeyType::EC2 => { - // todo: EC2Y::SignBit(y) let x_generic_array = GenericArray::from_slice(x.as_ref()); - let y_generic_array = GenericArray::from_slice(y.as_ref()); - Ok(EncodedPoint::from_affine_coordinates( - x_generic_array, - y_generic_array, - false, - )) + match get_y(&value)? { + Value::Bytes(y) => { + let y_generic_array = GenericArray::from_slice(y.as_ref()); + Ok(EncodedPoint::from_affine_coordinates( + x_generic_array, + y_generic_array, + false, + )) + } + Value::Bool(y) => { + let mut bytes = x.clone(); + if *y { + bytes.insert(0, 3) + } else { + bytes.insert(0, 2) + } + let encoded = EncodedPoint::from_bytes(bytes) + .map_err(|_e| Error::InvalidCoseKey)?; + Ok(encoded) + } + _ => Err(Error::InvalidTypeYSign(value.0.params[2].1.clone()))?, + } } iana::KeyType::OKP => { let x_generic_array: GenericArray<_, U8> = @@ -193,3 +201,200 @@ impl TryFrom for EncodedPoint { } } } + +impl From for CborValue { + /// # Panics + /// + /// If X or Y is missing. + fn from(key: CoseKey) -> CborValue { + let mut map = BTreeMap::new(); + let x = get_x(&key) + .unwrap() + .as_bytes() + .ok_or(Error::EC2MissingX) + .unwrap() + .clone(); + let y = get_y(&key) + .unwrap() + .as_bytes() + .ok_or(Error::EC2MissingY) + .unwrap() + .clone(); + if let KeyType::Assigned(kty) = key.0.kty { + match kty { + iana::KeyType::EC2 => { + // kty: 1, EC2: 2 + map.insert(CborValue::Integer(1), CborValue::Integer(2)); + // crv: -1 + map.insert( + CborValue::Integer(-1), + match key.0.params[0].1 { + Value::Integer(i) => CborValue::Integer(i.into()), + _ => CborValue::Integer(0), + }, + ); + // x: -2 + map.insert(CborValue::Integer(-2), CborValue::Bytes(x)); + // y: -3 + map.insert(CborValue::Integer(-3), y.into()); + } + iana::KeyType::OKP => { + // kty: 1, OKP: 1 + map.insert(CborValue::Integer(1), CborValue::Integer(1)); + // crv: -1 + map.insert( + CborValue::Integer(-1), + match key.0.params[0].1 { + Value::Integer(i) => CborValue::Integer(i.into()), + _ => CborValue::Integer(0), + }, + ); + // x: -2 + map.insert(CborValue::Integer(-2), CborValue::Bytes(x)); + } + _ => {} + } + } + CborValue::Map(map) + } +} + +impl TryFrom for CoseKey { + type Error = Error; + + fn try_from(v: CborValue) -> Result { + if let CborValue::Map(mut map) = v { + match ( + map.remove(&CborValue::Integer(1)), + map.remove(&CborValue::Integer(-1)), + map.remove(&CborValue::Integer(-2)), + ) { + ( + Some(CborValue::Integer(2)), + Some(CborValue::Integer(crv_id)), + Some(CborValue::Bytes(x)), + ) => { + let crv = EllipticCurve::from_i64(crv_id as i64).ok_or(Error::UnknownCurve)?; + let y = map + .remove(&CborValue::Integer(-3)) + .ok_or(Error::EC2MissingY)?; + let y = if let CborValue::Bytes(y) = y { + y + } else { + Err(Error::InvalidTypeY(y))? + }; + let key = coset::CoseKeyBuilder::new_ec2_pub_key(crv, x, y).build(); + let key = CoseKey(key); + Ok(key) + } + ( + Some(CborValue::Integer(1)), + Some(CborValue::Integer(crv_id)), + Some(CborValue::Bytes(x)), + ) => { + let crv = EllipticCurve::from_i64(crv_id as i64).ok_or(Error::UnknownCurve)?; + let key = coset::CoseKeyBuilder::new_okp_key() + .param(iana::Ec2KeyParameter::Crv as i64, Value::from(crv as u64)) + .param(iana::Ec2KeyParameter::X as i64, Value::Bytes(x)) + .build(); + let key = CoseKey(key); + Ok(key) + } + _ => Err(Error::UnsupportedKeyType), + } + } else { + Err(Error::NotAMap(v)) + } + } +} + +impl TryFrom for CoseKey { + type Error = Error; + + fn try_from(jwk: JWK) -> Result { + match jwk.params { + ssi_jwk::Params::EC(params) => { + let x = params + .x_coordinate + .as_ref() + .ok_or(Error::EC2MissingX)? + .0 + .clone(); + let key = coset::CoseKeyBuilder::new_ec2_pub_key( + into_curve(params.curve.clone().ok_or(Error::UnknownCurve)?)?, + x, + params + .y_coordinate + .as_ref() + .ok_or(Error::EC2MissingY)? + .0 + .clone(), + ) + .build(); + let key = CoseKey(key); + Ok(key) + } + ssi_jwk::Params::OKP(params) => { + let crv = EllipticCurve::from_i64(into_curve(params.curve.clone())? as i64) + .ok_or(Error::UnknownCurve)?; + let key = coset::CoseKeyBuilder::new_okp_key() + .param(iana::Ec2KeyParameter::Crv as i64, Value::from(crv as u64)) + .param( + iana::Ec2KeyParameter::X as i64, + Value::Bytes(params.public_key.0.clone()), + ) + .build(); + let key = CoseKey(key); + Ok(key) + } + _ => Err(Error::UnsupportedKeyType), + } + } +} + +fn into_curve(str: String) -> Result { + Ok(match str.as_str() { + "P_256" => EllipticCurve::P_256, + "P_384" => EllipticCurve::P_384, + "P_521" => EllipticCurve::P_521, + "Ed448" => EllipticCurve::Ed448, + "Ed25519" => EllipticCurve::Ed25519, + "Reserved" => EllipticCurve::Reserved, + "Secp256k1" => EllipticCurve::Secp256k1, + "X25519" => EllipticCurve::X25519, + _ => return Err(Error::UnknownCurve), + }) +} + +fn get_x(key: &CoseKey) -> Result<&Value, Error> { + for (key, value) in &key.0.params { + match key { + Label::Int(p) if *p == iana::Ec2KeyParameter::X as i64 => return Ok(value), + _ => continue, + } + } + Err(Error::EC2MissingX) +} + +fn get_y(key: &CoseKey) -> Result<&Value, Error> { + for (key, value) in &key.0.params { + match key { + Label::Int(p) if *p == iana::Ec2KeyParameter::Y as i64 => return Ok(value), + _ => continue, + } + } + Err(Error::EC2MissingY) +} + +fn get_crv(key: &CoseKey) -> Result { + for item in &key.0.params { + match item { + (Label::Int(p), Value::Integer(crv)) if *p == iana::Ec2KeyParameter::Crv as i64 => { + let crv: i128 = From::from(*crv); + return EllipticCurve::from_i64(crv as i64).ok_or(Error::UnknownCurve); + } + _ => continue, + } + } + Err(Error::EC2MissingX) +} From 5cffb264c71d3ed8c944f83ce30e7cbe60ff422b Mon Sep 17 00:00:00 2001 From: Radu Marias Date: Mon, 5 Aug 2024 13:21:39 +0300 Subject: [PATCH 11/24] Extract some duplicated code. --- src/cose.rs | 1 + src/cose/key.rs | 38 ++++++++++++---------- src/cose/mac0.rs | 81 ++++++----------------------------------------- src/cose/sign1.rs | 81 ++++++----------------------------------------- 4 files changed, 42 insertions(+), 159 deletions(-) diff --git a/src/cose.rs b/src/cose.rs index da9d9852..990fa28b 100644 --- a/src/cose.rs +++ b/src/cose.rs @@ -1,5 +1,6 @@ pub mod key; pub mod mac0; +mod serialize; pub mod sign1; use coset::iana; diff --git a/src/cose/key.rs b/src/cose/key.rs index 9767d3eb..7b18f5c5 100644 --- a/src/cose/key.rs +++ b/src/cose/key.rs @@ -160,12 +160,14 @@ impl<'de> Deserialize<'de> for CoseKey { impl TryFrom for EncodedPoint { type Error = Error; fn try_from(value: CoseKey) -> Result { - let x = get_x(&value)?.as_bytes().ok_or(Error::EC2MissingX)?; + let x = get_coord(&value, Element::X)? + .as_bytes() + .ok_or(Error::EC2MissingX)?; match value.0.kty { KeyType::Assigned(kty) => match kty { iana::KeyType::EC2 => { let x_generic_array = GenericArray::from_slice(x.as_ref()); - match get_y(&value)? { + match get_coord(&value, Element::Y)? { Value::Bytes(y) => { let y_generic_array = GenericArray::from_slice(y.as_ref()); Ok(EncodedPoint::from_affine_coordinates( @@ -208,13 +210,13 @@ impl From for CborValue { /// If X or Y is missing. fn from(key: CoseKey) -> CborValue { let mut map = BTreeMap::new(); - let x = get_x(&key) + let x = get_coord(&key, Element::X) .unwrap() .as_bytes() .ok_or(Error::EC2MissingX) .unwrap() .clone(); - let y = get_y(&key) + let y = get_coord(&key, Element::Y) .unwrap() .as_bytes() .ok_or(Error::EC2MissingY) @@ -366,24 +368,26 @@ fn into_curve(str: String) -> Result { }) } -fn get_x(key: &CoseKey) -> Result<&Value, Error> { - for (key, value) in &key.0.params { - match key { - Label::Int(p) if *p == iana::Ec2KeyParameter::X as i64 => return Ok(value), - _ => continue, - } - } - Err(Error::EC2MissingX) +#[derive(Debug, PartialEq, Eq, Copy, Clone)] +enum Element { + X, + Y, } -fn get_y(key: &CoseKey) -> Result<&Value, Error> { +fn get_coord(key: &CoseKey, element: Element) -> Result<&Value, Error> { for (key, value) in &key.0.params { - match key { - Label::Int(p) if *p == iana::Ec2KeyParameter::Y as i64 => return Ok(value), - _ => continue, + match element { + Element::X => match key { + Label::Int(p) if *p == iana::Ec2KeyParameter::X as i64 => return Ok(value), + _ => continue, + }, + Element::Y => match key { + Label::Int(p) if *p == iana::Ec2KeyParameter::Y as i64 => return Ok(value), + _ => continue, + }, } } - Err(Error::EC2MissingY) + Err(Error::EC2MissingX) } fn get_crv(key: &CoseKey) -> Result { diff --git a/src/cose/mac0.rs b/src/cose/mac0.rs index b865a0c3..d3d3fbc0 100644 --- a/src/cose/mac0.rs +++ b/src/cose/mac0.rs @@ -1,4 +1,4 @@ -use crate::cose::SignatureAlgorithm; +use crate::cose::{serialize, SignatureAlgorithm}; use ::hmac::Hmac; use coset::cbor::Value; use coset::cwt::ClaimsSet; @@ -7,9 +7,7 @@ use coset::{ RegisteredLabelWithPrivate, }; use digest::{Mac, MacError}; -use serde::ser::{SerializeMap, SerializeSeq}; use serde::{ser, Deserialize, Deserializer, Serialize}; -use serde_cbor::tags::Tagged; use sha2::Sha256; /// Prepared `COSE_Mac0` for remote signing. @@ -254,74 +252,15 @@ impl ser::Serialize for CoseMac0 { .clone() .to_cbor_value() .map_err(ser::Error::custom)?; - if self.tagged { - return Tagged::new(Some(iana::CborTag::CoseMac0 as u64), value).serialize(serializer); - } - match value { - Value::Bytes(x) => serializer.serialize_bytes(&x), - Value::Bool(x) => serializer.serialize_bool(x), - Value::Text(x) => serializer.serialize_str(x.as_str()), - Value::Null => serializer.serialize_unit(), - - Value::Tag(tag, ref v) => Tagged::new(Some(tag), v).serialize(serializer), - - Value::Float(x) => { - let y = x as f32; - if (y as f64).to_bits() == x.to_bits() { - serializer.serialize_f32(y) - } else { - serializer.serialize_f64(x) - } - } - - #[allow(clippy::unnecessary_fallible_conversions)] - Value::Integer(x) => { - if let Ok(x) = u8::try_from(x) { - serializer.serialize_u8(x) - } else if let Ok(x) = i8::try_from(x) { - serializer.serialize_i8(x) - } else if let Ok(x) = u16::try_from(x) { - serializer.serialize_u16(x) - } else if let Ok(x) = i16::try_from(x) { - serializer.serialize_i16(x) - } else if let Ok(x) = u32::try_from(x) { - serializer.serialize_u32(x) - } else if let Ok(x) = i32::try_from(x) { - serializer.serialize_i32(x) - } else if let Ok(x) = u64::try_from(x) { - serializer.serialize_u64(x) - } else if let Ok(x) = i64::try_from(x) { - serializer.serialize_i64(x) - } else if let Ok(x) = u128::try_from(x) { - serializer.serialize_u128(x) - } else if let Ok(x) = i128::try_from(x) { - serializer.serialize_i128(x) - } else { - unreachable!() - } - } - - Value::Array(x) => { - let mut map = serializer.serialize_seq(Some(x.len()))?; - - for v in x { - map.serialize_element(&v)?; - } - - map.end() - } - - Value::Map(x) => { - let mut map = serializer.serialize_map(Some(x.len()))?; - - for (k, v) in x { - map.serialize_entry(&k, &v)?; - } - - map.end() - } - _ => unimplemented!(), - } + serialize::serialize( + value, + if self.tagged { + Some(iana::CborTag::CoseMac0 as u64) + } else { + None + }, + serializer, + ) } } diff --git a/src/cose/sign1.rs b/src/cose/sign1.rs index 3490b0bc..36da7da3 100644 --- a/src/cose/sign1.rs +++ b/src/cose/sign1.rs @@ -4,12 +4,10 @@ use coset::{ iana, sig_structure_data, AsCborValue, CborSerializable, CoseError, RegisteredLabelWithPrivate, SignatureContext, }; -use serde::ser::{SerializeMap, SerializeSeq}; use serde::{ser, Deserialize, Deserializer, Serialize}; -use serde_cbor::tags::Tagged; use signature::Verifier; -use crate::cose::SignatureAlgorithm; +use crate::cose::{serialize, SignatureAlgorithm}; /// Prepared `COSE_Sign1` for remote signing. /// @@ -268,74 +266,15 @@ impl ser::Serialize for CoseSign1 { .clone() .to_cbor_value() .map_err(ser::Error::custom)?; - if self.tagged { - return Tagged::new(Some(iana::CborTag::CoseSign1 as u64), value).serialize(serializer); - } - match value { - Value::Bytes(x) => serializer.serialize_bytes(&x), - Value::Bool(x) => serializer.serialize_bool(x), - Value::Text(x) => serializer.serialize_str(x.as_str()), - Value::Null => serializer.serialize_unit(), - - Value::Tag(tag, ref v) => Tagged::new(Some(tag), v).serialize(serializer), - - Value::Float(x) => { - let y = x as f32; - if (y as f64).to_bits() == x.to_bits() { - serializer.serialize_f32(y) - } else { - serializer.serialize_f64(x) - } - } - - #[allow(clippy::unnecessary_fallible_conversions)] - Value::Integer(x) => { - if let Ok(x) = u8::try_from(x) { - serializer.serialize_u8(x) - } else if let Ok(x) = i8::try_from(x) { - serializer.serialize_i8(x) - } else if let Ok(x) = u16::try_from(x) { - serializer.serialize_u16(x) - } else if let Ok(x) = i16::try_from(x) { - serializer.serialize_i16(x) - } else if let Ok(x) = u32::try_from(x) { - serializer.serialize_u32(x) - } else if let Ok(x) = i32::try_from(x) { - serializer.serialize_i32(x) - } else if let Ok(x) = u64::try_from(x) { - serializer.serialize_u64(x) - } else if let Ok(x) = i64::try_from(x) { - serializer.serialize_i64(x) - } else if let Ok(x) = u128::try_from(x) { - serializer.serialize_u128(x) - } else if let Ok(x) = i128::try_from(x) { - serializer.serialize_i128(x) - } else { - unreachable!() - } - } - - Value::Array(x) => { - let mut map = serializer.serialize_seq(Some(x.len()))?; - - for v in x { - map.serialize_element(&v)?; - } - - map.end() - } - - Value::Map(x) => { - let mut map = serializer.serialize_map(Some(x.len()))?; - - for (k, v) in x { - map.serialize_entry(&k, &v)?; - } - - map.end() - } - _ => unimplemented!(), - } + serialize::serialize( + value, + if self.tagged { + Some(iana::CborTag::CoseSign1 as u64) + } else { + None + }, + serializer, + ) } } From 83280a7923d2671efeecda8bbd04083cbbd34fd6 Mon Sep 17 00:00:00 2001 From: Radu Marias Date: Mon, 5 Aug 2024 13:44:55 +0300 Subject: [PATCH 12/24] Extract some duplicated code. --- src/cose/serialize.rs | 81 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 81 insertions(+) create mode 100644 src/cose/serialize.rs diff --git a/src/cose/serialize.rs b/src/cose/serialize.rs new file mode 100644 index 00000000..053a30b1 --- /dev/null +++ b/src/cose/serialize.rs @@ -0,0 +1,81 @@ +use ciborium::Value; +use serde::ser::{SerializeMap, SerializeSeq}; +use serde::Serialize; +use serde_cbor::tags::Tagged; + +use crate::cose::mac0; + +pub(crate) fn serialize( + value: Value, + tag: Option, + serializer: S, +) -> mac0::Result { + if let Some(tag) = tag { + return Tagged::new(Some(tag), value).serialize(serializer); + } + match value { + Value::Bytes(x) => serializer.serialize_bytes(&x), + Value::Bool(x) => serializer.serialize_bool(x), + Value::Text(x) => serializer.serialize_str(x.as_str()), + Value::Null => serializer.serialize_unit(), + + Value::Tag(tag, ref v) => Tagged::new(Some(tag), v).serialize(serializer), + + Value::Float(x) => { + let y = x as f32; + if (y as f64).to_bits() == x.to_bits() { + serializer.serialize_f32(y) + } else { + serializer.serialize_f64(x) + } + } + + #[allow(clippy::unnecessary_fallible_conversions)] + Value::Integer(x) => { + if let Ok(x) = u8::try_from(x) { + serializer.serialize_u8(x) + } else if let Ok(x) = i8::try_from(x) { + serializer.serialize_i8(x) + } else if let Ok(x) = u16::try_from(x) { + serializer.serialize_u16(x) + } else if let Ok(x) = i16::try_from(x) { + serializer.serialize_i16(x) + } else if let Ok(x) = u32::try_from(x) { + serializer.serialize_u32(x) + } else if let Ok(x) = i32::try_from(x) { + serializer.serialize_i32(x) + } else if let Ok(x) = u64::try_from(x) { + serializer.serialize_u64(x) + } else if let Ok(x) = i64::try_from(x) { + serializer.serialize_i64(x) + } else if let Ok(x) = u128::try_from(x) { + serializer.serialize_u128(x) + } else if let Ok(x) = i128::try_from(x) { + serializer.serialize_i128(x) + } else { + unreachable!() + } + } + + Value::Array(x) => { + let mut map = serializer.serialize_seq(Some(x.len()))?; + + for v in x { + map.serialize_element(&v)?; + } + + map.end() + } + + Value::Map(x) => { + let mut map = serializer.serialize_map(Some(x.len()))?; + + for (k, v) in x { + map.serialize_entry(&k, &v)?; + } + + map.end() + } + _ => unimplemented!(), + } +} From bcde4d97a67a065ddce0b14e9750eb28aa7f4cef Mon Sep 17 00:00:00 2001 From: Radu Marias Date: Mon, 5 Aug 2024 16:39:59 +0300 Subject: [PATCH 13/24] Update src/cose/sign1.rs Co-authored-by: Jacob --- src/cose/sign1.rs | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/src/cose/sign1.rs b/src/cose/sign1.rs index 36da7da3..79c144c4 100644 --- a/src/cose/sign1.rs +++ b/src/cose/sign1.rs @@ -141,15 +141,12 @@ impl PreparedCoseSign1 { // Check if the payload is present and if it is attached or detached. // Needs to be exclusively attached or detached. - let payload = match (cose_sign1.payload.as_ref(), detached_payload) { + let payload = match (cose_sign1.payload.as_ref(), detached_payload.as_ref()) { (Some(_), Some(_)) => return Err(Error::DoublePayload), (None, None) => return Err(Error::NoPayload), - (Some(payload), None) => Some(payload.clone()), - (None, Some(payload)) => Some(payload), + (Some(payload), None) => payload, + (None, Some(payload)) => payload, }; - let payload = payload - // If payload is None, use cbor null as payload. - .unwrap_or_else(|| vec![246u8]); // Create the signature payload ot be used later on signing. let signature_payload = sig_structure_data( SignatureContext::CoseSign1, From 6c8aba99998d03a2b22923dd6a93c692b5473e98 Mon Sep 17 00:00:00 2001 From: Radu Marias Date: Mon, 5 Aug 2024 16:42:33 +0300 Subject: [PATCH 14/24] Update src/cose/mac0.rs Co-authored-by: Jacob --- src/cose/mac0.rs | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/src/cose/mac0.rs b/src/cose/mac0.rs index d3d3fbc0..0edc2a1d 100644 --- a/src/cose/mac0.rs +++ b/src/cose/mac0.rs @@ -135,16 +135,12 @@ impl PreparedCoseMac0 { // Check if the payload is present and if it is attached or detached. // Needs to be exclusively attached or detached. - let payload = match (cose_mac0.payload.as_ref(), detached_payload) { + let payload = match (cose_mac0.payload.as_ref(), detached_payload.as_ref()) { (Some(_), Some(_)) => return Err(Error::DoublePayload), (None, None) => return Err(Error::NoPayload), - (Some(payload), None) => Some(payload.clone()), - (None, Some(payload)) => Some(payload), + (Some(payload), None) => payload, + (None, Some(payload)) => payload, }; - let payload = payload - // Payload is mandatory - .as_ref() - .expect("payload missing"); // Create the signature payload ot be used later on signing. let tag_payload = mac_structure_data( MacContext::CoseMac0, From 797e8a785342218a63a67923648b5016bec97858 Mon Sep 17 00:00:00 2001 From: Radu Marias Date: Mon, 5 Aug 2024 15:52:03 +0300 Subject: [PATCH 15/24] Revert "Extract some duplicated code." This reverts commit 83280a7923d2671efeecda8bbd04083cbbd34fd6. --- src/cose/serialize.rs | 81 ------------------------------------------- 1 file changed, 81 deletions(-) delete mode 100644 src/cose/serialize.rs diff --git a/src/cose/serialize.rs b/src/cose/serialize.rs deleted file mode 100644 index 053a30b1..00000000 --- a/src/cose/serialize.rs +++ /dev/null @@ -1,81 +0,0 @@ -use ciborium::Value; -use serde::ser::{SerializeMap, SerializeSeq}; -use serde::Serialize; -use serde_cbor::tags::Tagged; - -use crate::cose::mac0; - -pub(crate) fn serialize( - value: Value, - tag: Option, - serializer: S, -) -> mac0::Result { - if let Some(tag) = tag { - return Tagged::new(Some(tag), value).serialize(serializer); - } - match value { - Value::Bytes(x) => serializer.serialize_bytes(&x), - Value::Bool(x) => serializer.serialize_bool(x), - Value::Text(x) => serializer.serialize_str(x.as_str()), - Value::Null => serializer.serialize_unit(), - - Value::Tag(tag, ref v) => Tagged::new(Some(tag), v).serialize(serializer), - - Value::Float(x) => { - let y = x as f32; - if (y as f64).to_bits() == x.to_bits() { - serializer.serialize_f32(y) - } else { - serializer.serialize_f64(x) - } - } - - #[allow(clippy::unnecessary_fallible_conversions)] - Value::Integer(x) => { - if let Ok(x) = u8::try_from(x) { - serializer.serialize_u8(x) - } else if let Ok(x) = i8::try_from(x) { - serializer.serialize_i8(x) - } else if let Ok(x) = u16::try_from(x) { - serializer.serialize_u16(x) - } else if let Ok(x) = i16::try_from(x) { - serializer.serialize_i16(x) - } else if let Ok(x) = u32::try_from(x) { - serializer.serialize_u32(x) - } else if let Ok(x) = i32::try_from(x) { - serializer.serialize_i32(x) - } else if let Ok(x) = u64::try_from(x) { - serializer.serialize_u64(x) - } else if let Ok(x) = i64::try_from(x) { - serializer.serialize_i64(x) - } else if let Ok(x) = u128::try_from(x) { - serializer.serialize_u128(x) - } else if let Ok(x) = i128::try_from(x) { - serializer.serialize_i128(x) - } else { - unreachable!() - } - } - - Value::Array(x) => { - let mut map = serializer.serialize_seq(Some(x.len()))?; - - for v in x { - map.serialize_element(&v)?; - } - - map.end() - } - - Value::Map(x) => { - let mut map = serializer.serialize_map(Some(x.len()))?; - - for (k, v) in x { - map.serialize_entry(&k, &v)?; - } - - map.end() - } - _ => unimplemented!(), - } -} From 40bde5145cfbe58011a5645dd62de6c072f734e0 Mon Sep 17 00:00:00 2001 From: Radu Marias Date: Mon, 5 Aug 2024 15:52:04 +0300 Subject: [PATCH 16/24] Revert "Extract some duplicated code." This reverts commit 5cffb264c71d3ed8c944f83ce30e7cbe60ff422b. --- src/cose.rs | 1 - src/cose/key.rs | 38 ++++++++++------------ src/cose/mac0.rs | 81 +++++++++++++++++++++++++++++++++++++++++------ src/cose/sign1.rs | 81 +++++++++++++++++++++++++++++++++++++++++------ 4 files changed, 159 insertions(+), 42 deletions(-) diff --git a/src/cose.rs b/src/cose.rs index 990fa28b..da9d9852 100644 --- a/src/cose.rs +++ b/src/cose.rs @@ -1,6 +1,5 @@ pub mod key; pub mod mac0; -mod serialize; pub mod sign1; use coset::iana; diff --git a/src/cose/key.rs b/src/cose/key.rs index 7b18f5c5..9767d3eb 100644 --- a/src/cose/key.rs +++ b/src/cose/key.rs @@ -160,14 +160,12 @@ impl<'de> Deserialize<'de> for CoseKey { impl TryFrom for EncodedPoint { type Error = Error; fn try_from(value: CoseKey) -> Result { - let x = get_coord(&value, Element::X)? - .as_bytes() - .ok_or(Error::EC2MissingX)?; + let x = get_x(&value)?.as_bytes().ok_or(Error::EC2MissingX)?; match value.0.kty { KeyType::Assigned(kty) => match kty { iana::KeyType::EC2 => { let x_generic_array = GenericArray::from_slice(x.as_ref()); - match get_coord(&value, Element::Y)? { + match get_y(&value)? { Value::Bytes(y) => { let y_generic_array = GenericArray::from_slice(y.as_ref()); Ok(EncodedPoint::from_affine_coordinates( @@ -210,13 +208,13 @@ impl From for CborValue { /// If X or Y is missing. fn from(key: CoseKey) -> CborValue { let mut map = BTreeMap::new(); - let x = get_coord(&key, Element::X) + let x = get_x(&key) .unwrap() .as_bytes() .ok_or(Error::EC2MissingX) .unwrap() .clone(); - let y = get_coord(&key, Element::Y) + let y = get_y(&key) .unwrap() .as_bytes() .ok_or(Error::EC2MissingY) @@ -368,26 +366,24 @@ fn into_curve(str: String) -> Result { }) } -#[derive(Debug, PartialEq, Eq, Copy, Clone)] -enum Element { - X, - Y, +fn get_x(key: &CoseKey) -> Result<&Value, Error> { + for (key, value) in &key.0.params { + match key { + Label::Int(p) if *p == iana::Ec2KeyParameter::X as i64 => return Ok(value), + _ => continue, + } + } + Err(Error::EC2MissingX) } -fn get_coord(key: &CoseKey, element: Element) -> Result<&Value, Error> { +fn get_y(key: &CoseKey) -> Result<&Value, Error> { for (key, value) in &key.0.params { - match element { - Element::X => match key { - Label::Int(p) if *p == iana::Ec2KeyParameter::X as i64 => return Ok(value), - _ => continue, - }, - Element::Y => match key { - Label::Int(p) if *p == iana::Ec2KeyParameter::Y as i64 => return Ok(value), - _ => continue, - }, + match key { + Label::Int(p) if *p == iana::Ec2KeyParameter::Y as i64 => return Ok(value), + _ => continue, } } - Err(Error::EC2MissingX) + Err(Error::EC2MissingY) } fn get_crv(key: &CoseKey) -> Result { diff --git a/src/cose/mac0.rs b/src/cose/mac0.rs index 0edc2a1d..264a9b78 100644 --- a/src/cose/mac0.rs +++ b/src/cose/mac0.rs @@ -1,4 +1,4 @@ -use crate::cose::{serialize, SignatureAlgorithm}; +use crate::cose::SignatureAlgorithm; use ::hmac::Hmac; use coset::cbor::Value; use coset::cwt::ClaimsSet; @@ -7,7 +7,9 @@ use coset::{ RegisteredLabelWithPrivate, }; use digest::{Mac, MacError}; +use serde::ser::{SerializeMap, SerializeSeq}; use serde::{ser, Deserialize, Deserializer, Serialize}; +use serde_cbor::tags::Tagged; use sha2::Sha256; /// Prepared `COSE_Mac0` for remote signing. @@ -248,15 +250,74 @@ impl ser::Serialize for CoseMac0 { .clone() .to_cbor_value() .map_err(ser::Error::custom)?; - serialize::serialize( - value, - if self.tagged { - Some(iana::CborTag::CoseMac0 as u64) - } else { - None - }, - serializer, - ) + if self.tagged { + return Tagged::new(Some(iana::CborTag::CoseMac0 as u64), value).serialize(serializer); + } + match value { + Value::Bytes(x) => serializer.serialize_bytes(&x), + Value::Bool(x) => serializer.serialize_bool(x), + Value::Text(x) => serializer.serialize_str(x.as_str()), + Value::Null => serializer.serialize_unit(), + + Value::Tag(tag, ref v) => Tagged::new(Some(tag), v).serialize(serializer), + + Value::Float(x) => { + let y = x as f32; + if (y as f64).to_bits() == x.to_bits() { + serializer.serialize_f32(y) + } else { + serializer.serialize_f64(x) + } + } + + #[allow(clippy::unnecessary_fallible_conversions)] + Value::Integer(x) => { + if let Ok(x) = u8::try_from(x) { + serializer.serialize_u8(x) + } else if let Ok(x) = i8::try_from(x) { + serializer.serialize_i8(x) + } else if let Ok(x) = u16::try_from(x) { + serializer.serialize_u16(x) + } else if let Ok(x) = i16::try_from(x) { + serializer.serialize_i16(x) + } else if let Ok(x) = u32::try_from(x) { + serializer.serialize_u32(x) + } else if let Ok(x) = i32::try_from(x) { + serializer.serialize_i32(x) + } else if let Ok(x) = u64::try_from(x) { + serializer.serialize_u64(x) + } else if let Ok(x) = i64::try_from(x) { + serializer.serialize_i64(x) + } else if let Ok(x) = u128::try_from(x) { + serializer.serialize_u128(x) + } else if let Ok(x) = i128::try_from(x) { + serializer.serialize_i128(x) + } else { + unreachable!() + } + } + + Value::Array(x) => { + let mut map = serializer.serialize_seq(Some(x.len()))?; + + for v in x { + map.serialize_element(&v)?; + } + + map.end() + } + + Value::Map(x) => { + let mut map = serializer.serialize_map(Some(x.len()))?; + + for (k, v) in x { + map.serialize_entry(&k, &v)?; + } + + map.end() + } + _ => unimplemented!(), + } } } diff --git a/src/cose/sign1.rs b/src/cose/sign1.rs index 79c144c4..90786978 100644 --- a/src/cose/sign1.rs +++ b/src/cose/sign1.rs @@ -4,10 +4,12 @@ use coset::{ iana, sig_structure_data, AsCborValue, CborSerializable, CoseError, RegisteredLabelWithPrivate, SignatureContext, }; +use serde::ser::{SerializeMap, SerializeSeq}; use serde::{ser, Deserialize, Deserializer, Serialize}; +use serde_cbor::tags::Tagged; use signature::Verifier; -use crate::cose::{serialize, SignatureAlgorithm}; +use crate::cose::SignatureAlgorithm; /// Prepared `COSE_Sign1` for remote signing. /// @@ -263,15 +265,74 @@ impl ser::Serialize for CoseSign1 { .clone() .to_cbor_value() .map_err(ser::Error::custom)?; - serialize::serialize( - value, - if self.tagged { - Some(iana::CborTag::CoseSign1 as u64) - } else { - None - }, - serializer, - ) + if self.tagged { + return Tagged::new(Some(iana::CborTag::CoseSign1 as u64), value).serialize(serializer); + } + match value { + Value::Bytes(x) => serializer.serialize_bytes(&x), + Value::Bool(x) => serializer.serialize_bool(x), + Value::Text(x) => serializer.serialize_str(x.as_str()), + Value::Null => serializer.serialize_unit(), + + Value::Tag(tag, ref v) => Tagged::new(Some(tag), v).serialize(serializer), + + Value::Float(x) => { + let y = x as f32; + if (y as f64).to_bits() == x.to_bits() { + serializer.serialize_f32(y) + } else { + serializer.serialize_f64(x) + } + } + + #[allow(clippy::unnecessary_fallible_conversions)] + Value::Integer(x) => { + if let Ok(x) = u8::try_from(x) { + serializer.serialize_u8(x) + } else if let Ok(x) = i8::try_from(x) { + serializer.serialize_i8(x) + } else if let Ok(x) = u16::try_from(x) { + serializer.serialize_u16(x) + } else if let Ok(x) = i16::try_from(x) { + serializer.serialize_i16(x) + } else if let Ok(x) = u32::try_from(x) { + serializer.serialize_u32(x) + } else if let Ok(x) = i32::try_from(x) { + serializer.serialize_i32(x) + } else if let Ok(x) = u64::try_from(x) { + serializer.serialize_u64(x) + } else if let Ok(x) = i64::try_from(x) { + serializer.serialize_i64(x) + } else if let Ok(x) = u128::try_from(x) { + serializer.serialize_u128(x) + } else if let Ok(x) = i128::try_from(x) { + serializer.serialize_i128(x) + } else { + unreachable!() + } + } + + Value::Array(x) => { + let mut map = serializer.serialize_seq(Some(x.len()))?; + + for v in x { + map.serialize_element(&v)?; + } + + map.end() + } + + Value::Map(x) => { + let mut map = serializer.serialize_map(Some(x.len()))?; + + for (k, v) in x { + map.serialize_entry(&k, &v)?; + } + + map.end() + } + _ => unimplemented!(), + } } } From 84acfb6f208e0ba4b39189d75d0a39e64c92f254 Mon Sep 17 00:00:00 2001 From: Radu Marias Date: Mon, 5 Aug 2024 15:52:04 +0300 Subject: [PATCH 17/24] Revert "Add serialize and convert for CoseKey" This reverts commit a9fdf0fa6407115241703445984fc0c4b1107b12. --- src/cose/key.rs | 265 ++++++------------------------------------------ 1 file changed, 30 insertions(+), 235 deletions(-) diff --git a/src/cose/key.rs b/src/cose/key.rs index 9767d3eb..0df3e36f 100644 --- a/src/cose/key.rs +++ b/src/cose/key.rs @@ -1,7 +1,7 @@ use aes::cipher::consts::U8; use aes::cipher::generic_array::GenericArray; use ciborium::Value; -use coset::iana::{Algorithm, EllipticCurve, EnumI64}; +use coset::iana::Algorithm; use coset::{iana, AsCborValue, KeyType, Label}; use p256::EncodedPoint; use serde::ser; @@ -9,9 +9,6 @@ use serde::ser::{SerializeMap, SerializeSeq}; use serde::Deserialize; use serde::Deserializer; use serde_cbor::tags::Tagged; -use serde_cbor::Value as CborValue; -use ssi_jwk::JWK; -use std::collections::BTreeMap; #[derive(Clone, Debug, PartialEq)] pub struct CoseKey(coset::CoseKey); @@ -24,11 +21,9 @@ pub enum Error { #[error("COSE_Key of kty 'EC2' missing y coordinate")] EC2MissingY, #[error("Expected to parse a CBOR bool or bstr for y-coordinate, received: '{0:?}'")] - InvalidTypeY(serde_cbor::Value), - #[error("Expected to parse a CBOR bool for y-coordinate, received: '{0:?}'")] - InvalidTypeYSign(Value), + InvalidTypeY(Value), #[error("Expected to parse a CBOR map, received: '{0:?}'")] - NotAMap(serde_cbor::Value), + NotAMap(Value), #[error("Unable to discern the elliptic curve")] UnknownCurve, #[error("This implementation of COSE_Key only supports P-256, P-384, P-521, Ed25519 and Ed448 elliptic curves")] @@ -51,15 +46,26 @@ impl CoseKey { pub fn signature_algorithm(&self) -> Option { match self.0.kty { KeyType::Assigned(kty) => match kty { - iana::KeyType::EC2 => match get_crv(self).unwrap() { - EllipticCurve::Ed448 => Some(Algorithm::EdDSA), - EllipticCurve::Ed25519 => Some(Algorithm::EdDSA), + iana::KeyType::EC2 => match self.0.params[1].0 { + Label::Int(crv) if crv == iana::EllipticCurve::Ed448 as i64 => { + Some(Algorithm::EdDSA) + } + Label::Int(crv) if crv == iana::EllipticCurve::Ed25519 as i64 => { + Some(Algorithm::EdDSA) + } _ => None, }, - iana::KeyType::OKP => match get_crv(self).unwrap() { - EllipticCurve::P_256 => Some(Algorithm::ES256), - EllipticCurve::P_384 => Some(Algorithm::ES384), - EllipticCurve::P_521 => Some(Algorithm::ES512), + iana::KeyType::OKP => match self.0.params[1].0 { + Label::Int(crv) if crv == iana::EllipticCurve::P_256 as i64 => { + Some(Algorithm::ES256) + } + Label::Int(crv) if crv == iana::EllipticCurve::P_384 as i64 => { + Some(Algorithm::ES384) + } + Label::Int(crv) if crv == iana::EllipticCurve::P_521 as i64 => { + Some(Algorithm::ES512) + } + Label::Text(_) => None, _ => None, }, _ => None, @@ -160,33 +166,19 @@ impl<'de> Deserialize<'de> for CoseKey { impl TryFrom for EncodedPoint { type Error = Error; fn try_from(value: CoseKey) -> Result { - let x = get_x(&value)?.as_bytes().ok_or(Error::EC2MissingX)?; + let x = value.0.params[1].1.as_bytes().ok_or(Error::EC2MissingX)?; + let y = value.0.params[2].1.as_bytes().ok_or(Error::EC2MissingY)?; match value.0.kty { KeyType::Assigned(kty) => match kty { iana::KeyType::EC2 => { + // todo: EC2Y::SignBit(y) let x_generic_array = GenericArray::from_slice(x.as_ref()); - match get_y(&value)? { - Value::Bytes(y) => { - let y_generic_array = GenericArray::from_slice(y.as_ref()); - Ok(EncodedPoint::from_affine_coordinates( - x_generic_array, - y_generic_array, - false, - )) - } - Value::Bool(y) => { - let mut bytes = x.clone(); - if *y { - bytes.insert(0, 3) - } else { - bytes.insert(0, 2) - } - let encoded = EncodedPoint::from_bytes(bytes) - .map_err(|_e| Error::InvalidCoseKey)?; - Ok(encoded) - } - _ => Err(Error::InvalidTypeYSign(value.0.params[2].1.clone()))?, - } + let y_generic_array = GenericArray::from_slice(y.as_ref()); + Ok(EncodedPoint::from_affine_coordinates( + x_generic_array, + y_generic_array, + false, + )) } iana::KeyType::OKP => { let x_generic_array: GenericArray<_, U8> = @@ -201,200 +193,3 @@ impl TryFrom for EncodedPoint { } } } - -impl From for CborValue { - /// # Panics - /// - /// If X or Y is missing. - fn from(key: CoseKey) -> CborValue { - let mut map = BTreeMap::new(); - let x = get_x(&key) - .unwrap() - .as_bytes() - .ok_or(Error::EC2MissingX) - .unwrap() - .clone(); - let y = get_y(&key) - .unwrap() - .as_bytes() - .ok_or(Error::EC2MissingY) - .unwrap() - .clone(); - if let KeyType::Assigned(kty) = key.0.kty { - match kty { - iana::KeyType::EC2 => { - // kty: 1, EC2: 2 - map.insert(CborValue::Integer(1), CborValue::Integer(2)); - // crv: -1 - map.insert( - CborValue::Integer(-1), - match key.0.params[0].1 { - Value::Integer(i) => CborValue::Integer(i.into()), - _ => CborValue::Integer(0), - }, - ); - // x: -2 - map.insert(CborValue::Integer(-2), CborValue::Bytes(x)); - // y: -3 - map.insert(CborValue::Integer(-3), y.into()); - } - iana::KeyType::OKP => { - // kty: 1, OKP: 1 - map.insert(CborValue::Integer(1), CborValue::Integer(1)); - // crv: -1 - map.insert( - CborValue::Integer(-1), - match key.0.params[0].1 { - Value::Integer(i) => CborValue::Integer(i.into()), - _ => CborValue::Integer(0), - }, - ); - // x: -2 - map.insert(CborValue::Integer(-2), CborValue::Bytes(x)); - } - _ => {} - } - } - CborValue::Map(map) - } -} - -impl TryFrom for CoseKey { - type Error = Error; - - fn try_from(v: CborValue) -> Result { - if let CborValue::Map(mut map) = v { - match ( - map.remove(&CborValue::Integer(1)), - map.remove(&CborValue::Integer(-1)), - map.remove(&CborValue::Integer(-2)), - ) { - ( - Some(CborValue::Integer(2)), - Some(CborValue::Integer(crv_id)), - Some(CborValue::Bytes(x)), - ) => { - let crv = EllipticCurve::from_i64(crv_id as i64).ok_or(Error::UnknownCurve)?; - let y = map - .remove(&CborValue::Integer(-3)) - .ok_or(Error::EC2MissingY)?; - let y = if let CborValue::Bytes(y) = y { - y - } else { - Err(Error::InvalidTypeY(y))? - }; - let key = coset::CoseKeyBuilder::new_ec2_pub_key(crv, x, y).build(); - let key = CoseKey(key); - Ok(key) - } - ( - Some(CborValue::Integer(1)), - Some(CborValue::Integer(crv_id)), - Some(CborValue::Bytes(x)), - ) => { - let crv = EllipticCurve::from_i64(crv_id as i64).ok_or(Error::UnknownCurve)?; - let key = coset::CoseKeyBuilder::new_okp_key() - .param(iana::Ec2KeyParameter::Crv as i64, Value::from(crv as u64)) - .param(iana::Ec2KeyParameter::X as i64, Value::Bytes(x)) - .build(); - let key = CoseKey(key); - Ok(key) - } - _ => Err(Error::UnsupportedKeyType), - } - } else { - Err(Error::NotAMap(v)) - } - } -} - -impl TryFrom for CoseKey { - type Error = Error; - - fn try_from(jwk: JWK) -> Result { - match jwk.params { - ssi_jwk::Params::EC(params) => { - let x = params - .x_coordinate - .as_ref() - .ok_or(Error::EC2MissingX)? - .0 - .clone(); - let key = coset::CoseKeyBuilder::new_ec2_pub_key( - into_curve(params.curve.clone().ok_or(Error::UnknownCurve)?)?, - x, - params - .y_coordinate - .as_ref() - .ok_or(Error::EC2MissingY)? - .0 - .clone(), - ) - .build(); - let key = CoseKey(key); - Ok(key) - } - ssi_jwk::Params::OKP(params) => { - let crv = EllipticCurve::from_i64(into_curve(params.curve.clone())? as i64) - .ok_or(Error::UnknownCurve)?; - let key = coset::CoseKeyBuilder::new_okp_key() - .param(iana::Ec2KeyParameter::Crv as i64, Value::from(crv as u64)) - .param( - iana::Ec2KeyParameter::X as i64, - Value::Bytes(params.public_key.0.clone()), - ) - .build(); - let key = CoseKey(key); - Ok(key) - } - _ => Err(Error::UnsupportedKeyType), - } - } -} - -fn into_curve(str: String) -> Result { - Ok(match str.as_str() { - "P_256" => EllipticCurve::P_256, - "P_384" => EllipticCurve::P_384, - "P_521" => EllipticCurve::P_521, - "Ed448" => EllipticCurve::Ed448, - "Ed25519" => EllipticCurve::Ed25519, - "Reserved" => EllipticCurve::Reserved, - "Secp256k1" => EllipticCurve::Secp256k1, - "X25519" => EllipticCurve::X25519, - _ => return Err(Error::UnknownCurve), - }) -} - -fn get_x(key: &CoseKey) -> Result<&Value, Error> { - for (key, value) in &key.0.params { - match key { - Label::Int(p) if *p == iana::Ec2KeyParameter::X as i64 => return Ok(value), - _ => continue, - } - } - Err(Error::EC2MissingX) -} - -fn get_y(key: &CoseKey) -> Result<&Value, Error> { - for (key, value) in &key.0.params { - match key { - Label::Int(p) if *p == iana::Ec2KeyParameter::Y as i64 => return Ok(value), - _ => continue, - } - } - Err(Error::EC2MissingY) -} - -fn get_crv(key: &CoseKey) -> Result { - for item in &key.0.params { - match item { - (Label::Int(p), Value::Integer(crv)) if *p == iana::Ec2KeyParameter::Crv as i64 => { - let crv: i128 = From::from(*crv); - return EllipticCurve::from_i64(crv as i64).ok_or(Error::UnknownCurve); - } - _ => continue, - } - } - Err(Error::EC2MissingX) -} From 33333b638f41ede47ca848666eb3b339135b0258 Mon Sep 17 00:00:00 2001 From: Radu Marias Date: Mon, 5 Aug 2024 15:52:05 +0300 Subject: [PATCH 18/24] Revert "Move to `key` module." This reverts commit a73e3e46417eed15a75a85417ba51b8e6167275d. --- src/cose.rs | 1 - src/{cose/key.rs => definitions/device_key/cose_key.rs} | 0 src/definitions/device_key/mod.rs | 3 ++- src/definitions/mod.rs | 2 +- src/presentation/mod.rs | 3 +-- 5 files changed, 4 insertions(+), 5 deletions(-) rename src/{cose/key.rs => definitions/device_key/cose_key.rs} (100%) diff --git a/src/cose.rs b/src/cose.rs index da9d9852..5aca68c1 100644 --- a/src/cose.rs +++ b/src/cose.rs @@ -1,4 +1,3 @@ -pub mod key; pub mod mac0; pub mod sign1; diff --git a/src/cose/key.rs b/src/definitions/device_key/cose_key.rs similarity index 100% rename from src/cose/key.rs rename to src/definitions/device_key/cose_key.rs diff --git a/src/definitions/device_key/mod.rs b/src/definitions/device_key/mod.rs index cf808e74..826adddf 100644 --- a/src/definitions/device_key/mod.rs +++ b/src/definitions/device_key/mod.rs @@ -3,7 +3,8 @@ use serde::{Deserialize, Serialize}; use serde_cbor::Value as CborValue; use std::collections::BTreeMap; -pub use crate::cose::key::CoseKey; +pub mod cose_key; +pub use cose_key::CoseKey; #[derive(Clone, Debug, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] diff --git a/src/definitions/mod.rs b/src/definitions/mod.rs index f3c72d79..0168cb79 100644 --- a/src/definitions/mod.rs +++ b/src/definitions/mod.rs @@ -11,10 +11,10 @@ pub mod session; pub mod traits; pub mod validity_info; -pub use crate::cose::key::Error; pub use device_engagement::{ BleOptions, DeviceEngagement, DeviceRetrievalMethod, NfcOptions, Security, WifiOptions, }; +pub use device_key::cose_key::Error; pub use device_key::{CoseKey, DeviceKeyInfo, KeyAuthorizations}; pub use device_request::DocRequest; pub use device_response::{DeviceResponse, Document}; diff --git a/src/presentation/mod.rs b/src/presentation/mod.rs index 67cc3248..e38a2dd9 100644 --- a/src/presentation/mod.rs +++ b/src/presentation/mod.rs @@ -25,8 +25,7 @@ impl Stringify for device::SessionManagerEngaged {} impl Stringify for device::SessionManager {} impl Stringify for reader::SessionManager {} -use crate::cose::key::CoseKey; -use crate::definitions::helpers::Tag24; +use crate::definitions::{device_key::cose_key::CoseKey, helpers::Tag24}; use hkdf::Hkdf; use sha2::Sha256; From ffe7d22bf6f9e8d0ddba37a8abd04de12a57da38 Mon Sep 17 00:00:00 2001 From: Radu Marias Date: Mon, 5 Aug 2024 15:52:05 +0300 Subject: [PATCH 19/24] Revert "Use CoseKey from coset" This reverts commit f3981aeca115782d12c051f900f7388d439cec64. --- src/cose/mac0.rs | 2 +- src/cose/sign1.rs | 2 +- src/definitions/device_engagement/error.rs | 7 + src/definitions/device_key/cose_key.rs | 504 +++++++++++++++------ src/definitions/device_key/mod.rs | 1 + src/definitions/mod.rs | 2 +- src/definitions/session.rs | 16 +- src/issuance/mdoc.rs | 26 +- 8 files changed, 395 insertions(+), 165 deletions(-) diff --git a/src/cose/mac0.rs b/src/cose/mac0.rs index 264a9b78..8b517e97 100644 --- a/src/cose/mac0.rs +++ b/src/cose/mac0.rs @@ -249,7 +249,7 @@ impl ser::Serialize for CoseMac0 { .inner .clone() .to_cbor_value() - .map_err(ser::Error::custom)?; + .map_err(ser::Error::custom)?; // Convert the inner CoseMac0 object to a tagged CBOR vector if self.tagged { return Tagged::new(Some(iana::CborTag::CoseMac0 as u64), value).serialize(serializer); } diff --git a/src/cose/sign1.rs b/src/cose/sign1.rs index 90786978..82648b6a 100644 --- a/src/cose/sign1.rs +++ b/src/cose/sign1.rs @@ -264,7 +264,7 @@ impl ser::Serialize for CoseSign1 { .inner .clone() .to_cbor_value() - .map_err(ser::Error::custom)?; + .map_err(ser::Error::custom)?; // Convert the inner CoseSign1 object to a tagged CBOR vector if self.tagged { return Tagged::new(Some(iana::CborTag::CoseSign1 as u64), value).serialize(serializer); } diff --git a/src/definitions/device_engagement/error.rs b/src/definitions/device_engagement/error.rs index eda5eaec..593cb6dc 100644 --- a/src/definitions/device_engagement/error.rs +++ b/src/definitions/device_engagement/error.rs @@ -1,3 +1,4 @@ +use crate::definitions::device_key::cose_key::Error as CoseKeyError; use crate::definitions::helpers::tag24::Error as Tag24Error; use serde_cbor::Error as SerdeCborError; @@ -30,6 +31,12 @@ pub enum Error { InvalidNfcResponseDataLengthError, } +impl From for Error { + fn from(_: CoseKeyError) -> Self { + Error::CoseKeyError + } +} + impl From for Error { fn from(_: Tag24Error) -> Self { Error::Tag24Error diff --git a/src/definitions/device_key/cose_key.rs b/src/definitions/device_key/cose_key.rs index 0df3e36f..d2cf0576 100644 --- a/src/definitions/device_key/cose_key.rs +++ b/src/definitions/device_key/cose_key.rs @@ -1,19 +1,46 @@ -use aes::cipher::consts::U8; -use aes::cipher::generic_array::GenericArray; -use ciborium::Value; -use coset::iana::Algorithm; -use coset::{iana, AsCborValue, KeyType, Label}; +use aes::cipher::generic_array::{typenum::U8, GenericArray}; +use coset::iana; use p256::EncodedPoint; -use serde::ser; -use serde::ser::{SerializeMap, SerializeSeq}; -use serde::Deserialize; -use serde::Deserializer; -use serde_cbor::tags::Tagged; +use serde::{Deserialize, Serialize}; +use serde_cbor::Value as CborValue; +use ssi_jwk::JWK; +use std::collections::BTreeMap; -#[derive(Clone, Debug, PartialEq)] -pub struct CoseKey(coset::CoseKey); +/// An implementation of RFC-8152 [COSE_Key](https://datatracker.ietf.org/doc/html/rfc8152#section-13) +/// restricted to the requirements of ISO/IEC 18013-5:2021. +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +#[serde(try_from = "CborValue", into = "CborValue")] +pub enum CoseKey { + EC2 { crv: EC2Curve, x: Vec, y: EC2Y }, + OKP { crv: OKPCurve, x: Vec }, +} + +/// The sign bit or value of the y-coordinate for the EC point. +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum EC2Y { + Value(Vec), + SignBit(bool), +} + +/// The RFC-8152 identifier of the curve, for EC2 key type. +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum EC2Curve { + P256, + P384, + P521, + P256K, +} + +/// The RFC-8152 identifier of the curve, for OKP key type. +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum OKPCurve { + X25519, + X448, + Ed25519, + Ed448, +} -/// Errors that can occur when deserializing a COSE_Key. +/// Errors that can occur when deserialising a COSE_Key. #[derive(Debug, Clone, thiserror::Error)] pub enum Error { #[error("COSE_Key of kty 'EC2' missing x coordinate")] @@ -21,9 +48,9 @@ pub enum Error { #[error("COSE_Key of kty 'EC2' missing y coordinate")] EC2MissingY, #[error("Expected to parse a CBOR bool or bstr for y-coordinate, received: '{0:?}'")] - InvalidTypeY(Value), + InvalidTypeY(CborValue), #[error("Expected to parse a CBOR map, received: '{0:?}'")] - NotAMap(Value), + NotAMap(CborValue), #[error("Unable to discern the elliptic curve")] UnknownCurve, #[error("This implementation of COSE_Key only supports P-256, P-384, P-521, Ed25519 and Ed448 elliptic curves")] @@ -36,160 +63,355 @@ pub enum Error { UnsupportedFormat, } -impl Eq for CoseKey {} - impl CoseKey { - pub fn new(key: coset::CoseKey) -> Self { - Self(key) + pub fn signature_algorithm(&self) -> Option { + match self { + CoseKey::EC2 { + crv: EC2Curve::P256, + .. + } => Some(iana::Algorithm::ES256), + CoseKey::EC2 { + crv: EC2Curve::P384, + .. + } => Some(iana::Algorithm::ES384), + CoseKey::EC2 { + crv: EC2Curve::P521, + .. + } => Some(iana::Algorithm::ES512), + CoseKey::OKP { + crv: OKPCurve::Ed448, + .. + } => Some(iana::Algorithm::EdDSA), + CoseKey::OKP { + crv: OKPCurve::Ed25519, + .. + } => Some(iana::Algorithm::EdDSA), + _ => None, + } } +} - pub fn signature_algorithm(&self) -> Option { - match self.0.kty { - KeyType::Assigned(kty) => match kty { - iana::KeyType::EC2 => match self.0.params[1].0 { - Label::Int(crv) if crv == iana::EllipticCurve::Ed448 as i64 => { - Some(Algorithm::EdDSA) - } - Label::Int(crv) if crv == iana::EllipticCurve::Ed25519 as i64 => { - Some(Algorithm::EdDSA) - } - _ => None, - }, - iana::KeyType::OKP => match self.0.params[1].0 { - Label::Int(crv) if crv == iana::EllipticCurve::P_256 as i64 => { - Some(Algorithm::ES256) - } - Label::Int(crv) if crv == iana::EllipticCurve::P_384 as i64 => { - Some(Algorithm::ES384) - } - Label::Int(crv) if crv == iana::EllipticCurve::P_521 as i64 => { - Some(Algorithm::ES512) - } - Label::Text(_) => None, - _ => None, - }, - _ => None, - }, - _ => None, +impl From for CborValue { + fn from(key: CoseKey) -> CborValue { + let mut map = BTreeMap::new(); + match key { + CoseKey::EC2 { crv, x, y } => { + // kty: 1, EC2: 2 + map.insert(CborValue::Integer(1), CborValue::Integer(2)); + // crv: -1 + map.insert(CborValue::Integer(-1), crv.into()); + // x: -2 + map.insert(CborValue::Integer(-2), CborValue::Bytes(x)); + // y: -3 + map.insert(CborValue::Integer(-3), y.into()); + } + CoseKey::OKP { crv, x } => { + // kty: 1, OKP: 1 + map.insert(CborValue::Integer(1), CborValue::Integer(1)); + // crv: -1 + map.insert(CborValue::Integer(-1), crv.into()); + // x: -2 + map.insert(CborValue::Integer(-2), CborValue::Bytes(x)); + } } + CborValue::Map(map) } } -/// Serialize [CoseKey] by serializing the [Value]. -impl ser::Serialize for CoseKey { - #[inline] - fn serialize(&self, serializer: S) -> Result { - let value = self.0.clone().to_cbor_value().map_err(ser::Error::custom)?; - match value { - Value::Bytes(x) => serializer.serialize_bytes(&x), - Value::Bool(x) => serializer.serialize_bool(x), - Value::Text(x) => serializer.serialize_str(x.as_str()), - Value::Null => serializer.serialize_unit(), - - Value::Tag(tag, ref v) => Tagged::new(Some(tag), v).serialize(serializer), - - Value::Float(x) => { - let y = x as f32; - if (y as f64).to_bits() == x.to_bits() { - serializer.serialize_f32(y) - } else { - serializer.serialize_f64(x) +impl TryFrom for CoseKey { + type Error = Error; + + fn try_from(v: CborValue) -> Result { + if let CborValue::Map(mut map) = v { + match ( + map.remove(&CborValue::Integer(1)), + map.remove(&CborValue::Integer(-1)), + map.remove(&CborValue::Integer(-2)), + ) { + ( + Some(CborValue::Integer(2)), + Some(CborValue::Integer(crv_id)), + Some(CborValue::Bytes(x)), + ) => { + let crv = crv_id.try_into()?; + let y = map + .remove(&CborValue::Integer(-3)) + .ok_or(Error::EC2MissingY)? + .try_into()?; + Ok(Self::EC2 { crv, x, y }) + } + ( + Some(CborValue::Integer(1)), + Some(CborValue::Integer(crv_id)), + Some(CborValue::Bytes(x)), + ) => { + let crv = crv_id.try_into()?; + Ok(Self::OKP { crv, x }) } + _ => Err(Error::UnsupportedKeyType), } + } else { + Err(Error::NotAMap(v)) + } + } +} + +impl TryFrom for EncodedPoint { + type Error = Error; + fn try_from(value: CoseKey) -> Result { + match value { + CoseKey::EC2 { + crv: EC2Curve::P256, + x, + y, + } => { + let x_generic_array = GenericArray::from_slice(x.as_ref()); + match y { + EC2Y::Value(y) => { + let y_generic_array = GenericArray::from_slice(y.as_ref()); + + Ok(EncodedPoint::from_affine_coordinates( + x_generic_array, + y_generic_array, + false, + )) + } + EC2Y::SignBit(y) => { + let mut bytes = x.clone(); + if y { + bytes.insert(0, 3) + } else { + bytes.insert(0, 2) + } - #[allow(clippy::unnecessary_fallible_conversions)] - Value::Integer(x) => { - if let Ok(x) = u8::try_from(x) { - serializer.serialize_u8(x) - } else if let Ok(x) = i8::try_from(x) { - serializer.serialize_i8(x) - } else if let Ok(x) = u16::try_from(x) { - serializer.serialize_u16(x) - } else if let Ok(x) = i16::try_from(x) { - serializer.serialize_i16(x) - } else if let Ok(x) = u32::try_from(x) { - serializer.serialize_u32(x) - } else if let Ok(x) = i32::try_from(x) { - serializer.serialize_i32(x) - } else if let Ok(x) = u64::try_from(x) { - serializer.serialize_u64(x) - } else if let Ok(x) = i64::try_from(x) { - serializer.serialize_i64(x) - } else if let Ok(x) = u128::try_from(x) { - serializer.serialize_u128(x) - } else if let Ok(x) = i128::try_from(x) { - serializer.serialize_i128(x) - } else { - unreachable!() + let encoded = + EncodedPoint::from_bytes(bytes).map_err(|_e| Error::InvalidCoseKey)?; + Ok(encoded) + } } } + CoseKey::OKP { crv: _, x } => { + let x_generic_array: GenericArray<_, U8> = + GenericArray::clone_from_slice(&x[0..42]); + let encoded = EncodedPoint::from_bytes(x_generic_array) + .map_err(|_e| Error::InvalidCoseKey)?; + Ok(encoded) + } + _ => Err(Error::InvalidCoseKey), + } + } +} - Value::Array(x) => { - let mut map = serializer.serialize_seq(Some(x.len()))?; +impl From for CborValue { + fn from(y: EC2Y) -> CborValue { + match y { + EC2Y::Value(s) => CborValue::Bytes(s), + EC2Y::SignBit(b) => CborValue::Bool(b), + } + } +} - for v in x { - map.serialize_element(&v)?; - } +impl TryFrom for EC2Y { + type Error = Error; - map.end() - } + fn try_from(v: CborValue) -> Result { + match v { + CborValue::Bytes(s) => Ok(EC2Y::Value(s)), + CborValue::Bool(b) => Ok(EC2Y::SignBit(b)), + _ => Err(Error::InvalidTypeY(v)), + } + } +} - Value::Map(x) => { - let mut map = serializer.serialize_map(Some(x.len()))?; +impl From for CborValue { + fn from(crv: EC2Curve) -> CborValue { + match crv { + EC2Curve::P256 => CborValue::Integer(1), + EC2Curve::P384 => CborValue::Integer(2), + EC2Curve::P521 => CborValue::Integer(3), + EC2Curve::P256K => CborValue::Integer(8), + } + } +} - for (k, v) in x { - map.serialize_entry(&k, &v)?; - } +impl TryFrom for EC2Curve { + type Error = Error; + + fn try_from(crv_id: i128) -> Result { + match crv_id { + 1 => Ok(EC2Curve::P256), + 2 => Ok(EC2Curve::P384), + 3 => Ok(EC2Curve::P521), + 8 => Ok(EC2Curve::P256K), + _ => Err(Error::UnsupportedCurve), + } + } +} + +impl From for CborValue { + fn from(crv: OKPCurve) -> CborValue { + match crv { + OKPCurve::X25519 => CborValue::Integer(4), + OKPCurve::X448 => CborValue::Integer(5), + OKPCurve::Ed25519 => CborValue::Integer(6), + OKPCurve::Ed448 => CborValue::Integer(7), + } + } +} + +impl TryFrom for OKPCurve { + type Error = Error; + + fn try_from(crv_id: i128) -> Result { + match crv_id { + 4 => Ok(OKPCurve::X25519), + 5 => Ok(OKPCurve::X448), + 6 => Ok(OKPCurve::Ed25519), + 7 => Ok(OKPCurve::Ed448), + _ => Err(Error::UnsupportedCurve), + } + } +} + +impl TryFrom for CoseKey { + type Error = Error; - map.end() + fn try_from(jwk: JWK) -> Result { + match jwk.params { + ssi_jwk::Params::EC(params) => { + let x = params + .x_coordinate + .as_ref() + .ok_or(Error::EC2MissingX)? + .0 + .clone(); + Ok(CoseKey::EC2 { + crv: (¶ms).try_into()?, + x, + y: params.try_into()?, + }) } - _ => unimplemented!(), + ssi_jwk::Params::OKP(params) => Ok(CoseKey::OKP { + crv: (¶ms).try_into()?, + x: params.public_key.0.clone(), + }), + _ => Err(Error::UnsupportedKeyType), + } + } +} + +impl TryFrom<&ssi_jwk::ECParams> for EC2Curve { + type Error = Error; + + fn try_from(params: &ssi_jwk::ECParams) -> Result { + match params.curve.as_ref() { + Some(crv) if crv == "P-256" => Ok(Self::P256), + Some(crv) if crv == "P-384" => Ok(Self::P384), + Some(crv) if crv == "P-521" => Ok(Self::P521), + Some(crv) if crv == "secp256k1" => Ok(Self::P256K), + Some(_) => Err(Error::UnsupportedCurve), + None => Err(Error::UnknownCurve), } } } -/// Deserialize [CoseKey] by first deserializing the [Value] and then using [coset::CoseSign1::from_cbor_value]. -impl<'de> Deserialize<'de> for CoseKey { - fn deserialize(deserializer: D) -> crate::cose::sign1::Result - where - D: Deserializer<'de>, - { - // Deserialize the input to a CBOR Value - let value = Value::deserialize(deserializer)?; - // Convert the CBOR Value to CoseKey - Ok(CoseKey( - coset::CoseKey::from_cbor_value(value).map_err(serde::de::Error::custom)?, - )) +impl TryFrom for EC2Y { + type Error = Error; + + fn try_from(params: ssi_jwk::ECParams) -> Result { + if let Some(y) = params.y_coordinate.as_ref() { + Ok(Self::Value(y.0.clone())) + } else { + Err(Error::EC2MissingY) + } } } -impl TryFrom for EncodedPoint { +impl TryFrom for JWK { type Error = Error; - fn try_from(value: CoseKey) -> Result { - let x = value.0.params[1].1.as_bytes().ok_or(Error::EC2MissingX)?; - let y = value.0.params[2].1.as_bytes().ok_or(Error::EC2MissingY)?; - match value.0.kty { - KeyType::Assigned(kty) => match kty { - iana::KeyType::EC2 => { - // todo: EC2Y::SignBit(y) - let x_generic_array = GenericArray::from_slice(x.as_ref()); - let y_generic_array = GenericArray::from_slice(y.as_ref()); - Ok(EncodedPoint::from_affine_coordinates( - x_generic_array, - y_generic_array, - false, - )) - } - iana::KeyType::OKP => { - let x_generic_array: GenericArray<_, U8> = - GenericArray::clone_from_slice(&x[0..42]); - let encoded = EncodedPoint::from_bytes(x_generic_array) - .map_err(|_e| Error::InvalidCoseKey)?; - Ok(encoded) - } - _ => Err(Error::UnsupportedKeyType), + fn try_from(cose: CoseKey) -> Result { + Ok(match cose { + CoseKey::EC2 { crv, x, y } => JWK { + params: ssi_jwk::Params::EC(ssi_jwk::ECParams { + curve: Some(match crv { + EC2Curve::P256 => "P-256".to_string(), + EC2Curve::P384 => "P-384".to_string(), + EC2Curve::P521 => "P-521".to_string(), + EC2Curve::P256K => "secp256k1".to_string(), + }), + x_coordinate: Some(ssi_jwk::Base64urlUInt(x)), + y_coordinate: match y { + EC2Y::Value(vec) => Some(ssi_jwk::Base64urlUInt(vec)), + EC2Y::SignBit(_) => return Err(Error::UnsupportedFormat), + }, + ecc_private_key: None, + }), + public_key_use: None, + key_operations: None, + algorithm: None, + key_id: None, + x509_url: None, + x509_certificate_chain: None, + x509_thumbprint_sha1: None, + x509_thumbprint_sha256: None, }, - _ => Err(Error::UnsupportedKeyType), + CoseKey::OKP { crv, x } => JWK { + params: ssi_jwk::Params::OKP(ssi_jwk::OctetParams { + curve: match crv { + OKPCurve::X25519 => "X25519".to_string(), + OKPCurve::X448 => "X448".to_string(), + OKPCurve::Ed25519 => "Ed25519".to_string(), + OKPCurve::Ed448 => "Ed448".to_string(), + }, + public_key: ssi_jwk::Base64urlUInt(x), + private_key: None, + }), + public_key_use: None, + key_operations: None, + algorithm: None, + key_id: None, + x509_url: None, + x509_certificate_chain: None, + x509_thumbprint_sha1: None, + x509_thumbprint_sha256: None, + }, + }) + } +} + +impl TryFrom<&ssi_jwk::OctetParams> for OKPCurve { + type Error = Error; + + fn try_from(params: &ssi_jwk::OctetParams) -> Result { + match params.curve.as_str() { + "Ed25519" => Ok(Self::Ed25519), + "Ed448" => Ok(Self::Ed448), + "X25519" => Ok(Self::X25519), + "X448" => Ok(Self::X448), + _ => Err(Error::UnsupportedCurve), } } } + +#[cfg(test)] +mod test { + use super::*; + use hex::FromHex; + + static EC_P256: &str = include_str!("../../../test/definitions/cose_key/ec_p256.cbor"); + + #[test] + fn ec_p256() { + let key_bytes = >::from_hex(EC_P256).expect("unable to convert cbor hex to bytes"); + let key = serde_cbor::from_slice(&key_bytes).unwrap(); + match &key { + CoseKey::EC2 { crv, .. } => assert_eq!(crv, &EC2Curve::P256), + _ => panic!("expected an EC2 cose key"), + }; + assert_eq!( + serde_cbor::to_vec(&key).unwrap(), + key_bytes, + "cbor encoding roundtrip failed" + ); + } +} diff --git a/src/definitions/device_key/mod.rs b/src/definitions/device_key/mod.rs index 826adddf..750615c0 100644 --- a/src/definitions/device_key/mod.rs +++ b/src/definitions/device_key/mod.rs @@ -5,6 +5,7 @@ use std::collections::BTreeMap; pub mod cose_key; pub use cose_key::CoseKey; +pub use cose_key::EC2Curve; #[derive(Clone, Debug, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] diff --git a/src/definitions/mod.rs b/src/definitions/mod.rs index 0168cb79..f146dccf 100644 --- a/src/definitions/mod.rs +++ b/src/definitions/mod.rs @@ -14,7 +14,7 @@ pub mod validity_info; pub use device_engagement::{ BleOptions, DeviceEngagement, DeviceRetrievalMethod, NfcOptions, Security, WifiOptions, }; -pub use device_key::cose_key::Error; +pub use device_key::cose_key::{EC2Curve, Error, EC2Y}; pub use device_key::{CoseKey, DeviceKeyInfo, KeyAuthorizations}; pub use device_request::DocRequest; pub use device_response::{DeviceResponse, Document}; diff --git a/src/definitions/session.rs b/src/definitions/session.rs index fa74a385..7502826f 100644 --- a/src/definitions/session.rs +++ b/src/definitions/session.rs @@ -1,7 +1,9 @@ use super::helpers::Tag24; use super::DeviceEngagement; use crate::definitions::device_engagement::EReaderKeyBytes; +use crate::definitions::device_key::cose_key::EC2Y; use crate::definitions::device_key::CoseKey; +use crate::definitions::device_key::EC2Curve; use crate::definitions::helpers::bytestr::ByteStr; use crate::definitions::session::EncodedPoints::{Ep256, Ep384}; @@ -12,7 +14,6 @@ use aes_gcm::{ Nonce, // Or `Aes128Gcm` }; use anyhow::Result; -use coset::iana::EllipticCurve; use ecdsa::EncodedPoint; use elliptic_curve::{ ecdh::EphemeralSecret, ecdh::SharedSecret, generic_array::sequence::Concat, @@ -139,13 +140,12 @@ pub fn create_p256_ephemeral_keys() -> Result<(p256::SecretKey, CoseKey), Error> let x_coordinate = encoded_point.x().ok_or(Error::EphemeralKeyError)?; let y_coordinate = encoded_point.y().ok_or(Error::EphemeralKeyError)?; - let public_key = coset::CoseKeyBuilder::new_ec2_pub_key( - EllipticCurve::P_256, - x_coordinate.to_vec(), - y_coordinate.to_vec(), - ) - .build(); - let public_key = CoseKey::new(public_key); + let crv = EC2Curve::try_from(1).map_err(|_e| Error::EphemeralKeyError)?; + let public_key = CoseKey::EC2 { + crv, + x: x_coordinate.to_vec(), + y: EC2Y::Value(y_coordinate.to_vec()), + }; Ok((private_key, public_key)) } diff --git a/src/issuance/mdoc.rs b/src/issuance/mdoc.rs index a5c6922e..c0b6e9b4 100644 --- a/src/issuance/mdoc.rs +++ b/src/issuance/mdoc.rs @@ -457,18 +457,18 @@ fn generate_digest_id(used_ids: &mut HashSet) -> DigestId { #[cfg(test)] pub mod test { - use crate::definitions::namespaces::{ - org_iso_18013_5_1::OrgIso1801351, org_iso_18013_5_1_aamva::OrgIso1801351Aamva, - }; - use crate::definitions::traits::{FromJson, ToNamespaceMap}; - use crate::definitions::CoseKey; - use coset::iana::EllipticCurve; use elliptic_curve::sec1::ToEncodedPoint; use p256::ecdsa::{Signature, SigningKey}; use p256::pkcs8::DecodePrivateKey; use p256::SecretKey; use time::OffsetDateTime; + use crate::definitions::device_key::cose_key::{CoseKey, EC2Curve, EC2Y}; + use crate::definitions::namespaces::{ + org_iso_18013_5_1::OrgIso1801351, org_iso_18013_5_1_aamva::OrgIso1801351Aamva, + }; + use crate::definitions::traits::{FromJson, ToNamespaceMap}; + use super::*; static ISSUER_CERT: &[u8] = include_bytes!("../../test/issuance/issuer-cert.pem"); @@ -604,13 +604,13 @@ pub mod test { let key = p256::SecretKey::from_sec1_der(&der_bytes).unwrap(); let pub_key = key.public_key(); let ec = pub_key.to_encoded_point(false); - let device_key = coset::CoseKeyBuilder::new_ec2_pub_key( - EllipticCurve::P_256, - ec.x().unwrap().to_vec(), - ec.y().unwrap().to_vec(), - ) - .build(); - let device_key = CoseKey::new(device_key); + let x = ec.x().unwrap().to_vec(); + let y = EC2Y::Value(ec.y().unwrap().to_vec()); + let device_key = CoseKey::EC2 { + crv: EC2Curve::P256, + x, + y, + }; let device_key_info = DeviceKeyInfo { device_key, From 63af7aa42d3a583c578c1f2e1125842045d6daa2 Mon Sep 17 00:00:00 2001 From: Radu Marias Date: Mon, 5 Aug 2024 16:46:50 +0300 Subject: [PATCH 20/24] Revert migration to `coset::CoseKey`. Small refactoring. --- src/cose.rs | 1 + src/cose/mac0.rs | 89 ++++++-------------------- src/cose/serialize.rs | 15 +++++ src/cose/sign1.rs | 83 ++++-------------------- src/definitions/device.rs | 1 + src/definitions/device_key/cose_key.rs | 67 ++++++++++++++++++- src/definitions/mod.rs | 1 + 7 files changed, 111 insertions(+), 146 deletions(-) create mode 100644 src/cose/serialize.rs create mode 100644 src/definitions/device.rs diff --git a/src/cose.rs b/src/cose.rs index 5aca68c1..e70cc68a 100644 --- a/src/cose.rs +++ b/src/cose.rs @@ -1,4 +1,5 @@ pub mod mac0; +mod serialize; pub mod sign1; use coset::iana; diff --git a/src/cose/mac0.rs b/src/cose/mac0.rs index 8b517e97..80f0ce9c 100644 --- a/src/cose/mac0.rs +++ b/src/cose/mac0.rs @@ -12,6 +12,8 @@ use serde::{ser, Deserialize, Deserializer, Serialize}; use serde_cbor::tags::Tagged; use sha2::Sha256; +use crate::cose::serialize; + /// Prepared `COSE_Mac0` for remote signing. /// /// To produce a `COSE_Mac0` do the following: @@ -28,7 +30,6 @@ use sha2::Sha256; /// use digest::Mac; /// use hex::FromHex;use hmac::Hmac; /// use sha2::Sha256; -/// use isomdl::cose::mac0::PreparedCoseMac0; /// /// let key = Vec::::from_hex("a361316953796d6d6574726963613305622d318f187418681869187318201869187318201874186818651820186b18651879").unwrap(); /// let signer = Hmac::::new_from_slice(&key).expect("failed to create HMAC signer"); @@ -143,6 +144,10 @@ impl PreparedCoseMac0 { (Some(payload), None) => payload, (None, Some(payload)) => payload, }; + let payload = payload + // Payload is mandatory + .as_ref() + .ok_or(Error::NoPayload)?; // Create the signature payload ot be used later on signing. let tag_payload = mac_structure_data( MacContext::CoseMac0, @@ -249,75 +254,16 @@ impl ser::Serialize for CoseMac0 { .inner .clone() .to_cbor_value() - .map_err(ser::Error::custom)?; // Convert the inner CoseMac0 object to a tagged CBOR vector - if self.tagged { - return Tagged::new(Some(iana::CborTag::CoseMac0 as u64), value).serialize(serializer); - } - match value { - Value::Bytes(x) => serializer.serialize_bytes(&x), - Value::Bool(x) => serializer.serialize_bool(x), - Value::Text(x) => serializer.serialize_str(x.as_str()), - Value::Null => serializer.serialize_unit(), - - Value::Tag(tag, ref v) => Tagged::new(Some(tag), v).serialize(serializer), - - Value::Float(x) => { - let y = x as f32; - if (y as f64).to_bits() == x.to_bits() { - serializer.serialize_f32(y) - } else { - serializer.serialize_f64(x) - } - } - - #[allow(clippy::unnecessary_fallible_conversions)] - Value::Integer(x) => { - if let Ok(x) = u8::try_from(x) { - serializer.serialize_u8(x) - } else if let Ok(x) = i8::try_from(x) { - serializer.serialize_i8(x) - } else if let Ok(x) = u16::try_from(x) { - serializer.serialize_u16(x) - } else if let Ok(x) = i16::try_from(x) { - serializer.serialize_i16(x) - } else if let Ok(x) = u32::try_from(x) { - serializer.serialize_u32(x) - } else if let Ok(x) = i32::try_from(x) { - serializer.serialize_i32(x) - } else if let Ok(x) = u64::try_from(x) { - serializer.serialize_u64(x) - } else if let Ok(x) = i64::try_from(x) { - serializer.serialize_i64(x) - } else if let Ok(x) = u128::try_from(x) { - serializer.serialize_u128(x) - } else if let Ok(x) = i128::try_from(x) { - serializer.serialize_i128(x) - } else { - unreachable!() - } - } - - Value::Array(x) => { - let mut map = serializer.serialize_seq(Some(x.len()))?; - - for v in x { - map.serialize_element(&v)?; - } - - map.end() - } - - Value::Map(x) => { - let mut map = serializer.serialize_map(Some(x.len()))?; - - for (k, v) in x { - map.serialize_entry(&k, &v)?; - } - - map.end() - } - _ => unimplemented!(), - } + .map_err(ser::Error::custom)?; + serialize::serialize( + &value, + if self.tagged { + Some(iana::CborTag::CoseMac0 as u64) + } else { + None + }, + serializer, + ) } } @@ -355,7 +301,6 @@ mod hmac { #[cfg(test)] mod tests { - use crate::cose::mac0::{CoseMac0, PreparedCoseMac0}; use coset::cwt::{ClaimsSet, Timestamp}; use coset::{iana, CborSerializable, Header}; use digest::Mac; @@ -363,6 +308,8 @@ mod tests { use hmac::Hmac; use sha2::Sha256; + use crate::cose::mac0::{CoseMac0, PreparedCoseMac0}; + static COSE_MAC0: &str = include_str!("../../test/definitions/cose/mac0/serialized.cbor"); static COSE_KEY: &str = include_str!("../../test/definitions/cose/mac0/secret_key"); diff --git a/src/cose/serialize.rs b/src/cose/serialize.rs new file mode 100644 index 00000000..ce85a217 --- /dev/null +++ b/src/cose/serialize.rs @@ -0,0 +1,15 @@ +use ciborium::Value; +use serde::{ser, Serialize}; +use serde_cbor::tags::Tagged; + +pub(crate) fn serialize( + value: &Value, + tag: Option, + serializer: S, +) -> Result { + if tag.is_some() { + Tagged::new(tag, value).serialize(serializer) + } else { + value.serialize(serializer) + } +} diff --git a/src/cose/sign1.rs b/src/cose/sign1.rs index 82648b6a..cf586e39 100644 --- a/src/cose/sign1.rs +++ b/src/cose/sign1.rs @@ -4,12 +4,10 @@ use coset::{ iana, sig_structure_data, AsCborValue, CborSerializable, CoseError, RegisteredLabelWithPrivate, SignatureContext, }; -use serde::ser::{SerializeMap, SerializeSeq}; use serde::{ser, Deserialize, Deserializer, Serialize}; -use serde_cbor::tags::Tagged; use signature::Verifier; -use crate::cose::SignatureAlgorithm; +use crate::cose::{serialize, SignatureAlgorithm}; /// Prepared `COSE_Sign1` for remote signing. /// @@ -264,75 +262,16 @@ impl ser::Serialize for CoseSign1 { .inner .clone() .to_cbor_value() - .map_err(ser::Error::custom)?; // Convert the inner CoseSign1 object to a tagged CBOR vector - if self.tagged { - return Tagged::new(Some(iana::CborTag::CoseSign1 as u64), value).serialize(serializer); - } - match value { - Value::Bytes(x) => serializer.serialize_bytes(&x), - Value::Bool(x) => serializer.serialize_bool(x), - Value::Text(x) => serializer.serialize_str(x.as_str()), - Value::Null => serializer.serialize_unit(), - - Value::Tag(tag, ref v) => Tagged::new(Some(tag), v).serialize(serializer), - - Value::Float(x) => { - let y = x as f32; - if (y as f64).to_bits() == x.to_bits() { - serializer.serialize_f32(y) - } else { - serializer.serialize_f64(x) - } - } - - #[allow(clippy::unnecessary_fallible_conversions)] - Value::Integer(x) => { - if let Ok(x) = u8::try_from(x) { - serializer.serialize_u8(x) - } else if let Ok(x) = i8::try_from(x) { - serializer.serialize_i8(x) - } else if let Ok(x) = u16::try_from(x) { - serializer.serialize_u16(x) - } else if let Ok(x) = i16::try_from(x) { - serializer.serialize_i16(x) - } else if let Ok(x) = u32::try_from(x) { - serializer.serialize_u32(x) - } else if let Ok(x) = i32::try_from(x) { - serializer.serialize_i32(x) - } else if let Ok(x) = u64::try_from(x) { - serializer.serialize_u64(x) - } else if let Ok(x) = i64::try_from(x) { - serializer.serialize_i64(x) - } else if let Ok(x) = u128::try_from(x) { - serializer.serialize_u128(x) - } else if let Ok(x) = i128::try_from(x) { - serializer.serialize_i128(x) - } else { - unreachable!() - } - } - - Value::Array(x) => { - let mut map = serializer.serialize_seq(Some(x.len()))?; - - for v in x { - map.serialize_element(&v)?; - } - - map.end() - } - - Value::Map(x) => { - let mut map = serializer.serialize_map(Some(x.len()))?; - - for (k, v) in x { - map.serialize_entry(&k, &v)?; - } - - map.end() - } - _ => unimplemented!(), - } + .map_err(ser::Error::custom)?; + serialize::serialize( + &value, + if self.tagged { + Some(iana::CborTag::CoseSign1 as u64) + } else { + None + }, + serializer, + ) } } diff --git a/src/definitions/device.rs b/src/definitions/device.rs new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/src/definitions/device.rs @@ -0,0 +1 @@ + diff --git a/src/definitions/device_key/cose_key.rs b/src/definitions/device_key/cose_key.rs index d2cf0576..e9a3610c 100644 --- a/src/definitions/device_key/cose_key.rs +++ b/src/definitions/device_key/cose_key.rs @@ -1,10 +1,14 @@ +use std::collections::BTreeMap; + use aes::cipher::generic_array::{typenum::U8, GenericArray}; -use coset::iana; +use coset::iana::EllipticCurve; +use coset::{iana, CborSerializable}; use p256::EncodedPoint; use serde::{Deserialize, Serialize}; use serde_cbor::Value as CborValue; use ssi_jwk::JWK; -use std::collections::BTreeMap; + +use crate::definitions::traits::ToCbor; /// An implementation of RFC-8152 [COSE_Key](https://datatracker.ietf.org/doc/html/rfc8152#section-13) /// restricted to the requirements of ISO/IEC 18013-5:2021. @@ -31,6 +35,23 @@ pub enum EC2Curve { P256K, } +impl From for EC2Curve { + fn from(value: EllipticCurve) -> Self { + match value { + EllipticCurve::Reserved => unimplemented!("{value:?} is not implemented"), + EllipticCurve::P_256 => EC2Curve::P256, + EllipticCurve::P_384 => EC2Curve::P384, + EllipticCurve::P_521 => EC2Curve::P521, + EllipticCurve::X25519 => unimplemented!("{value:?} is not implemented"), + EllipticCurve::X448 => unimplemented!("{value:?} is not implemented"), + EllipticCurve::Ed25519 => unimplemented!("{value:?} is not implemented"), + EllipticCurve::Ed448 => unimplemented!("{value:?} is not implemented"), + EllipticCurve::Secp256k1 => unimplemented!("{value:?} is not implemented"), + _ => unimplemented!("{value:?} is not implemented"), + } + } +} + /// The RFC-8152 identifier of the curve, for OKP key type. #[derive(Debug, Clone, PartialEq, Eq)] pub enum OKPCurve { @@ -40,6 +61,23 @@ pub enum OKPCurve { Ed448, } +impl From for OKPCurve { + fn from(value: EllipticCurve) -> Self { + match value { + EllipticCurve::Reserved => unimplemented!("{value:?} is not implemented"), + EllipticCurve::P_256 => unimplemented!("{value:?} is not implemented"), + EllipticCurve::P_384 => unimplemented!("{value:?} is not implemented"), + EllipticCurve::P_521 => unimplemented!("{value:?} is not implemented"), + EllipticCurve::X25519 => OKPCurve::X25519, + EllipticCurve::X448 => OKPCurve::X448, + EllipticCurve::Ed25519 => OKPCurve::Ed25519, + EllipticCurve::Ed448 => OKPCurve::Ed448, + EllipticCurve::Secp256k1 => unimplemented!("{value:?} is not implemented"), + _ => unimplemented!("{value:?} is not implemented"), + } + } +} + /// Errors that can occur when deserialising a COSE_Key. #[derive(Debug, Clone, thiserror::Error)] pub enum Error { @@ -393,11 +431,34 @@ impl TryFrom<&ssi_jwk::OctetParams> for OKPCurve { } } +impl TryFrom for coset::CoseKey { + type Error = coset::CoseError; + + fn try_from(value: CoseKey) -> Result { + coset::CoseKey::from_slice( + &value + .to_cbor_bytes() + .map_err(|_| coset::CoseError::EncodeFailed)? + .to_vec(), + ) + } +} + +impl TryFrom for CoseKey { + type Error = Error; + + fn try_from(value: coset::CoseKey) -> Result { + serde_cbor::from_slice(&value.to_vec().map_err(|_| Error::InvalidCoseKey)?) + .map_err(|_| Error::InvalidCoseKey) + } +} + #[cfg(test)] mod test { - use super::*; use hex::FromHex; + use super::*; + static EC_P256: &str = include_str!("../../../test/definitions/cose_key/ec_p256.cbor"); #[test] diff --git a/src/definitions/mod.rs b/src/definitions/mod.rs index f146dccf..e5a01783 100644 --- a/src/definitions/mod.rs +++ b/src/definitions/mod.rs @@ -1,3 +1,4 @@ +mod device; pub mod device_engagement; pub mod device_key; pub mod device_request; From 34bd8d6a020ab574eb36dfe5872808968d335c37 Mon Sep 17 00:00:00 2001 From: Radu Marias Date: Mon, 5 Aug 2024 16:52:29 +0300 Subject: [PATCH 21/24] Revert migration to `coset::CoseKey`. Small refactoring. --- src/cose/mac0.rs | 8 +------- src/cose/sign1.rs | 2 +- 2 files changed, 2 insertions(+), 8 deletions(-) diff --git a/src/cose/mac0.rs b/src/cose/mac0.rs index 80f0ce9c..5bc3e1bd 100644 --- a/src/cose/mac0.rs +++ b/src/cose/mac0.rs @@ -1,4 +1,3 @@ -use crate::cose::SignatureAlgorithm; use ::hmac::Hmac; use coset::cbor::Value; use coset::cwt::ClaimsSet; @@ -7,12 +6,11 @@ use coset::{ RegisteredLabelWithPrivate, }; use digest::{Mac, MacError}; -use serde::ser::{SerializeMap, SerializeSeq}; use serde::{ser, Deserialize, Deserializer, Serialize}; -use serde_cbor::tags::Tagged; use sha2::Sha256; use crate::cose::serialize; +use crate::cose::SignatureAlgorithm; /// Prepared `COSE_Mac0` for remote signing. /// @@ -144,10 +142,6 @@ impl PreparedCoseMac0 { (Some(payload), None) => payload, (None, Some(payload)) => payload, }; - let payload = payload - // Payload is mandatory - .as_ref() - .ok_or(Error::NoPayload)?; // Create the signature payload ot be used later on signing. let tag_payload = mac_structure_data( MacContext::CoseMac0, diff --git a/src/cose/sign1.rs b/src/cose/sign1.rs index cf586e39..5b0b83c8 100644 --- a/src/cose/sign1.rs +++ b/src/cose/sign1.rs @@ -153,7 +153,7 @@ impl PreparedCoseSign1 { cose_sign1.protected.clone(), None, aad.unwrap_or_default().as_ref(), - &payload, + payload, ); Ok(Self { From 837cce5a9067235343b503d2d97b0b2837cb4c4a Mon Sep 17 00:00:00 2001 From: Radu Marias Date: Mon, 5 Aug 2024 16:54:07 +0300 Subject: [PATCH 22/24] Fix missing import in docs examples. --- src/cose/mac0.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/cose/mac0.rs b/src/cose/mac0.rs index 5bc3e1bd..5324fe5b 100644 --- a/src/cose/mac0.rs +++ b/src/cose/mac0.rs @@ -28,6 +28,7 @@ use crate::cose::SignatureAlgorithm; /// use digest::Mac; /// use hex::FromHex;use hmac::Hmac; /// use sha2::Sha256; +/// use isomdl::cose::mac0::PreparedCoseMac0; /// /// let key = Vec::::from_hex("a361316953796d6d6574726963613305622d318f187418681869187318201869187318201874186818651820186b18651879").unwrap(); /// let signer = Hmac::::new_from_slice(&key).expect("failed to create HMAC signer"); From c2323de106ac8206c43a875e5751107bc806d348 Mon Sep 17 00:00:00 2001 From: Radu Marias Date: Mon, 5 Aug 2024 22:37:59 +0300 Subject: [PATCH 23/24] Implementation of MCD capability descriptor and SA-Attestation Object (SAAO) as per ISO/IEC JTC 1/SC 17/WG 4 N 4566. --- Cargo.toml | 1 + src/cose/mac0.rs | 16 +- src/issuance/mcd.rs | 435 ++++++++++++++++++++++++++++++++++++++++++++ src/issuance/mod.rs | 1 + 4 files changed, 445 insertions(+), 8 deletions(-) create mode 100644 src/issuance/mcd.rs diff --git a/Cargo.toml b/Cargo.toml index 964c6e5d..e4e18a5f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -45,6 +45,7 @@ clap = { version = "4", features = ["derive"] } clap-stdin = "0.5.1" digest = "0.10.7" serde_bytes = "0.11.15" +enum_stringify = "0.5.0" [dev-dependencies] hex = "0.4.3" diff --git a/src/cose/mac0.rs b/src/cose/mac0.rs index d3d3fbc0..6b2a7576 100644 --- a/src/cose/mac0.rs +++ b/src/cose/mac0.rs @@ -329,7 +329,7 @@ mod tests { #[test] fn tagging() { let key = Vec::::from_hex(COSE_KEY).unwrap(); - let signer = Hmac::::new_from_slice(&key).expect("failed to create HMAC signer"); + let tagger = Hmac::::new_from_slice(&key).expect("failed to create HMAC signer"); let protected = coset::HeaderBuilder::new() .algorithm(iana::Algorithm::HMAC_256_256) .build(); @@ -339,9 +339,9 @@ mod tests { .unprotected(unprotected) .payload(b"This is the content.".to_vec()); let prepared = PreparedCoseMac0::new(builder, None, None, true).unwrap(); - let signature_payload = prepared.signature_payload(); - let signature = tag(signature_payload, &signer).unwrap(); - let cose_mac0 = prepared.finalize(signature); + let tag_payload = prepared.signature_payload(); + let tag = tag(tag_payload, &tagger).unwrap(); + let cose_mac0 = prepared.finalize(tag); let serialized = serde_cbor::to_vec(&cose_mac0).expect("failed to serialize COSE_MAC0 to bytes"); @@ -378,7 +378,7 @@ mod tests { #[test] fn remote_tagging() { let key = Vec::::from_hex(COSE_KEY).unwrap(); - let signer = Hmac::::new_from_slice(&key).expect("failed to create HMAC signer"); + let tagger = Hmac::::new_from_slice(&key).expect("failed to create HMAC signer"); let protected = coset::HeaderBuilder::new() .algorithm(iana::Algorithm::HMAC_256_256) .build(); @@ -388,9 +388,9 @@ mod tests { .unprotected(unprotected) .payload(b"This is the content.".to_vec()); let prepared = PreparedCoseMac0::new(builder, None, None, true).unwrap(); - let signature_payload = prepared.signature_payload(); - let signature = tag(signature_payload, &signer).unwrap(); - let cose_mac0 = prepared.finalize(signature); + let tag_payload = prepared.signature_payload(); + let tag = tag(tag_payload, &tagger).unwrap(); + let cose_mac0 = prepared.finalize(tag); let serialized = serde_cbor::to_vec(&cose_mac0).expect("failed to serialize COSE_MAC0 to bytes"); diff --git a/src/issuance/mcd.rs b/src/issuance/mcd.rs new file mode 100644 index 00000000..4448b961 --- /dev/null +++ b/src/issuance/mcd.rs @@ -0,0 +1,435 @@ +//! Implementation of MCD capability descriptor and SA-Attestation Object (SAAO) as per ISO/IEC JTC 1/SC 17/WG 4 N 4566. +use crate::cose::sign1::CoseSign1; +use serde::{Deserialize, Serialize}; +use std::string::ToString; +use thiserror::Error; +use x509_cert::der::Encode; + +#[derive(Serialize, Deserialize, Debug, PartialEq)] +#[serde(rename_all = "camelCase")] +pub struct MobileIdCapabilityDescriptor { + pub version: u32, + pub mobile_id_application_descriptor: MobileIdApplicationDescriptor, + pub secure_area_attestation_objects: Vec, +} + +#[derive(Serialize, Deserialize, Debug, PartialEq)] +#[serde(rename_all = "camelCase")] +pub struct MobileIdApplicationDescriptor { + pub app_supported_device_features: Option>, + pub app_engagement_interfaces: Option>, + pub app_data_transmission_interface: Option>, + pub certifications: Option>, +} + +#[derive(Serialize, Deserialize, Debug, PartialEq)] +#[serde(rename_all = "camelCase")] +pub enum AppSupportedDeviceFeatures { + WebviewFeature = 0, + SimpleViewFeature = 1, +} + +#[derive(Serialize, Deserialize, Debug, PartialEq)] +#[serde(rename_all = "camelCase")] +pub enum AppEngagementInterfaces { + QR = 0, + Nfc = 1, +} + +#[derive(Serialize, Deserialize, Debug, PartialEq)] +#[serde(rename_all = "camelCase")] +pub enum AppDataTransmissionInterface { + Nfc = 0, + Ble = 1, + WiFiAware = 2, + Internet = 3, +} + +#[derive(Serialize, Deserialize, Debug, PartialEq)] +#[serde(rename_all = "camelCase")] +pub enum Certification { + CommonCriteriaProtectionProfileNumber, + CommonCriteriaCertificationNumber, + #[allow(clippy::enum_variant_names)] + CertificationNumberAccordingToISO19790_2012, + ReferenceToDigitalLetterOfApprovalOfTheSecureAreaPlatform, + ReferenceToDigitalLetterOfApprovalOfTheSAApplication, + SAApplicationCertificationAccordingTo23220_6, +} + +impl TryFrom for String { + type Error = Error; + + fn try_from(value: Certification) -> Result { + match value { + Certification::ReferenceToDigitalLetterOfApprovalOfTheSecureAreaPlatform => { + Ok("3".to_string()) + } + Certification::ReferenceToDigitalLetterOfApprovalOfTheSAApplication => { + Ok("4".to_string()) + } + _ => Err(Error::CertificationConversionError), + } + } +} + +impl TryFrom for Vec { + type Error = Error; + + fn try_from(value: Certification) -> Result { + match value { + Certification::CommonCriteriaProtectionProfileNumber => b"0" + .to_vec() + .map_err(|_| Error::CertificationConversionError), + Certification::CommonCriteriaCertificationNumber => b"1" + .to_vec() + .map_err(|_| Error::CertificationConversionError), + Certification::CertificationNumberAccordingToISO19790_2012 => b"2" + .to_vec() + .map_err(|_| Error::CertificationConversionError), + Certification::SAApplicationCertificationAccordingTo23220_6 => b"5" + .to_vec() + .map_err(|_| Error::CertificationConversionError), + _ => Err(Error::CertificationConversionError), + } + } +} + +#[derive(Debug, Error)] +pub enum Error { + #[error("Failed to convert Certification to corresponding encoding")] + CertificationConversionError, +} + +#[derive(Serialize, Deserialize, Debug, PartialEq)] +#[serde(rename_all = "camelCase")] +pub enum CertificationValue { + String(String), + Number(u32), + Bytes(Vec), +} + +#[derive(Serialize, Deserialize, Debug, PartialEq)] +#[serde(rename_all = "camelCase")] +pub struct SecureAreaAttestationObject { + pub sa_encoding: u32, + pub sa_attestation_object_value: SaAttestationObjectValue, +} + +#[derive(Serialize, Deserialize, Debug, PartialEq)] +#[serde(rename_all = "camelCase")] +pub struct SaAttestationObjectValue { + /// Unique index of SA-Application within MCD + pub sa_index: u32, + /// Determines a basic type of secure area according to ISO/IEC 23220-1 + pub sa_type: Option, + /// SA provided mechanisms for user authentication + pub sa_supported_user_auth: Option>, + /// SA supported cryptographic protocols and key derivation mechanisms + pub sa_crypto_suites: Option>, + /// SA supported cryptographic primitives and key or block sizes + pub sa_crypto_key_definition: Option>, + /// Reference to interface specification of SA-Application to be used by discovery + pub sa_interface: Option, + /// Public part of attestation key of SA-Application, i.e., SA-AttestationPublicKey + pub sa_attestation_key_bytes: Option>, + /// Attestation statement over SA-AttestationPublicKey and SA-AttestationChallenge + pub sa_attestation_statement: Option>, + /// Determines structure and encoding of an attestation statement + /// if an attestation statement is not part of SAAO (see identifier 7) + pub sa_attestation_format: Option, + /// List of certifications issued to the SA Applications + pub certifications: Option>, +} + +#[derive(Serialize, Deserialize, Debug, PartialEq)] +#[serde(rename_all = "camelCase")] +pub enum SaType { + EmbeddedSecureElement = 0, + RemovableSecureElement = 1, + IntegratedSecureElement = 2, + ExternalSecureElement = 3, + TEEBased = 4, + SoftwareComponent = 5, +} + +#[derive(Serialize, Deserialize, Debug, PartialEq)] +#[serde(rename_all = "camelCase")] +pub enum SaInterface { + OrgIso232203BasicSa = 0, + OrgIso232203HpkeSa = 1, + // OrgIso232203GpSa = 1, + OrgIso23220_3YyySa = 2, + ComAndroidIdentityCredential = 3, +} + +#[derive(Serialize, Deserialize, Debug, PartialEq)] +#[serde(rename_all = "camelCase")] +pub enum SaAttestationFormat { + JSONWebToken = 0, +} + +#[allow(dead_code)] +pub type MobileIdCapabilityBytes = (Vec, MobileIdCapabilityDescriptor); + +#[allow(dead_code)] +pub type MobileIdCapability = CoseSign1; + +#[allow(dead_code)] +pub type SaEncoding = u32; + +#[cfg(test)] +mod tests { + use crate::cose::mac0::{CoseMac0, PreparedCoseMac0}; + use crate::cose::sign1::{CoseSign1, Error, PreparedCoseSign1, VerificationResult}; + use crate::cose::{mac0, SignatureAlgorithm}; + use coset::iana; + use digest::Mac; + use hex::FromHex; + use hmac::Hmac; + use p256::ecdsa::{Signature, SigningKey, VerifyingKey}; + use p256::SecretKey; + use sha2::Sha256; + use signature::{SignatureEncoding, Signer}; + + use super::*; + + static COSE_KEY: &str = include_str!("../../test/definitions/cose/sign1/secret_key"); + + #[test] + fn test_serialization_deserialization() { + let app_descriptor = MobileIdApplicationDescriptor { + app_supported_device_features: Some(vec![ + AppSupportedDeviceFeatures::SimpleViewFeature as u32, + ]), + app_engagement_interfaces: Some(vec![AppEngagementInterfaces::QR as u32]), + app_data_transmission_interface: Some(vec![ + AppDataTransmissionInterface::WiFiAware as u32, + ]), + certifications: Some(vec![CertificationValue::String("FIPS".to_string())]), + }; + + let sa_attestation_value = SaAttestationObjectValue { + sa_index: 0, + sa_type: Some(SaType::EmbeddedSecureElement as u32), + sa_supported_user_auth: Some(vec![42]), + sa_crypto_suites: Some(vec![37]), + sa_crypto_key_definition: Some(vec![23]), + sa_interface: Some(SaInterface::ComAndroidIdentityCredential as u32), + sa_attestation_key_bytes: Some(b"42".to_vec().unwrap()), + sa_attestation_statement: Some(b"37".to_vec().unwrap()), + sa_attestation_format: Some(SaAttestationFormat::JSONWebToken as u32), + certifications: Some(vec![CertificationValue::String("FIPS".to_string())]), + }; + + let saao = SecureAreaAttestationObject { + sa_encoding: 0, + sa_attestation_object_value: sa_attestation_value, + }; + + let mcd = MobileIdCapabilityDescriptor { + version: 1, + mobile_id_application_descriptor: app_descriptor, + secure_area_attestation_objects: vec![saao], + }; + + // Serialize to CBOR + let serialized = serde_cbor::to_vec(&mcd).unwrap(); + println!("Serialized MCD: {:?}", serialized); + + // Deserialize from CBOR + let deserialized: MobileIdCapabilityDescriptor = + serde_cbor::from_slice(&serialized).unwrap(); + println!("Deserialized MCD: {:?}", deserialized); + + assert_eq!(mcd, deserialized); + } + + #[test] + fn test_signing_cose_sign1_and_verification() { + let key = Vec::::from_hex(COSE_KEY).unwrap(); + + let app_descriptor = MobileIdApplicationDescriptor { + app_supported_device_features: Some(vec![ + AppSupportedDeviceFeatures::SimpleViewFeature as u32, + ]), + app_engagement_interfaces: Some(vec![AppEngagementInterfaces::QR as u32]), + app_data_transmission_interface: Some(vec![ + AppDataTransmissionInterface::WiFiAware as u32, + ]), + certifications: Some(vec![CertificationValue::String("FIPS".to_string())]), + }; + + let sa_attestation_value = SaAttestationObjectValue { + sa_index: 0, + sa_type: Some(SaType::EmbeddedSecureElement as u32), + sa_supported_user_auth: Some(vec![42]), + sa_crypto_suites: Some(vec![37]), + sa_crypto_key_definition: Some(vec![23]), + sa_interface: Some(SaInterface::ComAndroidIdentityCredential as u32), + sa_attestation_key_bytes: Some(b"42".to_vec().unwrap()), + sa_attestation_statement: Some(b"37".to_vec().unwrap()), + sa_attestation_format: Some(SaAttestationFormat::JSONWebToken as u32), + certifications: Some(vec![CertificationValue::String("FIPS".to_string())]), + }; + + let saao = SecureAreaAttestationObject { + sa_encoding: 0, + sa_attestation_object_value: sa_attestation_value, + }; + + let mcd = MobileIdCapabilityDescriptor { + version: 1, + mobile_id_application_descriptor: app_descriptor, + secure_area_attestation_objects: vec![saao], + }; + + // Serialize to CBOR + let serialized = serde_cbor::to_vec(&mcd).unwrap(); + println!("Serialized MCD: {:?}", serialized); + + // Create a COSE_Sign1 structure + let signer: SigningKey = SecretKey::from_slice(&key).unwrap().into(); + let protected = coset::HeaderBuilder::new() + .algorithm(iana::Algorithm::ES256) + .build(); + let unprotected = coset::HeaderBuilder::new() + .key_id(b"11".to_vec().unwrap()) + .build(); + let builder = coset::CoseSign1Builder::new() + .protected(protected) + .unprotected(unprotected); + let prepared = + PreparedCoseSign1::new(builder, Some(serialized.clone()), None, true).unwrap(); + let signature_payload = prepared.signature_payload(); + let signature = sign::(signature_payload, &signer).unwrap(); + let cose_sign1 = prepared.finalize(signature); + + // Serialize COSE_Sign1 to CBOR + let cose_sign1_serialized = serde_cbor::to_vec(&cose_sign1).unwrap(); + println!("Serialized COSE_Sign1: {:?}", cose_sign1_serialized); + + // Deserialize COSE_Sign1 from CBOR + let cose_sign1_deserialized: CoseSign1 = + serde_cbor::from_slice(&cose_sign1_serialized).unwrap(); + println!("Deserialized COSE_Sign1: {:?}", cose_sign1_deserialized); + + // Verify the signature + let verifier: VerifyingKey = (&signer).into(); + let verification_result = cose_sign1_deserialized.verify::( + &verifier, + Some(serialized), + None, + ); + match &verification_result { + VerificationResult::Success => println!("Signature verification succeeded."), + VerificationResult::Failure(err) => { + println!("Signature verification failed: {:?}", err) + } + VerificationResult::Error(err) => println!("Signature verification failed: {:?}", err), + } + + assert!(verification_result.success()); + } + + #[test] + fn test_tagging_cose_mac0_and_verification() { + let key = Vec::::from_hex(COSE_KEY).unwrap(); + + let app_descriptor = MobileIdApplicationDescriptor { + app_supported_device_features: Some(vec![ + AppSupportedDeviceFeatures::SimpleViewFeature as u32, + ]), + app_engagement_interfaces: Some(vec![AppEngagementInterfaces::QR as u32]), + app_data_transmission_interface: Some(vec![ + AppDataTransmissionInterface::WiFiAware as u32, + ]), + certifications: Some(vec![CertificationValue::String("FIPS".to_string())]), + }; + + let sa_attestation_value = SaAttestationObjectValue { + sa_index: 0, + sa_type: Some(SaType::EmbeddedSecureElement as u32), + sa_supported_user_auth: Some(vec![42]), + sa_crypto_suites: Some(vec![37]), + sa_crypto_key_definition: Some(vec![23]), + sa_interface: Some(SaInterface::ComAndroidIdentityCredential as u32), + sa_attestation_key_bytes: Some(b"42".to_vec().unwrap()), + sa_attestation_statement: Some(b"37".to_vec().unwrap()), + sa_attestation_format: Some(SaAttestationFormat::JSONWebToken as u32), + certifications: Some(vec![CertificationValue::String("FIPS".to_string())]), + }; + + let saao = SecureAreaAttestationObject { + sa_encoding: 0, + sa_attestation_object_value: sa_attestation_value, + }; + + let mcd = MobileIdCapabilityDescriptor { + version: 1, + mobile_id_application_descriptor: app_descriptor, + secure_area_attestation_objects: vec![saao], + }; + + // Serialize to CBOR + let serialized = serde_cbor::to_vec(&mcd).unwrap(); + println!("Serialized MCD: {:?}", serialized); + + let tagger = Hmac::::new_from_slice(&key).expect("failed to create HMAC signer"); + let protected = coset::HeaderBuilder::new() + .algorithm(iana::Algorithm::HMAC_256_256) + .build(); + let unprotected = coset::HeaderBuilder::new() + .key_id(b"11".to_vec().unwrap()) + .build(); + let builder = coset::CoseMac0Builder::new() + .protected(protected) + .unprotected(unprotected); + let prepared = + PreparedCoseMac0::new(builder, Some(serialized.clone()), None, true).unwrap(); + let tag_payload = prepared.signature_payload(); + let signature = tag(tag_payload, &tagger).unwrap(); + let cose_mac0 = prepared.finalize(signature); + + // Serialize COSE_Mac0 to CBOR + let cose_mac0_serialized = serde_cbor::to_vec(&cose_mac0).unwrap(); + println!("Serialized COSE_Mac0: {:?}", cose_mac0_serialized); + + // Deserialize COSE_Sign1 from CBOR + let cose_mac0_deserialized: CoseMac0 = + serde_cbor::from_slice(&cose_mac0_serialized).unwrap(); + println!("Deserialized COSE_Mac0: {:?}", cose_mac0_deserialized); + + // Verify the signature + let verifier = Hmac::::new_from_slice(&key).expect("failed to create HMAC signer"); + let verification_result = cose_mac0_deserialized.verify(&verifier, Some(serialized), None); + match &verification_result { + mac0::VerificationResult::Success => println!("Signature verification succeeded."), + mac0::VerificationResult::Failure(err) => { + println!("Signature verification failed: {:?}", err) + } + mac0::VerificationResult::Error(err) => { + println!("Signature verification failed: {:?}", err) + } + } + + assert!(verification_result.success()); + } + + fn sign(signature_payload: &[u8], s: &S) -> anyhow::Result> + where + S: Signer + SignatureAlgorithm, + Sig: SignatureEncoding, + { + Ok(s.try_sign(signature_payload) + .map_err(Error::Signing)? + .to_vec()) + } + + fn tag(signature_payload: &[u8], s: &Hmac) -> anyhow::Result> { + let mut mac = s.clone(); + mac.reset(); + mac.update(signature_payload); + Ok(mac.finalize().into_bytes().to_vec()) + } +} diff --git a/src/issuance/mod.rs b/src/issuance/mod.rs index 27129fdc..a5fca77c 100644 --- a/src/issuance/mod.rs +++ b/src/issuance/mod.rs @@ -1,3 +1,4 @@ +mod mcd; pub mod mdoc; pub mod x5chain; From a22e550863f0ef93d6275605180b1246a98349b8 Mon Sep 17 00:00:00 2001 From: Radu Marias Date: Wed, 7 Aug 2024 01:11:19 +0300 Subject: [PATCH 24/24] Rename the `CoseMac0` key variables. Removed check if the signature is already present when creating `PreparedCose*` objects. --- src/cose/mac0.rs | 20 +++++++------------- src/cose/sign1.rs | 6 ------ 2 files changed, 7 insertions(+), 19 deletions(-) diff --git a/src/cose/mac0.rs b/src/cose/mac0.rs index 5324fe5b..e6e8f99c 100644 --- a/src/cose/mac0.rs +++ b/src/cose/mac0.rs @@ -129,12 +129,6 @@ impl PreparedCoseMac0 { ) -> Result { let cose_mac0 = builder.build(); - // Check if the signature is already present. - match (&cose_mac0.tag, detached_payload.as_ref()) { - (v, Some(_)) if !v.is_empty() => return Err(Error::AlreadyTagged), - _ => {} - } - // Check if the payload is present and if it is attached or detached. // Needs to be exclusively attached or detached. let payload = match (cose_mac0.payload.as_ref(), detached_payload.as_ref()) { @@ -306,10 +300,10 @@ mod tests { use crate::cose::mac0::{CoseMac0, PreparedCoseMac0}; static COSE_MAC0: &str = include_str!("../../test/definitions/cose/mac0/serialized.cbor"); - static COSE_KEY: &str = include_str!("../../test/definitions/cose/mac0/secret_key"); + static KEY: &str = include_str!("../../test/definitions/cose/mac0/secret_key"); const RFC8392_KEY: &str = "6c1382765aec5358f117733d281c1c7bdc39884d04a45a1e6c67c858bc206c19"; - const RFC8392_COSE_MAC0: &str = "d18443a10126a104524173796d6d657472696345434453413235365850a70175636f61703a2f2f61732e6578616d706c652e636f6d02656572696b77037818636f61703a2f2f6c696768742e6578616d706c652e636f6d041a5612aeb0051a5610d9f0061a5610d9f007420b715820a377dfe17a3c3c3bdb363c426f85d3c1a1f11007765965017602f207700071b0"; + const RFC8392_MAC0: &str = "d18443a10126a104524173796d6d657472696345434453413235365850a70175636f61703a2f2f61732e6578616d706c652e636f6d02656572696b77037818636f61703a2f2f6c696768742e6578616d706c652e636f6d041a5612aeb0051a5610d9f0061a5610d9f007420b715820a377dfe17a3c3c3bdb363c426f85d3c1a1f11007765965017602f207700071b0"; #[test] fn roundtrip() { @@ -327,7 +321,7 @@ mod tests { #[test] fn tagging() { - let key = Vec::::from_hex(COSE_KEY).unwrap(); + let key = Vec::::from_hex(KEY).unwrap(); let signer = Hmac::::new_from_slice(&key).expect("failed to create HMAC signer"); let protected = coset::HeaderBuilder::new() .algorithm(iana::Algorithm::HMAC_256_256) @@ -360,7 +354,7 @@ mod tests { #[test] fn verifying() { - let key = Vec::::from_hex(COSE_KEY).unwrap(); + let key = Vec::::from_hex(KEY).unwrap(); let verifier = Hmac::::new_from_slice(&key).expect("failed to create HMAC verifier"); @@ -376,7 +370,7 @@ mod tests { #[test] fn remote_tagging() { - let key = Vec::::from_hex(COSE_KEY).unwrap(); + let key = Vec::::from_hex(KEY).unwrap(); let signer = Hmac::::new_from_slice(&key).expect("failed to create HMAC signer"); let protected = coset::HeaderBuilder::new() .algorithm(iana::Algorithm::HMAC_256_256) @@ -449,7 +443,7 @@ mod tests { let cose_mac0 = prepared.finalize(signature); let serialized = serde_cbor::to_vec(&cose_mac0).expect("failed to serialize COSE_MAC0 to bytes"); - let expected = hex::decode(RFC8392_COSE_MAC0).unwrap(); + let expected = hex::decode(RFC8392_MAC0).unwrap(); assert_eq!( expected, serialized, @@ -459,7 +453,7 @@ mod tests { #[test] fn deserializing_tdeserializing_signed_cwtagged_cwt() { - let cose_mac0_bytes = hex::decode(RFC8392_COSE_MAC0).unwrap(); + let cose_mac0_bytes = hex::decode(RFC8392_MAC0).unwrap(); let cose_mac0: CoseMac0 = serde_cbor::from_slice(&cose_mac0_bytes).expect("failed to parse COSE_MAC0 from bytes"); let parsed_claims_set = cose_mac0 diff --git a/src/cose/sign1.rs b/src/cose/sign1.rs index 5b0b83c8..1bcdf73c 100644 --- a/src/cose/sign1.rs +++ b/src/cose/sign1.rs @@ -133,12 +133,6 @@ impl PreparedCoseSign1 { ) -> Result { let cose_sign1 = builder.build(); - // Check if the signature is already present. - match (&cose_sign1.signature, detached_payload.as_ref()) { - (v, Some(_)) if !v.is_empty() => return Err(Error::AlreadySigned), - _ => {} - } - // Check if the payload is present and if it is attached or detached. // Needs to be exclusively attached or detached. let payload = match (cose_sign1.payload.as_ref(), detached_payload.as_ref()) {