From 08ffd56fa6be26534aafcf1e3d07ddb0c1b77013 Mon Sep 17 00:00:00 2001 From: Florian Franzen Date: Sat, 3 Oct 2026 15:26:47 +0200 Subject: [PATCH] Support ECC key import in all asymmetric slots --- CHANGELOG.md | 4 + Cargo.toml | 2 + src/lib.rs | 39 +++++++++- src/piv_types.rs | 10 +++ tests/command_response.ron | 109 +++++++++++++++++++++++++++ tests/command_response.rs | 150 +++++++++++++++++++++++++++++++++++++ 6 files changed, 313 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3e1a0096..4464bc00 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,10 @@ default (5) - Fix the inverted parsing of the touch requirement in SET MANAGEMENT KEY's P2 (0xFF means no touch, 0xFE means touch required) +- Support importing ECC keys (P-256 and P-384) through the Yubico IMPORT ASYMMETRIC KEY + extension, into any asymmetric key slot including the retired ones ([#16][]) + +[#16]: https://github.com/trussed-dev/piv-authenticator/issues/16 ## [v0.7.0][] (2026-08-17) diff --git a/Cargo.toml b/Cargo.toml index 8ad1af7b..3072d4c1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -46,6 +46,8 @@ env_logger = "0.10" serde = { version = "1", features = ["derive"] } serde_cbor = { version = "0.11", features = ["std"] } hex = "0.4" +p256 = { version = "0.13", features = ["ecdsa"] } +p384 = { version = "0.13", features = ["ecdsa"] } test-log = "0.2.11" ron = "0.8" des = "0.9" diff --git a/src/lib.rs b/src/lib.rs index 52aefebf..abd72c46 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1051,7 +1051,7 @@ impl LoadedAuthenticator<'_, T> { &mut self, algo: AsymmetricAlgorithms, key: AsymmetricKeyReference, - #[cfg_attr(not(feature = "rsa"), allow(unused))] data: &[u8], + data: &[u8], mut _reply: Reply<'_>, ) -> Result { if !self @@ -1095,6 +1095,43 @@ impl LoadedAuthenticator<'_, T> { ); Ok(()) } + (AsymmetricAlgorithms::P256 | AsymmetricAlgorithms::P384, _) => { + // The private scalar comes in DO 0x06, zero-padded to the curve's scalar size + let scalar = tlv::get_do(&[0x06], data).ok_or(Status::IncorrectDataParameter)?; + let expected_len = algo.scalar_len().ok_or(Status::FunctionNotSupported)?; + if scalar.len() != expected_len { + warn!( + "ECC import with scalar of length {}, expected {expected_len}", + scalar.len() + ); + return Err(Status::IncorrectDataParameter); + } + // The software backend accepts the zero scalar, real ones do not + if scalar.iter().all(|&b| b == 0) { + warn!("ECC import with the zero scalar"); + return Err(Status::IncorrectDataParameter); + } + let id = try_syscall!(self.trussed.unsafe_inject_key( + algo.key_mechanism(), + scalar, + key.storage(self.options.storage), + KeySerialization::Raw + )) + .map_err(|_err| { + warn!("Failed ECC import: {_err:?}"); + Status::IncorrectDataParameter + })? + .key; + self.state.persistent.replace_asymmetric_key( + key, + algo, + id, + self.trussed, + self.options.storage, + ); + Ok(()) + } + #[cfg(feature = "rsa")] _ => Err(Status::FunctionNotSupported), } } diff --git a/src/piv_types.rs b/src/piv_types.rs index 3d6fd051..aadcda68 100644 --- a/src/piv_types.rs +++ b/src/piv_types.rs @@ -221,6 +221,16 @@ impl AsymmetricAlgorithms { } } + /// Length of the private scalar, none for RSA + pub fn scalar_len(self) -> Option { + match self { + #[cfg(feature = "rsa")] + Self::Rsa2048 | Self::Rsa3072 | Self::Rsa4096 => None, + Self::P256 => Some(32), + Self::P384 => Some(48), + } + } + pub fn sign_serialization(self) -> SignatureSerialization { match self { #[cfg(feature = "rsa")] diff --git a/tests/command_response.ron b/tests/command_response.ron index 7bc7ecea..1b3645ad 100644 --- a/tests/command_response.ron +++ b/tests/command_response.ron @@ -487,6 +487,115 @@ ) ] ), + IoTest( + name: "ECC key import", + uuid_config: WithBoth("00112233445566778899AABBCCDDEEFF"), + cmd_resp: [ + // import requires management key authentication + ImportEccKey( + key_reference: 0x9A, + scalar: "0102030405060708090A0B0C0D0E0F101112131415161718191A1B1C1D1E1F20", + expected_status: SecurityStatusNotSatisfied, + ), + AuthenticateManagement( + key: ( + algorithm: Tdes, + key: "0102030405060708 0102030405060708 0102030405060708" + ), + mutual: false, + ), + // P-256 into the four standard-ish slots and a retired one + ImportEccKey( + key_reference: 0x9A, + scalar: "0102030405060708090A0B0C0D0E0F101112131415161718191A1B1C1D1E1F20", + ), + ImportEccKey( + key_reference: 0x9C, + scalar: "1111111111111111111111111111111111111111111111111111111111111111", + ), + ImportEccKey( + key_reference: 0x9E, + scalar: "2222222222222222222222222222222222222222222222222222222222222222", + ), + ImportEccKey( + key_reference: 0x82, + scalar: "3333333333333333333333333333333333333333333333333333333333333333", + ), + // P-384 into another retired slot + ImportEccKey( + key_reference: 0x83, + scalar: "404142434445464748494A4B4C4D4E4F505152535455565758595A5B5C5D5E5F606162636465666768696A6B6C6D6E6F", + ), + // a truncated scalar is rejected + ImportEccKey( + key_reference: 0x9A, + algo: Some(P256), + scalar: "0102030405060708", + expected_status: IncorrectDataParameter, + ), + // a scalar of another curve's length is rejected + ImportEccKey( + key_reference: 0x9A, + algo: Some(P384), + scalar: "0102030405060708090A0B0C0D0E0F101112131415161718191A1B1C1D1E1F20", + expected_status: IncorrectDataParameter, + ), + // the zero scalar is not a valid private key + ImportEccKey( + key_reference: 0x9A, + scalar: "0000000000000000000000000000000000000000000000000000000000000000", + expected_status: IncorrectDataParameter, + ), + // neither is one at or above the group order + ImportEccKey( + key_reference: 0x9A, + scalar: "FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF", + expected_status: IncorrectDataParameter, + ), + // 9E (card authentication) signs without a PIN + SignEcc( + key_reference: 0x9E, + data: "D1D2D3D4D5D6D7D8D9DADBDCDDDEDFD0E1E2E3E4E5E6E7E8E9EAEBECEDEEEFE0", + scalar: "2222222222222222222222222222222222222222222222222222222222222222", + ), + // 9A refuses without a verified PIN + SignEcc( + key_reference: 0x9A, + data: "D1D2D3D4D5D6D7D8D9DADBDCDDDEDFD0E1E2E3E4E5E6E7E8E9EAEBECEDEEEFE0", + scalar: "0102030405060708090A0B0C0D0E0F101112131415161718191A1B1C1D1E1F20", + expected_status: SecurityStatusNotSatisfied, + ), + VerifyApplicationPin(), + SignEcc( + key_reference: 0x9A, + data: "D1D2D3D4D5D6D7D8D9DADBDCDDDEDFD0E1E2E3E4E5E6E7E8E9EAEBECEDEEEFE0", + scalar: "0102030405060708090A0B0C0D0E0F101112131415161718191A1B1C1D1E1F20", + ), + SignEcc( + key_reference: 0x82, + data: "D1D2D3D4D5D6D7D8D9DADBDCDDDEDFD0E1E2E3E4E5E6E7E8E9EAEBECEDEEEFE0", + scalar: "3333333333333333333333333333333333333333333333333333333333333333", + ), + SignEcc( + key_reference: 0x83, + data: "D1D2D3D4D5D6D7D8D9DADBDCDDDEDFD0E1E2E3E4E5E6E7E8E9EAEBECEDEEEFE0F1F2F3F4F5F6F7F8F9FAFBFCFDFEFFF0", + scalar: "404142434445464748494A4B4C4D4E4F505152535455565758595A5B5C5D5E5F606162636465666768696A6B6C6D6E6F", + ), + // 9C (digital signature) requires the PIN immediately before each use + SignEcc( + key_reference: 0x9C, + data: "D1D2D3D4D5D6D7D8D9DADBDCDDDEDFD0E1E2E3E4E5E6E7E8E9EAEBECEDEEEFE0", + scalar: "1111111111111111111111111111111111111111111111111111111111111111", + expected_status: SecurityStatusNotSatisfied, + ), + VerifyApplicationPin(), + SignEcc( + key_reference: 0x9C, + data: "D1D2D3D4D5D6D7D8D9DADBDCDDDEDFD0E1E2E3E4E5E6E7E8E9EAEBECEDEEEFE0", + scalar: "1111111111111111111111111111111111111111111111111111111111111111", + ), + ] + ), IoTest( name: "Protected DOS", uuid_config: WithBoth("00112233445566778899AABBCCDDEEFF"), diff --git a/tests/command_response.rs b/tests/command_response.rs index b0e2287d..83a21417 100644 --- a/tests/command_response.rs +++ b/tests/command_response.rs @@ -132,6 +132,65 @@ fn tlv(tag: &[u8], data: &[u8]) -> Vec { buf } +/// Parse one TLV with a single-byte tag, returning (tag, value, remainder) +fn take_tlv(data: &[u8]) -> Option<(u8, &[u8], &[u8])> { + let tag = *data.first()?; + let (len, rest) = match *data.get(1)? { + len if len <= 0x7F => (len as usize, &data[2..]), + 0x81 => (*data.get(2)? as usize, &data[3..]), + 0x82 => ( + u16::from_be_bytes([*data.get(2)?, *data.get(3)?]) as usize, + &data[4..], + ), + _ => return None, + }; + (rest.len() >= len).then(|| (tag, &rest[..len], &rest[len..])) +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +enum Curve { + P256, + P384, +} + +impl Curve { + fn of_scalar(scalar: &[u8]) -> Self { + match scalar.len() { + 32 => Self::P256, + 48 => Self::P384, + len => panic!("no supported curve has a {len} byte scalar"), + } + } + + fn algorithm(self) -> Algorithm { + match self { + Self::P256 => Algorithm::P256, + Self::P384 => Algorithm::P384, + } + } + + /// Verify a DER signature over `digest` against the key derived from `scalar` + fn verify(self, scalar: &[u8], digest: &[u8], signature: &[u8]) { + use p256::ecdsa::signature::hazmat::PrehashVerifier; + match self { + Self::P256 => { + let key = p256::ecdsa::SigningKey::from_slice(scalar).unwrap(); + let signature = p256::ecdsa::Signature::from_der(signature).unwrap(); + key.verifying_key() + .verify_prehash(digest, &signature) + .expect("bad P-256 signature"); + } + Self::P384 => { + let key = p384::ecdsa::SigningKey::from_slice(scalar).unwrap(); + let signature = p384::ecdsa::Signature::from_der(signature).unwrap(); + key.verifying_key() + .verify_prehash(digest, &signature) + .expect("bad P-384 signature"); + } + } + } +} + fn build_command(cla: u8, ins: u8, p1: u8, p2: u8, data: &[u8], le: u16) -> Vec { iso7816::command::CommandBuilder::new(cla.try_into().unwrap(), ins.into(), p1, p2, data, le) .serialize_to_vec() @@ -320,6 +379,28 @@ enum IoCmd { #[serde(default)] expected_status: Status, }, + ImportEccKey { + key_reference: u8, + /// Hex-encoded private scalar, zero-padded to the curve's scalar size + scalar: String, + /// Sent as given, to provoke a mismatch; derived from the scalar otherwise + #[serde(default)] + algo: Option, + #[serde(default)] + expected_status: Status, + }, + /// Sign a digest with an imported ECC key and verify the signature against + /// the verifying key recomputed from the imported scalar (ECDSA is + /// randomized, so the signature bytes cannot be matched directly) + SignEcc { + key_reference: u8, + /// Hex-encoded digest to sign (32 or 48 bytes, selects the curve) + data: String, + /// Hex-encoded private scalar that was imported into the slot + scalar: String, + #[serde(default)] + expected_status: Status, + }, Sign { algo: u8, key_reference: u8, @@ -415,6 +496,18 @@ impl IoCmd { e, expected_status, } => Self::run_import_rsa_key(p, q, e, *expected_status, card), + Self::ImportEccKey { + key_reference, + scalar, + algo, + expected_status, + } => Self::run_import_ecc_key(*key_reference, scalar, *algo, *expected_status, card), + Self::SignEcc { + key_reference, + data, + scalar, + expected_status, + } => Self::run_sign_ecc(*key_reference, data, scalar, *expected_status, card), Self::Sign { algo, key_reference, @@ -481,6 +574,63 @@ impl IoCmd { ); } + fn run_import_ecc_key( + key_reference: u8, + scalar: &str, + algo: Option, + expected_status: Status, + card: &mut setup::Piv, + ) { + let scalar = parse_hex(scalar); + let algo = algo.unwrap_or_else(|| Curve::of_scalar(&scalar).algorithm()); + let data = tlv(&[0x06], &scalar); + Self::run_bytes( + &build_command(0x00, 0xFE, algo as u8, key_reference, &data, 0), + &MATCH_EMPTY, + expected_status, + card, + ); + } + + fn run_sign_ecc( + key_reference: u8, + data: &str, + scalar: &str, + expected_status: Status, + card: &mut setup::Piv, + ) { + let digest = parse_hex(data); + let scalar = parse_hex(scalar); + let curve = Curve::of_scalar(&scalar); + let payload: Vec = [tlv(&[0x81], &digest), tlv(&[0x82], &[])] + .into_iter() + .flatten() + .collect(); + let payload = tlv(&[0x7C], &payload); + let rep = Self::run_bytes( + &build_command( + 0x00, + 0x87, + curve.algorithm() as u8, + key_reference, + &payload, + 0xFF, + ), + &MATCH_ANY, + expected_status, + card, + ); + if expected_status != Status::Success { + return; + } + // response: 7C L 82 L + let (tag, value, _) = take_tlv(&rep).expect("no dynamic authentication template"); + assert_eq!(tag, 0x7C); + let (tag, signature, _) = take_tlv(value).expect("no response DO"); + assert_eq!(tag, 0x82); + curve.verify(&scalar, &digest, signature); + } + fn run_set_administration_key( alg: Algorithm, key: &str,