From c8a501b2bff0d15456785b5048c79428f6f1c8c5 Mon Sep 17 00:00:00 2001 From: Thanh Nguyen Date: Wed, 16 Apr 2025 00:18:05 -0400 Subject: [PATCH 1/6] Port x509/mod.rs --- Cargo.toml | 12 +++-- src/x509/mod.rs | 114 +++++++++++++++++++----------------------------- 2 files changed, 49 insertions(+), 77 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 543a3ef..db1f59b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -44,11 +44,11 @@ wasm_experimental = ["ring/wasm32_unknown_unknown_js"] # if you need to work with fido data, without needing to generate it. fido-support = ["ctap-hid-fido2", "fido-lite"] fido-support-mozilla = ["authenticator", "fido-lite"] -fido-lite = ["minicbor", "x509-parser"] +fido-lite = ["minicbor", "x509-cert"] rsa-signing = ["simple_asn1", "num-bigint"] -x509-support = ["der-parser", "x509", "x509-parser"] +x509-support = ["x509-cert"] yubikey-support = ["rcgen", "yubikey", "yubikey-lite"] -yubikey-lite = ["x509-support", "der", "x509-cert"] +yubikey-lite = ["x509-support"] [dependencies] base64 = "0.13" @@ -64,11 +64,9 @@ num-bigint = { version = "0.4", optional = true } yubikey = { version = "0.8", features = ["untested"], optional = true } lexical-core = { version = ">0.7.4", optional = true } rcgen = { version = "0.11", optional = true } -x509 = { version = "0.2", optional = true } -x509-parser = { version = "0.15", features = ["verify"], optional = true } -der-parser = { version = "5", optional = true } -der = { version = "0.7", optional = true } +# der = { version = "0.7", optional = true } x509-cert = { version = "0.2", optional = true } +# spki = { version = "0.7.3", optional = true } # Dependencies for encrypted-keys aes = { version = "0.7", features = ["ctr"], optional = true } diff --git a/src/x509/mod.rs b/src/x509/mod.rs index 9eaaab2..5e1ded5 100644 --- a/src/x509/mod.rs +++ b/src/x509/mod.rs @@ -1,58 +1,36 @@ -use x509_parser::prelude::FromDer; - use crate::error::Error; use crate::ssh::{Curve, EcdsaPublicKey, KeyType, PublicKey, PublicKeyKind}; -use der_parser::der::parse_der_sequence; -use der_parser::error::BerError; - -const OID_RSA_ENCRYPTION: &str = "1.2.840.113549.1.1.1"; -const OID_EC_PUBLIC_KEY: &str = "1.2.840.10045.2.1"; -const OID_NIST_P256: &str = "1.2.840.10045.3.1.7"; -const OID_NIST_P384: &str = "1.3.132.0.34"; +use x509_cert::der::Encode; +use x509_cert::{ + der::{oid::ObjectIdentifier, Decode}, + spki::SubjectPublicKeyInfo, +}; -impl From> for Error { - fn from(_: x509_parser::nom::Err) -> Error { - Error::ParsingError - } -} +const RSA_ENCRYPTION_OID: ObjectIdentifier = ObjectIdentifier::new_unwrap("1.2.840.113549.1.1.1"); +const EC_PUBLIC_KEY_OID: ObjectIdentifier = ObjectIdentifier::new_unwrap("1.2.840.10045.2.1"); +const NISTP256_OID: ObjectIdentifier = ObjectIdentifier::new_unwrap("1.2.840.10045.3.1.7"); +const NISTP384_OID: ObjectIdentifier = ObjectIdentifier::new_unwrap("1.3.132.0.34"); -impl From for Error { - fn from(_: BerError) -> Error { - Error::ParsingError - } -} /// Helper function to convert a DER encoded public key, into an SSH formatted /// public key that can be used with the rest of the SSHCerts library. This /// function only supports NISTP256 and NISTP384 Ecdsa keys pub fn der_encoding_to_ssh_public_key(key: &[u8]) -> Result { - let (_rem, parsed) = parse_der_sequence(key).map_err(|_| Error::ParsingError)?; - let parsed = parsed.as_sequence().map_err(|_| Error::ParsingError)?; + let spki = SubjectPublicKeyInfo::from_der(key).map_err(|_| Error::ParsingError)?; - if parsed.len() != 2 { - return Err(Error::ParsingError); - } + let oid_alg = spki + .algorithm + .parameters_oid() + .map_err(|_| Error::ParsingError)?; - let oids = &parsed[0].as_sequence()?; - if oids.len() != 2 { - return Err(Error::ParsingError); - } - - let type_oid = oids[0].as_oid()?; - let key_size_oid = oids[1].as_oid()?; - if type_oid.to_id_string() != OID_EC_PUBLIC_KEY { - return Err(Error::ParsingError); - } - - let data = &parsed[1].as_bitstring()?.data; - let (key_type, curve) = match key_size_oid.to_id_string().as_str() { - OID_NIST_P256 => { + let (key_type, curve) = match oid_alg { + NISTP256_OID => { let key_type = KeyType::from_name("ecdsa-sha2-nistp256").unwrap(); let curve = Curve::from_identifier("nistp256").unwrap(); (key_type, curve) } - OID_NIST_P384 => { + NISTP384_OID => { let key_type = KeyType::from_name("ecdsa-sha2-nistp384").unwrap(); let curve = Curve::from_identifier("nistp384").unwrap(); (key_type, curve) @@ -62,7 +40,7 @@ pub fn der_encoding_to_ssh_public_key(key: &[u8]) -> Result { let kind = EcdsaPublicKey { curve, - key: data.to_vec(), + key: spki.subject_public_key, sk_application: None, }; @@ -76,50 +54,46 @@ pub fn der_encoding_to_ssh_public_key(key: &[u8]) -> Result { /// This function is used to extract an SSH public key from an x509 /// certificate pub fn extract_ssh_pubkey_from_x509_certificate(cert: &[u8]) -> Result { - let parsed_cert = match x509_parser::parse_x509_certificate(cert) { - Ok((_, c)) => c, - Err(_) => return Err(Error::ParsingError), - }; - let pki = &parsed_cert.tbs_certificate.subject_pki; - convert_x509_pki_to_pubkey(pki) + let cert = x509_cert::Certificate::from_der(cert) + .map_err(|_| Error::ParsingError)?; + let spki = cert.tbs_certificate.subject_public_key_info.to_der() + .map_err(|_| Error::ParsingError)?; + let spki = SubjectPublicKeyInfo::>::from_der(&spki) + .map_err(|_| Error::ParsingError)?; + convert_x509_pki_to_pubkey(spki) } /// This function is used to extract an SSH public key from an x509 /// certificate signing request pub fn extract_ssh_pubkey_from_x509_csr(csr: &[u8]) -> Result { - let parsed_csr = - match x509_parser::certification_request::X509CertificationRequest::from_der(csr) { - Ok((_, csr)) => csr, - Err(_) => return Err(Error::ParsingError), - }; - let pki = &parsed_csr.certification_request_info.subject_pki; - convert_x509_pki_to_pubkey(pki) + let parsed_csr = x509_cert::request::CertReqInfo::from_der(csr) + .map_err(|_| Error::ParsingError)?; + let spki = &parsed_csr.public_key.to_der() + .map_err(|_| Error::ParsingError)?; + let spki = SubjectPublicKeyInfo::>::from_der(&spki) + .map_err(|_| Error::ParsingError)?; + convert_x509_pki_to_pubkey(spki) } -fn convert_x509_pki_to_pubkey( - pki: &x509_parser::x509::SubjectPublicKeyInfo<'_>, +fn convert_x509_pki_to_pubkey>>( + pki: SubjectPublicKeyInfo, ) -> Result { - return match pki.algorithm.algorithm.to_string().as_str() { - OID_RSA_ENCRYPTION => Err(Error::Unsupported), - OID_EC_PUBLIC_KEY => { - let key_bytes = &pki.subject_public_key.data; - let algorithm_parameters = pki + return match pki.algorithm.oid { + RSA_ENCRYPTION_OID => Err(Error::Unsupported), + EC_PUBLIC_KEY_OID => { + let key_bytes: Vec = pki.subject_public_key.into(); + let curve_oid = pki .algorithm .parameters - .as_ref() .ok_or(Error::ParsingError)?; - let curve_oid = algorithm_parameters - .as_oid() - .map_err(|_| Error::ParsingError)?; - - match curve_oid.to_string().as_str() { - OID_NIST_P256 => { + match curve_oid { + NISTP256_OID => { let key_type = KeyType::from_name("ecdsa-sha2-nistp256").unwrap(); let curve = Curve::from_identifier("nistp256").unwrap(); let kind = EcdsaPublicKey { curve, - key: key_bytes.to_vec(), + key: key_bytes, sk_application: None, }; @@ -129,12 +103,12 @@ fn convert_x509_pki_to_pubkey( comment: None, }) } - OID_NIST_P384 => { + NISTP384_OID => { let key_type = KeyType::from_name("ecdsa-sha2-nistp384").unwrap(); let curve = Curve::from_identifier("nistp384").unwrap(); let kind = EcdsaPublicKey { curve, - key: key_bytes.to_vec(), + key: key_bytes, sk_application: None, }; From 4ab3ebb90e6ea13ed662d5c59eee3f6750ea463e Mon Sep 17 00:00:00 2001 From: Thanh Nguyen Date: Wed, 16 Apr 2025 01:17:14 -0400 Subject: [PATCH 2/6] WIP PIV verification --- src/yubikey/verification.rs | 70 ++++++++++++++++++++----------------- 1 file changed, 37 insertions(+), 33 deletions(-) diff --git a/src/yubikey/verification.rs b/src/yubikey/verification.rs index 8722d91..c6001b7 100644 --- a/src/yubikey/verification.rs +++ b/src/yubikey/verification.rs @@ -1,11 +1,13 @@ -use crate::{error::Error, x509::extract_ssh_pubkey_from_x509_certificate, PublicKey}; +use crate::{error::Error, x509::{self, extract_ssh_pubkey_from_x509_certificate}, PublicKey}; -use x509_parser::der_parser::ber::BerObjectContent; -use x509_parser::der_parser::der::parse_der_integer; -use x509_parser::prelude::*; +use x509_cert::{der::{self, Decode, DecodePem, ObjectIdentifier}, Certificate}; use std::convert::TryInto; +const FIRMWARE_OID: ObjectIdentifier = ObjectIdentifier::new_unwrap("1.3.6.1.4.1.41482.3.3"); +const SERIAL_OID: ObjectIdentifier = ObjectIdentifier::new_unwrap("1.3.6.1.4.1.41482.3.7"); +const POLICY_OID: ObjectIdentifier = ObjectIdentifier::new_unwrap("1.3.6.1.4.1.41482.3.8"); + const YUBICO_PIV_ROOT_CA: &str = "-----BEGIN CERTIFICATE----- MIIDFzCCAf+gAwIBAgIDBAZHMA0GCSqGSIb3DQEBCwUAMCsxKTAnBgNVBAMMIFl1 YmljbyBQSVYgUm9vdCBDQSBTZXJpYWwgMjYzNzUxMCAXDTE2MDMxNDAwMDAwMFoY @@ -45,43 +47,44 @@ pub struct ValidPIVKey { fn extract_certificate_extension_data( public_key: PublicKey, - certificate: &X509Certificate<'_>, + certificate: x509_cert::Certificate, ) -> Result { let mut firmware: Option = None; let mut serial: Option = None; let mut policies: Option<[u8; 2]> = None; - let extensions = certificate.extensions(); + let extensions = certificate.tbs_certificate.extensions + .ok_or_else(|| Error::ParsingError)?; + for ext in extensions.iter() { - match ext.oid.to_id_string().as_str() { - // Firmware - "1.3.6.1.4.1.41482.3.3" => { - if ext.value.len() != 3 { + match ext.extn_id { + FIRMWARE_OID => { + if ext.extn_value.len().into() != 3 { continue; } + let value = ext.extn_value.as_bytes(); firmware = Some(format!( "{}.{}.{}", - ext.value[0], ext.value[1], ext.value[2] + value[0], value[1], value[2] )); } // Serial - "1.3.6.1.4.1.41482.3.7" => { - let (_, obj) = parse_der_integer(ext.value).map_err(|_| Error::ParsingError)?; - if let BerObjectContent::Integer(s) = obj.content { - if s.len() > 8 { - continue; - } - - let mut padded_serial = vec![0; 8 - s.len()]; - padded_serial.extend_from_slice(s); - serial = Some(u64::from_be_bytes( - padded_serial.try_into().map_err(|_| Error::ParsingError)?, - )); + SERIAL_OID => { + let value = der::asn1::Int::from_der(ext.extn_value.as_bytes()) + .map_err(|_| Error::ParsingError)?; + if value.len().into() > 8 { + continue; } + let mut padded_serial = vec![0; 8 - value.len().into()]; + padded_serial.extend_from_slice(s); + serial = Some(u64::from_be_bytes( + padded_serial.try_into().map_err(|_| Error::ParsingError)?, + )); } // Policy - "1.3.6.1.4.1.41482.3.8" => { - policies = Some([ext.value[0], ext.value[1]]); + POLICY_OID => { + let value = ext.extn_value.as_bytes(); + policies = Some([value[0], value[1]]); } _ => (), } @@ -114,15 +117,16 @@ pub fn verify_certificate_chain( let root_ca_pem = root_pem.unwrap_or(YUBICO_PIV_ROOT_CA); // Parse the root ca - let (_, root_ca) = parse_x509_pem(root_ca_pem.as_bytes()).unwrap(); - let root_ca = Pem::parse_x509(&root_ca).unwrap(); + let parsed_root_ca = x509_cert::Certificate::from_pem(root_ca_pem.as_bytes()) + .map_err(|_| Error::ParsingError)?; - // Parse the certificates - let (_, parsed_intermediate) = - parse_x509_certificate(intermediate).map_err(|_| Error::ParsingError)?; - let (_, parsed_client) = parse_x509_certificate(client).map_err(|_| Error::ParsingError)?; + // Parse the intermediate and client certificates + let parsed_intermediate = x509_cert::Certificate::from_der(intermediate) + .map_err(|_| Error::ParsingError)?; + let parsed_client = x509_cert::Certificate::from_der(client) + .map_err(|_| Error::ParsingError)?; - // Validate that the provided intermediate certificate is signed by the Yubico Attestation Root CA + // Validate that the provded intermediate certificate is signed by the Yubico Attestation Root CA parsed_intermediate .verify_signature(Some(&root_ca.tbs_certificate.subject_pki)) .map_err(|_| Error::InvalidSignature)?; @@ -138,5 +142,5 @@ pub fn verify_certificate_chain( Err(_) => return Err(Error::ParsingError), }; - extract_certificate_extension_data(public_key, &parsed_client) + extract_certificate_extension_data(public_key, parsed_client) } From c4ffc4c9f79f3392b35447cd16b54083669879b8 Mon Sep 17 00:00:00 2001 From: Thanh Nguyen Date: Wed, 16 Apr 2025 20:03:44 -0400 Subject: [PATCH 3/6] Add support for new Yubico FIDO and PIV attestation root CA (#26) * Add Yubico FIDO Root CA Serial 450203556 * Use Attestation Root instead of FIDO Root CA * Refactor * Make Tests Consistent When I was originally writing the Mozilla handler for this library, I didn't implement hashing of the challenge because there is no way to take in a challenge from a remote host. This is why @timweri had to remove the hashing from the test in his earlier commits. We now generate random data first, then hash it such that we can get the preimage to test it properly. This also will allow us to extend the API into the future to allow remote challenges for key generation verification. * Bump version * Add new PIV root CA and intermediates --------- Co-authored-by: Mitchell Grenier --- Cargo.toml | 2 +- src/fido/generate/mozilla.rs | 20 +++- src/fido/verification.rs | 207 +++++++++++++++++++++++++++++++--- src/yubikey/verification.rs | 211 ++++++++++++++++++++++++++++++++--- tests/fido-lite.rs | 100 +++++++++++++++++ tests/yubikey-lite.rs | 109 ++++++++++++++++++ 6 files changed, 615 insertions(+), 34 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 6cc0653..02892bb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "sshcerts" -version = "0.13.2" +version = "0.14.0" authors = ["Mitchell Grenier "] edition = "2021" license-file = "LICENSE" diff --git a/src/fido/generate/mozilla.rs b/src/fido/generate/mozilla.rs index f17642d..e7a21d9 100644 --- a/src/fido/generate/mozilla.rs +++ b/src/fido/generate/mozilla.rs @@ -8,8 +8,8 @@ use crate::{ PrivateKey, }; -use ring::rand::SecureRandom; -use ring::rand::SystemRandom; +use ring::digest; +use ring::rand::{SecureRandom, SystemRandom}; use crate::fido::Error as FidoError; @@ -47,12 +47,20 @@ pub fn generate_new_ssh_key( }; manager.add_u2f_usb_hid_platform_transports(); - let mut chall_bytes = [0u8; 32]; - SystemRandom::new().fill(&mut chall_bytes).unwrap(); + // This forms the challenge + let mut client_data = [0u8; 32]; + // Fill it with random data because we don't support taking in + // challenge data at this point. + SystemRandom::new().fill(&mut client_data).unwrap(); + + // Hash the data because that is what will actually be signed + let client_data_digest = digest::digest(&digest::SHA256, &client_data); + let mut client_data_hash = [0u8; 32]; + client_data_hash.copy_from_slice(client_data_digest.as_ref()); let origin = application.to_string(); let ctap_args = RegisterArgs { - client_data_hash: chall_bytes, + client_data_hash, relying_party: RelyingParty { id: origin.clone(), name: None, @@ -170,7 +178,7 @@ pub fn generate_new_ssh_key( auth_data: raw_auth_data, auth_data_sig, intermediate, - challenge: chall_bytes.to_vec(), + challenge: client_data_hash.into(), alg, }; diff --git a/src/fido/verification.rs b/src/fido/verification.rs index 5b272aa..ddac684 100644 --- a/src/fido/verification.rs +++ b/src/fido/verification.rs @@ -10,7 +10,8 @@ use super::AuthData; use ring::signature::{UnparsedPublicKey, ECDSA_P256_SHA256_ASN1}; -const YUBICO_U2F_ROOT_CA: &str = "-----BEGIN CERTIFICATE----- +/// From https://developers.yubico.com/PKI/yubico-ca-certs.txt +const YUBICO_U2F_ROOT_CA_457200631: &str = "-----BEGIN CERTIFICATE----- MIIDHjCCAgagAwIBAgIEG0BT9zANBgkqhkiG9w0BAQsFADAuMSwwKgYDVQQDEyNZ dWJpY28gVTJGIFJvb3QgQ0EgU2VyaWFsIDQ1NzIwMDYzMTAgFw0xNDA4MDEwMDAw MDBaGA8yMDUwMDkwNDAwMDAwMFowLjEsMCoGA1UEAxMjWXViaWNvIFUyRiBSb290 @@ -30,6 +31,116 @@ sG/5xUb/Btwb2X2g4InpiB/yt/3CpQXpiWX/K4mBvUKiGn05ZsqeY1gx4g0xLBqc U9psmyPzK+Vsgw2jeRQ5JlKDyqE0hebfC1tvFu0CCrJFcw== -----END CERTIFICATE-----"; +/// From https://developers.yubico.com/PKI/yubico-ca-certs.txt +const YUBICO_ATTESTATION_ROOT_1: &str = "-----BEGIN CERTIFICATE----- +MIIDPjCCAiagAwIBAgIUXzeiEDJEOTt14F5n0o6Zf/bBwiUwDQYJKoZIhvcNAQEN +BQAwJDEiMCAGA1UEAwwZWXViaWNvIEF0dGVzdGF0aW9uIFJvb3QgMTAgFw0yNDEy +MDEwMDAwMDBaGA85OTk5MTIzMTIzNTk1OVowJDEiMCAGA1UEAwwZWXViaWNvIEF0 +dGVzdGF0aW9uIFJvb3QgMTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEB +AMZ6/TxM8rIT+EaoPvG81ontMOo/2mQ2RBwJHS0QZcxVaNXvl12LUhBZ5LmiBScI +Zd1Rnx1od585h+/dhK7hEm7JAALkKKts1fO53KGNLZujz5h3wGncr4hyKF0G74b/ +U3K9hE5mGND6zqYchCRAHfrYMYRDF4YL0X4D5nGdxvppAy6nkEmtWmMnwO3i0TAu +csrbE485HvGM4r0VpgVdJpvgQjiTJCTIq+D35hwtT8QDIv+nGvpcyi5wcIfCkzyC +imJukhYy6KoqNMKQEdpNiSOvWyDMTMt1bwCvEzpw91u+msUt4rj0efnO9s0ZOwdw +MRDnH4xgUl5ZLwrrPkfC1/0CAwEAAaNmMGQwHQYDVR0OBBYEFNLu71oijTptXCOX +PfKF1SbxJXuSMB8GA1UdIwQYMBaAFNLu71oijTptXCOXPfKF1SbxJXuSMBIGA1Ud +EwEB/wQIMAYBAf8CAQMwDgYDVR0PAQH/BAQDAgGGMA0GCSqGSIb3DQEBDQUAA4IB +AQC3IW/sgB9pZ8apJNjxuGoX+FkILks0wMNrdXL/coUvsrhzsvl6mePMrbGJByJ1 +XnquB5sgcRENFxdQFma3mio8Upf1owM1ZreXrJ0mADG2BplqbJnxiyYa+R11reIF +TWeIhMNcZKsDZrFAyPuFjCWSQvJmNWe9mFRYFgNhXJKkXIb5H1XgEDlwiedYRM7V +olBNlld6pRFKlX8ust6OTMOeADl2xNF0m1LThSdeuXvDyC1g9+ILfz3S6OIYgc3i +roRcFD354g7rKfu67qFAw9gC4yi0xBTPrY95rh4/HqaUYCA/L8ldRk6H7Xk35D+W +Vpmq2Sh/xT5HiFuhf4wJb0bK +-----END CERTIFICATE-----"; + +/// From https://developers.yubico.com/PKI/yubico-intermediate.pem +const YUBICO_ATTESTATION_INTERMEDIATE_A_1: &str = "-----BEGIN CERTIFICATE----- +MIIDSDCCAjCgAwIBAgIUUcmMXzRIFOgGTK0Tb3gEuZYZkBIwDQYJKoZIhvcNAQEL +BQAwJDEiMCAGA1UEAwwZWXViaWNvIEF0dGVzdGF0aW9uIFJvb3QgMTAgFw0yNDEy +MDEwMDAwMDBaGA85OTk5MTIzMTIzNTk1OVowLjEsMCoGA1UEAwwjWXViaWNvIEF0 +dGVzdGF0aW9uIEludGVybWVkaWF0ZSBBIDEwggEiMA0GCSqGSIb3DQEBAQUAA4IB +DwAwggEKAoIBAQDm555bWY9WW+tOY0rIWHldh+aNanoCZCFh7Gk3YZrQmPUw0hkS +G6qYHQtP+fZyS33VErvg+BQqnmumgNhfxFrkwEZELeidBcC8C4Ag4nqqiPWpzsvI +17NcxYlInLNLFcZY/+gOiN6ZOTihO5/vBZMbj9riaAcqliYmNGJPgTcMGaEAyMzE +MNy2nm6Ep+pjP5aF6gi21t/UQFsuJ1j2Rj/ynM/SdRt+ecal5OYotxHkFbL9vvv2 +A2Ov5ITZClw4bOS9npypQimOZ5QAYytmYaQpWl/pMYz6zSj8RqkVDNEJGqNfTKA2 +ivLYwX6lSttMPapg0J84l9X0voVN/FpS4VCVAgMBAAGjZjBkMB0GA1UdDgQWBBQg +KFAhG6RaW+hTy52dxeT8bC96HzAfBgNVHSMEGDAWgBTS7u9aIo06bVwjlz3yhdUm +8SV7kjASBgNVHRMBAf8ECDAGAQH/AgECMA4GA1UdDwEB/wQEAwIBhjANBgkqhkiG +9w0BAQsFAAOCAQEAYMzgLrJLIr0OovQnAZrRIGuabiHSUKSmbLRWpRkWeAtsChDE +HpXcJ/bgDNKYWoHqQ8xRUjB4CyepYevc3YlrG8o7zHxpfVcaoL5SeuJkzHxKn4bT +aSp9+Mvwamnp64kZMiNbFLknfP9kYKoRHkMWheRJ1UsP1z4ScmkCeILfsMs6vqov +qjWClFsJpBcsluYHWF7bBJ1n4Rwg+ATEopY4IgGv6Zvwc+A9r+AT2hqpoSkYoAl+ +ANYwgslOf9sJe0V+TA9YY/UlaBmPPTd0//r9wvcePWZkPjKoAC/zUNhfDbh4LV8G +Hs3lyX2XomL/LNc8JYzyIaDEhGQveoPhh/tr1g== +-----END CERTIFICATE-----"; + +/// From https://developers.yubico.com/PKI/yubico-intermediate.pem +const YUBICO_ATTESTATION_INTERMEDIATE_B_1: &str = "-----BEGIN CERTIFICATE----- +MIIDSDCCAjCgAwIBAgIUDqERw+4RnGSggxgUewJFEPDRZ3YwDQYJKoZIhvcNAQEL +BQAwJDEiMCAGA1UEAwwZWXViaWNvIEF0dGVzdGF0aW9uIFJvb3QgMTAgFw0yNDEy +MDEwMDAwMDBaGA85OTk5MTIzMTIzNTk1OVowLjEsMCoGA1UEAwwjWXViaWNvIEF0 +dGVzdGF0aW9uIEludGVybWVkaWF0ZSBCIDEwggEiMA0GCSqGSIb3DQEBAQUAA4IB +DwAwggEKAoIBAQDI7XnH+ZvDwMCQU8M8ZeV5qscublvVYaaRt3Ybaxn9godLx5sw +H0lXrdgjh5h7FpVgCgYYX7E4bl1vbzULemrMWT8N3WMGUe8QAJbBeioV7W/E+hTZ +P/0SKJVa3ewKBo6ULeMnfQZDrVORAk8wTLq2v5Llj5vMj7JtOotKa9J7nHS8kLmz +XXSaj0SwEPh5OAZUTNV4zs1bvoTAQQWrL4/J9QuKt6WCFE5nUNiRQcEbVF8mlqK2 +bx2z6okVltyDVLCxYbpUTELvY1usR3DTGPUoIClOm4crpwnDRLVHvjYePGBB//pE +yzxA/gcScxjwaH1ZUw9bnSbHyurKqbTa1KvjAgMBAAGjZjBkMB0GA1UdDgQWBBTq +t0KQngx7ZHrbVHwDunxOn9ihYTAfBgNVHSMEGDAWgBTS7u9aIo06bVwjlz3yhdUm +8SV7kjASBgNVHRMBAf8ECDAGAQH/AgECMA4GA1UdDwEB/wQEAwIBhjANBgkqhkiG +9w0BAQsFAAOCAQEAqQaCWMxTGqVVX7Sk7kkJmUueTSYKuU6+KBBSgwIRnlw9K7He +1IpxZ0hdwpPNikKjmcyFgFPzhImwHJgxxuT90Pw3vYOdcJJNktDg35PXOfzSn15c +FAx1RO0mPTmIb8dXiEWOpzoXvdwXDM41ZaCDYMT7w4IQtMyvE7xUBZq2bjtAnq/N +DUA7be4H8H3ipC+/+NKlUrcUh+j48K67WI0u1m6FeQueBA7n06j825rqDqsaLs9T +b7KAHAw8PmrWaNPG2kjKerxPEfecivlFawp2RWZvxrVtn3TV2SBxyCJCkXsND05d +CErVHSJIs+BdtTVNY9AwtyPmnyb0v4mSTzvWdw== +-----END CERTIFICATE-----"; + +/// From https://developers.yubico.com/PKI/yubico-intermediate.pem +const YUBICO_FIDO_ATTESTATION_A_1: &str = "-----BEGIN CERTIFICATE----- +MIIDSjCCAjKgAwIBAgIUTnbbGIR2NHvzqIKFAeQwG1XBis0wDQYJKoZIhvcNAQEL +BQAwLjEsMCoGA1UEAwwjWXViaWNvIEF0dGVzdGF0aW9uIEludGVybWVkaWF0ZSBB +IDEwIBcNMjQxMjAxMDAwMDAwWhgPOTk5OTEyMzEyMzU5NTlaMCYxJDAiBgNVBAMM +G1l1YmljbyBGSURPIEF0dGVzdGF0aW9uIEEgMTCCASIwDQYJKoZIhvcNAQEBBQAD +ggEPADCCAQoCggEBAOsXj3k04Ban4TYdtZKqD/OPJxyDyaPmCBUFUiaZIgTteZnj +3X25DhgpZZXsC4D0ydIcrlA6wNUInORL/L9zBbTEIMAVMGo6g7UKAmb2MF6AHbnh +YJd9eikupVNWShHNYNc4GBdO1YN6AfUqvJhHbe3V4SNMPmBREKJPVz7ThwgmggTe +8Ws2K0/wsqv2wSE7pbCBsUZhIX51bZM3pqDwJPTmRFEvt0/6tG5eO8F3j14OXqfE +hmjn1VvxKDYQOLZAxCwwgC0P4CdfWv3y8PSR8I354hO1Y+GzNjvIqX38NKLywuIY +HFerOxNlxEMBvFhYBuRuYAkkgUaPqN6UBhsILrsCAwEAAaNmMGQwHQYDVR0OBBYE +FCCoRHhiyNnbnXRWIL6ZBXoBX9YTMB8GA1UdIwQYMBaAFCAoUCEbpFpb6FPLnZ3F +5PxsL3ofMBIGA1UdEwEB/wQIMAYBAf8CAQAwDgYDVR0PAQH/BAQDAgGGMA0GCSqG +SIb3DQEBCwUAA4IBAQCQFafJI1/5Wg9CEEimE1RP54RgQwTNTOOQsLACTe+rItlF +QzC9ZDhrV828yX7jzy+AAsp3izK7T1th2dl7m+tu0sw2Pa/olc02nt6PyIw348ga +HzhI1+0KE45qxvFDeL2lMxbPfCYvyEEaYzjiQELU5951pXGWyKMa/4fLtO+ZKOXh +MuVeq4rXDPI54W6JHOiAaiKdiw+5e3c2kt/jFIQtM6vMXg9LNFzdjETNt20VX9Qe +vRpFZfucMG9wCaQDoFlPzpTMJKhPev/imJmZYhKfr0lLcemtqjIxLAoqZdOYfHBg +6+vAcdPI/iauGpUAv7X+UKNmDwjZ2BaH4sLwhB2m +-----END CERTIFICATE-----"; + +/// From https://developers.yubico.com/PKI/yubico-intermediate.pem +const YUBICO_FIDO_ATTESTATION_B_1: &str = "-----BEGIN CERTIFICATE----- +MIIDSjCCAjKgAwIBAgIUR38mq26Sf2szVV2BdG6WEN7kuWUwDQYJKoZIhvcNAQEL +BQAwLjEsMCoGA1UEAwwjWXViaWNvIEF0dGVzdGF0aW9uIEludGVybWVkaWF0ZSBC +IDEwIBcNMjQxMjAxMDAwMDAwWhgPOTk5OTEyMzEyMzU5NTlaMCYxJDAiBgNVBAMM +G1l1YmljbyBGSURPIEF0dGVzdGF0aW9uIEIgMTCCASIwDQYJKoZIhvcNAQEBBQAD +ggEPADCCAQoCggEBANY0Wb9oPoRoKoQyWPaJpz11vrWTg6zTtmNj2VoKRnyvKGRq +pzb83w5l6YA96UYkYBDQP0ilO2DPe6wWqVR5zDfRzdcH8bh+L7dGGvae6hRTZhkF +kCpXDs4HccknrDf8FClJ7He39Jf42/G1Qm2zz9WWmrPXtgiK/x05GjsQfGuDG1zf +5QTUUie8lwymK3TfdOvNeeJAAPe2pn7ItfRb+rVrNWiDzlRn2vNnZ2wPo4wH/WJ6 +dhXZG+rMWT+a6Bocg1UfIw6kdunG4bTpZzsvacFYyR0mpf+DeOnpSWAmywJWHvTl +f2YXxFyeXcTACdQlcMNGJ2VhZQ48xtP5/RBP/8kCAwEAAaNmMGQwHQYDVR0OBBYE +FChy42okiqcTS1iqa/HRWjkBn4H/MB8GA1UdIwQYMBaAFOq3QpCeDHtkettUfAO6 +fE6f2KFhMBIGA1UdEwEB/wQIMAYBAf8CAQAwDgYDVR0PAQH/BAQDAgGGMA0GCSqG +SIb3DQEBCwUAA4IBAQAn+RHIPbtMEDNdT1g8H/RitAkUdLgAt1tWGWnlj9knbv4/ +4GlX7C9p45efPO9/aZL6OV1XRKBi6KmtBW5K7nuYEnMx/5BqBSbLT7rhduC49TBe +Mb9PHdXsTlSVNYefr1dGidr4j0xVBQLb1rknDAbdWDzKfvnayKO8Frwe7Hx843MG +/rJ+c0XruUvbfVTCHLiIWhM7oNDhL8xob6xUo9KLKcSL+ItYsO3/9Wb8Q9GjsqL4 +FXsDcG1SaYh7KpfuMmOixqzJZO2nIicPYRg1I2SuiUfYO70tmdHcbl+kSQmSYt7r +q4viILg2Gx3j9rITuWTjbaUaSSQxgOmMSHuyzMAC +-----END CERTIFICATE-----"; + /// Defines the transports supported by the FIDO standard #[derive(Clone, Debug, PartialEq)] pub enum Transport { @@ -132,8 +243,82 @@ fn extract_certificate_extension_data( Ok(valid_attestation) } +/// Verify that the intermediates are chained to the root CA. +fn verify_intermediates( + parsed_intermediate: &X509Certificate<'_>, + ca_pems: Vec<&str>, +) -> Result<(), Error> { + // There has to be at least the root CA + if ca_pems.is_empty() { + return Err(Error::InvalidSignature); + } + + let mut ca_parsed_pems = vec![]; + + // Parse all the pems + for pem in ca_pems { + let (_, parsed_pem) = parse_x509_pem(pem.as_bytes()).map_err(|_| Error::ParsingError)?; + ca_parsed_pems.push(parsed_pem); + } + + // Parse the root CA + let root_ca = ca_parsed_pems.first().ok_or(Error::ParsingError)?; + let mut parent_ca = Pem::parse_x509(&root_ca).map_err(|_| Error::ParsingError)?; + + // Iteratively verify the chain + for intermediate_ca in ca_parsed_pems.iter().skip(1) { + let intermediate_ca = Pem::parse_x509(&intermediate_ca).map_err(|_| Error::ParsingError)?; + + // Check the parent CA has signed this intermediate, return error if not + intermediate_ca + .verify_signature(Some(&parent_ca.tbs_certificate.public_key())) + .map_err(|_| Error::InvalidSignature)?; + + parent_ca = intermediate_ca; + } + + // Check the parent intermediate CA has signed the final intermediate, return error if not + parsed_intermediate + .verify_signature(Some(&parent_ca.tbs_certificate.public_key())) + .map_err(|_| Error::InvalidSignature)?; + + Ok(()) +} + +/// Verify that the intermediate chains to some Yubico root CA for FIDO attestation +/// We try all known Yubico Root CAs for backward compatibility +fn verify_yubico_intermediates(parsed_intermediate: &X509Certificate<'_>) -> Result<(), Error> { + if verify_intermediates( + &parsed_intermediate, + vec![ + YUBICO_ATTESTATION_ROOT_1, + YUBICO_ATTESTATION_INTERMEDIATE_A_1, + YUBICO_FIDO_ATTESTATION_A_1, + ], + ) + .is_ok() + { + return Ok(()); + } + + if verify_intermediates( + &parsed_intermediate, + vec![ + YUBICO_ATTESTATION_ROOT_1, + YUBICO_ATTESTATION_INTERMEDIATE_B_1, + YUBICO_FIDO_ATTESTATION_B_1, + ], + ) + .is_ok() + { + return Ok(()); + } + + verify_intermediates(&parsed_intermediate, vec![YUBICO_U2F_ROOT_CA_457200631]) +} + /// Verify a provided U2F attestation, signature, and certificate are valid -/// against the root. If no root is given, the Yubico U2F Root is used. +/// against the root. If no root is given, the Yubico U2F Root and FIDO root are used. pub fn verify_auth_data( auth_data: &[u8], auth_data_signature: &[u8], @@ -145,20 +330,16 @@ pub fn verify_auth_data( match alg { // Verify using ECDSA256 -7 => { - let root_ca_pem = root_pem.unwrap_or(YUBICO_U2F_ROOT_CA); - - // Parse the U2F root CA - let (_, root_ca) = - parse_x509_pem(root_ca_pem.as_bytes()).map_err(|_| Error::ParsingError)?; - let root_ca = Pem::parse_x509(&root_ca).map_err(|_| Error::ParsingError)?; - let (_, parsed_intermediate) = X509Certificate::from_der(intermediate).map_err(|_| Error::ParsingError)?; - // Check the root CA has signed the intermediate, return error if not - parsed_intermediate - .verify_signature(Some(&root_ca.tbs_certificate.public_key())) - .map_err(|_| Error::InvalidSignature)?; + // If a custom root CA is provided, we use that for verification. + // If not, we will try all the known Yubico Root CAs for backward compatibility + if let Some(pem) = root_pem { + verify_intermediates(&parsed_intermediate, vec![pem])?; + } else { + verify_yubico_intermediates(&parsed_intermediate)?; + } // Extract public key from verified intermediate certificate let key_bytes = parsed_intermediate diff --git a/src/yubikey/verification.rs b/src/yubikey/verification.rs index 8722d91..73d55a5 100644 --- a/src/yubikey/verification.rs +++ b/src/yubikey/verification.rs @@ -6,7 +6,8 @@ use x509_parser::prelude::*; use std::convert::TryInto; -const YUBICO_PIV_ROOT_CA: &str = "-----BEGIN CERTIFICATE----- +/// From https://developers.yubico.com/PKI/yubico-ca-certs.txt +const YUBICO_PIV_ROOT_CA_263751: &str = "-----BEGIN CERTIFICATE----- MIIDFzCCAf+gAwIBAgIDBAZHMA0GCSqGSIb3DQEBCwUAMCsxKTAnBgNVBAMMIFl1 YmljbyBQSVYgUm9vdCBDQSBTZXJpYWwgMjYzNzUxMCAXDTE2MDMxNDAwMDAwMFoY DzIwNTIwNDE3MDAwMDAwWjArMSkwJwYDVQQDDCBZdWJpY28gUElWIFJvb3QgQ0Eg @@ -26,6 +27,116 @@ Fqyi4+JE014cSgR57Jcu3dZiehB6UtAPgad9L5cNvua/IWRmm+ANy3O2LH++Pyl8 SREzU8onbBsjMg9QDiSf5oJLKvd/Ren+zGY7 -----END CERTIFICATE-----"; +/// From https://developers.yubico.com/PKI/yubico-ca-certs.txt +const YUBICO_ATTESTATION_ROOT_1: &str = "-----BEGIN CERTIFICATE----- +MIIDPjCCAiagAwIBAgIUXzeiEDJEOTt14F5n0o6Zf/bBwiUwDQYJKoZIhvcNAQEN +BQAwJDEiMCAGA1UEAwwZWXViaWNvIEF0dGVzdGF0aW9uIFJvb3QgMTAgFw0yNDEy +MDEwMDAwMDBaGA85OTk5MTIzMTIzNTk1OVowJDEiMCAGA1UEAwwZWXViaWNvIEF0 +dGVzdGF0aW9uIFJvb3QgMTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEB +AMZ6/TxM8rIT+EaoPvG81ontMOo/2mQ2RBwJHS0QZcxVaNXvl12LUhBZ5LmiBScI +Zd1Rnx1od585h+/dhK7hEm7JAALkKKts1fO53KGNLZujz5h3wGncr4hyKF0G74b/ +U3K9hE5mGND6zqYchCRAHfrYMYRDF4YL0X4D5nGdxvppAy6nkEmtWmMnwO3i0TAu +csrbE485HvGM4r0VpgVdJpvgQjiTJCTIq+D35hwtT8QDIv+nGvpcyi5wcIfCkzyC +imJukhYy6KoqNMKQEdpNiSOvWyDMTMt1bwCvEzpw91u+msUt4rj0efnO9s0ZOwdw +MRDnH4xgUl5ZLwrrPkfC1/0CAwEAAaNmMGQwHQYDVR0OBBYEFNLu71oijTptXCOX +PfKF1SbxJXuSMB8GA1UdIwQYMBaAFNLu71oijTptXCOXPfKF1SbxJXuSMBIGA1Ud +EwEB/wQIMAYBAf8CAQMwDgYDVR0PAQH/BAQDAgGGMA0GCSqGSIb3DQEBDQUAA4IB +AQC3IW/sgB9pZ8apJNjxuGoX+FkILks0wMNrdXL/coUvsrhzsvl6mePMrbGJByJ1 +XnquB5sgcRENFxdQFma3mio8Upf1owM1ZreXrJ0mADG2BplqbJnxiyYa+R11reIF +TWeIhMNcZKsDZrFAyPuFjCWSQvJmNWe9mFRYFgNhXJKkXIb5H1XgEDlwiedYRM7V +olBNlld6pRFKlX8ust6OTMOeADl2xNF0m1LThSdeuXvDyC1g9+ILfz3S6OIYgc3i +roRcFD354g7rKfu67qFAw9gC4yi0xBTPrY95rh4/HqaUYCA/L8ldRk6H7Xk35D+W +Vpmq2Sh/xT5HiFuhf4wJb0bK +-----END CERTIFICATE-----"; + +/// From https://developers.yubico.com/PKI/yubico-intermediate.pem +const YUBICO_ATTESTATION_INTERMEDIATE_A_1: &str = "-----BEGIN CERTIFICATE----- +MIIDSDCCAjCgAwIBAgIUUcmMXzRIFOgGTK0Tb3gEuZYZkBIwDQYJKoZIhvcNAQEL +BQAwJDEiMCAGA1UEAwwZWXViaWNvIEF0dGVzdGF0aW9uIFJvb3QgMTAgFw0yNDEy +MDEwMDAwMDBaGA85OTk5MTIzMTIzNTk1OVowLjEsMCoGA1UEAwwjWXViaWNvIEF0 +dGVzdGF0aW9uIEludGVybWVkaWF0ZSBBIDEwggEiMA0GCSqGSIb3DQEBAQUAA4IB +DwAwggEKAoIBAQDm555bWY9WW+tOY0rIWHldh+aNanoCZCFh7Gk3YZrQmPUw0hkS +G6qYHQtP+fZyS33VErvg+BQqnmumgNhfxFrkwEZELeidBcC8C4Ag4nqqiPWpzsvI +17NcxYlInLNLFcZY/+gOiN6ZOTihO5/vBZMbj9riaAcqliYmNGJPgTcMGaEAyMzE +MNy2nm6Ep+pjP5aF6gi21t/UQFsuJ1j2Rj/ynM/SdRt+ecal5OYotxHkFbL9vvv2 +A2Ov5ITZClw4bOS9npypQimOZ5QAYytmYaQpWl/pMYz6zSj8RqkVDNEJGqNfTKA2 +ivLYwX6lSttMPapg0J84l9X0voVN/FpS4VCVAgMBAAGjZjBkMB0GA1UdDgQWBBQg +KFAhG6RaW+hTy52dxeT8bC96HzAfBgNVHSMEGDAWgBTS7u9aIo06bVwjlz3yhdUm +8SV7kjASBgNVHRMBAf8ECDAGAQH/AgECMA4GA1UdDwEB/wQEAwIBhjANBgkqhkiG +9w0BAQsFAAOCAQEAYMzgLrJLIr0OovQnAZrRIGuabiHSUKSmbLRWpRkWeAtsChDE +HpXcJ/bgDNKYWoHqQ8xRUjB4CyepYevc3YlrG8o7zHxpfVcaoL5SeuJkzHxKn4bT +aSp9+Mvwamnp64kZMiNbFLknfP9kYKoRHkMWheRJ1UsP1z4ScmkCeILfsMs6vqov +qjWClFsJpBcsluYHWF7bBJ1n4Rwg+ATEopY4IgGv6Zvwc+A9r+AT2hqpoSkYoAl+ +ANYwgslOf9sJe0V+TA9YY/UlaBmPPTd0//r9wvcePWZkPjKoAC/zUNhfDbh4LV8G +Hs3lyX2XomL/LNc8JYzyIaDEhGQveoPhh/tr1g== +-----END CERTIFICATE-----"; + +/// From https://developers.yubico.com/PKI/yubico-intermediate.pem +const YUBICO_ATTESTATION_INTERMEDIATE_B_1: &str = "-----BEGIN CERTIFICATE----- +MIIDSDCCAjCgAwIBAgIUDqERw+4RnGSggxgUewJFEPDRZ3YwDQYJKoZIhvcNAQEL +BQAwJDEiMCAGA1UEAwwZWXViaWNvIEF0dGVzdGF0aW9uIFJvb3QgMTAgFw0yNDEy +MDEwMDAwMDBaGA85OTk5MTIzMTIzNTk1OVowLjEsMCoGA1UEAwwjWXViaWNvIEF0 +dGVzdGF0aW9uIEludGVybWVkaWF0ZSBCIDEwggEiMA0GCSqGSIb3DQEBAQUAA4IB +DwAwggEKAoIBAQDI7XnH+ZvDwMCQU8M8ZeV5qscublvVYaaRt3Ybaxn9godLx5sw +H0lXrdgjh5h7FpVgCgYYX7E4bl1vbzULemrMWT8N3WMGUe8QAJbBeioV7W/E+hTZ +P/0SKJVa3ewKBo6ULeMnfQZDrVORAk8wTLq2v5Llj5vMj7JtOotKa9J7nHS8kLmz +XXSaj0SwEPh5OAZUTNV4zs1bvoTAQQWrL4/J9QuKt6WCFE5nUNiRQcEbVF8mlqK2 +bx2z6okVltyDVLCxYbpUTELvY1usR3DTGPUoIClOm4crpwnDRLVHvjYePGBB//pE +yzxA/gcScxjwaH1ZUw9bnSbHyurKqbTa1KvjAgMBAAGjZjBkMB0GA1UdDgQWBBTq +t0KQngx7ZHrbVHwDunxOn9ihYTAfBgNVHSMEGDAWgBTS7u9aIo06bVwjlz3yhdUm +8SV7kjASBgNVHRMBAf8ECDAGAQH/AgECMA4GA1UdDwEB/wQEAwIBhjANBgkqhkiG +9w0BAQsFAAOCAQEAqQaCWMxTGqVVX7Sk7kkJmUueTSYKuU6+KBBSgwIRnlw9K7He +1IpxZ0hdwpPNikKjmcyFgFPzhImwHJgxxuT90Pw3vYOdcJJNktDg35PXOfzSn15c +FAx1RO0mPTmIb8dXiEWOpzoXvdwXDM41ZaCDYMT7w4IQtMyvE7xUBZq2bjtAnq/N +DUA7be4H8H3ipC+/+NKlUrcUh+j48K67WI0u1m6FeQueBA7n06j825rqDqsaLs9T +b7KAHAw8PmrWaNPG2kjKerxPEfecivlFawp2RWZvxrVtn3TV2SBxyCJCkXsND05d +CErVHSJIs+BdtTVNY9AwtyPmnyb0v4mSTzvWdw== +-----END CERTIFICATE-----"; + +/// From https://developers.yubico.com/PKI/yubico-intermediate.pem +const YUBICO_PIV_ATTESTATION_A_1: &str = "-----BEGIN CERTIFICATE----- +MIIDSTCCAjGgAwIBAgIUSiefkiKiicP9B63XwO7fKqevCkQwDQYJKoZIhvcNAQEL +BQAwLjEsMCoGA1UEAwwjWXViaWNvIEF0dGVzdGF0aW9uIEludGVybWVkaWF0ZSBB +IDEwIBcNMjQxMjAxMDAwMDAwWhgPOTk5OTEyMzEyMzU5NTlaMCUxIzAhBgNVBAMM +Gll1YmljbyBQSVYgQXR0ZXN0YXRpb24gQSAxMIIBIjANBgkqhkiG9w0BAQEFAAOC +AQ8AMIIBCgKCAQEAyGCyrZjNrdPfChdDe4JWd+4TMLr8nbugcKJz12egglWi7oy5 +L9GT99/if9i1OrONdpEt0YrCa+qMb+dJJ0WUa8M5zXYnUDpn72vhFjH+Anb9P9+v ++ZrRqaj/jnR/MYP7NpVpeLHiH2dRCe/PX/NH1XE41GvdUEncDtqUUGaXUea0DfDY +McRDpPT2Qn5e8rn9FjzDA37SbOVuws5VlFTDzDdqR0FnqeWeIW0DFu17rzCqXcaB +VRDnQLTc5EEPDTpiRrQE/Ag+7Wg9ieLrueos75YMQ1EIkfjL49OBVogU1A7kwRGv +OnG8l7sYaY8LZ2b5FROe2hKqmsIy600qjn6b/QIDAQABo2YwZDAdBgNVHQ4EFgQU +hAuLXXtpQVBkcsbqyFlj6LVAadgwHwYDVR0jBBgwFoAUIChQIRukWlvoU8udncXk +/Gwveh8wEgYDVR0TAQH/BAgwBgEB/wIBATAOBgNVHQ8BAf8EBAMCAYYwDQYJKoZI +hvcNAQELBQADggEBAFxL/2oFjxkLh2KVnFKdhy7Nf7MmEfYXDDFSx1rFDn445jHO +UP5kxQPbZc9r53jdvL5W0SQBqBjqA95PYh0r1CPMFsFJdiFXli8Hf3NQ0bTkeFSN +G3LsQCOKMb+o2WjYU3vHkRVjKgKGLxysxxKxGfMUcXdJ0qM6ZVeRHehC2zy7XuI6 +TQn7/V0ZHXjk7So7dUV55xQde094/3cCTnh9Q3j2aqMjkGx6tDboCsz/+W+tne7W +nMHG92ZiAAmOkP2bABjan461Qty/qBXPHomkfjqNbjUTluPXiMLYKCXHIyKwdkX6 +cphouSMU3QOTsb35Y2PeWNk54xu+Eds/3nhRMso= +-----END CERTIFICATE-----"; + +/// From https://developers.yubico.com/PKI/yubico-intermediate.pem +const YUBICO_PIV_ATTESTATION_B_1: &str = "-----BEGIN CERTIFICATE----- +MIIDSTCCAjGgAwIBAgIUWVf2oJG+t1qP8t8TicWgJ2KYan4wDQYJKoZIhvcNAQEL +BQAwLjEsMCoGA1UEAwwjWXViaWNvIEF0dGVzdGF0aW9uIEludGVybWVkaWF0ZSBC +IDEwIBcNMjQxMjAxMDAwMDAwWhgPOTk5OTEyMzEyMzU5NTlaMCUxIzAhBgNVBAMM +Gll1YmljbyBQSVYgQXR0ZXN0YXRpb24gQiAxMIIBIjANBgkqhkiG9w0BAQEFAAOC +AQ8AMIIBCgKCAQEAv7WBL9/5AKxSpCMoL63183WqRtFrOHY7tdyuGtoidoYWQrxV +aV9S+ZwH0aynh0IzD5A/PvCtuxdtL5w2cAI3tgsborOlEert4IZ904CZQfq3ooar +1an/wssbtMpPOQkC3MQiqrUyHlFS2BTbuwbBXY66lSVX/tGRuUgnBdfBJtcQKS6M +O4bU5ndPQqhGPyzcyY1LvlfzK7KJ1r/bixCRFqjhJRnPs0Czpg6rkRrFgC6cd5bK +1UgTsJy+3wrIqkv4CeV3EhSVnhnQjZgIrdIcI5WZ8T1Oq3OhMlWmY0K0dy/oZdP/ +bpbG2qbyHLa6gprLT/qChQWLmffxn6D2DAB1zQIDAQABo2YwZDAdBgNVHQ4EFgQU +M0Nt3QHo7eGzaKMZn2SmXT74vpcwHwYDVR0jBBgwFoAU6rdCkJ4Me2R621R8A7p8 +Tp/YoWEwEgYDVR0TAQH/BAgwBgEB/wIBATAOBgNVHQ8BAf8EBAMCAYYwDQYJKoZI +hvcNAQELBQADggEBAI0HwoS84fKMUyIof1LdUXvyeAMmEwW7+nVETvxNNlTMuwv7 +zPJ4XZAm9Fv95tz9CqZBj6l1PAPQn6Zht9LQA92OF7W7buuXuxuusBTgLM0C1iX2 +CGXqY/k/uSNvi3ZYfrpd44TIrfrr8bCG9ux7B5ZCRqb8adDUm92Yz3lK1aX2M6Cw +jC9IZVTXQWhLyP8Ys3p7rb20CO2jJzV94deJ/+AsEb+bnCQImPat1GDKwrBosar+ +BxtU7k6kgkxZ0G384O59GFXqnwkbw2b5HhORvOsX7nhOUhePFufzi1vT1g8Tzbwr ++TUfTwo2biKHHcI762KGtp8o6Bcv5y8WgExFuWY= +-----END CERTIFICATE-----"; + /// Represents the collection of data that has been validated from /// the client leaf certificate, all the way up to the root CA. #[derive(Debug)] @@ -104,6 +215,80 @@ fn extract_certificate_extension_data( }) } +/// Verify that the intermediates are chained to the root CA. +fn verify_intermediates( + parsed_intermediate: &X509Certificate<'_>, + ca_pems: Vec<&str>, +) -> Result<(), Error> { + // There has to be at least the root CA + if ca_pems.is_empty() { + return Err(Error::InvalidSignature); + } + + let mut ca_parsed_pems = vec![]; + + // Parse all the pems + for pem in ca_pems { + let (_, parsed_pem) = parse_x509_pem(pem.as_bytes()).map_err(|_| Error::ParsingError)?; + ca_parsed_pems.push(parsed_pem); + } + + // Parse the root CA + let root_ca = ca_parsed_pems.first().ok_or(Error::ParsingError)?; + let mut parent_ca = Pem::parse_x509(&root_ca).map_err(|_| Error::ParsingError)?; + + // Iteratively verify the chain + for intermediate_ca in ca_parsed_pems.iter().skip(1) { + let intermediate_ca = Pem::parse_x509(&intermediate_ca).map_err(|_| Error::ParsingError)?; + + // Check the parent CA has signed this intermediate, return error if not + intermediate_ca + .verify_signature(Some(&parent_ca.tbs_certificate.public_key())) + .map_err(|_| Error::InvalidSignature)?; + + parent_ca = intermediate_ca; + } + + // Check the parent intermediate CA has signed the final intermediate, return error if not + parsed_intermediate + .verify_signature(Some(&parent_ca.tbs_certificate.public_key())) + .map_err(|_| Error::InvalidSignature)?; + + Ok(()) +} + +/// Verify a provided Yubikey attestation certification and intermediate +/// certificate are valid against the Yubico Attestation Root CA. +fn verify_yubico_intermediates(parsed_intermediate: &X509Certificate<'_>) -> Result<(), Error> { + if verify_intermediates( + &parsed_intermediate, + vec![ + YUBICO_ATTESTATION_ROOT_1, + YUBICO_ATTESTATION_INTERMEDIATE_A_1, + YUBICO_PIV_ATTESTATION_A_1, + ], + ) + .is_ok() + { + return Ok(()); + } + + if verify_intermediates( + &parsed_intermediate, + vec![ + YUBICO_ATTESTATION_ROOT_1, + YUBICO_ATTESTATION_INTERMEDIATE_B_1, + YUBICO_PIV_ATTESTATION_B_1, + ], + ) + .is_ok() + { + return Ok(()); + } + + verify_intermediates(&parsed_intermediate, vec![YUBICO_PIV_ROOT_CA_263751]) +} + /// Verify a provided yubikey attestation certification and intermediate /// certificate are valid against the Yubico attestation Root CA. pub fn verify_certificate_chain( @@ -111,23 +296,21 @@ pub fn verify_certificate_chain( intermediate: &[u8], root_pem: Option<&str>, ) -> Result { - let root_ca_pem = root_pem.unwrap_or(YUBICO_PIV_ROOT_CA); + let (_, parsed_intermediate) = X509Certificate::from_der(intermediate) + .map_err(|_| Error::ParsingError)?; - // Parse the root ca - let (_, root_ca) = parse_x509_pem(root_ca_pem.as_bytes()).unwrap(); - let root_ca = Pem::parse_x509(&root_ca).unwrap(); + // If a custom root CA is provided, we use that for verification. + // If not, we will try all the known Yubico Root CAs for backward compatibility + if let Some(pem) = root_pem { + verify_intermediates(&parsed_intermediate, vec![pem])?; + } else { + verify_yubico_intermediates(&parsed_intermediate)?; + } - // Parse the certificates - let (_, parsed_intermediate) = - parse_x509_certificate(intermediate).map_err(|_| Error::ParsingError)?; + // Parse the client cert let (_, parsed_client) = parse_x509_certificate(client).map_err(|_| Error::ParsingError)?; - // Validate that the provided intermediate certificate is signed by the Yubico Attestation Root CA - parsed_intermediate - .verify_signature(Some(&root_ca.tbs_certificate.subject_pki)) - .map_err(|_| Error::InvalidSignature)?; - - // Validate that the provided certificate is signed by the intermediate CA + // Validate that the provided client certificate is signed by the intermediate CA parsed_client .verify_signature(Some(&parsed_intermediate.tbs_certificate.subject_pki)) .map_err(|_| Error::InvalidSignature)?; diff --git a/tests/fido-lite.rs b/tests/fido-lite.rs index c1ab872..17b5581 100644 --- a/tests/fido-lite.rs +++ b/tests/fido-lite.rs @@ -3,6 +3,68 @@ use sshcerts::fido::*; use ring::digest; +const YUBIKEY_5C_NFC_5_7_4_AUTH_DATA_ED25519: [u8; 225] = [ + 24, 215, 9, 61, 183, 202, 121, 173, 239, 111, 95, 74, 89, 136, 195, 47, 121, 236, 155, 71, 59, + 195, 142, 93, 31, 100, 49, 19, 43, 73, 121, 223, 65, 0, 0, 0, 2, 215, 120, 30, 93, 227, 83, 70, + 170, 175, 226, 60, 164, 159, 19, 51, 42, 0, 128, 98, 98, 229, 4, 183, 103, 139, 128, 52, 133, + 73, 67, 19, 182, 97, 7, 69, 202, 137, 239, 194, 92, 4, 4, 141, 211, 236, 86, 50, 174, 36, 25, + 121, 3, 188, 192, 34, 181, 231, 164, 247, 91, 236, 16, 48, 104, 25, 60, 88, 199, 131, 196, 98, + 193, 93, 133, 128, 173, 125, 249, 164, 163, 48, 61, 66, 63, 2, 30, 36, 176, 179, 86, 214, 43, + 196, 0, 17, 77, 172, 8, 144, 50, 203, 164, 224, 25, 33, 247, 107, 194, 50, 80, 86, 230, 12, + 200, 220, 208, 44, 31, 128, 145, 203, 205, 110, 249, 52, 73, 133, 207, 37, 198, 9, 146, 183, + 31, 150, 63, 228, 237, 64, 83, 253, 178, 185, 180, 182, 191, 164, 1, 1, 3, 39, 32, 6, 33, 88, + 32, 49, 157, 86, 25, 233, 182, 125, 243, 238, 178, 127, 166, 95, 30, 135, 219, 202, 200, 10, + 252, 115, 193, 102, 75, 210, 246, 2, 7, 27, 57, 51, 49, +]; + +const YUBIKEY_5C_NFC_5_7_4_AUTH_SIG_ED25519: [u8; 72] = [ + 48, 70, 2, 33, 0, 253, 18, 60, 143, 204, 99, 61, 141, 84, 31, 48, 3, 97, 146, 206, 116, 205, + 23, 226, 251, 141, 172, 254, 200, 201, 200, 94, 174, 196, 232, 191, 156, 2, 33, 0, 236, 235, + 250, 190, 18, 41, 99, 204, 96, 21, 146, 184, 56, 77, 175, 88, 187, 93, 203, 175, 83, 3, 150, + 40, 245, 82, 192, 227, 128, 241, 184, 148, +]; + +const YUBIKEY_5C_NFC_5_7_4_CHALLENGE_ED25519: [u8; 32] = [ + 218, 43, 84, 240, 214, 90, 54, 239, 73, 96, 123, 70, 114, 116, 188, 66, 101, 174, 210, 190, + 106, 26, 124, 2, 48, 14, 158, 215, 185, 48, 59, 252, +]; + +const YUBIKEY_5C_NFC_5_7_4_INTERMEDIATE: [u8; 731] = [ + 48, 130, 2, 215, 48, 130, 1, 191, 160, 3, 2, 1, 2, 2, 9, 0, 181, 31, 70, 127, 92, 146, 129, 57, + 48, 13, 6, 9, 42, 134, 72, 134, 247, 13, 1, 1, 11, 5, 0, 48, 38, 49, 36, 48, 34, 6, 3, 85, 4, + 3, 12, 27, 89, 117, 98, 105, 99, 111, 32, 70, 73, 68, 79, 32, 65, 116, 116, 101, 115, 116, 97, + 116, 105, 111, 110, 32, 66, 32, 49, 48, 32, 23, 13, 50, 52, 49, 50, 48, 49, 48, 48, 48, 48, 48, + 48, 90, 24, 15, 57, 57, 57, 57, 49, 50, 51, 49, 50, 51, 53, 57, 53, 57, 90, 48, 117, 49, 11, + 48, 9, 6, 3, 85, 4, 6, 19, 2, 83, 69, 49, 18, 48, 16, 6, 3, 85, 4, 10, 12, 9, 89, 117, 98, 105, + 99, 111, 32, 65, 66, 49, 34, 48, 32, 6, 3, 85, 4, 11, 12, 25, 65, 117, 116, 104, 101, 110, 116, + 105, 99, 97, 116, 111, 114, 32, 65, 116, 116, 101, 115, 116, 97, 116, 105, 111, 110, 49, 46, + 48, 44, 6, 3, 85, 4, 3, 12, 37, 89, 117, 98, 105, 99, 111, 32, 70, 73, 68, 79, 32, 69, 69, 32, + 83, 101, 114, 105, 97, 108, 32, 49, 52, 48, 54, 57, 54, 54, 55, 51, 57, 56, 52, 52, 51, 55, 48, + 89, 48, 19, 6, 7, 42, 134, 72, 206, 61, 2, 1, 6, 8, 42, 134, 72, 206, 61, 3, 1, 7, 3, 66, 0, 4, + 91, 49, 68, 249, 200, 24, 0, 95, 15, 104, 216, 30, 216, 204, 75, 50, 21, 139, 193, 38, 21, 237, + 235, 86, 19, 36, 46, 30, 181, 227, 135, 211, 247, 22, 185, 45, 243, 59, 182, 172, 188, 233, 93, + 231, 105, 63, 3, 28, 86, 63, 243, 7, 76, 222, 73, 33, 38, 127, 190, 193, 110, 86, 144, 233, + 163, 129, 129, 48, 127, 48, 19, 6, 10, 43, 6, 1, 4, 1, 130, 196, 10, 13, 1, 4, 5, 4, 3, 5, 7, + 4, 48, 34, 6, 9, 43, 6, 1, 4, 1, 130, 196, 10, 2, 4, 21, 49, 46, 51, 46, 54, 46, 49, 46, 52, + 46, 49, 46, 52, 49, 52, 56, 50, 46, 49, 46, 55, 48, 19, 6, 11, 43, 6, 1, 4, 1, 130, 229, 28, 2, + 1, 1, 4, 4, 3, 2, 4, 48, 48, 33, 6, 11, 43, 6, 1, 4, 1, 130, 229, 28, 1, 1, 4, 4, 18, 4, 16, + 215, 120, 30, 93, 227, 83, 70, 170, 175, 226, 60, 164, 159, 19, 51, 42, 48, 12, 6, 3, 85, 29, + 19, 1, 1, 255, 4, 2, 48, 0, 48, 13, 6, 9, 42, 134, 72, 134, 247, 13, 1, 1, 11, 5, 0, 3, 130, 1, + 1, 0, 13, 245, 58, 88, 199, 17, 70, 58, 18, 10, 7, 187, 196, 155, 3, 4, 45, 60, 249, 165, 0, + 45, 239, 11, 113, 34, 188, 225, 108, 59, 148, 177, 83, 88, 150, 48, 121, 196, 75, 16, 236, 30, + 27, 157, 180, 233, 12, 81, 170, 142, 158, 124, 57, 170, 128, 34, 10, 47, 240, 224, 137, 243, + 136, 130, 204, 165, 49, 201, 126, 234, 8, 79, 248, 135, 193, 194, 73, 178, 174, 117, 73, 125, + 115, 150, 190, 245, 44, 220, 1, 200, 161, 25, 239, 113, 82, 16, 85, 66, 175, 135, 32, 146, 7, + 208, 215, 36, 37, 14, 88, 33, 212, 16, 190, 165, 244, 0, 106, 208, 233, 221, 254, 15, 55, 102, + 67, 4, 190, 94, 183, 96, 179, 29, 19, 246, 201, 223, 82, 107, 184, 246, 3, 192, 130, 210, 174, + 121, 234, 91, 244, 191, 236, 141, 92, 172, 193, 254, 182, 221, 154, 108, 60, 59, 8, 255, 100, + 197, 23, 235, 201, 71, 88, 213, 76, 83, 232, 102, 121, 154, 179, 180, 235, 206, 121, 148, 215, + 188, 111, 223, 59, 197, 14, 41, 197, 22, 198, 180, 96, 162, 182, 244, 157, 161, 107, 15, 25, + 76, 145, 171, 139, 181, 64, 6, 48, 172, 109, 5, 230, 148, 193, 22, 180, 246, 149, 169, 180, + 209, 139, 95, 48, 194, 134, 37, 113, 163, 66, 221, 228, 108, 43, 19, 148, 246, 210, 38, 223, + 224, 246, 208, 68, 255, 11, 24, 82, 74, 195, 81, +]; + const YUBIKEY_BIO_AUTH_DATA_ED25519: [u8; 225] = [ 159, 134, 208, 129, 136, 76, 125, 101, 154, 47, 234, 160, 197, 90, 208, 21, 163, 191, 79, 27, 43, 11, 130, 44, 209, 93, 108, 21, 176, 240, 10, 8, 69, 0, 0, 0, 4, 216, 82, 45, 159, 87, 91, @@ -127,6 +189,7 @@ const YUBIKEY_5C_NFC_INTERMEDIATE: [u8; 705] = [ ]; #[test] +/// This uses the old YUBICO_U2F_ROOT_CA_457200631 fn verify_and_parse_auth_data_yubikey_bio() { let hash_challenge = digest::digest(&digest::SHA256, &YUBIKEY_BIO_CHALLENGE_ED25519); let verified_data = verification::verify_auth_data( @@ -159,6 +222,7 @@ fn verify_and_parse_auth_data_yubikey_bio() { } #[test] +/// This uses the old YUBICO_U2F_ROOT_CA_457200631 fn verify_and_parse_auth_data_yubikey_5c_nfc() { let hash_challenge = digest::digest(&digest::SHA256, &YUBIKEY_5C_NFC_CHALLENGE_ED25519); let verified_data = verification::verify_auth_data( @@ -191,3 +255,39 @@ fn verify_and_parse_auth_data_yubikey_5c_nfc() { hex::decode("9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08").unwrap() ); } + +#[test] +/// YUBICO_ATTESTATION_ROOT_1 was rolled out for Yubikey 5c 5.7.4 so this will test the new +/// FIDO root CA +fn verify_and_parse_auth_data_yubikey_5c_nfc_5_7_4() { + let hash_challenge = digest::digest(&digest::SHA256, &YUBIKEY_5C_NFC_5_7_4_CHALLENGE_ED25519); + let verified_data = verification::verify_auth_data( + &YUBIKEY_5C_NFC_5_7_4_AUTH_DATA_ED25519, + &YUBIKEY_5C_NFC_5_7_4_AUTH_SIG_ED25519, + &hash_challenge.as_ref(), + -7, + &YUBIKEY_5C_NFC_5_7_4_INTERMEDIATE, + None, + ) + .unwrap(); + + // Verify data pulled from the intermediate certificate + assert_eq!( + verified_data.transports, + Some(vec![Transport::USB, Transport::NFC]) + ); + assert_eq!( + verified_data.aaguid, + Some(hex::decode("d7781e5de35346aaafe23ca49f13332a").unwrap()) + ); + + // Verify data pulled from the auth data + assert_eq!( + verified_data.auth_data.aaguid, + verified_data.aaguid.unwrap() + ); + assert_eq!( + verified_data.auth_data.rpid_hash, + hex::decode("18d7093db7ca79adef6f5f4a5988c32f79ec9b473bc38e5d1f6431132b4979df").unwrap() + ); +} diff --git a/tests/yubikey-lite.rs b/tests/yubikey-lite.rs index da40ce1..3756b4a 100644 --- a/tests/yubikey-lite.rs +++ b/tests/yubikey-lite.rs @@ -93,6 +93,100 @@ const YUBIKEY_5CI_INTERMEDIATE: [u8; 746] = [ 0xaf, 0x34, 0x22, 0xad, 0x63, 0x3e, 0xf7, 0x66, 0xe6, 0x0c, ]; +const YUBIKEY_5C_5_7_4_ATTESTATION: [u8; 631] = [ + 0x30, 0x82, 0x02, 0x73, 0x30, 0x82, 0x01, 0x5b, 0xa0, 0x03, 0x02, 0x01, 0x02, 0x02, 0x10, 0x01, + 0xae, 0x6a, 0x32, 0xa8, 0x8e, 0x74, 0x5d, 0xba, 0x5d, 0xf3, 0x0c, 0x0e, 0xeb, 0x16, 0xfd, 0x30, + 0x0d, 0x06, 0x09, 0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, 0x01, 0x01, 0x0b, 0x05, 0x00, 0x30, 0x22, + 0x31, 0x20, 0x30, 0x1e, 0x06, 0x03, 0x55, 0x04, 0x03, 0x0c, 0x17, 0x59, 0x75, 0x62, 0x69, 0x4b, + 0x65, 0x79, 0x20, 0x50, 0x49, 0x56, 0x20, 0x41, 0x74, 0x74, 0x65, 0x73, 0x74, 0x61, 0x74, 0x69, + 0x6f, 0x6e, 0x30, 0x20, 0x17, 0x0d, 0x32, 0x34, 0x31, 0x32, 0x30, 0x31, 0x30, 0x30, 0x30, 0x30, + 0x30, 0x30, 0x5a, 0x18, 0x0f, 0x39, 0x39, 0x39, 0x39, 0x31, 0x32, 0x33, 0x31, 0x32, 0x33, 0x35, + 0x39, 0x35, 0x39, 0x5a, 0x30, 0x25, 0x31, 0x23, 0x30, 0x21, 0x06, 0x03, 0x55, 0x04, 0x03, 0x0c, + 0x1a, 0x59, 0x75, 0x62, 0x69, 0x4b, 0x65, 0x79, 0x20, 0x50, 0x49, 0x56, 0x20, 0x41, 0x74, 0x74, + 0x65, 0x73, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x20, 0x38, 0x32, 0x30, 0x76, 0x30, 0x10, 0x06, + 0x07, 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x02, 0x01, 0x06, 0x05, 0x2b, 0x81, 0x04, 0x00, 0x22, 0x03, + 0x62, 0x00, 0x04, 0xd7, 0xb4, 0x2c, 0x12, 0x0e, 0xb2, 0x97, 0xb3, 0xf3, 0x2d, 0xe9, 0x7c, 0xb3, + 0x76, 0xe9, 0x77, 0xfd, 0x2e, 0x0c, 0x47, 0x97, 0xba, 0x28, 0xe7, 0xe8, 0x0e, 0x36, 0xae, 0x15, + 0xb1, 0x18, 0x0a, 0x09, 0xfc, 0xcd, 0x64, 0xb5, 0x1f, 0xa1, 0xb8, 0x00, 0x13, 0x69, 0x4c, 0x47, + 0x5c, 0x0f, 0xae, 0x55, 0xb1, 0x28, 0x65, 0x7c, 0xb4, 0xf7, 0x15, 0x63, 0x6a, 0xc1, 0x5e, 0x33, + 0x5b, 0x96, 0xd8, 0x0b, 0x5b, 0x53, 0x57, 0x1d, 0xb5, 0xf3, 0x26, 0xeb, 0xc9, 0x79, 0x07, 0x64, + 0xc4, 0xb2, 0x63, 0x6d, 0xb1, 0x7c, 0x21, 0x5c, 0x02, 0x86, 0x61, 0xf8, 0x3d, 0x60, 0xa4, 0x39, + 0x47, 0x3b, 0xe2, 0xa3, 0x4e, 0x30, 0x4c, 0x30, 0x11, 0x06, 0x0a, 0x2b, 0x06, 0x01, 0x04, 0x01, + 0x82, 0xc4, 0x0a, 0x03, 0x03, 0x04, 0x03, 0x05, 0x07, 0x04, 0x30, 0x14, 0x06, 0x0a, 0x2b, 0x06, + 0x01, 0x04, 0x01, 0x82, 0xc4, 0x0a, 0x03, 0x07, 0x04, 0x06, 0x02, 0x04, 0x01, 0xfa, 0x05, 0x3a, + 0x30, 0x10, 0x06, 0x0a, 0x2b, 0x06, 0x01, 0x04, 0x01, 0x82, 0xc4, 0x0a, 0x03, 0x08, 0x04, 0x02, + 0x01, 0x02, 0x30, 0x0f, 0x06, 0x0a, 0x2b, 0x06, 0x01, 0x04, 0x01, 0x82, 0xc4, 0x0a, 0x03, 0x09, + 0x04, 0x01, 0x03, 0x30, 0x0d, 0x06, 0x09, 0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, 0x01, 0x01, 0x0b, + 0x05, 0x00, 0x03, 0x82, 0x01, 0x01, 0x00, 0x5e, 0xb9, 0x1c, 0x19, 0x7b, 0x1a, 0x50, 0x7f, 0xed, + 0xc5, 0xaf, 0x2e, 0xa5, 0x72, 0xbf, 0x17, 0x9d, 0x0c, 0x5f, 0x1b, 0xa3, 0x7f, 0x11, 0x59, 0xea, + 0xe5, 0xb2, 0x03, 0xcd, 0x84, 0xa2, 0x69, 0x5f, 0x51, 0x43, 0xb7, 0x78, 0x42, 0xc7, 0xf9, 0x51, + 0x95, 0x4c, 0x86, 0x83, 0x8c, 0x73, 0xca, 0x79, 0x5c, 0x06, 0xfa, 0xa9, 0x38, 0xd0, 0xc3, 0xe6, + 0x4e, 0x94, 0x4f, 0x26, 0x1a, 0x8f, 0x5a, 0x17, 0x58, 0x0c, 0xf7, 0x54, 0x71, 0x00, 0x8c, 0x20, + 0x1b, 0x62, 0xd9, 0xb0, 0x4e, 0xd0, 0xe5, 0x07, 0x08, 0x68, 0x4c, 0x11, 0x83, 0xc5, 0x0e, 0x69, + 0x99, 0xbc, 0x78, 0xaf, 0xd5, 0xbe, 0xe8, 0x2c, 0x3b, 0xe5, 0xdd, 0xf6, 0x1d, 0x97, 0x1c, 0x81, + 0x93, 0x29, 0x20, 0x5d, 0xe7, 0x8a, 0xfd, 0x42, 0x7d, 0xe8, 0x72, 0x59, 0xcc, 0x33, 0xb9, 0x80, + 0x68, 0xc1, 0xa1, 0xa7, 0x6a, 0xc5, 0xa7, 0x54, 0xa3, 0xbe, 0xe8, 0x0f, 0xbd, 0xb3, 0x6e, 0x05, + 0x72, 0xe9, 0x2f, 0x40, 0xd5, 0x61, 0xd7, 0x5d, 0x85, 0xf1, 0xab, 0xa9, 0xd6, 0xf5, 0x27, 0xe2, + 0xd2, 0x22, 0x19, 0xd0, 0x1a, 0x21, 0xff, 0xbf, 0x53, 0xc5, 0xb5, 0x5d, 0x2d, 0xcb, 0xbf, 0x2d, + 0xf7, 0xd7, 0xe6, 0x77, 0xad, 0x90, 0xb9, 0xd7, 0x96, 0x95, 0x68, 0xc2, 0x3c, 0xf8, 0x73, 0xc1, + 0xc6, 0xd0, 0x7f, 0xb9, 0xf3, 0xb1, 0x43, 0x48, 0xa9, 0x62, 0x24, 0x5e, 0x91, 0x2b, 0x98, 0x6a, + 0xa3, 0xc9, 0x29, 0x5d, 0x0a, 0xc1, 0x03, 0x0c, 0xeb, 0xca, 0x61, 0x15, 0x0c, 0x00, 0xc8, 0x49, + 0x78, 0xbf, 0xb4, 0xc5, 0x19, 0xe2, 0x27, 0xf4, 0xe6, 0x14, 0xb6, 0xf4, 0x64, 0x32, 0xcf, 0x31, + 0xe0, 0x21, 0x22, 0x8c, 0xba, 0x96, 0xc5, 0xf8, 0x78, 0x84, 0x93, 0xd7, 0x01, 0x2b, 0xc4, 0xa0, + 0xb2, 0xe6, 0xb1, 0x40, 0x3e, 0xad, 0xfd, +]; + +const YUBIKEY_5C_5_7_4_INTERMEDIATE: [u8; 761] = [ + 0x30, 0x82, 0x02, 0xf5, 0x30, 0x82, 0x01, 0xdd, 0xa0, 0x03, 0x02, 0x01, 0x02, 0x02, 0x09, 0x00, + 0x95, 0xdc, 0x17, 0x99, 0x3b, 0x2d, 0xb7, 0xfc, 0x30, 0x0d, 0x06, 0x09, 0x2a, 0x86, 0x48, 0x86, + 0xf7, 0x0d, 0x01, 0x01, 0x0b, 0x05, 0x00, 0x30, 0x25, 0x31, 0x23, 0x30, 0x21, 0x06, 0x03, 0x55, + 0x04, 0x03, 0x0c, 0x1a, 0x59, 0x75, 0x62, 0x69, 0x63, 0x6f, 0x20, 0x50, 0x49, 0x56, 0x20, 0x41, + 0x74, 0x74, 0x65, 0x73, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x20, 0x42, 0x20, 0x31, 0x30, 0x20, + 0x17, 0x0d, 0x32, 0x34, 0x31, 0x32, 0x30, 0x31, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x5a, 0x18, + 0x0f, 0x39, 0x39, 0x39, 0x39, 0x31, 0x32, 0x33, 0x31, 0x32, 0x33, 0x35, 0x39, 0x35, 0x39, 0x5a, + 0x30, 0x22, 0x31, 0x20, 0x30, 0x1e, 0x06, 0x03, 0x55, 0x04, 0x03, 0x0c, 0x17, 0x59, 0x75, 0x62, + 0x69, 0x4b, 0x65, 0x79, 0x20, 0x50, 0x49, 0x56, 0x20, 0x41, 0x74, 0x74, 0x65, 0x73, 0x74, 0x61, + 0x74, 0x69, 0x6f, 0x6e, 0x30, 0x82, 0x01, 0x22, 0x30, 0x0d, 0x06, 0x09, 0x2a, 0x86, 0x48, 0x86, + 0xf7, 0x0d, 0x01, 0x01, 0x01, 0x05, 0x00, 0x03, 0x82, 0x01, 0x0f, 0x00, 0x30, 0x82, 0x01, 0x0a, + 0x02, 0x82, 0x01, 0x01, 0x00, 0xb7, 0xc6, 0xb0, 0x99, 0x54, 0x26, 0x54, 0x85, 0x5b, 0xd5, 0x35, + 0x2e, 0x23, 0xc9, 0x7b, 0x62, 0x74, 0x1c, 0x4c, 0xb4, 0xc1, 0xa7, 0x31, 0x6b, 0x9d, 0xdc, 0xd1, + 0x41, 0x1e, 0xc6, 0xbf, 0xd9, 0xb6, 0x09, 0xab, 0x21, 0xff, 0xc2, 0x56, 0x1f, 0x24, 0xa8, 0x4e, + 0x2a, 0xa4, 0xcb, 0x39, 0x8a, 0x07, 0xb2, 0xab, 0x1a, 0x99, 0xa8, 0xb8, 0xdd, 0x36, 0xa6, 0x59, + 0x99, 0xd6, 0x05, 0x7c, 0x72, 0x4b, 0xe8, 0x02, 0x92, 0xa6, 0x90, 0x9c, 0x59, 0x89, 0xda, 0x1c, + 0xde, 0xab, 0x4a, 0x3e, 0x8e, 0x80, 0x1a, 0x62, 0x76, 0x0f, 0x6e, 0x69, 0x09, 0x4d, 0xc7, 0xed, + 0x37, 0x7d, 0x79, 0xfb, 0x18, 0x91, 0xd0, 0x85, 0x66, 0x20, 0xbd, 0x35, 0xfd, 0xa9, 0xcc, 0xe6, + 0x7e, 0x4a, 0xa1, 0xda, 0xb8, 0x78, 0x6b, 0x5e, 0x01, 0x83, 0x68, 0xa9, 0x80, 0x17, 0x8f, 0xc3, + 0x0b, 0xcf, 0x52, 0x27, 0x47, 0x49, 0xda, 0xb2, 0xb5, 0xf3, 0x20, 0xab, 0xe1, 0xf5, 0x63, 0x36, + 0xe8, 0xb9, 0xa9, 0x97, 0xc8, 0x77, 0x5c, 0xa5, 0x6f, 0xce, 0x3c, 0xf3, 0x9e, 0xa1, 0x3c, 0x2f, + 0xf9, 0x7e, 0x1f, 0xaf, 0x76, 0xe5, 0x76, 0x9c, 0x00, 0x06, 0xa1, 0xc3, 0x4f, 0xf9, 0x8c, 0xff, + 0x39, 0xe1, 0xd7, 0xa7, 0x61, 0x42, 0xee, 0xd0, 0x94, 0xf1, 0x60, 0x9c, 0x6d, 0xeb, 0x34, 0xae, + 0x9d, 0x74, 0x22, 0x1d, 0x56, 0xe1, 0x6c, 0xf8, 0xba, 0x98, 0x95, 0x4e, 0xd4, 0xb5, 0xef, 0x9f, + 0x41, 0xb5, 0x69, 0xf0, 0x58, 0xab, 0x6a, 0xfc, 0x5c, 0xcd, 0x46, 0xf5, 0x19, 0x14, 0x4a, 0x30, + 0xd9, 0x16, 0x1a, 0xd0, 0x3a, 0x9b, 0x93, 0xfc, 0xef, 0x3c, 0x80, 0xc3, 0xcb, 0xd4, 0x6e, 0x73, + 0x25, 0xb1, 0xf9, 0x63, 0xea, 0x96, 0x87, 0x03, 0x03, 0xa9, 0xcd, 0x77, 0xc0, 0x4f, 0x8f, 0xc6, + 0x93, 0xae, 0x54, 0x86, 0xa5, 0x02, 0x03, 0x01, 0x00, 0x01, 0xa3, 0x29, 0x30, 0x27, 0x30, 0x11, + 0x06, 0x0a, 0x2b, 0x06, 0x01, 0x04, 0x01, 0x82, 0xc4, 0x0a, 0x03, 0x03, 0x04, 0x03, 0x05, 0x07, + 0x04, 0x30, 0x12, 0x06, 0x03, 0x55, 0x1d, 0x13, 0x01, 0x01, 0xff, 0x04, 0x08, 0x30, 0x06, 0x01, + 0x01, 0xff, 0x02, 0x01, 0x00, 0x30, 0x0d, 0x06, 0x09, 0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, 0x01, + 0x01, 0x0b, 0x05, 0x00, 0x03, 0x82, 0x01, 0x01, 0x00, 0x4b, 0x32, 0x50, 0xcd, 0xc6, 0xc5, 0x40, + 0x96, 0x48, 0x79, 0x62, 0x4d, 0xf7, 0x05, 0xf7, 0xea, 0xf9, 0x04, 0x42, 0x0b, 0x7d, 0x1f, 0xf9, + 0xd9, 0xc7, 0x69, 0x45, 0xf6, 0x89, 0x59, 0x27, 0x56, 0xfb, 0xf6, 0xc7, 0x51, 0xf7, 0xdd, 0x19, + 0x64, 0x43, 0xe8, 0xf7, 0x24, 0x63, 0x70, 0x10, 0x06, 0x98, 0x1a, 0xa7, 0x7c, 0xc1, 0x83, 0x31, + 0x39, 0xca, 0xad, 0x8a, 0xbf, 0x70, 0x35, 0x6e, 0x70, 0x05, 0xa3, 0xa0, 0xc7, 0x8c, 0xf0, 0xa7, + 0xc1, 0xaa, 0x2c, 0xf6, 0x11, 0x66, 0xfd, 0x85, 0x62, 0x77, 0x18, 0xd6, 0xbe, 0x2d, 0x1b, 0xac, + 0x7c, 0x0a, 0x1c, 0x71, 0x86, 0x41, 0xd4, 0xa6, 0x43, 0x32, 0xc0, 0x1b, 0xbf, 0xda, 0x69, 0xba, + 0x5f, 0x69, 0x57, 0xc4, 0xba, 0xb4, 0x03, 0x04, 0x15, 0x28, 0x64, 0x09, 0xc1, 0xc5, 0xd3, 0x0f, + 0xb4, 0xb6, 0x92, 0xf8, 0x71, 0xa1, 0x01, 0x56, 0xd3, 0x83, 0x28, 0x7d, 0x82, 0x56, 0x6b, 0x7e, + 0xa6, 0x62, 0xfd, 0xaa, 0x0c, 0xe7, 0xb2, 0x41, 0xe7, 0x20, 0x59, 0xf0, 0x60, 0xb3, 0x51, 0xac, + 0xcd, 0xe8, 0x27, 0x44, 0xe5, 0x39, 0xc4, 0xb3, 0xea, 0x28, 0x55, 0x05, 0xdf, 0x40, 0xd3, 0xf6, + 0xc0, 0xd9, 0x0c, 0xd8, 0x83, 0x0e, 0x13, 0xf0, 0x8d, 0x5d, 0xaf, 0xd4, 0x56, 0x9f, 0x6c, 0xdc, + 0x91, 0xfb, 0x86, 0x5a, 0x16, 0xfd, 0x53, 0x68, 0xca, 0x72, 0xb9, 0x4a, 0x53, 0x2b, 0x27, 0xe6, + 0x9c, 0xa1, 0xe8, 0x4c, 0xdd, 0xf0, 0xcc, 0xe0, 0xd8, 0x08, 0xa4, 0x4f, 0xaa, 0xf1, 0xe8, 0xbc, + 0xfa, 0xc8, 0xd4, 0x93, 0xc3, 0x44, 0x76, 0x27, 0xbe, 0xbc, 0xf2, 0x04, 0x82, 0x94, 0xbc, 0x64, + 0x41, 0x97, 0xd7, 0x4d, 0xf4, 0x92, 0xe4, 0x5f, 0xc3, 0x8e, 0xc6, 0x1b, 0x80, 0x89, 0x10, 0xdf, + 0x5f, 0x6c, 0x1d, 0xa3, 0x6a, 0xac, 0xd4, 0xfc, 0xc8, +]; + #[test] pub fn test_5c_attestation_verification() { let attested_key_data = verification::verify_certificate_chain( @@ -107,3 +201,18 @@ pub fn test_5c_attestation_verification() { assert_eq!(attested_key_data.touch_policy, 3); assert_eq!(attested_key_data.pin_policy, 1); } + +#[test] +pub fn test_5c_5_7_4_attestation_verification() { + let attested_key_data = verification::verify_certificate_chain( + &YUBIKEY_5C_5_7_4_ATTESTATION, + &YUBIKEY_5C_5_7_4_INTERMEDIATE, + None, + ) + .unwrap(); + + assert_eq!(attested_key_data.serial, 33162554); + assert_eq!(attested_key_data.firmware, "5.7.4"); + assert_eq!(attested_key_data.touch_policy, 2); + assert_eq!(attested_key_data.pin_policy, 1); +} From 4ae4dd34367494d95f1126276110be1764b10328 Mon Sep 17 00:00:00 2001 From: Thanh Nguyen Date: Thu, 17 Apr 2025 00:13:43 -0400 Subject: [PATCH 4/6] Revert x509-parser -> x509-cert migration --- Cargo.toml | 11 ++-- src/x509/mod.rs | 114 ++++++++++++++++++++++-------------- src/yubikey/verification.rs | 59 ++++++++++--------- 3 files changed, 106 insertions(+), 78 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index db1f59b..cc5aff4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -46,7 +46,7 @@ fido-support = ["ctap-hid-fido2", "fido-lite"] fido-support-mozilla = ["authenticator", "fido-lite"] fido-lite = ["minicbor", "x509-cert"] rsa-signing = ["simple_asn1", "num-bigint"] -x509-support = ["x509-cert"] +x509-support = ["der-parser", "x509", "x509-parser"] yubikey-support = ["rcgen", "yubikey", "yubikey-lite"] yubikey-lite = ["x509-support"] @@ -64,9 +64,10 @@ num-bigint = { version = "0.4", optional = true } yubikey = { version = "0.8", features = ["untested"], optional = true } lexical-core = { version = ">0.7.4", optional = true } rcgen = { version = "0.11", optional = true } -# der = { version = "0.7", optional = true } +x509 = { version = "0.2", optional = true } +x509-parser = { version = "0.15", features = ["verify"], optional = true } +der-parser = { version = "5", optional = true } x509-cert = { version = "0.2", optional = true } -# spki = { version = "0.7.3", optional = true } # Dependencies for encrypted-keys aes = { version = "0.7", features = ["ctr"], optional = true } @@ -95,8 +96,8 @@ env_logger = "0.8.2" hex = "0.4.2" clap = "3.0.5" criterion = "0.3" -p256 = "*" -p384 = "*" +p256 = "0.13.2" +p384 = "0.13.1" [[bench]] name = "certs_per_second" diff --git a/src/x509/mod.rs b/src/x509/mod.rs index 5e1ded5..9eaaab2 100644 --- a/src/x509/mod.rs +++ b/src/x509/mod.rs @@ -1,36 +1,58 @@ +use x509_parser::prelude::FromDer; + use crate::error::Error; use crate::ssh::{Curve, EcdsaPublicKey, KeyType, PublicKey, PublicKeyKind}; -use x509_cert::der::Encode; -use x509_cert::{ - der::{oid::ObjectIdentifier, Decode}, - spki::SubjectPublicKeyInfo, -}; +use der_parser::der::parse_der_sequence; +use der_parser::error::BerError; + +const OID_RSA_ENCRYPTION: &str = "1.2.840.113549.1.1.1"; +const OID_EC_PUBLIC_KEY: &str = "1.2.840.10045.2.1"; +const OID_NIST_P256: &str = "1.2.840.10045.3.1.7"; +const OID_NIST_P384: &str = "1.3.132.0.34"; -const RSA_ENCRYPTION_OID: ObjectIdentifier = ObjectIdentifier::new_unwrap("1.2.840.113549.1.1.1"); -const EC_PUBLIC_KEY_OID: ObjectIdentifier = ObjectIdentifier::new_unwrap("1.2.840.10045.2.1"); -const NISTP256_OID: ObjectIdentifier = ObjectIdentifier::new_unwrap("1.2.840.10045.3.1.7"); -const NISTP384_OID: ObjectIdentifier = ObjectIdentifier::new_unwrap("1.3.132.0.34"); +impl From> for Error { + fn from(_: x509_parser::nom::Err) -> Error { + Error::ParsingError + } +} +impl From for Error { + fn from(_: BerError) -> Error { + Error::ParsingError + } +} /// Helper function to convert a DER encoded public key, into an SSH formatted /// public key that can be used with the rest of the SSHCerts library. This /// function only supports NISTP256 and NISTP384 Ecdsa keys pub fn der_encoding_to_ssh_public_key(key: &[u8]) -> Result { - let spki = SubjectPublicKeyInfo::from_der(key).map_err(|_| Error::ParsingError)?; + let (_rem, parsed) = parse_der_sequence(key).map_err(|_| Error::ParsingError)?; + let parsed = parsed.as_sequence().map_err(|_| Error::ParsingError)?; - let oid_alg = spki - .algorithm - .parameters_oid() - .map_err(|_| Error::ParsingError)?; + if parsed.len() != 2 { + return Err(Error::ParsingError); + } - let (key_type, curve) = match oid_alg { - NISTP256_OID => { + let oids = &parsed[0].as_sequence()?; + if oids.len() != 2 { + return Err(Error::ParsingError); + } + + let type_oid = oids[0].as_oid()?; + let key_size_oid = oids[1].as_oid()?; + if type_oid.to_id_string() != OID_EC_PUBLIC_KEY { + return Err(Error::ParsingError); + } + + let data = &parsed[1].as_bitstring()?.data; + let (key_type, curve) = match key_size_oid.to_id_string().as_str() { + OID_NIST_P256 => { let key_type = KeyType::from_name("ecdsa-sha2-nistp256").unwrap(); let curve = Curve::from_identifier("nistp256").unwrap(); (key_type, curve) } - NISTP384_OID => { + OID_NIST_P384 => { let key_type = KeyType::from_name("ecdsa-sha2-nistp384").unwrap(); let curve = Curve::from_identifier("nistp384").unwrap(); (key_type, curve) @@ -40,7 +62,7 @@ pub fn der_encoding_to_ssh_public_key(key: &[u8]) -> Result { let kind = EcdsaPublicKey { curve, - key: spki.subject_public_key, + key: data.to_vec(), sk_application: None, }; @@ -54,46 +76,50 @@ pub fn der_encoding_to_ssh_public_key(key: &[u8]) -> Result { /// This function is used to extract an SSH public key from an x509 /// certificate pub fn extract_ssh_pubkey_from_x509_certificate(cert: &[u8]) -> Result { - let cert = x509_cert::Certificate::from_der(cert) - .map_err(|_| Error::ParsingError)?; - let spki = cert.tbs_certificate.subject_public_key_info.to_der() - .map_err(|_| Error::ParsingError)?; - let spki = SubjectPublicKeyInfo::>::from_der(&spki) - .map_err(|_| Error::ParsingError)?; - convert_x509_pki_to_pubkey(spki) + let parsed_cert = match x509_parser::parse_x509_certificate(cert) { + Ok((_, c)) => c, + Err(_) => return Err(Error::ParsingError), + }; + let pki = &parsed_cert.tbs_certificate.subject_pki; + convert_x509_pki_to_pubkey(pki) } /// This function is used to extract an SSH public key from an x509 /// certificate signing request pub fn extract_ssh_pubkey_from_x509_csr(csr: &[u8]) -> Result { - let parsed_csr = x509_cert::request::CertReqInfo::from_der(csr) - .map_err(|_| Error::ParsingError)?; - let spki = &parsed_csr.public_key.to_der() - .map_err(|_| Error::ParsingError)?; - let spki = SubjectPublicKeyInfo::>::from_der(&spki) - .map_err(|_| Error::ParsingError)?; - convert_x509_pki_to_pubkey(spki) + let parsed_csr = + match x509_parser::certification_request::X509CertificationRequest::from_der(csr) { + Ok((_, csr)) => csr, + Err(_) => return Err(Error::ParsingError), + }; + let pki = &parsed_csr.certification_request_info.subject_pki; + convert_x509_pki_to_pubkey(pki) } -fn convert_x509_pki_to_pubkey>>( - pki: SubjectPublicKeyInfo, +fn convert_x509_pki_to_pubkey( + pki: &x509_parser::x509::SubjectPublicKeyInfo<'_>, ) -> Result { - return match pki.algorithm.oid { - RSA_ENCRYPTION_OID => Err(Error::Unsupported), - EC_PUBLIC_KEY_OID => { - let key_bytes: Vec = pki.subject_public_key.into(); - let curve_oid = pki + return match pki.algorithm.algorithm.to_string().as_str() { + OID_RSA_ENCRYPTION => Err(Error::Unsupported), + OID_EC_PUBLIC_KEY => { + let key_bytes = &pki.subject_public_key.data; + let algorithm_parameters = pki .algorithm .parameters + .as_ref() .ok_or(Error::ParsingError)?; - match curve_oid { - NISTP256_OID => { + let curve_oid = algorithm_parameters + .as_oid() + .map_err(|_| Error::ParsingError)?; + + match curve_oid.to_string().as_str() { + OID_NIST_P256 => { let key_type = KeyType::from_name("ecdsa-sha2-nistp256").unwrap(); let curve = Curve::from_identifier("nistp256").unwrap(); let kind = EcdsaPublicKey { curve, - key: key_bytes, + key: key_bytes.to_vec(), sk_application: None, }; @@ -103,12 +129,12 @@ fn convert_x509_pki_to_pubkey>>( comment: None, }) } - NISTP384_OID => { + OID_NIST_P384 => { let key_type = KeyType::from_name("ecdsa-sha2-nistp384").unwrap(); let curve = Curve::from_identifier("nistp384").unwrap(); let kind = EcdsaPublicKey { curve, - key: key_bytes, + key: key_bytes.to_vec(), sk_application: None, }; diff --git a/src/yubikey/verification.rs b/src/yubikey/verification.rs index 6584426..8c707c2 100644 --- a/src/yubikey/verification.rs +++ b/src/yubikey/verification.rs @@ -1,13 +1,11 @@ -use crate::{error::Error, x509::{self, extract_ssh_pubkey_from_x509_certificate}, PublicKey}; +use crate::{error::Error, x509::extract_ssh_pubkey_from_x509_certificate, PublicKey}; -use x509_cert::{der::{self, Decode, DecodePem, ObjectIdentifier}, Certificate}; +use x509_parser::der_parser::ber::BerObjectContent; +use x509_parser::der_parser::der::parse_der_integer; +use x509_parser::prelude::*; use std::convert::TryInto; -const FIRMWARE_OID: ObjectIdentifier = ObjectIdentifier::new_unwrap("1.3.6.1.4.1.41482.3.3"); -const SERIAL_OID: ObjectIdentifier = ObjectIdentifier::new_unwrap("1.3.6.1.4.1.41482.3.7"); -const POLICY_OID: ObjectIdentifier = ObjectIdentifier::new_unwrap("1.3.6.1.4.1.41482.3.8"); - /// From https://developers.yubico.com/PKI/yubico-ca-certs.txt const YUBICO_PIV_ROOT_CA_263751: &str = "-----BEGIN CERTIFICATE----- MIIDFzCCAf+gAwIBAgIDBAZHMA0GCSqGSIb3DQEBCwUAMCsxKTAnBgNVBAMMIFl1 @@ -158,44 +156,43 @@ pub struct ValidPIVKey { fn extract_certificate_extension_data( public_key: PublicKey, - certificate: x509_cert::Certificate, + certificate: &X509Certificate<'_>, ) -> Result { let mut firmware: Option = None; let mut serial: Option = None; let mut policies: Option<[u8; 2]> = None; - let extensions = certificate.tbs_certificate.extensions - .ok_or_else(|| Error::ParsingError)?; - + let extensions = certificate.extensions(); for ext in extensions.iter() { - match ext.extn_id { - FIRMWARE_OID => { - if ext.extn_value.len().into() != 3 { + match ext.oid.to_id_string().as_str() { + // Firmware + "1.3.6.1.4.1.41482.3.3" => { + if ext.value.len() != 3 { continue; } - let value = ext.extn_value.as_bytes(); firmware = Some(format!( "{}.{}.{}", - value[0], value[1], value[2] + ext.value[0], ext.value[1], ext.value[2] )); } // Serial - SERIAL_OID => { - let value = der::asn1::Int::from_der(ext.extn_value.as_bytes()) - .map_err(|_| Error::ParsingError)?; - if value.len().into() > 8 { - continue; + "1.3.6.1.4.1.41482.3.7" => { + let (_, obj) = parse_der_integer(ext.value).map_err(|_| Error::ParsingError)?; + if let BerObjectContent::Integer(s) = obj.content { + if s.len() > 8 { + continue; + } + + let mut padded_serial = vec![0; 8 - s.len()]; + padded_serial.extend_from_slice(s); + serial = Some(u64::from_be_bytes( + padded_serial.try_into().map_err(|_| Error::ParsingError)?, + )); } - let mut padded_serial = vec![0; 8 - value.len().into()]; - padded_serial.extend_from_slice(s); - serial = Some(u64::from_be_bytes( - padded_serial.try_into().map_err(|_| Error::ParsingError)?, - )); } // Policy - POLICY_OID => { - let value = ext.extn_value.as_bytes(); - policies = Some([value[0], value[1]]); + "1.3.6.1.4.1.41482.3.8" => { + policies = Some([ext.value[0], ext.value[1]]); } _ => (), } @@ -318,11 +315,15 @@ pub fn verify_certificate_chain( .verify_signature(Some(&parsed_intermediate.tbs_certificate.subject_pki)) .map_err(|_| Error::InvalidSignature)?; + println!("Extract public key from client cert"); + // Extract the certificate public key and convert to an sshcerts PublicKey let public_key = match extract_ssh_pubkey_from_x509_certificate(client) { Ok(ssh) => ssh, Err(_) => return Err(Error::ParsingError), }; - extract_certificate_extension_data(public_key, parsed_client) + println!("Extract extensions from client cert"); + + extract_certificate_extension_data(public_key, &parsed_client) } From 005d10657c5510ce878ebc52707a8326a9ed98f9 Mon Sep 17 00:00:00 2001 From: Thanh Nguyen Date: Thu, 17 Apr 2025 00:31:56 -0400 Subject: [PATCH 5/6] Create nongeneric yk::provision variants --- Cargo.toml | 6 +++--- examples/yk-provision.rs | 13 ++++++++----- src/yubikey/piv/management.rs | 26 ++++++++++++++++++++++++++ 3 files changed, 37 insertions(+), 8 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index cc5aff4..f4333ff 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -48,7 +48,7 @@ fido-lite = ["minicbor", "x509-cert"] rsa-signing = ["simple_asn1", "num-bigint"] x509-support = ["der-parser", "x509", "x509-parser"] yubikey-support = ["rcgen", "yubikey", "yubikey-lite"] -yubikey-lite = ["x509-support"] +yubikey-lite = ["x509-support", "x509-cert", "p256", "p384"] [dependencies] base64 = "0.13" @@ -68,6 +68,8 @@ x509 = { version = "0.2", optional = true } x509-parser = { version = "0.15", features = ["verify"], optional = true } der-parser = { version = "5", optional = true } x509-cert = { version = "0.2", optional = true } +p256 = { version = "0.13.2", optional = true } +p384 = { version = "0.13.1", optional = true } # Dependencies for encrypted-keys aes = { version = "0.7", features = ["ctr"], optional = true } @@ -96,8 +98,6 @@ env_logger = "0.8.2" hex = "0.4.2" clap = "3.0.5" criterion = "0.3" -p256 = "0.13.2" -p384 = "0.13.1" [[bench]] name = "certs_per_second" diff --git a/examples/yk-provision.rs b/examples/yk-provision.rs index bf102f7..1a4cbad 100644 --- a/examples/yk-provision.rs +++ b/examples/yk-provision.rs @@ -30,15 +30,18 @@ fn provision_new_key( let mut yk = Yubikey::new().unwrap(); yk.unlock(pin.as_bytes(), mgm_key).unwrap(); match alg { - "p256" => match yk.provision::(&slot, subject, policy, PinPolicy::Never) { - Ok(pk) => { - println!("New hardware backed SSH Public Key: {}", pk); + "p256" => { + println!("Using P256"); + match yk.provision_p256(&slot, subject, policy, PinPolicy::Never) { + Ok(pk) => { + println!("New hardware backed SSH Public Key: {}", pk); + } + Err(e) => panic!("Could not provision device with new key: {:?}", e), } - Err(e) => panic!("Could not provision device with new key: {:?}", e), }, _ => { println!("Using P384"); - match yk.provision::(&slot, subject, policy, PinPolicy::Never) { + match yk.provision_p384(&slot, subject, policy, PinPolicy::Never) { Ok(pk) => { println!("New hardware backed SSH Public Key: {}", pk); } diff --git a/src/yubikey/piv/management.rs b/src/yubikey/piv/management.rs index a3cc01e..438317c 100644 --- a/src/yubikey/piv/management.rs +++ b/src/yubikey/piv/management.rs @@ -237,6 +237,32 @@ impl super::Yubikey { self.ssh_cert_fetch_pubkey(slot) } + /// Provisions the YubiKey with a new certificate generated on the device. + /// Only keys that are generated this way can use the attestation functionality. + /// This is a nongeneric version to generate a p384 key + pub fn provision_p384( + &mut self, + slot: &SlotId, + common_name: &str, + touch_policy: TouchPolicy, + pin_policy: PinPolicy, + ) -> Result { + self.provision::(slot, common_name, touch_policy, pin_policy) + } + + /// Provisions the YubiKey with a new certificate generated on the device. + /// Only keys that are generated this way can use the attestation functionality. + /// This is a nongeneric version to generate a p256 key + pub fn provision_p256( + &mut self, + slot: &SlotId, + common_name: &str, + touch_policy: TouchPolicy, + pin_policy: PinPolicy, + ) -> Result { + self.provision::(slot, common_name, touch_policy, pin_policy) + } + /// Take data, an algorithm, and a slot and attempt to sign the data field /// /// If the requested algorithm doesn't match the key in the slot (or the slot From 530f9942423e8b6bb47caf7a244cf585a3a7c4ab Mon Sep 17 00:00:00 2001 From: Thanh Nguyen Date: Thu, 17 Apr 2025 02:39:19 -0400 Subject: [PATCH 6/6] Add generate csr example --- Cargo.toml | 4 ++ examples/yk-generate-csr.rs | 89 +++++++++++++++++++++++++++++++++++ src/yubikey/piv/management.rs | 10 +++- 3 files changed, 101 insertions(+), 2 deletions(-) create mode 100644 examples/yk-generate-csr.rs diff --git a/Cargo.toml b/Cargo.toml index f4333ff..28fcf82 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -112,6 +112,10 @@ required-features = ["yubikey-support"] name = "yk-provision" required-features = ["yubikey-support"] +[[example]] +name = "yk-generate-csr" +required-features = ["yubikey-support"] + [[example]] name = "sign-cert-with-yubikey" required-features = ["yubikey-support"] diff --git a/examples/yk-generate-csr.rs b/examples/yk-generate-csr.rs new file mode 100644 index 0000000..98ac54e --- /dev/null +++ b/examples/yk-generate-csr.rs @@ -0,0 +1,89 @@ +use std::env; + +use clap::{Arg, Command}; + +use sshcerts::yubikey::piv::Yubikey; +use sshcerts::yubikey::piv::{RetiredSlotId, SlotId}; + +use x509_parser::prelude::*; + +use std::convert::TryFrom; + +fn slot_parser(slot: &str) -> Option { + // If first character is R, then we need to parse the nice + // notation + if (slot.len() == 2 || slot.len() == 3) && slot.starts_with('R') { + let slot_value = slot[1..].parse::(); + match slot_value { + Ok(v) if v <= 20 => Some(SlotId::try_from(0x81_u8 + v).unwrap()), + _ => None, + } + } else if slot.len() == 4 && slot.starts_with("0x") { + let slot_value = hex::decode(&slot[2..]).unwrap()[0]; + Some(SlotId::try_from(slot_value).unwrap()) + } else { + None + } +} + +fn slot_validator(slot: &str) -> Result<(), String> { + match slot_parser(slot) { + Some(_) => Ok(()), + None => Err(String::from( + "Provided slot was not valid. Should be R1 - R20 or a raw hex identifier", + )), + } +} + +/// This routine will generate a new PIV key and use the new key to create a CSR signer. +/// Then use this CSR signer to sign a random blob and then verify the signature. +fn main() { + env_logger::init(); + let matches = Command::new("yk-generate-csr") + .version(env!("CARGO_PKG_VERSION")) + .author("Thanh Nguyen slot_parser(x).unwrap(), + None => SlotId::Retired(RetiredSlotId::R17), + }; + let pin = matches.value_of("pin").unwrap(); + let mgm_key = &hex::decode(matches.value_of("management-key").unwrap()).unwrap(); + + let mut yk = Yubikey::new().unwrap(); + yk.unlock(pin.as_bytes(), mgm_key).unwrap(); + + let csr = yk.generate_csr(&slot, "TestCSR").unwrap(); + + let (_, parsed_csr) = + x509_parser::certification_request::X509CertificationRequest::from_der(&csr).unwrap(); + parsed_csr.verify_signature().unwrap(); +} diff --git a/src/yubikey/piv/management.rs b/src/yubikey/piv/management.rs index 438317c..2e27791 100644 --- a/src/yubikey/piv/management.rs +++ b/src/yubikey/piv/management.rs @@ -4,7 +4,7 @@ use ring::digest; use yubikey::certificate::Certificate; use yubikey::piv::{attest, sign_data as yk_sign_data, AlgorithmId, SlotId}; -use yubikey::{MgmKey, YubiKey}; +use yubikey::{MgmKey, Serial, YubiKey}; use yubikey::{PinPolicy, TouchPolicy}; use super::{Error, Result}; @@ -144,6 +144,12 @@ impl super::Yubikey { Ok(()) } + /// Fetch the serial nummbere of the Yubikey + pub fn serial(&mut self) -> Result { + let serial = self.yk.serial(); + Ok(serial) + } + /// Check to see that a provided Yubikey and slot is configured for signing pub fn configured(&mut self, slot: &SlotId) -> Result { let cert = Certificate::read(&mut self.yk, *slot)?; @@ -247,7 +253,7 @@ impl super::Yubikey { touch_policy: TouchPolicy, pin_policy: PinPolicy, ) -> Result { - self.provision::(slot, common_name, touch_policy, pin_policy) + self.provision::(slot, common_name, touch_policy, pin_policy) } /// Provisions the YubiKey with a new certificate generated on the device.