Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
2 changes: 2 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
39 changes: 38 additions & 1 deletion src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1051,7 +1051,7 @@ impl<T: Client> 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
Expand Down Expand Up @@ -1095,6 +1095,43 @@ impl<T: Client> 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),
}
}
Expand Down
10 changes: 10 additions & 0 deletions src/piv_types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -221,6 +221,16 @@ impl AsymmetricAlgorithms {
}
}

/// Length of the private scalar, none for RSA
pub fn scalar_len(self) -> Option<usize> {
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")]
Expand Down
109 changes: 109 additions & 0 deletions tests/command_response.ron
Original file line number Diff line number Diff line change
Expand Up @@ -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"),
Expand Down
150 changes: 150 additions & 0 deletions tests/command_response.rs
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,65 @@ fn tlv(tag: &[u8], data: &[u8]) -> Vec<u8> {
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<u8> {
iso7816::command::CommandBuilder::new(cla.try_into().unwrap(), ins.into(), p1, p2, data, le)
.serialize_to_vec()
Expand Down Expand Up @@ -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<Algorithm>,
#[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,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -481,6 +574,63 @@ impl IoCmd {
);
}

fn run_import_ecc_key(
key_reference: u8,
scalar: &str,
algo: Option<Algorithm>,
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<u8> = [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 <DER encoded signature>
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,
Expand Down
Loading