diff --git a/native/rust/Cargo.toml b/native/rust/Cargo.toml index 0298a4d1..8c3f997c 100644 --- a/native/rust/Cargo.toml +++ b/native/rust/Cargo.toml @@ -1,6 +1,8 @@ [workspace] resolver = "2" members = [ + "cose_openssl/cose_openssl", + "cose_openssl/cose_openssl_ffi", "cose_sign1_validation", "cose_sign1_validation_trust", "cose_sign1_validation_certificates", diff --git a/native/rust/cose_openssl/cose_openssl/Cargo.toml b/native/rust/cose_openssl/cose_openssl/Cargo.toml new file mode 100644 index 00000000..626f1bb0 --- /dev/null +++ b/native/rust/cose_openssl/cose_openssl/Cargo.toml @@ -0,0 +1,21 @@ +[package] +name = "cose_openssl" +version = "0.1.0" +edition = "2024" + +[lib] +crate-type = ["lib"] + +[features] +pqc = [] + +[lints.rust] +warnings = "deny" + +[dependencies] +hex = "0.4" +openssl-sys = "0.9" + +# Once https://github.com/project-everest/everparse/pull/260 is merged, +# change to official mirror. +cborrs-nondet = { git = "https://github.com/maxtropets/everparse.git", branch = "f/unique-cargo-crates" } diff --git a/native/rust/cose_openssl/cose_openssl/src/cose.rs b/native/rust/cose_openssl/cose_openssl/src/cose.rs new file mode 100644 index 00000000..1253b7db --- /dev/null +++ b/native/rust/cose_openssl/cose_openssl/src/cose.rs @@ -0,0 +1,405 @@ +use crate::ossl_wrappers::{ + EvpKey, KeyType, WhichEC, ecdsa_der_to_fixed, ecdsa_fixed_to_der, +}; +use cborrs_nondet::cbornondet::*; + +#[cfg(feature = "pqc")] +use crate::ossl_wrappers::WhichMLDSA; + +const COSE_SIGN1_TAG: u64 = 18; +const COSE_HEADER_ALG: u64 = 1; +const SIG_STRUCTURE1_CONTEXT: &str = "Signature1"; +const CBOR_SIMPLE_VALUE_NULL: u8 = 22; + +fn cbor_serialize(item: CborNondet) -> Result, String> { + let sz = cbor_nondet_size(item, usize::MAX) + .ok_or("Failed to estimate CBOR serialization size")?; + let mut buf = vec![0u8; sz]; + + let written = cbor_nondet_serialize(item, &mut buf) + .ok_or("Failed to serialize CBOR item")?; + + if sz != written { + return Err(format!( + "Failed to serialize CBOR, written {written} != expected {sz}" + )); + } + + Ok(buf) +} + +fn cose_alg(key: &EvpKey) -> Result<(CborNondetIntKind, u64), String> { + // EverCBOR starts counting negs from -1, so Neg 7 is -8, for instance. + // Therefore, substract 1 from the absolute value before convertion. + // + // https://www.iana.org/assignments/cose/cose.xhtml + match &key.typ { + KeyType::EC(WhichEC::P256) => Ok((CborNondetIntKind::NegInt64, 7 - 1)), + KeyType::EC(WhichEC::P384) => Ok((CborNondetIntKind::NegInt64, 35 - 1)), + KeyType::EC(WhichEC::P521) => Ok((CborNondetIntKind::NegInt64, 36 - 1)), + #[cfg(feature = "pqc")] + KeyType::MLDSA(which) => match which { + WhichMLDSA::P44 => Ok((CborNondetIntKind::NegInt64, 48 - 1)), + WhichMLDSA::P65 => Ok((CborNondetIntKind::NegInt64, 49 - 1)), + WhichMLDSA::P87 => Ok((CborNondetIntKind::NegInt64, 50 - 1)), + }, + } +} + +/// Parse a COSE_Sign1 envelope and return (phdr, payload, signature). +fn parse_cose_sign1<'a>( + envelope: &'a [u8], +) -> Result<(CborNondet<'a>, CborNondet<'a>, CborNondet<'a>), String> { + let (tag, _) = cbor_nondet_parse(None, false, envelope) + .ok_or("Failed to parse COSE envelope")?; + + let rest = match cbor_nondet_destruct(tag) { + CborNondetView::Tagged { tag, payload } if tag == COSE_SIGN1_TAG => { + Ok(payload) + } + CborNondetView::Tagged { tag, .. } => Err(format!( + "Wrong COSE tag: expected {COSE_SIGN1_TAG}, got {tag}" + )), + _ => Err("Expected COSE_Sign1 tagged item".to_string()), + }?; + + let arr = match cbor_nondet_destruct(rest) { + CborNondetView::Array { _0 } => Ok(_0), + _ => Err("Expected COSE_Sign1 array inside tag".to_string()), + }?; + + if cbor_nondet_get_array_length(arr) != 4 { + return Err("COSE_Sign1 array length is not 4".to_string()); + } + + let phdr = cbor_nondet_get_array_item(arr, 0) + .ok_or("Failed to get protected header from COSE array")?; + let payload = cbor_nondet_get_array_item(arr, 2) + .ok_or("Failed to get payload from COSE array")?; + let signature = cbor_nondet_get_array_item(arr, 3) + .ok_or("Failed to get signature from COSE array")?; + + Ok((phdr, payload, signature)) +} + +/// Insert alg(1), return error if already exists. +fn insert_alg(key: &EvpKey, phdr: &[u8]) -> Result, String> { + let (parsed, _) = cbor_nondet_parse(None, false, phdr) + .ok_or("Failed to parse protected header map")?; + + let entries = match cbor_nondet_destruct(parsed) { + CborNondetView::Map { _0 } => Ok(_0), + _ => Err("Protected header is not a CBOR map".to_string()), + }?; + + let alg_label = + cbor_nondet_mk_int64(CborNondetIntKind::UInt64, COSE_HEADER_ALG); + if cbor_nondet_map_get(entries, alg_label).is_some() { + return Err("Algorithm already set in protected header".to_string()); + } + + // Insert alg(1) to the beginning. + let (kind, val) = cose_alg(key)?; + let mut map = Vec::::new(); + map.push(cbor_nondet_mk_map_entry( + cbor_nondet_mk_int64(CborNondetIntKind::UInt64, COSE_HEADER_ALG), + cbor_nondet_mk_int64(kind, val), + )); + + for entry in entries { + map.push(entry); + } + + let map = cbor_nondet_mk_map(&mut map) + .ok_or("Failed to build protected header map")?; + + cbor_serialize(map) +} + +/// To-be-signed (TBS). +/// https://www.rfc-editor.org/rfc/rfc9052.html#section-4.4. +fn sig_structure(phdr: &[u8], payload: &[u8]) -> Result, String> { + let items = [ + cbor_nondet_mk_text_string(SIG_STRUCTURE1_CONTEXT) + .ok_or("Failed to make Sig_structure context string")?, + cbor_nondet_mk_byte_string(phdr) + .ok_or("Failed to make protected header byte string")?, + cbor_nondet_mk_byte_string(&[]) + .ok_or("Failed to make external AAD byte string")?, + cbor_nondet_mk_byte_string(payload) + .ok_or("Failed to make payload byte string")?, + ]; + let arr = + cbor_nondet_mk_array(&items).ok_or("Failed to build TBS array")?; + + cbor_serialize(arr) +} + +/// Produce a COSE_Sign1 envelope. +pub fn cose_sign1( + key: &EvpKey, + phdr: &[u8], + uhdr: &[u8], + payload: &[u8], + detached: bool, +) -> Result, String> { + let phdr_bytes = insert_alg(key, phdr)?; + let tbs = sig_structure(&phdr_bytes, payload)?; + let sig = crate::sign::sign(key, &tbs)?; + + let sig = match &key.typ { + KeyType::EC(_) => ecdsa_der_to_fixed(&sig, key.ec_field_size()?)?, + #[cfg(feature = "pqc")] + KeyType::MLDSA(_) => sig, + }; + + let payload_item = if detached { + cbor_nondet_mk_simple_value(CBOR_SIMPLE_VALUE_NULL) + .ok_or("Failed to make CBOR null for detached payload")? + } else { + cbor_nondet_mk_byte_string(payload) + .ok_or("Failed to make payload byte string")? + }; + + // Parse uhdr so we can embed it as-is. + let (uhdr_item, _) = cbor_nondet_parse(None, false, uhdr) + .ok_or("Failed to parse unprotected header")?; + + let arr = [ + cbor_nondet_mk_byte_string(&phdr_bytes) + .ok_or("Failed to make protected header byte string")?, + uhdr_item, + payload_item, + cbor_nondet_mk_byte_string(&sig) + .ok_or("Failed to make signature byte string")?, + ]; + + let inner = + cbor_nondet_mk_array(&arr).ok_or("Failed to build COSE_Sign1 array")?; + let tagged = cbor_nondet_mk_tagged(COSE_SIGN1_TAG, &inner); + + cbor_serialize(tagged) +} + +/// Check that the algorithm encoded in the phdr matches the key type. +fn check_phdr_alg(key: &EvpKey, phdr_bytes: &[u8]) -> Result<(), String> { + let (parsed, _) = cbor_nondet_parse(None, false, phdr_bytes) + .ok_or("Failed to parse protected header for algorithm check")?; + let entries = match cbor_nondet_destruct(parsed) { + CborNondetView::Map { _0 } => Ok(_0), + _ => Err("Protected header is not a CBOR map".to_string()), + }?; + + let alg_label = + cbor_nondet_mk_int64(CborNondetIntKind::UInt64, COSE_HEADER_ALG); + let alg_item = cbor_nondet_map_get(entries, alg_label) + .ok_or("Algorithm not found in protected header")?; + + let (phdr_kind, phdr_val) = + match cbor_nondet_destruct(alg_item) { + CborNondetView::Int64 { kind, value } => Ok((kind, value)), + _ => Err("Algorithm value in protected header is not an integer" + .to_string()), + }?; + + let (key_kind, key_val) = cose_alg(key)?; + if phdr_kind != key_kind || phdr_val != key_val { + return Err( + "Algorithm mismatch between protected header and key".to_string() + ); + } + Ok(()) +} + +/// Verify a COSE_Sign1 envelope. If `payload` is `Some`, it is used +/// as the detached payload; otherwise the embedded payload is used. +pub fn cose_verify1( + key: &EvpKey, + envelope: &[u8], + payload: Option<&[u8]>, +) -> Result { + let (cose_phdr, cose_payload, cose_sig) = parse_cose_sign1(envelope)?; + + let phdr_bytes = match cbor_nondet_destruct(cose_phdr) { + CborNondetView::ByteString { payload } => Ok(payload.to_vec()), + _ => Err("Protected header is not a byte string".to_string()), + }?; + + check_phdr_alg(key, &phdr_bytes)?; + + let actual_payload = match payload { + Some(p) => p.to_vec(), + None => match cbor_nondet_destruct(cose_payload) { + CborNondetView::ByteString { payload } => Ok(payload.to_vec()), + _ => Err("Embedded payload is not a byte string".to_string()), + }?, + }; + + let sig = match cbor_nondet_destruct(cose_sig) { + CborNondetView::ByteString { payload } => Ok(payload.to_vec()), + _ => Err("Signature is not a byte string".to_string()), + }?; + + let sig = match &key.typ { + KeyType::EC(_) => ecdsa_fixed_to_der(&sig, key.ec_field_size()?)?, + #[cfg(feature = "pqc")] + KeyType::MLDSA(_) => sig, + }; + + let tbs = sig_structure(&phdr_bytes, &actual_payload)?; + crate::verify::verify(key, &sig, &tbs) +} + +#[cfg(test)] +mod tests { + use super::*; + use hex; + + const TEST_PHDR: &str = "A319018B020FA3061A698B72820173736572766963652E6578616D706C652E636F6D02706C65646765722E7369676E6174757265666363662E7631A1647478696465322E313334"; + + #[test] + fn test_parse_cose() { + let in_str = "d284588da50138220458406661363331386532666561643537313035326231383230393236653865653531313030623630633161383239393362333031353133383561623334343237303019018b020fa3061a698b72820173736572766963652e6578616d706c652e636f6d02706c65646765722e7369676e6174757265666363662e7631a1647478696465322e313334a119018ca12081590100a2018358204208b5b5378c253f49641ab2edb58b557c75cdbb85ae9327930362c84ebba694784963653a322e3133333a3066646666336265663338346237383231316363336434306463363333663336383364353963643930303864613037653030623266356464323734613365633758200000000000000000000000000000000000000000000000000000000000000000028382f5582081980abb4e161b2f3d306c185ef9f7ce84cf5a3b0c8978da82e049d761adfd0082f55820610e8b89721667f99305e7ce4befe0b3b393821a3f72713f89961ebc7e81de6382f55820cbe0d3307b00aa9f324e29c8fb26508404af81044c7adcd4f5b41043d92aff23f6586005784bfccce87452a35a0cd14df5ed8a38c8937f63fb6b522fb94a1551c0e061893bb35fba1fa6fea322b080a14c0894c3864bf4e76df04ffb0f7c350366f91c0d522652d8fa3ebad6ba0270b48e43a065312c759d8bc9a413d4270d5ba86182"; + let v = hex::decode(in_str).unwrap(); + let (_phdr, _payload, _sig) = parse_cose_sign1(&v).unwrap(); + } + + #[test] + fn test_insert_alg() { + let key = EvpKey::new(KeyType::EC(WhichEC::P256)).unwrap(); + let phdr = hex::decode(TEST_PHDR).unwrap(); + let phdr = insert_alg(&key, &phdr).unwrap(); + + // Parse result and verify alg is present. + let (parsed, _) = cbor_nondet_parse(None, false, &phdr).unwrap(); + let entries = match cbor_nondet_destruct(parsed) { + CborNondetView::Map { _0 } => _0, + _ => panic!("Expected map"), + }; + + // Check alg. + let alg_label = + cbor_nondet_mk_int64(CborNondetIntKind::UInt64, COSE_HEADER_ALG); + let alg_item = cbor_nondet_map_get(entries, alg_label) + .expect("Algorithm not found in protected header"); + let (kind, val) = match cbor_nondet_destruct(alg_item) { + CborNondetView::Int64 { kind, value } => (kind, value), + _ => panic!("Algorithm value is not an integer"), + }; + let (expected_kind, expected_val) = cose_alg(&key).unwrap(); + assert!(kind == expected_kind); + assert!(val == expected_val); + + // Inserting again must fail. + assert!(insert_alg(&key, &phdr).is_err()); + } + + fn sign_verify_cose(key_type: KeyType) { + let key = EvpKey::new(key_type).unwrap(); + let phdr = hex::decode(TEST_PHDR).unwrap(); + let uhdr = b"\xa0"; // empty map + let payload = b"Good boy..."; + + let envelope = cose_sign1(&key, &phdr, uhdr, payload, false).unwrap(); + assert!(cose_verify1(&key, &envelope, None).unwrap()); + } + + #[test] + fn cose_ec_p256() { + sign_verify_cose(KeyType::EC(WhichEC::P256)); + } + + #[test] + fn cose_ec_p384() { + sign_verify_cose(KeyType::EC(WhichEC::P384)); + } + + #[test] + fn cose_ec_p521() { + sign_verify_cose(KeyType::EC(WhichEC::P521)); + } + + #[test] + fn cose_detached_payload() { + let key = EvpKey::new(KeyType::EC(WhichEC::P256)).unwrap(); + let phdr = hex::decode(TEST_PHDR).unwrap(); + let uhdr = b"\xa0"; // empty map + let payload = b"Good boy..."; + + let envelope = cose_sign1(&key, &phdr, uhdr, payload, true).unwrap(); + + // Verify with the detached payload supplied externally. + assert!(cose_verify1(&key, &envelope, Some(payload)).unwrap()); + + // Verify without supplying the payload must fail. + assert!(cose_verify1(&key, &envelope, None).is_err()); + } + + #[test] + fn cose_with_der_imported_key() { + // Create key pair + let original_key = EvpKey::new(KeyType::EC(WhichEC::P384)).unwrap(); + + // Export private key to DER and reimport for signing + let priv_der = original_key.to_der_private().unwrap(); + let signing_key = EvpKey::from_der_private(&priv_der).unwrap(); + + // Export public key DER and reimport for verification + let pub_der = original_key.to_der_public().unwrap(); + let verification_key = EvpKey::from_der_public(&pub_der).unwrap(); + + let phdr = hex::decode(TEST_PHDR).unwrap(); + let uhdr = b"\xa0"; + let payload = b"test with DER-imported key"; + + // Sign with DER-reimported private key + let envelope = + cose_sign1(&signing_key, &phdr, uhdr, payload, false).unwrap(); + + // Verify with DER-imported public key + assert!(cose_verify1(&verification_key, &envelope, None).unwrap()); + } + + #[cfg(feature = "pqc")] + mod pqc_tests { + use super::*; + #[test] + fn cose_mldsa44() { + sign_verify_cose(KeyType::MLDSA(WhichMLDSA::P44)); + } + #[test] + fn cose_mldsa65() { + sign_verify_cose(KeyType::MLDSA(WhichMLDSA::P65)); + } + #[test] + fn cose_mldsa87() { + sign_verify_cose(KeyType::MLDSA(WhichMLDSA::P87)); + } + + #[test] + fn cose_mldsa_with_der_imported_key() { + // Create ML-DSA key pair + let original_key = + EvpKey::new(KeyType::MLDSA(WhichMLDSA::P65)).unwrap(); + + // Export private key to DER and reimport for signing + let priv_der = original_key.to_der_private().unwrap(); + let signing_key = EvpKey::from_der_private(&priv_der).unwrap(); + + // Export public key DER and reimport for verification + let pub_der = original_key.to_der_public().unwrap(); + let verification_key = EvpKey::from_der_public(&pub_der).unwrap(); + + let phdr = hex::decode(TEST_PHDR).unwrap(); + let uhdr = b"\xa0"; + let payload = b"ML-DSA with DER-imported key"; + + // Sign with DER-reimported private key + let envelope = + cose_sign1(&signing_key, &phdr, uhdr, payload, false).unwrap(); + + // Verify with DER-imported public key + assert!(cose_verify1(&verification_key, &envelope, None).unwrap()); + } + } +} diff --git a/native/rust/cose_openssl/cose_openssl/src/lib.rs b/native/rust/cose_openssl/cose_openssl/src/lib.rs new file mode 100644 index 00000000..dad23cee --- /dev/null +++ b/native/rust/cose_openssl/cose_openssl/src/lib.rs @@ -0,0 +1,10 @@ +mod cose; +mod ossl_wrappers; +mod sign; +mod verify; + +pub use cose::{cose_sign1, cose_verify1}; +pub use ossl_wrappers::{EvpKey, KeyType, WhichEC}; + +#[cfg(feature = "pqc")] +pub use ossl_wrappers::WhichMLDSA; diff --git a/native/rust/cose_openssl/cose_openssl/src/ossl_wrappers.rs b/native/rust/cose_openssl/cose_openssl/src/ossl_wrappers.rs new file mode 100644 index 00000000..419df464 --- /dev/null +++ b/native/rust/cose_openssl/cose_openssl/src/ossl_wrappers.rs @@ -0,0 +1,648 @@ +use openssl_sys as ossl; +use std::ffi::CString; +use std::marker::PhantomData; +use std::ptr; + +// Not exposed by openssl-sys 0.9, but available at link time (OpenSSL 3.0+). +unsafe extern "C" { + fn EVP_PKEY_is_a( + pkey: *const ossl::EVP_PKEY, + name: *const std::ffi::c_char, + ) -> std::ffi::c_int; + + fn EVP_PKEY_get_group_name( + pkey: *const ossl::EVP_PKEY, + name: *mut std::ffi::c_char, + name_sz: usize, + gname_len: *mut usize, + ) -> std::ffi::c_int; +} + +#[cfg(feature = "pqc")] +#[derive(Debug)] +pub enum WhichMLDSA { + P44, + P65, + P87, +} + +#[cfg(feature = "pqc")] +impl WhichMLDSA { + fn openssl_str(&self) -> &'static str { + match self { + WhichMLDSA::P44 => "ML-DSA-44", + WhichMLDSA::P65 => "ML-DSA-65", + WhichMLDSA::P87 => "ML-DSA-87", + } + } +} + +#[derive(Debug)] +pub enum WhichEC { + P256, + P384, + P521, +} + +impl WhichEC { + fn openssl_str(&self) -> &'static str { + match self { + WhichEC::P256 => "P-256", + WhichEC::P384 => "P-384", + WhichEC::P521 => "P-521", + } + } + + fn openssl_group(&self) -> &'static str { + match self { + WhichEC::P256 => "prime256v1", + WhichEC::P384 => "secp384r1", + WhichEC::P521 => "secp521r1", + } + } +} + +#[derive(Debug)] +pub enum KeyType { + EC(WhichEC), + + #[cfg(feature = "pqc")] + MLDSA(WhichMLDSA), +} + +#[derive(Debug)] +pub struct EvpKey { + pub key: *mut ossl::EVP_PKEY, + pub typ: KeyType, +} + +impl EvpKey { + pub fn new(typ: KeyType) -> Result { + unsafe { + let key = match &typ { + KeyType::EC(which) => { + let crv = CString::new(which.openssl_str()).unwrap(); + let alg = CString::new("EC").unwrap(); + ossl::EVP_PKEY_Q_keygen( + ptr::null_mut(), + ptr::null_mut(), + alg.as_ptr(), + crv.as_ptr(), + ) + } + + #[cfg(feature = "pqc")] + KeyType::MLDSA(which) => { + let alg = CString::new(which.openssl_str()).unwrap(); + ossl::EVP_PKEY_Q_keygen( + ptr::null_mut(), + ptr::null_mut(), + alg.as_ptr(), + ) + } + }; + + if key.is_null() { + return Err("Failed to create signing key".to_string()); + } + + Ok(EvpKey { key, typ }) + } + } + + /// Create an `EvpKey` from a DER-encoded SubjectPublicKeyInfo. + /// Automatically detects key type (EC curve or ML-DSA variant). + pub fn from_der_public(der: &[u8]) -> Result { + let key = unsafe { + let mut ptr = der.as_ptr(); + let key = + ossl::d2i_PUBKEY(ptr::null_mut(), &mut ptr, der.len() as i64); + if key.is_null() { + return Err("Failed to parse DER public key".to_string()); + } + key + }; + + let typ = match Self::detect_key_type_raw(key) { + Ok(t) => t, + Err(e) => { + unsafe { + ossl::EVP_PKEY_free(key); + } + return Err(e); + } + }; + + Ok(EvpKey { key, typ }) + } + + /// Create an `EvpKey` from a DER-encoded private key + /// (PKCS#8 or traditional format). + /// Automatically detects key type (EC curve or ML-DSA variant). + pub fn from_der_private(der: &[u8]) -> Result { + let key = unsafe { + let mut ptr = der.as_ptr(); + let key = ossl::d2i_AutoPrivateKey( + ptr::null_mut(), + &mut ptr, + der.len() as i64, + ); + if key.is_null() { + return Err("Failed to parse DER private key".to_string()); + } + key + }; + + let typ = match Self::detect_key_type_raw(key) { + Ok(t) => t, + Err(e) => { + unsafe { + ossl::EVP_PKEY_free(key); + } + return Err(e); + } + }; + + Ok(EvpKey { key, typ }) + } + + fn detect_key_type_raw( + pkey: *mut ossl::EVP_PKEY, + ) -> Result { + unsafe { + let ec = CString::new("EC").unwrap(); + if EVP_PKEY_is_a(pkey as *const _, ec.as_ptr()) == 1 { + let mut buf = [0u8; 64]; + let mut len: usize = 0; + if EVP_PKEY_get_group_name( + pkey as *const _, + buf.as_mut_ptr() as *mut std::ffi::c_char, + buf.len(), + &mut len, + ) != 1 + { + return Err("Failed to get EC group name".to_string()); + } + let group = std::str::from_utf8(&buf[..len]) + .map_err(|_| "EC group name is not UTF-8".to_string())?; + + for variant in [WhichEC::P256, WhichEC::P384, WhichEC::P521] { + if group == variant.openssl_group() { + return Ok(KeyType::EC(variant)); + } + } + return Err(format!("Unsupported EC curve: {}", group)); + } + + #[cfg(feature = "pqc")] + for variant in [WhichMLDSA::P44, WhichMLDSA::P65, WhichMLDSA::P87] { + let cname = CString::new(variant.openssl_str()).unwrap(); + if EVP_PKEY_is_a(pkey as *const _, cname.as_ptr()) == 1 { + return Ok(KeyType::MLDSA(variant)); + } + } + + Err("Unsupported key type".to_string()) + } + } + + /// Export the public key as DER-encoded SubjectPublicKeyInfo. + pub fn to_der_public(&self) -> Result, String> { + unsafe { + let mut der_ptr: *mut u8 = ptr::null_mut(); + let len = ossl::i2d_PUBKEY(self.key, &mut der_ptr); + + if len <= 0 || der_ptr.is_null() { + return Err(format!( + "Failed to encode public key to DER (rc={})", + len + )); + } + + // Copy the DER data into a Vec and free the OpenSSL-allocated memory + let der_slice = std::slice::from_raw_parts(der_ptr, len as usize); + let der = der_slice.to_vec(); + ossl::CRYPTO_free( + der_ptr as *mut std::ffi::c_void, + concat!(file!(), "\0").as_ptr() as *const i8, + line!() as i32, + ); + + Ok(der) + } + } + + /// Export the private key as DER-encoded traditional format. + pub fn to_der_private(&self) -> Result, String> { + unsafe { + let mut der_ptr: *mut u8 = ptr::null_mut(); + let len = ossl::i2d_PrivateKey(self.key, &mut der_ptr); + + if len <= 0 || der_ptr.is_null() { + return Err(format!( + "Failed to encode private key to DER (rc={})", + len + )); + } + + let der_slice = std::slice::from_raw_parts(der_ptr, len as usize); + let der = der_slice.to_vec(); + ossl::CRYPTO_free( + der_ptr as *mut std::ffi::c_void, + concat!(file!(), "\0").as_ptr() as *const i8, + line!() as i32, + ); + + Ok(der) + } + } + + /// Compute the EC field-element byte size from the key's bit size. + /// Returns an error if the key is not an EC key. + pub fn ec_field_size(&self) -> Result { + if !matches!(self.typ, KeyType::EC(_)) { + return Err("ec_field_size called on a non-EC key".to_string()); + } + unsafe { + let bits = ossl::EVP_PKEY_bits(self.key); + if bits <= 0 { + return Err("EVP_PKEY_bits failed".to_string()); + } + Ok(((bits + 7) / 8) as usize) + } + } + + /// Return the OpenSSL digest matching the key's COSE algorithm. + /// Returns null for algorithms that do not use a separate digest + /// (e.g. ML-DSA). + pub fn digest(&self) -> *const ossl::EVP_MD { + unsafe { + match &self.typ { + KeyType::EC(WhichEC::P256) => ossl::EVP_sha256(), + KeyType::EC(WhichEC::P384) => ossl::EVP_sha384(), + KeyType::EC(WhichEC::P521) => ossl::EVP_sha512(), + #[cfg(feature = "pqc")] + KeyType::MLDSA(_) => ptr::null(), + } + } + } +} + +impl Drop for EvpKey { + fn drop(&mut self) { + unsafe { + if !self.key.is_null() { + ossl::EVP_PKEY_free(self.key); + } + } + } +} + +// --------------------------------------------------------------------------- +// ECDSA signature format conversion (DER <-> IEEE P1363 fixed-size) +// using OpenSSL's ECDSA_SIG API. +// +// OpenSSL produces/consumes DER-encoded ECDSA signatures: +// SEQUENCE { INTEGER r, INTEGER s } +// +// COSE (RFC 9053) requires the fixed-size (r || s) representation. +// --------------------------------------------------------------------------- + +/// Convert a DER-encoded ECDSA signature to fixed-size (r || s). +pub fn ecdsa_der_to_fixed( + der: &[u8], + field_size: usize, +) -> Result, String> { + unsafe { + let mut p = der.as_ptr(); + let sig = ossl::d2i_ECDSA_SIG( + ptr::null_mut(), + &mut p, + der.len() as std::ffi::c_long, + ); + if sig.is_null() { + return Err("Failed to parse DER ECDSA signature".to_string()); + } + + let mut r: *const ossl::BIGNUM = ptr::null(); + let mut s: *const ossl::BIGNUM = ptr::null(); + ossl::ECDSA_SIG_get0(sig, &mut r, &mut s); + + let mut fixed = vec![0u8; field_size * 2]; + let rc_r = ossl::BN_bn2binpad( + r, + fixed.as_mut_ptr(), + field_size as std::ffi::c_int, + ); + let rc_s = ossl::BN_bn2binpad( + s, + fixed[field_size..].as_mut_ptr(), + field_size as std::ffi::c_int, + ); + ossl::ECDSA_SIG_free(sig); + + if rc_r != field_size as std::ffi::c_int + || rc_s != field_size as std::ffi::c_int + { + return Err("BN_bn2binpad failed for ECDSA r or s".to_string()); + } + + Ok(fixed) + } +} + +/// Convert a fixed-size (r || s) ECDSA signature to DER. +pub fn ecdsa_fixed_to_der( + fixed: &[u8], + field_size: usize, +) -> Result, String> { + if fixed.len() != field_size * 2 { + return Err(format!( + "Expected {} byte ECDSA signature, got {}", + field_size * 2, + fixed.len() + )); + } + + unsafe { + let r = ossl::BN_bin2bn( + fixed.as_ptr(), + field_size as std::ffi::c_int, + ptr::null_mut(), + ); + if r.is_null() { + return Err("BN_bin2bn failed for ECDSA r".to_string()); + } + + let s = ossl::BN_bin2bn( + fixed[field_size..].as_ptr(), + field_size as std::ffi::c_int, + ptr::null_mut(), + ); + if s.is_null() { + ossl::BN_free(r); + return Err("BN_bin2bn failed for ECDSA s".to_string()); + } + + let sig = ossl::ECDSA_SIG_new(); + if sig.is_null() { + ossl::BN_free(r); + ossl::BN_free(s); + return Err("ECDSA_SIG_new failed".to_string()); + } + + if ossl::ECDSA_SIG_set0(sig, r, s) != 1 { + ossl::ECDSA_SIG_free(sig); + ossl::BN_free(r); + ossl::BN_free(s); + return Err("ECDSA_SIG_set0 failed".to_string()); + } + // ECDSA_SIG_set0 takes ownership of r and s on success. + + let mut out_ptr: *mut u8 = ptr::null_mut(); + let len = ossl::i2d_ECDSA_SIG(sig, &mut out_ptr); + ossl::ECDSA_SIG_free(sig); + + if len <= 0 || out_ptr.is_null() { + return Err("i2d_ECDSA_SIG failed".to_string()); + } + + let der = std::slice::from_raw_parts(out_ptr, len as usize).to_vec(); + ossl::CRYPTO_free( + out_ptr as *mut std::ffi::c_void, + concat!(file!(), "\0").as_ptr() as *const i8, + line!() as i32, + ); + + Ok(der) + } +} + +#[derive(Debug)] +pub struct EvpMdContext { + op: PhantomData, + pub ctx: *mut ossl::EVP_MD_CTX, +} + +pub struct SignOp; +pub struct VerifyOp; + +pub trait ContextInit { + fn init( + ctx: *mut ossl::EVP_MD_CTX, + md: *const ossl::EVP_MD, + key: *mut ossl::EVP_PKEY, + ) -> Result<(), i32>; + fn purpose() -> &'static str; +} + +impl ContextInit for SignOp { + fn init( + ctx: *mut ossl::EVP_MD_CTX, + md: *const ossl::EVP_MD, + key: *mut ossl::EVP_PKEY, + ) -> Result<(), i32> { + unsafe { + let rc = ossl::EVP_DigestSignInit( + ctx, + ptr::null_mut(), + md, + ptr::null_mut(), + key, + ); + match rc { + 1 => Ok(()), + err => Err(err), + } + } + } + fn purpose() -> &'static str { + "Sign" + } +} + +impl ContextInit for VerifyOp { + fn init( + ctx: *mut ossl::EVP_MD_CTX, + md: *const ossl::EVP_MD, + key: *mut ossl::EVP_PKEY, + ) -> Result<(), i32> { + unsafe { + let rc = ossl::EVP_DigestVerifyInit( + ctx, + ptr::null_mut(), + md, + ptr::null_mut(), + key, + ); + match rc { + 1 => Ok(()), + err => Err(err), + } + } + } + fn purpose() -> &'static str { + "Verify" + } +} + +impl EvpMdContext { + pub fn new(key: &EvpKey) -> Result { + unsafe { + let ctx = ossl::EVP_MD_CTX_new(); + if ctx.is_null() { + return Err(format!( + "Failed to create ctx for: {}", + T::purpose() + )); + } + if let Err(err) = T::init(ctx, key.digest(), key.key) { + ossl::EVP_MD_CTX_free(ctx); + return Err(format!( + "Failed to init context for {} with err {}", + T::purpose(), + err + )); + } + Ok(EvpMdContext { + op: PhantomData, + ctx, + }) + } + } +} + +impl Drop for EvpMdContext { + fn drop(&mut self) { + unsafe { + if !self.ctx.is_null() { + ossl::EVP_MD_CTX_free(self.ctx); + } + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + #[test] + #[cfg(feature = "pqc")] + fn create_ml_dsa_keys() { + assert!(EvpKey::new(KeyType::MLDSA(WhichMLDSA::P44)).is_ok()); + assert!(EvpKey::new(KeyType::MLDSA(WhichMLDSA::P65)).is_ok()); + assert!(EvpKey::new(KeyType::MLDSA(WhichMLDSA::P87)).is_ok()); + } + + #[test] + fn create_ec_keys() { + assert!(EvpKey::new(KeyType::EC(WhichEC::P256)).is_ok()); + assert!(EvpKey::new(KeyType::EC(WhichEC::P384)).is_ok()); + assert!(EvpKey::new(KeyType::EC(WhichEC::P521)).is_ok()); + } + + #[test] + fn ec_key_from_der_roundtrip() { + for which in [WhichEC::P256, WhichEC::P384, WhichEC::P521] { + let key = EvpKey::new(KeyType::EC(which)).unwrap(); + let der = key.to_der_public().unwrap(); + let imported = EvpKey::from_der_public(&der).unwrap(); + assert!( + matches!(imported.typ, KeyType::EC(_)), + "Expected EC key type" + ); + + // Verify the reimported key exports the same DER + let der2 = imported.to_der_public().unwrap(); + assert_eq!(der, der2); + } + } + + #[test] + fn ec_key_from_der_p256() { + let key = EvpKey::new(KeyType::EC(WhichEC::P256)).unwrap(); + let der = key.to_der_public().unwrap(); + let imported = EvpKey::from_der_public(&der).unwrap(); + + assert!(matches!(imported.typ, KeyType::EC(WhichEC::P256))); + } + + #[test] + fn from_der_rejects_garbage() { + assert!(EvpKey::from_der_public(&[0xde, 0xad, 0xbe, 0xef]).is_err()); + } + + #[test] + fn from_der_private_rejects_garbage() { + assert!(EvpKey::from_der_private(&[0xde, 0xad, 0xbe, 0xef]).is_err()); + } + + #[test] + fn ec_key_private_der_roundtrip() { + for which in [WhichEC::P256, WhichEC::P384, WhichEC::P521] { + let key = EvpKey::new(KeyType::EC(which)).unwrap(); + let priv_der = key.to_der_private().unwrap(); + let imported = EvpKey::from_der_private(&priv_der).unwrap(); + assert!( + matches!(imported.typ, KeyType::EC(_)), + "Expected EC key type" + ); + + // Private key re-export must be identical. + let priv_der2 = imported.to_der_private().unwrap(); + assert_eq!(priv_der, priv_der2); + + // Public key extracted from the reimported private key must + // match the original. + let pub1 = key.to_der_public().unwrap(); + let pub2 = imported.to_der_public().unwrap(); + assert_eq!(pub1, pub2); + } + } + + #[test] + #[cfg(feature = "pqc")] + fn ml_dsa_key_from_der_roundtrip() { + for which in [WhichMLDSA::P44, WhichMLDSA::P65, WhichMLDSA::P87] { + let key = EvpKey::new(KeyType::MLDSA(which)).unwrap(); + let der = key.to_der_public().unwrap(); + let imported = EvpKey::from_der_public(&der).unwrap(); + assert!( + matches!(imported.typ, KeyType::MLDSA(_)), + "Expected ML-DSA key type" + ); + let der2 = imported.to_der_public().unwrap(); + assert_eq!(der, der2); + } + } + + #[test] + #[cfg(feature = "pqc")] + fn ml_dsa_key_private_der_roundtrip() { + for which in [WhichMLDSA::P44, WhichMLDSA::P65, WhichMLDSA::P87] { + let key = EvpKey::new(KeyType::MLDSA(which)).unwrap(); + let priv_der = key.to_der_private().unwrap(); + let imported = EvpKey::from_der_private(&priv_der).unwrap(); + assert!( + matches!(imported.typ, KeyType::MLDSA(_)), + "Expected ML-DSA key type" + ); + + // Private key re-export must be identical. + let priv_der2 = imported.to_der_private().unwrap(); + assert_eq!(priv_der, priv_der2); + + let pub1 = key.to_der_public().unwrap(); + let pub2 = imported.to_der_public().unwrap(); + assert_eq!(pub1, pub2); + } + } + + #[test] + #[ignore] + fn intentional_leak_for_sanitizer_validation() { + // This test intentionally leaks memory to verify sanitizers + // detect it if not ignored. + let key = EvpKey::new(KeyType::EC(WhichEC::P256)).unwrap(); + std::mem::forget(key); + } +} diff --git a/native/rust/cose_openssl/cose_openssl/src/sign.rs b/native/rust/cose_openssl/cose_openssl/src/sign.rs new file mode 100644 index 00000000..214a1951 --- /dev/null +++ b/native/rust/cose_openssl/cose_openssl/src/sign.rs @@ -0,0 +1,40 @@ +use crate::ossl_wrappers::{EvpKey, EvpMdContext, SignOp}; + +use openssl_sys as ossl; +use std::ptr; + +pub fn sign(key: &EvpKey, msg: &[u8]) -> Result, String> { + unsafe { + let ctx = EvpMdContext::::new(key)?; + + let mut sig_size: usize = 0; + let res = ossl::EVP_DigestSign( + ctx.ctx, + ptr::null_mut(), + &mut sig_size, + msg.as_ptr(), + msg.len(), + ); + if res != 1 { + return Err(format!("Failed to signature size, err: {}", res)); + } + + let mut sig = vec![0u8; sig_size]; + let res = ossl::EVP_DigestSign( + ctx.ctx, + sig.as_mut_ptr(), + &mut sig_size, + msg.as_ptr(), + msg.len(), + ); + if res != 1 { + return Err(format!("Failed to sign, err: {}", res)); + } + + // Not always fixed size, e.g. for EC keys. More on this here: + // https://docs.openssl.org/3.0/man3/EVP_DigestSignInit/#description. + sig.truncate(sig_size); + + Ok(sig) + } +} diff --git a/native/rust/cose_openssl/cose_openssl/src/verify.rs b/native/rust/cose_openssl/cose_openssl/src/verify.rs new file mode 100644 index 00000000..f89a352e --- /dev/null +++ b/native/rust/cose_openssl/cose_openssl/src/verify.rs @@ -0,0 +1,23 @@ +use crate::ossl_wrappers::{EvpKey, EvpMdContext, VerifyOp}; + +use openssl_sys as ossl; + +pub fn verify(key: &EvpKey, sig: &[u8], msg: &[u8]) -> Result { + unsafe { + let ctx = EvpMdContext::::new(key)?; + + let res = ossl::EVP_DigestVerify( + ctx.ctx, + sig.as_ptr(), + sig.len(), + msg.as_ptr(), + msg.len(), + ); + + match res { + 1 => Ok(true), + 0 => Ok(false), + err => Err(format!("Failed to verify signature, err: {}", err)), + } + } +} diff --git a/native/rust/cose_openssl/cose_openssl_ffi/Cargo.toml b/native/rust/cose_openssl/cose_openssl_ffi/Cargo.toml new file mode 100644 index 00000000..781e4bab --- /dev/null +++ b/native/rust/cose_openssl/cose_openssl_ffi/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "cose_openssl_ffi" +version = "0.1.0" +edition = "2024" + +[lib] +crate-type = ["staticlib"] + +[features] +pqc = ["cose_openssl/pqc"] + +[lints.rust] +warnings = "deny" + +[dependencies] +cose_openssl = { path = "../cose_openssl" } diff --git a/native/rust/cose_openssl/cose_openssl_ffi/src/ffi/helpers.rs b/native/rust/cose_openssl/cose_openssl_ffi/src/ffi/helpers.rs new file mode 100644 index 00000000..2bf1b41c --- /dev/null +++ b/native/rust/cose_openssl/cose_openssl_ffi/src/ffi/helpers.rs @@ -0,0 +1,70 @@ +use cose_openssl::EvpKey; +use cose_openssl::cose_sign1; +use std::slice; + +/// Raw pointer + length to slice. Returns an empty slice for null/zero-length. +pub unsafe fn as_slice<'a>(ptr: *const u8, len: usize) -> &'a [u8] { + if ptr.is_null() || len == 0 { + &[] + } else { + unsafe { slice::from_raw_parts(ptr, len) } + } +} + +/// Write a `Vec` result into caller-supplied output pointers. +pub unsafe fn write_output( + v: Vec, + out_ptr: *mut *mut u8, + out_len: *mut usize, +) { + let b = v.into_boxed_slice(); + let len = b.len(); + let ptr = Box::into_raw(b) as *mut u8; + unsafe { + *out_len = len; + *out_ptr = ptr; + } +} + +/// Shared implementation for `cose_sign` and `cose_sign_detached`. +pub unsafe fn sign_inner( + phdr_ptr: *const u8, + phdr_len: usize, + uhdr_ptr: *const u8, + uhdr_len: usize, + payload_ptr: *const u8, + payload_len: usize, + key_der_ptr: *const u8, + key_der_len: usize, + out_ptr: *mut *mut u8, + out_len: *mut usize, + detached: bool, +) -> i32 { + unsafe { + if out_ptr.is_null() || out_len.is_null() { + return -1; + } + + let key = match EvpKey::from_der_private(as_slice( + key_der_ptr, + key_der_len, + )) { + Ok(k) => k, + Err(_) => return -1, + }; + + match cose_sign1( + &key, + as_slice(phdr_ptr, phdr_len), + as_slice(uhdr_ptr, uhdr_len), + as_slice(payload_ptr, payload_len), + detached, + ) { + Ok(envelope) => { + write_output(envelope, out_ptr, out_len); + 0 + } + Err(_) => -1, + } + } +} diff --git a/native/rust/cose_openssl/cose_openssl_ffi/src/ffi/mod.rs b/native/rust/cose_openssl/cose_openssl_ffi/src/ffi/mod.rs new file mode 100644 index 00000000..85da5bda --- /dev/null +++ b/native/rust/cose_openssl/cose_openssl_ffi/src/ffi/mod.rs @@ -0,0 +1,404 @@ +mod helpers; + +use cose_openssl::EvpKey; +use cose_openssl::cose_verify1; +use helpers::*; + +/// Free a buffer previously returned by `cose_sign` / `cose_sign_detached`. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn cose_free(ptr: *mut u8, len: usize) { + if !ptr.is_null() && len > 0 { + unsafe { + drop(Vec::from_raw_parts(ptr, len, len)); + } + } +} + +/// Sign with embedded payload. +/// +/// Produces a complete COSE_Sign1 envelope (tag 18). +/// +/// * `phdr` - serialised CBOR map (protected header, **without** alg). +/// * `uhdr` - serialised CBOR map (unprotected header). +/// * `payload` - raw payload bytes. +/// * `key_der` - DER-encoded private key. +/// * `out_ptr` / `out_len` - on success, receives the COSE_Sign1 bytes +/// (caller must free with `cose_free`). +/// +/// Returns 0 on success, -1 on error. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn cose_sign( + phdr_ptr: *const u8, + phdr_len: usize, + uhdr_ptr: *const u8, + uhdr_len: usize, + payload_ptr: *const u8, + payload_len: usize, + key_der_ptr: *const u8, + key_der_len: usize, + out_ptr: *mut *mut u8, + out_len: *mut usize, +) -> i32 { + unsafe { + sign_inner( + phdr_ptr, + phdr_len, + uhdr_ptr, + uhdr_len, + payload_ptr, + payload_len, + key_der_ptr, + key_der_len, + out_ptr, + out_len, + false, + ) + } +} + +/// Sign with detached payload. +/// +/// Same as `cose_sign` but the COSE_Sign1 envelope carries a CBOR null +/// instead of the payload (the payload must be supplied separately at +/// verification time). +/// +/// Returns 0 on success, -1 on error. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn cose_sign_detached( + phdr_ptr: *const u8, + phdr_len: usize, + uhdr_ptr: *const u8, + uhdr_len: usize, + payload_ptr: *const u8, + payload_len: usize, + key_der_ptr: *const u8, + key_der_len: usize, + out_ptr: *mut *mut u8, + out_len: *mut usize, +) -> i32 { + unsafe { + sign_inner( + phdr_ptr, + phdr_len, + uhdr_ptr, + uhdr_len, + payload_ptr, + payload_len, + key_der_ptr, + key_der_len, + out_ptr, + out_len, + true, + ) + } +} + +/// Verify a COSE_Sign1 envelope with embedded payload. +/// +/// * `envelope` - the full COSE_Sign1 bytes. +/// * `key_der` - DER-encoded public key (SubjectPublicKeyInfo). +/// +/// Returns 1 if the signature is valid, 0 if invalid, -1 on error. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn cose_verify( + envelope_ptr: *const u8, + envelope_len: usize, + key_der_ptr: *const u8, + key_der_len: usize, +) -> i32 { + unsafe { + let key = + match EvpKey::from_der_public(as_slice(key_der_ptr, key_der_len)) { + Ok(k) => k, + Err(_) => return -1, + }; + + match cose_verify1(&key, as_slice(envelope_ptr, envelope_len), None) { + Ok(true) => 1, + Ok(false) => 0, + Err(_) => -1, + } + } +} + +/// Verify a COSE_Sign1 envelope with a detached payload. +/// +/// * `envelope` - the COSE_Sign1 bytes (payload slot is CBOR null). +/// * `payload` - the detached payload bytes. +/// * `key_der` - DER-encoded public key (SubjectPublicKeyInfo). +/// +/// Returns 1 if the signature is valid, 0 if invalid, -1 on error. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn cose_verify_detached( + envelope_ptr: *const u8, + envelope_len: usize, + payload_ptr: *const u8, + payload_len: usize, + key_der_ptr: *const u8, + key_der_len: usize, +) -> i32 { + unsafe { + let key = + match EvpKey::from_der_public(as_slice(key_der_ptr, key_der_len)) { + Ok(k) => k, + Err(_) => return -1, + }; + + match cose_verify1( + &key, + as_slice(envelope_ptr, envelope_len), + Some(as_slice(payload_ptr, payload_len)), + ) { + Ok(true) => 1, + Ok(false) => 0, + Err(_) => -1, + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use cose_openssl::{KeyType, WhichEC}; + use std::ptr; + + const TEST_PHDR: &str = "A319018B020FA3061A698B72820173736572766963652E6578616D706C652E636F6D02706C65646765722E7369676E6174757265666363662E7631A1647478696465322E313334"; + + /// Helper: generate a key pair, return (priv_der, pub_der). + fn make_key_pair(typ: KeyType) -> (Vec, Vec) { + let key = EvpKey::new(typ).unwrap(); + let priv_der = key.to_der_private().unwrap(); + let pub_der = key.to_der_public().unwrap(); + (priv_der, pub_der) + } + + #[test] + fn ffi_sign_verify_ec() { + let (priv_der, pub_der) = make_key_pair(KeyType::EC(WhichEC::P256)); + let phdr = hex::decode(TEST_PHDR).unwrap(); + let uhdr = b"\xa0"; + let payload = b"ffi roundtrip"; + + let mut out_ptr: *mut u8 = ptr::null_mut(); + let mut out_len: usize = 0; + + let rc = unsafe { + cose_sign( + phdr.as_ptr(), + phdr.len(), + uhdr.as_ptr(), + uhdr.len(), + payload.as_ptr(), + payload.len(), + priv_der.as_ptr(), + priv_der.len(), + &mut out_ptr, + &mut out_len, + ) + }; + assert_eq!(rc, 0); + assert!(!out_ptr.is_null()); + assert!(out_len > 0); + + let rc = unsafe { + cose_verify(out_ptr, out_len, pub_der.as_ptr(), pub_der.len()) + }; + assert_eq!(rc, 1); + + unsafe { cose_free(out_ptr, out_len) }; + } + + #[test] + fn ffi_sign_detached_verify_detached_ec() { + let (priv_der, pub_der) = make_key_pair(KeyType::EC(WhichEC::P384)); + let phdr = hex::decode(TEST_PHDR).unwrap(); + let uhdr = b"\xa0"; + let payload = b"detached ffi"; + + let mut out_ptr: *mut u8 = ptr::null_mut(); + let mut out_len: usize = 0; + + let rc = unsafe { + cose_sign_detached( + phdr.as_ptr(), + phdr.len(), + uhdr.as_ptr(), + uhdr.len(), + payload.as_ptr(), + payload.len(), + priv_der.as_ptr(), + priv_der.len(), + &mut out_ptr, + &mut out_len, + ) + }; + assert_eq!(rc, 0); + + // Verify with detached payload. + let rc = unsafe { + cose_verify_detached( + out_ptr, + out_len, + payload.as_ptr(), + payload.len(), + pub_der.as_ptr(), + pub_der.len(), + ) + }; + assert_eq!(rc, 1); + + // Non-detached verify must fail (payload slot is null). + let rc = unsafe { + cose_verify(out_ptr, out_len, pub_der.as_ptr(), pub_der.len()) + }; + assert_eq!(rc, -1); + + unsafe { cose_free(out_ptr, out_len) }; + } + + #[test] + fn ffi_verify_wrong_key_returns_zero() { + let (priv_der, _) = make_key_pair(KeyType::EC(WhichEC::P256)); + let (_, other_pub) = make_key_pair(KeyType::EC(WhichEC::P256)); + let phdr = hex::decode(TEST_PHDR).unwrap(); + let uhdr = b"\xa0"; + let payload = b"wrong key"; + + let mut out_ptr: *mut u8 = ptr::null_mut(); + let mut out_len: usize = 0; + + let rc = unsafe { + cose_sign( + phdr.as_ptr(), + phdr.len(), + uhdr.as_ptr(), + uhdr.len(), + payload.as_ptr(), + payload.len(), + priv_der.as_ptr(), + priv_der.len(), + &mut out_ptr, + &mut out_len, + ) + }; + assert_eq!(rc, 0); + + // Verify with a different public key -- signature invalid. + let rc = unsafe { + cose_verify(out_ptr, out_len, other_pub.as_ptr(), other_pub.len()) + }; + assert_eq!(rc, 0); + + unsafe { cose_free(out_ptr, out_len) }; + } + + #[test] + fn ffi_sign_bad_key_returns_error() { + let phdr = hex::decode(TEST_PHDR).unwrap(); + let uhdr = b"\xa0"; + let payload = b"bad key"; + let garbage_key = [0xde, 0xad, 0xbe, 0xef]; + + let mut out_ptr: *mut u8 = ptr::null_mut(); + let mut out_len: usize = 0; + + let rc = unsafe { + cose_sign( + phdr.as_ptr(), + phdr.len(), + uhdr.as_ptr(), + uhdr.len(), + payload.as_ptr(), + payload.len(), + garbage_key.as_ptr(), + garbage_key.len(), + &mut out_ptr, + &mut out_len, + ) + }; + assert_eq!(rc, -1); + } + + #[cfg(feature = "pqc")] + mod pqc_tests { + use super::*; + use cose_openssl::WhichMLDSA; + + #[test] + fn ffi_sign_verify_mldsa() { + let (priv_der, pub_der) = + make_key_pair(KeyType::MLDSA(WhichMLDSA::P65)); + let phdr = hex::decode(TEST_PHDR).unwrap(); + let uhdr = b"\xa0"; + let payload = b"mldsa ffi roundtrip"; + + let mut out_ptr: *mut u8 = ptr::null_mut(); + let mut out_len: usize = 0; + + let rc = unsafe { + cose_sign( + phdr.as_ptr(), + phdr.len(), + uhdr.as_ptr(), + uhdr.len(), + payload.as_ptr(), + payload.len(), + priv_der.as_ptr(), + priv_der.len(), + &mut out_ptr, + &mut out_len, + ) + }; + assert_eq!(rc, 0); + + let rc = unsafe { + cose_verify(out_ptr, out_len, pub_der.as_ptr(), pub_der.len()) + }; + assert_eq!(rc, 1); + + unsafe { cose_free(out_ptr, out_len) }; + } + + #[test] + fn ffi_sign_detached_verify_detached_mldsa() { + let (priv_der, pub_der) = + make_key_pair(KeyType::MLDSA(WhichMLDSA::P44)); + let phdr = hex::decode(TEST_PHDR).unwrap(); + let uhdr = b"\xa0"; + let payload = b"mldsa detached ffi"; + + let mut out_ptr: *mut u8 = ptr::null_mut(); + let mut out_len: usize = 0; + + let rc = unsafe { + cose_sign_detached( + phdr.as_ptr(), + phdr.len(), + uhdr.as_ptr(), + uhdr.len(), + payload.as_ptr(), + payload.len(), + priv_der.as_ptr(), + priv_der.len(), + &mut out_ptr, + &mut out_len, + ) + }; + assert_eq!(rc, 0); + + let rc = unsafe { + cose_verify_detached( + out_ptr, + out_len, + payload.as_ptr(), + payload.len(), + pub_der.as_ptr(), + pub_der.len(), + ) + }; + assert_eq!(rc, 1); + + unsafe { cose_free(out_ptr, out_len) }; + } + } +} diff --git a/native/rust/cose_openssl/cose_openssl_ffi/src/lib.rs b/native/rust/cose_openssl/cose_openssl_ffi/src/lib.rs new file mode 100644 index 00000000..9078be84 --- /dev/null +++ b/native/rust/cose_openssl/cose_openssl_ffi/src/lib.rs @@ -0,0 +1,4 @@ +mod ffi; +pub use ffi::{ + cose_free, cose_sign, cose_sign_detached, cose_verify, cose_verify_detached, +}; diff --git a/native/rust/cose_openssl/cpp/cose_openssl_ffi.h b/native/rust/cose_openssl/cpp/cose_openssl_ffi.h new file mode 100644 index 00000000..419fcce0 --- /dev/null +++ b/native/rust/cose_openssl/cpp/cose_openssl_ffi.h @@ -0,0 +1,86 @@ +// C header for the cose-openssl-ffi static library. + +#ifndef COSE_OPENSSL_FFI_H +#define COSE_OPENSSL_FFI_H + +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * Free a buffer previously returned by cose_sign / cose_sign_detached. + */ +void cose_free(uint8_t *ptr, size_t len); + +/** + * Sign with embedded payload. + * + * Produces a complete COSE_Sign1 envelope (tag 18). + * + * @param phdr Serialised CBOR map (protected header, without alg). + * @param phdr_len Length of phdr. + * @param uhdr Serialised CBOR map (unprotected header). + * @param uhdr_len Length of uhdr. + * @param payload Raw payload bytes. + * @param payload_len Length of payload. + * @param key_der DER-encoded private key. + * @param key_der_len Length of key_der. + * @param out_ptr On success, receives a pointer to the COSE_Sign1 bytes + * (caller must free with cose_free). + * @param out_len On success, receives the length of the output. + * @return 0 on success, -1 on error. + */ +int32_t cose_sign(const uint8_t *phdr, size_t phdr_len, const uint8_t *uhdr, + size_t uhdr_len, const uint8_t *payload, size_t payload_len, + const uint8_t *key_der, size_t key_der_len, uint8_t **out_ptr, + size_t *out_len); + +/** + * Sign with detached payload. + * + * Same as cose_sign but the COSE_Sign1 envelope carries a CBOR null instead + * of the payload. + * + * @return 0 on success, -1 on error. + */ +int32_t cose_sign_detached(const uint8_t *phdr, size_t phdr_len, + const uint8_t *uhdr, size_t uhdr_len, + const uint8_t *payload, size_t payload_len, + const uint8_t *key_der, size_t key_der_len, + uint8_t **out_ptr, size_t *out_len); + +/** + * Verify a COSE_Sign1 envelope with embedded payload. + * + * @param envelope Full COSE_Sign1 bytes. + * @param envelope_len Length of envelope. + * @param key_der DER-encoded public key (SubjectPublicKeyInfo). + * @param key_der_len Length of key_der. + * @return 1 if valid, 0 if invalid, -1 on error. + */ +int32_t cose_verify(const uint8_t *envelope, size_t envelope_len, + const uint8_t *key_der, size_t key_der_len); + +/** + * Verify a COSE_Sign1 envelope with detached payload. + * + * @param envelope COSE_Sign1 bytes (payload slot is CBOR null). + * @param envelope_len Length of envelope. + * @param payload Detached payload bytes. + * @param payload_len Length of payload. + * @param key_der DER-encoded public key (SubjectPublicKeyInfo). + * @param key_der_len Length of key_der. + * @return 1 if valid, 0 if invalid, -1 on error. + */ +int32_t cose_verify_detached(const uint8_t *envelope, size_t envelope_len, + const uint8_t *payload, size_t payload_len, + const uint8_t *key_der, size_t key_der_len); + +#ifdef __cplusplus +} /* extern "C" */ +#endif + +#endif /* COSE_OPENSSL_FFI_H */