diff --git a/Cargo.lock b/Cargo.lock index a6d064c..1affb2c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -187,9 +187,9 @@ checksum = "0dabb6555f92fb9ee4140454eb5dcd14c7960e1225c6d1a6cc361f032947713e" [[package]] name = "cpufeatures" -version = "0.2.11" +version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce420fe07aecd3e67c5f910618fe65e94158f6dcc0adf44e00d69ce2bdfe0fd0" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" dependencies = [ "libc", ] @@ -229,6 +229,33 @@ dependencies = [ "rand_core", ] +[[package]] +name = "curve25519-dalek" +version = "5.0.0-pre.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f9200d1d13637f15a6acb71e758f64624048d85b31a5fdbfd8eca1e2687d0b7" +dependencies = [ + "cfg-if", + "cpufeatures", + "curve25519-dalek-derive", + "digest", + "fiat-crypto", + "rustc_version", + "subtle", + "zeroize", +] + +[[package]] +name = "curve25519-dalek-derive" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "der" version = "0.8.0-rc.9" @@ -289,6 +316,31 @@ dependencies = [ "zeroize", ] +[[package]] +name = "ed25519" +version = "3.0.0-rc.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ef49c0b20c0ad088893ad2a790a29c06a012b3f05bcfc66661fd22a94b32129" +dependencies = [ + "pkcs8", + "signature", +] + +[[package]] +name = "ed25519-dalek" +version = "3.0.0-pre.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad207ed88a133091f83224265eac21109930db09bedcad05d5252f2af2de20a1" +dependencies = [ + "curve25519-dalek", + "ed25519", + "serde", + "sha2", + "signature", + "subtle", + "zeroize", +] + [[package]] name = "elliptic-curve" version = "0.14.0-rc.14" @@ -716,6 +768,15 @@ dependencies = [ "zeroize", ] +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + [[package]] name = "sec1" version = "0.8.0-rc.10" @@ -738,6 +799,12 @@ dependencies = [ "zeroize", ] +[[package]] +name = "semver" +version = "1.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" + [[package]] name = "serde" version = "1.0.193" @@ -1084,6 +1151,18 @@ dependencies = [ "bitflags 2.5.0", ] +[[package]] +name = "x25519-dalek" +version = "3.0.0-pre.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a45998121837fd8c92655d2334aa8f3e5ef0645cdfda5b321b13760c548fd55" +dependencies = [ + "curve25519-dalek", + "rand_core", + "serde", + "zeroize", +] + [[package]] name = "x509-cert" version = "0.3.0-rc.2" @@ -1106,9 +1185,11 @@ dependencies = [ "base16ct 0.2.0", "bitflags 2.5.0", "cipher", + "curve25519-dalek", "der", "des", "ecdsa", + "ed25519-dalek", "elliptic-curve", "env_logger", "log", @@ -1127,6 +1208,7 @@ dependencies = [ "signature", "subtle", "uuid", + "x25519-dalek", "x509-cert", "zeroize", ] diff --git a/Cargo.toml b/Cargo.toml index cb0e814..0112ebd 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -37,6 +37,9 @@ ecdsa = { version = "0.17.0-rc.6", features = ["digest", "pem"] } p256 = "=0.14.0-pre.11" p384 = "=0.14.0-pre.11" pbkdf2 = { version = "0.13.0-rc.1", default-features = false, features = ["hmac"] } +curve25519-dalek = "5.0.0-pre.0" +x25519-dalek = "3.0.0-pre.0" +ed25519-dalek = { version = "3.0.0-pre.0", features = ["alloc", "pkcs8"] } pcsc = "2.3.1" rand = "0.9" rand_core = { version = "0.9", features = ["os_rng"] } diff --git a/src/certificate.rs b/src/certificate.rs index 31885d0..ad41689 100644 --- a/src/certificate.rs +++ b/src/certificate.rs @@ -278,7 +278,7 @@ pub mod yubikey_signer { oid::db::rfc5912, Encode, Sequence, }; - use sha2::{Digest, Sha256, Sha384}; + use sha2::{Digest, Sha256, Sha384, Sha512}; use signature::Keypair; use std::{cell::RefCell, fmt, io::Write, marker::PhantomData}; use x509_cert::spki::{ @@ -313,6 +313,22 @@ pub mod yubikey_signer { fn read_signature(input: &[u8]) -> SigResult; } + impl KeyType for ed25519_dalek::SigningKey { + const ALGORITHM: AlgorithmId = AlgorithmId::Ed25519; + type Error = ed25519_dalek::SignatureError; + type Signature = ed25519_dalek::Signature; + type VerifyingKey = ed25519_dalek::VerifyingKey; + type PublicKey = ed25519_dalek::VerifyingKey; + + fn prepare(input: &[u8]) -> SigResult> { + Ok(Sha512::digest(input).to_vec()) + } + + fn read_signature(input: &[u8]) -> SigResult { + Self::Signature::try_from(input) + } + } + impl KeyType for p256::NistP256 { const ALGORITHM: AlgorithmId = AlgorithmId::EccP256; type Error = ecdsa::Error; diff --git a/src/piv.rs b/src/piv.rs index 0ea379d..a1268a7 100644 --- a/src/piv.rs +++ b/src/piv.rs @@ -62,7 +62,10 @@ use std::{ fmt::{Display, Formatter}, str::FromStr, }; -use x509_cert::{der::Decode, spki::SubjectPublicKeyInfoOwned}; +use x509_cert::{ + der::{asn1::BitString, Decode}, + spki::{AlgorithmIdentifier, ObjectIdentifier, SubjectPublicKeyInfoOwned}, +}; #[cfg(feature = "untested")] use zeroize::Zeroizing; @@ -86,6 +89,9 @@ const TAG_RSA_MODULUS: u8 = 0x81; const TAG_RSA_EXP: u8 = 0x82; const TAG_ECC_POINT: u8 = 0x86; +/// OID for ed25519 and x25519 algorithms +pub const OID_X25519: ObjectIdentifier = ObjectIdentifier::new_unwrap("1.3.101.110"); + #[cfg(feature = "untested")] const KEYDATA_LEN: usize = 1024; @@ -492,6 +498,12 @@ pub enum AlgorithmId { /// ECDSA with the NIST P384 curve. EccP384, + + /// ED25519 + Ed25519, + + /// X25519 + X25519, } impl TryFrom for AlgorithmId { @@ -505,6 +517,8 @@ impl TryFrom for AlgorithmId { 0x16 => Ok(AlgorithmId::Rsa4096), 0x11 => Ok(AlgorithmId::EccP256), 0x14 => Ok(AlgorithmId::EccP384), + 0xE0 => Ok(AlgorithmId::Ed25519), + 0xE1 => Ok(AlgorithmId::X25519), _ => Err(Error::AlgorithmError), } } @@ -519,6 +533,8 @@ impl From for u8 { AlgorithmId::Rsa4096 => 0x16, AlgorithmId::EccP256 => 0x11, AlgorithmId::EccP384 => 0x14, + AlgorithmId::Ed25519 => 0xE0, + AlgorithmId::X25519 => 0xE1, } } } @@ -538,6 +554,8 @@ impl AlgorithmId { AlgorithmId::Rsa4096 => 256, AlgorithmId::EccP256 => 32, AlgorithmId::EccP384 => 48, + AlgorithmId::Ed25519 => 32, + AlgorithmId::X25519 => 32, } } @@ -549,6 +567,8 @@ impl AlgorithmId { | AlgorithmId::Rsa3072 | AlgorithmId::Rsa4096 => 0x01, AlgorithmId::EccP256 | AlgorithmId::EccP384 => 0x6, + AlgorithmId::Ed25519 => 0x07, + AlgorithmId::X25519 => 0x08, } } } @@ -898,6 +918,34 @@ pub fn import_ecc_key( Ok(()) } +/// Imports a private ECDH/EdDSA encryption or signing key into the YubiKey. +/// +/// Errors if `algorithm` isn't `AlgorithmId::Ed25519` or ` AlgorithmId::X25519`. +#[cfg(feature = "untested")] +pub fn import_cv_key( + yubikey: &mut YubiKey, + slot: SlotId, + algorithm: AlgorithmId, + key_data: &[u8], + touch_policy: TouchPolicy, + pin_policy: PinPolicy, +) -> Result<()> { + match algorithm { + AlgorithmId::Ed25519 | AlgorithmId::X25519 => (), + _ => return Err(Error::AlgorithmError), + } + + if key_data.len() > KEYDATA_LEN { + return Err(Error::SizeError); + } + + let params = vec![key_data]; + + write_key(yubikey, slot, params, pin_policy, touch_policy, algorithm)?; + + Ok(()) +} + /// Generate an attestation certificate for a stored key. /// /// @@ -1128,6 +1176,36 @@ fn read_public_key( // // 0x7f 0x49 -> Application | Constructed | 0x49 match algorithm { + AlgorithmId::X25519 | AlgorithmId::Ed25519 => { + // 2-byte ASN.1 tag, 1-byte length (because all supported EC pubkey lengths + // are shorter than 128 bytes, fitting into a definite short ASN.1 length). + let data = if skip_asn1_tag { &input[3..] } else { input }; + + let (_, tlv) = Tlv::parse(data)?; + let pk_data: [u8; 32] = tlv.value.try_into().map_err(|_| Error::InvalidObject)?; + + match algorithm { + AlgorithmId::Ed25519 => SubjectPublicKeyInfoOwned::from_der( + ed25519_dalek::VerifyingKey::from_bytes(&pk_data) + .map_err(|_| Error::InvalidObject)? + .to_public_key_der() + .map_err(|_| Error::InvalidObject)? + .as_bytes(), + ) + .map_err(|_| Error::InvalidObject), + AlgorithmId::X25519 => Ok(SubjectPublicKeyInfoOwned { + algorithm: AlgorithmIdentifier { + oid: OID_X25519, + parameters: None, + }, + subject_public_key: BitString::from_bytes( + x25519_dalek::PublicKey::from(pk_data).as_bytes(), + ) + .map_err(|_| Error::InvalidObject)?, + }), + _ => Err(Error::AlgorithmError), + } + } AlgorithmId::Rsa1024 | AlgorithmId::Rsa2048 | AlgorithmId::Rsa3072 diff --git a/src/transaction.rs b/src/transaction.rs index bc046f8..8fba3d2 100644 --- a/src/transaction.rs +++ b/src/transaction.rs @@ -320,6 +320,19 @@ impl<'tx> Transaction<'tx> { return Err(Error::SizeError); } } + AlgorithmId::X25519 => { + if !decipher { + return Err(Error::NotSupported); + } + if in_len != 32 { + return Err(Error::SizeError); + } + } + AlgorithmId::Ed25519 => { + if decipher { + return Err(Error::NotSupported); + } + } } let bytes = if in_len < 0x80 { @@ -336,7 +349,9 @@ impl<'tx> Transaction<'tx> { Tlv::write( &mut buf[2..], match (algorithm, decipher) { - (AlgorithmId::EccP256, true) | (AlgorithmId::EccP384, true) => 0x85, + (AlgorithmId::EccP256, true) + | (AlgorithmId::EccP384, true) + | (AlgorithmId::X25519, true) => 0x85, _ => 0x81, }, sign_in diff --git a/tests/integration.rs b/tests/integration.rs index 742cb3a..5eae3cb 100644 --- a/tests/integration.rs +++ b/tests/integration.rs @@ -43,7 +43,10 @@ static YUBIKEY: Lazy> = Lazy::new(|| { #[test] #[ignore] fn test_get_cccid() { - let mut yubikey = YUBIKEY.lock().unwrap(); + let mut yubikey = match YUBIKEY.lock() { + Ok(yubikey) => yubikey, + Err(poison) => poison.into_inner(), + }; match yubikey.cccid() { Ok(cccid) => trace!("CCCID: {:?}", cccid), @@ -59,7 +62,10 @@ fn test_get_cccid() { #[test] #[ignore] fn test_get_chuid() { - let mut yubikey = YUBIKEY.lock().unwrap(); + let mut yubikey = match YUBIKEY.lock() { + Ok(yubikey) => yubikey, + Err(poison) => poison.into_inner(), + }; match yubikey.chuid() { Ok(chuid) => trace!("CHUID: {:?}", chuid), @@ -75,7 +81,10 @@ fn test_get_chuid() { #[test] #[ignore] fn test_get_config() { - let mut yubikey = YUBIKEY.lock().unwrap(); + let mut yubikey = match YUBIKEY.lock() { + Ok(yubikey) => yubikey, + Err(poison) => poison.into_inner(), + }; let config_result = yubikey.config(); assert!(config_result.is_ok()); trace!("config: {:?}", config_result.unwrap()); @@ -88,7 +97,10 @@ fn test_get_config() { #[test] #[ignore] fn test_list_keys() { - let mut yubikey = YUBIKEY.lock().unwrap(); + let mut yubikey = match YUBIKEY.lock() { + Ok(yubikey) => yubikey, + Err(poison) => poison.into_inner(), + }; let keys_result = Key::list(&mut yubikey); assert!(keys_result.is_ok()); trace!("keys: {:?}", keys_result.unwrap()); @@ -101,7 +113,10 @@ fn test_list_keys() { #[test] #[ignore] fn test_verify_pin() { - let mut yubikey = YUBIKEY.lock().unwrap(); + let mut yubikey = match YUBIKEY.lock() { + Ok(yubikey) => yubikey, + Err(poison) => poison.into_inner(), + }; assert!(yubikey.verify_pin(b"000000").is_err()); assert!(yubikey.verify_pin(b"123456").is_ok()); } @@ -115,7 +130,10 @@ fn test_verify_pin() { #[ignore] fn test_set_mgmkey() { let mut rng = OsRng; - let mut yubikey = YUBIKEY.lock().unwrap(); + let mut yubikey = match YUBIKEY.lock() { + Ok(yubikey) => yubikey, + Err(poison) => poison.into_inner(), + }; let default_key = MgmKey::get_default(&yubikey).unwrap(); assert!(yubikey.verify_pin(b"123456").is_ok()); @@ -267,6 +285,31 @@ fn generate_self_signed_ec_cert() { assert!(vk.verify(msg, &sig).is_ok()); } +#[test] +#[ignore] +fn generate_self_signed_cv_cert() { + let cert = generate_self_signed_cert::(); + + // + // Verify that the certificate is signed correctly + // + + let pubkey = + ed25519_dalek::VerifyingKey::try_from(cert.subject_pki()).expect("ed25519 key expected"); + + let data = cert.cert.to_der().expect("serialize certificate"); + let cert_len = data[2] as usize; + let tbs_cert_len = data[5] as usize; + let sig_algo_len: usize = 64; + let sig_start = cert_len - sig_algo_len + 3; + let msg = &data[3..6 + tbs_cert_len]; + let sig = + ed25519_dalek::Signature::from_slice(&data[sig_start..sig_start + sig_algo_len]).unwrap(); + + use ed25519_dalek::Verifier; + assert!(pubkey.verify(msg, &sig).is_ok()); +} + #[test] #[ignore] fn test_slot_id_display() { @@ -320,7 +363,10 @@ fn test_slot_id_display() { #[test] #[ignore] fn test_read_metadata() { - let mut yubikey = YUBIKEY.lock().unwrap(); + let mut yubikey = match YUBIKEY.lock() { + Ok(yubikey) => yubikey, + Err(poison) => poison.into_inner(), + }; let default_key = MgmKey::get_default(&yubikey).unwrap(); assert!(yubikey.verify_pin(b"123456").is_ok());