diff --git a/native/rust/cose_openssl/src/cose.rs b/native/rust/cose_openssl/src/cose.rs index d0bf5250..cc15f36e 100644 --- a/native/rust/cose_openssl/src/cose.rs +++ b/native/rust/cose_openssl/src/cose.rs @@ -560,6 +560,49 @@ mod tests { ); } + #[test] + fn cose_sign_verify_with_pem_imported_key() { + let original = EvpKey::new(KeyType::EC(WhichEC::P256)).unwrap(); + + let priv_pem = original.to_pem_private().unwrap(); + let signing_key = EvpKey::from_pem_private(&priv_pem).unwrap(); + + let pub_pem = original.to_pem_public().unwrap(); + let verification_key = EvpKey::from_pem_public(&pub_pem).unwrap(); + + let phdr_bytes = hex_decode(TEST_PHDR); + let phdr = CborValue::from_bytes(&phdr_bytes).unwrap(); + let uhdr = CborValue::Map(vec![]); + let payload = b"signed with PEM-imported key"; + + let envelope = + cose_sign1(&signing_key, phdr, uhdr, payload, false).unwrap(); + + let parsed = CborValue::from_bytes(&envelope).unwrap(); + let inner = match parsed { + CborValue::Tagged { payload, .. } => *payload, + _ => panic!("not tagged"), + }; + let items = match inner { + CborValue::Array(v) => v, + _ => panic!("not array"), + }; + let phdr_raw = match &items[0] { + CborValue::ByteString(b) => b.clone(), + _ => panic!("phdr not bstr"), + }; + let sig_raw = match &items[3] { + CborValue::ByteString(b) => b.clone(), + _ => panic!("sig not bstr"), + }; + + let alg = cose_alg(&verification_key).unwrap(); + assert!( + cose_verify1(&verification_key, alg, &phdr_raw, payload, &sig_raw) + .unwrap() + ); + } + #[cfg(feature = "pqc")] mod pqc_tests { use super::*; diff --git a/native/rust/cose_openssl/src/ossl_wrappers.rs b/native/rust/cose_openssl/src/ossl_wrappers.rs index fb7f6b2c..c3fc19bd 100644 --- a/native/rust/cose_openssl/src/ossl_wrappers.rs +++ b/native/rust/cose_openssl/src/ossl_wrappers.rs @@ -342,6 +342,241 @@ impl EvpKey { } } + /// Create an `EvpKey` from a PEM-encoded public key. + pub fn from_pem_public(pem: &[u8]) -> Result { + let pem_len: std::ffi::c_int = pem.len().try_into().map_err(|_| { + format!("PEM input too large ({} bytes)", pem.len()) + })?; + let key = unsafe { + let bio = ossl::BIO_new_mem_buf( + pem.as_ptr() as *const std::ffi::c_void, + pem_len, + ); + if bio.is_null() { + return Err(format!( + "BIO_new_mem_buf failed: {}", + ossl_err_string() + )); + } + let key = ossl::PEM_read_bio_PUBKEY( + bio, + ptr::null_mut(), + None, + ptr::null_mut(), + ); + ossl::BIO_free_all(bio); + if key.is_null() { + return Err(format!( + "PEM_read_bio_PUBKEY failed: {}", + ossl_err_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 PEM-encoded private key. + pub fn from_pem_private(pem: &[u8]) -> Result { + let pem_len: std::ffi::c_int = pem.len().try_into().map_err(|_| { + format!("PEM input too large ({} bytes)", pem.len()) + })?; + let key = unsafe { + let bio = ossl::BIO_new_mem_buf( + pem.as_ptr() as *const std::ffi::c_void, + pem_len, + ); + if bio.is_null() { + return Err(format!( + "BIO_new_mem_buf failed: {}", + ossl_err_string() + )); + } + let key = ossl::PEM_read_bio_PrivateKey( + bio, + ptr::null_mut(), + None, + ptr::null_mut(), + ); + ossl::BIO_free_all(bio); + if key.is_null() { + return Err(format!( + "PEM_read_bio_PrivateKey failed: {}", + ossl_err_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 }) + } + + /// Extract the public key from a PEM-encoded X.509 certificate. + pub fn from_pem_cert(pem: &[u8]) -> Result { + let pem_len: std::ffi::c_int = pem.len().try_into().map_err(|_| { + format!("Certificate too large ({} bytes)", pem.len()) + })?; + + unsafe { + let bio = ossl::BIO_new_mem_buf( + pem.as_ptr() as *const std::ffi::c_void, + pem_len, + ); + if bio.is_null() { + return Err(format!( + "BIO_new_mem_buf failed: {}", + ossl_err_string() + )); + } + let cert = ossl::PEM_read_bio_X509( + bio, + ptr::null_mut(), + None, + ptr::null_mut(), + ); + ossl::BIO_free_all(bio); + if cert.is_null() { + return Err(format!( + "PEM_read_bio_X509 failed: {}", + ossl_err_string() + )); + } + + Self::pubkey_from_x509(cert) + } + } + + /// Extract the public key from a DER-encoded X.509 certificate. + pub fn from_der_cert(der: &[u8]) -> Result { + unsafe { + let mut ptr = der.as_ptr(); + let cert = ossl::d2i_X509( + ptr::null_mut(), + &mut ptr, + der.len() as std::ffi::c_long, + ); + if cert.is_null() { + return Err(format!("d2i_X509 failed: {}", ossl_err_string())); + } + + Self::pubkey_from_x509(cert) + } + } + + /// Extract the public key from a parsed X509 and free the cert. + unsafe fn pubkey_from_x509(cert: *mut ossl::X509) -> Result { + unsafe { + let key = ossl::X509_get_pubkey(cert); + ossl::X509_free(cert); + if key.is_null() { + return Err(format!( + "X509_get_pubkey failed: {}", + ossl_err_string() + )); + } + + let typ = match Self::detect_key_type_raw(key) { + Ok(t) => t, + Err(e) => { + ossl::EVP_PKEY_free(key); + return Err(e); + } + }; + + Ok(EvpKey { key, typ }) + } + } + + /// Export the public key as PEM. + pub fn to_pem_public(&self) -> Result, String> { + unsafe { + let bio = ossl::BIO_new(ossl::BIO_s_mem()); + if bio.is_null() { + return Err(format!("BIO_new failed: {}", ossl_err_string())); + } + if ossl::PEM_write_bio_PUBKEY(bio, self.key) != 1 { + ossl::BIO_free_all(bio); + return Err(format!( + "PEM_write_bio_PUBKEY failed: {}", + ossl_err_string() + )); + } + let mut data_ptr: *mut std::ffi::c_char = ptr::null_mut(); + let len = ossl::BIO_get_mem_data(bio, &mut data_ptr); + if len <= 0 || data_ptr.is_null() { + ossl::BIO_free_all(bio); + return Err(format!( + "BIO_get_mem_data returned {}: {}", + len, + ossl_err_string() + )); + } + let pem = + std::slice::from_raw_parts(data_ptr as *const u8, len as usize) + .to_vec(); + ossl::BIO_free_all(bio); + Ok(pem) + } + } + + /// Export the private key as PEM (unencrypted PKCS#8). + pub fn to_pem_private(&self) -> Result, String> { + unsafe { + let bio = ossl::BIO_new(ossl::BIO_s_mem()); + if bio.is_null() { + return Err(format!("BIO_new failed: {}", ossl_err_string())); + } + if ossl::PEM_write_bio_PrivateKey( + bio, + self.key, + ptr::null(), + ptr::null_mut(), + 0, + None, + ptr::null_mut(), + ) != 1 + { + ossl::BIO_free_all(bio); + return Err(format!( + "PEM_write_bio_PrivateKey failed: {}", + ossl_err_string() + )); + } + let mut data_ptr: *mut std::ffi::c_char = ptr::null_mut(); + let len = ossl::BIO_get_mem_data(bio, &mut data_ptr); + if len <= 0 || data_ptr.is_null() { + ossl::BIO_free_all(bio); + return Err(format!( + "BIO_get_mem_data returned {}: {}", + len, + ossl_err_string() + )); + } + let pem = + std::slice::from_raw_parts(data_ptr as *const u8, len as usize) + .to_vec(); + ossl::BIO_free_all(bio); + Ok(pem) + } + } + /// 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 { @@ -888,4 +1123,223 @@ mod tests { "unexpected error: {err}" ); } + + #[test] + fn ec_pem_public_roundtrip() { + for which in [WhichEC::P256, WhichEC::P384, WhichEC::P521] { + let key = EvpKey::new(KeyType::EC(which)).unwrap(); + let pem = key.to_pem_public().unwrap(); + let pem_str = std::str::from_utf8(&pem).unwrap(); + assert!( + pem_str.starts_with("-----BEGIN PUBLIC KEY-----"), + "{pem_str}" + ); + let imported = EvpKey::from_pem_public(&pem).unwrap(); + assert!(matches!(imported.typ, KeyType::EC(_))); + // PEM -> DER -> PEM must be identical. + let der1 = key.to_der_public().unwrap(); + let der2 = imported.to_der_public().unwrap(); + assert_eq!(der1, der2); + } + } + + #[test] + fn ec_pem_private_roundtrip() { + for which in [WhichEC::P256, WhichEC::P384, WhichEC::P521] { + let key = EvpKey::new(KeyType::EC(which)).unwrap(); + let pem = key.to_pem_private().unwrap(); + let pem_str = std::str::from_utf8(&pem).unwrap(); + assert!( + pem_str.starts_with("-----BEGIN PRIVATE KEY-----"), + "{pem_str}" + ); + let imported = EvpKey::from_pem_private(&pem).unwrap(); + assert!(matches!(imported.typ, KeyType::EC(_))); + let der1 = key.to_der_private().unwrap(); + let der2 = imported.to_der_private().unwrap(); + assert_eq!(der1, der2); + } + } + + #[test] + fn rsa_pem_public_roundtrip() { + let key = EvpKey::new(KeyType::RSA(WhichRSA::PS256)).unwrap(); + let pem = key.to_pem_public().unwrap(); + let imported = EvpKey::from_pem_public(&pem).unwrap(); + assert!(matches!(imported.typ, KeyType::RSA(_))); + assert_eq!( + key.to_der_public().unwrap(), + imported.to_der_public().unwrap() + ); + } + + #[test] + fn rsa_pem_private_roundtrip() { + let key = EvpKey::new(KeyType::RSA(WhichRSA::PS256)).unwrap(); + let pem = key.to_pem_private().unwrap(); + let imported = EvpKey::from_pem_private(&pem).unwrap(); + assert!(matches!(imported.typ, KeyType::RSA(_))); + assert_eq!( + key.to_der_private().unwrap(), + imported.to_der_private().unwrap() + ); + } + + #[test] + fn pem_der_pem_public_roundtrip() { + let key = EvpKey::new(KeyType::EC(WhichEC::P256)).unwrap(); + let pem1 = key.to_pem_public().unwrap(); + let der = EvpKey::from_pem_public(&pem1) + .unwrap() + .to_der_public() + .unwrap(); + let pem2 = EvpKey::from_der_public(&der) + .unwrap() + .to_pem_public() + .unwrap(); + assert_eq!(pem1, pem2); + } + + #[test] + fn pem_der_pem_private_roundtrip() { + let key = EvpKey::new(KeyType::EC(WhichEC::P384)).unwrap(); + let pem1 = key.to_pem_private().unwrap(); + let der = EvpKey::from_pem_private(&pem1) + .unwrap() + .to_der_private() + .unwrap(); + let pem2 = EvpKey::from_der_private(&der) + .unwrap() + .to_pem_private() + .unwrap(); + assert_eq!(pem1, pem2); + } + + #[test] + fn from_pem_public_rejects_garbage() { + let err = EvpKey::from_pem_public(b"not a pem").unwrap_err(); + assert!( + err.starts_with("PEM_read_bio_PUBKEY failed: error:"), + "unexpected error: {err}" + ); + } + + #[test] + fn from_pem_private_rejects_garbage() { + let err = EvpKey::from_pem_private(b"not a pem").unwrap_err(); + assert!( + err.starts_with("PEM_read_bio_PrivateKey failed: error:"), + "unexpected error: {err}" + ); + } + + // --------------------------------------------------------------- + // Certificate tests + // --------------------------------------------------------------- + + /// Build a minimal self-signed certificate from the given key. + /// Returns (DER, PEM) of the certificate. + fn make_self_signed_cert(key: &EvpKey) -> (Vec, Vec) { + unsafe { + let cert = ossl::X509_new(); + assert!(!cert.is_null()); + + ossl::ASN1_INTEGER_set(ossl::X509_get_serialNumber(cert), 1); + ossl::X509_gmtime_adj(ossl::X509_getm_notBefore(cert), 0); + ossl::X509_gmtime_adj( + ossl::X509_getm_notAfter(cert), + 365 * 24 * 3600, + ); + ossl::X509_set_pubkey(cert, key.key); + ossl::X509_set_issuer_name(cert, ossl::X509_get_subject_name(cert)); + let rc = ossl::X509_sign(cert, key.key, key.digest()); + assert!(rc > 0, "X509_sign failed"); + + // DER + let mut der_ptr: *mut u8 = ptr::null_mut(); + let der_len = ossl::i2d_X509(cert, &mut der_ptr); + assert!(der_len > 0); + let der = + std::slice::from_raw_parts(der_ptr, der_len as usize).to_vec(); + ossl::CRYPTO_free( + der_ptr as *mut std::ffi::c_void, + concat!(file!(), "\0").as_ptr() as *const i8, + line!() as i32, + ); + + // PEM + let bio = ossl::BIO_new(ossl::BIO_s_mem()); + assert!(!bio.is_null()); + assert_eq!(ossl::PEM_write_bio_X509(bio, cert), 1); + let mut data_ptr: *mut std::ffi::c_char = ptr::null_mut(); + let pem_len = ossl::BIO_get_mem_data(bio, &mut data_ptr); + assert!(pem_len > 0); + let pem = std::slice::from_raw_parts( + data_ptr as *const u8, + pem_len as usize, + ) + .to_vec(); + ossl::BIO_free_all(bio); + + ossl::X509_free(cert); + (der, pem) + } + } + + #[test] + fn from_pem_cert_ec() { + let key = EvpKey::new(KeyType::EC(WhichEC::P256)).unwrap(); + let (_, pem) = make_self_signed_cert(&key); + let imported = EvpKey::from_pem_cert(&pem).unwrap(); + assert!(matches!(imported.typ, KeyType::EC(WhichEC::P256))); + assert_eq!( + key.to_der_public().unwrap(), + imported.to_der_public().unwrap() + ); + } + + #[test] + fn from_der_cert_ec() { + let key = EvpKey::new(KeyType::EC(WhichEC::P384)).unwrap(); + let (der, _) = make_self_signed_cert(&key); + let imported = EvpKey::from_der_cert(&der).unwrap(); + assert!(matches!(imported.typ, KeyType::EC(WhichEC::P384))); + assert_eq!( + key.to_der_public().unwrap(), + imported.to_der_public().unwrap() + ); + } + + #[test] + fn from_cert_rsa() { + let key = EvpKey::new(KeyType::RSA(WhichRSA::PS256)).unwrap(); + let (der, pem) = make_self_signed_cert(&key); + + let from_der = EvpKey::from_der_cert(&der).unwrap(); + let from_pem = EvpKey::from_pem_cert(&pem).unwrap(); + assert!(matches!(from_der.typ, KeyType::RSA(_))); + assert!(matches!(from_pem.typ, KeyType::RSA(_))); + + let expected_pub = key.to_der_public().unwrap(); + assert_eq!(from_der.to_der_public().unwrap(), expected_pub); + assert_eq!(from_pem.to_der_public().unwrap(), expected_pub); + } + + #[test] + fn from_pem_cert_rejects_garbage() { + let err = EvpKey::from_pem_cert(b"not a cert").unwrap_err(); + assert!( + err.starts_with("PEM_read_bio_X509 failed: error:"), + "unexpected error: {err}" + ); + } + + #[test] + fn from_der_cert_rejects_garbage() { + let err = EvpKey::from_der_cert(&[0xde, 0xad, 0xbe, 0xef]).unwrap_err(); + assert!( + err.starts_with("d2i_X509 failed: error:"), + "unexpected error: {err}" + ); + } }