#![allow(non_camel_case_types)]
use ring::{digest, signature};
use std::convert::TryFrom;
use crate::u2ferror::U2fError;
pub struct X509PublicKey<'a> {
#[allow(dead_code)]
cert: webpki::EndEntityCert<'a>,
cert_der: &'a [u8],
}
impl std::fmt::Debug for X509PublicKey<'_> {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(f, "X509PublicKey")
}
}
impl<'a> TryFrom<&'a [u8]> for X509PublicKey<'a> {
type Error = U2fError;
fn try_from(cert_der: &'a [u8]) -> Result<Self, Self::Error> {
let cert = webpki::EndEntityCert::try_from(cert_der).map_err(|_| U2fError::BadCertificate)?;
Ok(X509PublicKey { cert, cert_der })
}
}
const OID_COMMON_NAME: &[u8] = &[0x55, 0x04, 0x03];
fn extract_common_name(cert_der: &[u8]) -> Option<String> {
if let Some(subject_position) = find_sequence_after_tag(cert_der, 0x30)
&& let Some(cn_value) = find_value_by_oid(&cert_der[subject_position..], OID_COMMON_NAME)
{
return String::from_utf8(cn_value.to_vec()).ok();
}
None
}
fn find_sequence_after_tag(data: &[u8], tag: u8) -> Option<usize> {
for i in 0..data.len() {
if data[i] == tag && i + 1 < data.len() {
return Some(i + 2); }
}
None
}
fn find_value_by_oid<'a>(data: &'a [u8], oid: &[u8]) -> Option<&'a [u8]> {
for i in 0..data.len() {
if i + oid.len() < data.len() && data[i..i + oid.len()] == *oid {
if i + oid.len() + 2 < data.len() {
let len_byte = data[i + oid.len() + 1];
let len = len_byte as usize;
let start = i + oid.len() + 2;
if start + len <= data.len() {
return Some(&data[start..start + len]);
}
}
}
}
None
}
impl<'a> X509PublicKey<'a> {
pub(crate) fn common_name(&self) -> Option<String> {
extract_common_name(self.cert_der)
}
pub(crate) fn is_secp256r1(&self) -> Result<bool, U2fError> {
let secp256r1_oid = [0x2a, 0x86, 0x48, 0xce, 0x3d, 0x03, 0x01, 0x07];
for i in 0..self.cert_der.len() {
if i + secp256r1_oid.len() <= self.cert_der.len() && self.cert_der[i..i + secp256r1_oid.len()] == secp256r1_oid {
return Ok(true);
}
}
Ok(false)
}
pub(crate) fn verify_signature(&self, signature: &[u8], verification_data: &[u8]) -> Result<bool, U2fError> {
let public_key_bytes = match extract_ec_public_key_bytes(self.cert_der) {
Some(bytes) => bytes,
None => return Err(U2fError::BadCertificate),
};
let verification_alg = &signature::ECDSA_P256_SHA256_ASN1;
let key_bytes = if public_key_bytes.len() > 1 && public_key_bytes[0] == 0x00 {
&public_key_bytes[1..]
} else {
public_key_bytes
};
let key_data = if key_bytes.len() > 1 && key_bytes[0] == 0x04 {
key_bytes
} else {
return Err(U2fError::BadCertificate);
};
let public_key = signature::UnparsedPublicKey::new(verification_alg, key_data);
let message_digest = digest::digest(&digest::SHA256, verification_data);
match public_key.verify(message_digest.as_ref(), signature) {
Ok(()) => Ok(true),
Err(_) => Ok(false), }
}
}
fn extract_ec_public_key_bytes(cert_der: &[u8]) -> Option<&[u8]> {
let ec_pubkey_oid = [0x2A, 0x86, 0x48, 0xCE, 0x3D, 0x02, 0x01]; let p256_oid = [0x2A, 0x86, 0x48, 0xCE, 0x3D, 0x03, 0x01, 0x07];
for i in 0..cert_der.len() - ec_pubkey_oid.len() {
if cert_der[i..i + ec_pubkey_oid.len()] == ec_pubkey_oid {
let mut p256_found = false;
for j in i + ec_pubkey_oid.len()..std::cmp::min(i + ec_pubkey_oid.len() + 20, cert_der.len() - p256_oid.len()) {
if cert_der[j..j + p256_oid.len()] == p256_oid {
p256_found = true;
break;
}
}
if p256_found {
for j in i + ec_pubkey_oid.len()..std::cmp::min(i + 100, cert_der.len() - 3) {
if cert_der[j] == 0x03 {
let (length, offset) = if (cert_der[j + 1] & 0x80) == 0 {
(cert_der[j + 1] as usize, 2)
} else {
let len_bytes = cert_der[j + 1] & 0x7F;
if len_bytes == 0 || j + 2 + len_bytes as usize > cert_der.len() {
continue; }
let mut len = 0usize;
for k in 0..len_bytes as usize {
len = (len << 8) | (cert_der[j + 2 + k] as usize);
}
(len, 2 + len_bytes as usize)
};
if j + offset + length > cert_der.len() || length < 2 {
continue;
}
if cert_der[j + offset] == 0x00 && j + offset + 1 < cert_der.len() && cert_der[j + offset + 1] == 0x04 {
if j + offset + 1 + 65 <= cert_der.len() {
return Some(&cert_der[j + offset + 1..j + offset + 1 + 65]);
}
}
else if cert_der[j + offset] == 0x04 {
if j + offset + 65 <= cert_der.len() {
return Some(&cert_der[j + offset..j + offset + 65]);
}
}
}
}
}
break;
}
}
None
}
pub struct NISTP256Key {
pub x: [u8; 32],
pub y: [u8; 32],
}
impl NISTP256Key {
pub fn from_bytes(public_key_bytes: &[u8]) -> Result<Self, U2fError> {
if public_key_bytes.len() != 65 {
return Err(U2fError::InvalidPublicKey);
}
if public_key_bytes[0] != 0x04 {
return Err(U2fError::InvalidPublicKey);
}
let mut x: [u8; 32] = Default::default();
x.copy_from_slice(&public_key_bytes[1..=32]);
let mut y: [u8; 32] = Default::default();
y.copy_from_slice(&public_key_bytes[33..=64]);
Ok(NISTP256Key { x, y })
}
fn to_uncompressed_point(&self) -> Vec<u8> {
let mut result = Vec::with_capacity(65);
result.push(0x04); result.extend_from_slice(&self.x);
result.extend_from_slice(&self.y);
result
}
#[allow(dead_code)]
pub fn to_spki(&self) -> Vec<u8> {
let prefix = [
0x30, 0x59, 0x30, 0x13, 0x06, 0x07, 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x02, 0x01, 0x06, 0x08, 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x03, 0x01, 0x07, 0x03, 0x42, 0x00, ];
let mut result = Vec::with_capacity(prefix.len() + 65);
result.extend_from_slice(&prefix);
result.extend_from_slice(&self.to_uncompressed_point());
result
}
pub fn verify_signature(&self, signature: &[u8], verification_data: &[u8]) -> Result<bool, U2fError> {
let point = self.to_uncompressed_point();
let message_digest = digest::digest(&digest::SHA256, verification_data);
let public_key = signature::UnparsedPublicKey::new(&signature::ECDSA_P256_SHA256_ASN1, &point);
match public_key.verify(message_digest.as_ref(), signature) {
Ok(()) => Ok(true),
Err(_) => Ok(false), }
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::convert::TryFrom;
use std::fs;
use std::fs::File;
use std::io::Read;
use std::path::Path;
use std::process::Command;
#[test]
fn test_01_generate_test_files() {
let test_dir = Path::new("tests/data");
if !test_dir.exists() {
fs::create_dir_all(test_dir).expect("Failed to create test directory");
}
let verification_data_path = test_dir.join("verification_data.bin");
if !verification_data_path.exists() {
fs::write(&verification_data_path, b"test verification data").expect("Failed to write verification data");
}
let ec_key_path = test_dir.join("test_key.pem");
let openssl_available = Command::new("openssl").arg("version").status().is_ok();
if !ec_key_path.exists() && openssl_available {
let ec_param_path = test_dir.join("ecparam.pem");
fs::write(
&ec_param_path,
"-----BEGIN EC PARAMETERS-----\nBggqhkjOPQMBBw==\n-----END EC PARAMETERS-----\n",
)
.expect("Failed to write EC parameters");
let status = Command::new("openssl")
.args(&["ecparam", "-name", "prime256v1", "-genkey", "-noout", "-out"])
.arg(&ec_key_path)
.status()
.expect("Failed to execute OpenSSL command");
if !status.success() {
create_dummy_test_files(test_dir);
return;
}
let pub_key_der_path = test_dir.join("pubkey.der");
let status = Command::new("openssl")
.args(&["ec", "-in"])
.arg(&ec_key_path)
.args(&["-pubout", "-outform", "DER", "-out"])
.arg(&pub_key_der_path)
.status()
.expect("Failed to execute OpenSSL command");
if !status.success() {
create_dummy_test_files(test_dir);
return;
}
extract_raw_pubkey_from_der(&pub_key_der_path, &test_dir.join("test_pub_raw.bin"));
let cert_path = test_dir.join("valid_cert.pem");
let status = Command::new("openssl")
.args(&["req", "-new", "-x509", "-key"])
.arg(&ec_key_path)
.args(&["-out"])
.arg(&cert_path)
.args(&["-subj", "/CN=test.example.com", "-nodes"])
.status()
.expect("Failed to execute OpenSSL command");
if !status.success() {
create_dummy_test_files(test_dir);
return;
}
let cert_der_path = test_dir.join("valid_cert.der");
let status = Command::new("openssl")
.args(&["x509", "-in"])
.arg(&cert_path)
.args(&["-outform", "DER", "-out"])
.arg(&cert_der_path)
.status()
.expect("Failed to execute OpenSSL command");
if !status.success() {
create_dummy_test_files(test_dir);
return;
}
fs::copy(&cert_der_path, test_dir.join("test_cert.der")).expect("Failed to copy certificate");
let signature_path = test_dir.join("valid_signature.bin");
let status = Command::new("openssl")
.args(&["dgst", "-sha256", "-sign"])
.arg(&ec_key_path)
.args(&["-out"])
.arg(&signature_path)
.arg(&verification_data_path)
.status()
.expect("Failed to execute OpenSSL command");
if !status.success() {
create_dummy_test_files(test_dir);
return;
}
fs::copy(&signature_path, test_dir.join("valid_p256_signature.bin")).expect("Failed to copy signature");
let cert_types = [
("cert_p256.pem", "req -new -x509 -key", &ec_key_path, "/CN=example.com"),
(
"cert_not_p256.pem",
"req -new -x509 -newkey rsa:2048 -nodes -keyout",
&test_dir.join("rsa_key.pem"),
"/O=TestOrg",
),
(
"cert_with_cn.pem",
"req -new -x509 -newkey rsa:2048 -nodes -keyout",
&test_dir.join("rsa_key2.pem"),
"/CN=example.com",
),
(
"cert_without_cn.pem",
"req -new -x509 -newkey rsa:2048 -nodes -keyout",
&test_dir.join("rsa_key3.pem"),
"/O=TestOrg",
),
];
for (cert_file, cmd, key_path, subject) in cert_types.iter() {
let cert_path = test_dir.join(cert_file);
let mut command = Command::new("openssl");
for arg in cmd.split_whitespace() {
command.arg(arg);
}
command.arg(key_path);
command.args(&["-out", cert_path.to_str().unwrap(), "-subj", subject]);
let status = command.status().expect("Failed to execute OpenSSL command");
if !status.success() {
continue;
}
let der_path = cert_path.with_extension("der");
Command::new("openssl")
.args(&["x509", "-in"])
.arg(&cert_path)
.args(&["-outform", "DER", "-out"])
.arg(&der_path)
.status()
.expect("Failed to execute OpenSSL command");
}
} else if !openssl_available {
create_dummy_test_files(test_dir);
}
}
fn create_dummy_test_files(test_dir: &Path) {
println!("Creating dummy test files for testing without OpenSSL");
let sample_p256_key = [
0x04, 0x60, 0xf7, 0xf8, 0x5b, 0x4b, 0x72, 0xac, 0x6a, 0x24, 0x02, 0x87, 0x5c, 0x4a, 0xe9, 0x32, 0xc3, 0x9f, 0x16, 0xaf, 0x54, 0xc3,
0xb4, 0x56, 0xf5, 0xb5, 0xea, 0x0a, 0x7c, 0xe8, 0xf7, 0x87, 0x82, 0xe5, 0xca, 0x8d, 0x4c, 0xe9, 0x42, 0x2b, 0x35, 0x0a, 0x0d, 0x2e,
0xf8, 0x96, 0x01, 0x81, 0xba, 0x14, 0x8f, 0xd0, 0x5b, 0x35, 0x39, 0x08, 0x4a, 0xc5, 0xd7, 0x35, 0x80, 0x74, 0x2a, 0xce,
];
fs::write(test_dir.join("test_pub_raw.bin"), &sample_p256_key).expect("Failed to write sample public key");
let sample_cert_p256 = include_bytes!("../tests/data/cert_p256.der");
fs::write(test_dir.join("cert_p256.der"), sample_cert_p256).expect("Failed to write sample P-256 certificate");
let sample_cert_rsa = include_bytes!("../tests/data/cert_not_p256.der");
fs::write(test_dir.join("cert_not_p256.der"), sample_cert_rsa).expect("Failed to write sample RSA certificate");
let sample_cert_with_cn = include_bytes!("../tests/data/cert_with_cn.der");
fs::write(test_dir.join("cert_with_cn.der"), sample_cert_with_cn).expect("Failed to write sample certificate with CN");
let sample_cert_without_cn = include_bytes!("../tests/data/cert_without_cn.der");
fs::write(test_dir.join("cert_without_cn.der"), sample_cert_without_cn).expect("Failed to write sample certificate without CN");
fs::write(test_dir.join("valid_cert.der"), sample_cert_p256).expect("Failed to write sample valid certificate");
fs::write(test_dir.join("test_cert.der"), sample_cert_p256).expect("Failed to write sample test certificate");
let mut sample_signature = vec![0x30, 0x44, 0x02, 0x20];
sample_signature.extend(vec![0x01; 32]);
sample_signature.extend(vec![0x02, 0x20]);
sample_signature.extend(vec![0x01; 32]);
fs::write(test_dir.join("valid_signature.bin"), &sample_signature).expect("Failed to write sample signature");
fs::write(test_dir.join("valid_p256_signature.bin"), &sample_signature).expect("Failed to write sample P-256 signature");
}
fn extract_raw_pubkey_from_der(der_path: &Path, output_path: &Path) {
if !der_path.exists() {
return;
}
let mut file = File::open(der_path).expect("Failed to open DER file");
let mut der_data = Vec::new();
file.read_to_end(&mut der_data).expect("Failed to read DER data");
for i in 0..der_data.len() {
if i + 64 < der_data.len() && der_data[i] == 0x04 {
if i > 0 && (der_data[i - 1] == 0x00 || der_data[i - 1] == 0x03) {
let mut ec_point = Vec::with_capacity(65);
ec_point.push(0x04); ec_point.extend_from_slice(&der_data[i + 1..i + 65]);
fs::write(output_path, ec_point).expect("Failed to write EC point");
return;
}
}
}
let mut dummy_point = vec![0x04];
dummy_point.extend(vec![0x01; 64]);
fs::write(output_path, dummy_point).expect("Failed to write dummy EC point");
}
fn read_test_file(filename: &str) -> Vec<u8> {
let path = Path::new("tests/data").join(filename);
if path.exists() {
let mut file = File::open(&path).expect("Failed to open existing test file");
let mut data = Vec::new();
file.read_to_end(&mut data).expect("Failed to read test file");
data
} else {
let fallback_path = Path::new("tests/sample_data").join(filename);
if fallback_path.exists() {
let mut file = File::open(&fallback_path).expect("Failed to open fallback test file");
let mut data = Vec::new();
file.read_to_end(&mut data).expect("Failed to read fallback test file");
data
} else {
panic!("Failed to find test file: {} in either tests/data or tests/sample_data", filename);
}
}
}
#[test]
fn test_x509_from_der() {
let cert_der = read_test_file("test_cert.der");
let result = X509PublicKey::try_from(cert_der.as_slice());
assert!(result.is_ok(), "Failed to parse valid certificate DER");
let invalid_data = vec![0x01, 0x02, 0x03]; let result = X509PublicKey::try_from(invalid_data.as_slice());
assert!(result.is_err(), "Should fail with invalid certificate data");
}
#[test]
fn test_x509_common_name() {
let cert_with_cn = read_test_file("cert_with_cn.der");
let cert = X509PublicKey::try_from(cert_with_cn.as_slice()).expect("Valid certificate");
let cn = cert.common_name();
assert!(cn.is_some(), "Common name should be extracted");
println!("Extracted CN: {:?}", cn);
let cert_without_cn = read_test_file("cert_without_cn.der");
let cert = X509PublicKey::try_from(cert_without_cn.as_slice()).expect("Valid certificate");
let cn = cert.common_name();
assert!(cn.is_none(), "No common name should be extracted");
}
#[test]
fn test_x509_is_secp256r1() {
let cert_p256 = read_test_file("cert_p256.der");
let cert = X509PublicKey::try_from(cert_p256.as_slice()).expect("Valid certificate");
let result = cert.is_secp256r1();
assert!(result.is_ok(), "Should not error");
assert!(result.unwrap(), "Certificate should use secp256r1");
let cert_not_p256 = read_test_file("cert_not_p256.der");
let cert = X509PublicKey::try_from(cert_not_p256.as_slice()).expect("Valid certificate");
let result = cert.is_secp256r1();
assert!(result.is_ok(), "Should not error");
assert!(!result.unwrap(), "Certificate should not use secp256r1");
}
#[test]
fn test_x509_verify_signature() {
if !Path::new("tests/data/valid_signature.bin").exists() {
println!("Skipping test_x509_verify_signature because test files are not available");
return;
}
let cert_der = read_test_file("valid_cert.der");
let cert = X509PublicKey::try_from(cert_der.as_slice()).expect("Valid certificate");
let signature = read_test_file("valid_signature.bin");
let verification_data = read_test_file("verification_data.bin");
let key_bytes = extract_ec_public_key_bytes(&cert_der);
if key_bytes.is_none() {
println!("Could not extract EC key from certificate, skipping verification test");
return;
}
let key_bytes = key_bytes.unwrap();
println!("Extracted key bytes: {:02X?}", key_bytes);
let verification_alg = &signature::ECDSA_P256_SHA256_ASN1;
let public_key = signature::UnparsedPublicKey::new(verification_alg, key_bytes);
let message_digest = digest::digest(&digest::SHA256, &verification_data);
let manual_result = public_key.verify(message_digest.as_ref(), &signature);
println!("Manual verification result: {:?}", manual_result);
let result = cert.verify_signature(&signature, &verification_data);
assert!(result.is_ok(), "Verification should not error");
println!("X509 verification result: {:?}", result);
}
#[test]
fn test_nistp256key_from_bytes() {
let valid_point = read_test_file("test_pub_raw.bin");
println!("Key bytes length: {}", valid_point.len());
println!("Key bytes prefix: {:02X?}", &valid_point[0..min(5, valid_point.len())]);
let result = NISTP256Key::from_bytes(&valid_point);
assert!(result.is_ok(), "Valid EC point should parse successfully");
let invalid_length = vec![0x04; 64]; let result = NISTP256Key::from_bytes(&invalid_length);
assert!(result.is_err(), "EC point with invalid length should fail");
match result {
Err(U2fError::InvalidPublicKey) => (),
_ => panic!("Expected InvalidPublicKey error"),
}
let mut invalid_format = vec![0x00; 65];
invalid_format[0] = 0x03; let result = NISTP256Key::from_bytes(&invalid_format);
assert!(result.is_err(), "EC point with invalid format should fail");
match result {
Err(U2fError::InvalidPublicKey) => (),
_ => panic!("Expected InvalidPublicKey error"),
}
}
#[test]
fn test_nistp256key_to_uncompressed_point() {
let x = [0x11; 32];
let y = [0x22; 32];
let key = NISTP256Key { x, y };
let point = key.to_uncompressed_point();
assert_eq!(point.len(), 65, "Uncompressed point should be 65 bytes");
assert_eq!(point[0], 0x04, "Uncompressed point should start with 0x04");
assert_eq!(&point[1..33], &x, "X coordinate should match");
assert_eq!(&point[33..65], &y, "Y coordinate should match");
}
#[test]
fn test_nistp256key_to_spki() {
let x = [0x11; 32];
let y = [0x22; 32];
let key = NISTP256Key { x, y };
let spki = key.to_spki();
assert!(spki.len() > 65, "SPKI should be longer than just the point");
assert_eq!(spki[0], 0x30, "SPKI should start with SEQUENCE tag");
let mut found = false;
for i in 0..spki.len() - 64 {
if i + 65 <= spki.len() && spki[i] == 0x04 {
if &spki[i + 1..i + 33] == &x && &spki[i + 33..i + 65] == &y {
found = true;
break;
}
}
}
assert!(found, "SPKI should contain the EC point");
}
#[test]
fn test_nistp256key_verify_signature() {
if !Path::new("tests/data/valid_p256_signature.bin").exists() {
println!("Skipping test_nistp256key_verify_signature because test files are not available");
return;
}
let key_bytes = read_test_file("test_pub_raw.bin");
let key = NISTP256Key::from_bytes(&key_bytes).expect("Valid EC key");
let signature = read_test_file("valid_p256_signature.bin");
let verification_data = read_test_file("verification_data.bin");
let result = key.verify_signature(&signature, &verification_data);
println!("Signature verification result: {:?}", result);
assert!(result.is_ok(), "Verification should not error");
let mut invalid_signature = signature.clone();
if !invalid_signature.is_empty() {
invalid_signature[0] ^= 0xFF; }
let result = key.verify_signature(&invalid_signature, &verification_data);
assert!(result.is_ok(), "Verification should not error");
println!("Invalid signature verification result: {:?}", result);
}
#[test]
fn test_extract_ec_public_key_bytes() {
let cert_der = read_test_file("cert_p256.der");
let key_bytes = extract_ec_public_key_bytes(&cert_der);
assert!(key_bytes.is_some(), "EC key should be extracted from certificate");
let key_bytes = key_bytes.unwrap();
println!(
"Extracted key bytes length: {}, prefix: {:02X?}",
key_bytes.len(),
&key_bytes[0..min(5, key_bytes.len())]
);
assert!(key_bytes.len() > 0, "Extracted key should not be empty");
assert!(
key_bytes[0] == 0x04,
"EC key should be in uncompressed format, got: {:02X?}",
key_bytes[0]
);
let cert_der = read_test_file("cert_not_p256.der");
let key_bytes = extract_ec_public_key_bytes(&cert_der);
assert!(key_bytes.is_none(), "Should NOT extract EC key from non-EC certificate");
}
fn min(a: usize, b: usize) -> usize {
if a < b { a } else { b }
}
}