diff --git a/Cargo.lock b/Cargo.lock index e36b63c..5bde90b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4,7 +4,7 @@ version = 4 [[package]] name = "aadhaar-offline-kyc" -version = "0.2.0" +version = "0.3.0" dependencies = [ "base64", "chrono", @@ -23,7 +23,7 @@ dependencies = [ [[package]] name = "aamva" -version = "0.2.0" +version = "0.3.0" dependencies = [ "chrono", "image", @@ -345,6 +345,30 @@ dependencies = [ "hybrid-array", ] +[[package]] +name = "bp256" +version = "0.14.0-rc.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5c715b9f158d362cc00afc7a3ea6c33da4b018293ff3227402886325c22f947" +dependencies = [ + "ecdsa 0.17.0", + "elliptic-curve 0.14.1", + "primefield", + "primeorder 0.14.0", +] + +[[package]] +name = "bp384" +version = "0.14.0-rc.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dad8bed63122ad85f81781099e541e81985e4ff6814a6c6e6bb5ddac481f443f" +dependencies = [ + "ecdsa 0.17.0", + "elliptic-curve 0.14.1", + "primefield", + "primeorder 0.14.0", +] + [[package]] name = "built" version = "0.8.1" @@ -744,11 +768,13 @@ dependencies = [ [[package]] name = "dmrtd" -version = "0.2.0" +version = "0.3.0" dependencies = [ "aes", "asn1", "base64", + "bp256", + "bp384", "cbc", "chrono", "cipher", @@ -761,7 +787,10 @@ dependencies = [ "num-bigint", "num-traits", "once_cell", + "p224", "p256 0.14.0", + "p384", + "p521", "rand 0.10.1", "sha1 0.11.0", "sha2 0.11.0", @@ -1288,7 +1317,7 @@ checksum = "89194689a993ab15268672e99e7b0e19da2da3268ac682e8f02d29d4d1434cd7" [[package]] name = "incometax-pan-qr" -version = "0.2.0" +version = "0.3.0" dependencies = [ "base64", "flate2", @@ -1481,7 +1510,7 @@ dependencies = [ [[package]] name = "mrz-parser" -version = "0.2.0" +version = "0.3.0" dependencies = [ "chrono", "once_cell", @@ -1684,6 +1713,18 @@ dependencies = [ "ttf-parser", ] +[[package]] +name = "p224" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30c06436d66652bc2f01ade021592c80a2aad401570a18aa18b82e440d2b9aa1" +dependencies = [ + "ecdsa 0.16.9", + "elliptic-curve 0.13.8", + "primeorder 0.13.6", + "sha2 0.10.9", +] + [[package]] name = "p256" version = "0.13.2" diff --git a/Cargo.toml b/Cargo.toml index a5a5007..932dff7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,7 +11,7 @@ resolver = "2" members = ["crates/*"] [workspace.package] -version = "0.2.0" +version = "0.3.0" authors = ["Balamurali Pandranki "] edition = "2021" # 1.92 is required by the `xml-sec` dependency (Aadhaar XML-DSig verification); @@ -70,5 +70,9 @@ num-bigint = "0.4" num-traits = "0.2" rand = "0.10" p256 = { version = "0.14", features = ["arithmetic", "ecdh", "ecdsa"] } -p384 = { version = "0.13", features = ["ecdsa"] } +p384 = { version = "0.13", features = ["arithmetic", "ecdh", "ecdsa"] } +p521 = { version = "0.13", features = ["arithmetic", "ecdh", "ecdsa"] } +p224 = { version = "0.13", features = ["arithmetic", "ecdh", "ecdsa"] } +bp256 = { version = "0.14.0-rc.15", features = ["arithmetic"] } +bp384 = { version = "0.14.0-rc.15", features = ["arithmetic"] } elliptic-curve = { version = "0.14", features = ["arithmetic"] } diff --git a/README.md b/README.md index 261ef4d..806c199 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ offline e-KYC, and Income-Tax PAN. | Crate | Path | Description | |-------|------|-------------| -| [`dmrtd`](crates/dmrtd) | `crates/dmrtd` | eMRTD / ICAO 9303 reader core — BAC, PACE (ECDH-GM P-256 and DH-GM), MRZ, LDS parsing, secure messaging. Transport-agnostic (NFC plugs in via a `Transceiver` trait). | +| [`dmrtd`](crates/dmrtd) | `crates/dmrtd` | eMRTD / ICAO 9303 reader core — BAC, PACE (ECDH-GM NIST & Brainpool, DH-GM), MRZ, LDS parsing, secure messaging. Transport-agnostic (NFC plugs in via a `Transceiver` trait). | | [`mrz-parser`](crates/mrz-parser) | `crates/mrz-parser` | ICAO 9303 Machine Readable Zone (TD1 / TD2 / TD3) text parser — fields + check digits, country patterns, OCR-defect fixer. | | [`aamva`](crates/aamva) | `crates/aamva` | AAMVA DL/ID — PDF417 barcode scan + payload parser (US / Canadian driver's licenses). | | [`aadhaar-offline-kyc`](crates/aadhaar-offline-kyc) | `crates/aadhaar-offline-kyc` | Aadhaar offline e-KYC (India UIDAI) — Secure QR v2 + Paperless Offline e-KYC (ZIP/XML) with signature verification; Verhoeff number validation. | diff --git a/crates/dmrtd/Cargo.toml b/crates/dmrtd/Cargo.toml index 3461897..8176ddf 100644 --- a/crates/dmrtd/Cargo.toml +++ b/crates/dmrtd/Cargo.toml @@ -47,5 +47,10 @@ zeroize.workspace = true num-bigint.workspace = true num-traits.workspace = true rand.workspace = true -p256.workspace = true -elliptic-curve.workspace = true +p256 = { workspace = true } +p384 = { workspace = true } +p521 = { workspace = true } +p224 = { workspace = true } +bp256 = { workspace = true } +bp384 = { workspace = true } +elliptic-curve = { workspace = true } diff --git a/crates/dmrtd/README.md b/crates/dmrtd/README.md index 3e2d2cd..f3d7099 100644 --- a/crates/dmrtd/README.md +++ b/crates/dmrtd/README.md @@ -17,12 +17,12 @@ Android (Dart FFI), a desktop reader, or tests alike. | Keys (`DBAKey`, `CanKey`, `AccessKey` trait) | ✅ | | SM ciphers (3DES, AES) + `MrtdSM` wrapper | ✅ | | BAC — pure helpers, session keys, `BacSession` state machine | ✅ | -| PACE — helpers, `PaceSession` state machine (ECDH-GM on NIST P-256) | ✅ | +| PACE — helpers, `PaceSession` state machine (ECDH-GM on NIST & Brainpool) | ✅ | | DH-PACE (RFC 5114 groups) | ✅ | -| ECDH-PACE (NIST P-256 via `p256`) | ✅ | +| ECDH-PACE (NIST P-224/256/384/521 & Brainpool P-256/384) | ✅ | | `MrtdApi` (sync ICC orchestrator) + `Transceiver` trait | ✅ | | `Passport` — high-level BAC/PACE + file-read API | ✅ | -| Additional PACE curves (Brainpool etc.) | ⏳ pending | +| Additional PACE curves (Brainpool, NIST P-224/384/521) | ✅ | Reference vectors verified include ICAO 9303 Appendix A check digits + EF.COM + EF.DG1 TD1 sample, Appendix D.2 `K_seed`, Appendix D.3 BAC SSC + session keys diff --git a/crates/dmrtd/src/proto/domain_parameter.rs b/crates/dmrtd/src/proto/domain_parameter.rs index 0022f70..9ae4489 100644 --- a/crates/dmrtd/src/proto/domain_parameter.rs +++ b/crates/dmrtd/src/proto/domain_parameter.rs @@ -100,7 +100,7 @@ pub static ICAO_DOMAIN_PARAMETERS: Lazy> = Lazy::n name: "NIST P-224 (secp224r1)", size: 224, kind: DomainParameterType::Ecp, - is_supported: false, + is_supported: true, }, DomainParameter { id: 11, @@ -121,7 +121,7 @@ pub static ICAO_DOMAIN_PARAMETERS: Lazy> = Lazy::n name: "BrainpoolP256r1", size: 256, kind: DomainParameterType::Ecp, - is_supported: false, + is_supported: true, }, DomainParameter { id: 14, @@ -135,14 +135,14 @@ pub static ICAO_DOMAIN_PARAMETERS: Lazy> = Lazy::n name: "NIST P-384 (secp384r1)", size: 384, kind: DomainParameterType::Ecp, - is_supported: false, + is_supported: true, }, DomainParameter { id: 16, name: "BrainpoolP384r1", size: 384, kind: DomainParameterType::Ecp, - is_supported: false, + is_supported: true, }, DomainParameter { id: 17, @@ -156,7 +156,7 @@ pub static ICAO_DOMAIN_PARAMETERS: Lazy> = Lazy::n name: "NIST P-521 (secp521r1)", size: 521, kind: DomainParameterType::Ecp, - is_supported: false, + is_supported: true, }, ]; entries.into_iter().map(|p| (p.id, p)).collect() @@ -176,15 +176,15 @@ mod tests { use super::*; #[test] - fn supported_entries_are_p256_and_rfc5114_dh_groups() { + fn supported_entries_include_brainpool_and_nist_curves() { let mut supported: Vec = ICAO_DOMAIN_PARAMETERS .values() .filter(|p| p.is_supported) .map(|p| p.id) .collect(); supported.sort_unstable(); - // P-256 (12, ECDH) + RFC 5114 MODP/DH groups (0/1/2). - assert_eq!(supported, vec![0, 1, 2, 12]); + // P-256 (12, ECDH) + RFC 5114 MODP/DH groups (0/1/2) + Brainpool + NIST P-224/384/521. + assert_eq!(supported, vec![0, 1, 2, 10, 12, 13, 15, 16, 18]); } #[test] diff --git a/crates/dmrtd/src/proto/ecdh_pace.rs b/crates/dmrtd/src/proto/ecdh_pace.rs index cc65876..55d031f 100644 --- a/crates/dmrtd/src/proto/ecdh_pace.rs +++ b/crates/dmrtd/src/proto/ecdh_pace.rs @@ -1,11 +1,6 @@ -//! ECDH PACE engine. +//! ECDH PACE engine supporting NIST and Brainpool curves. //! -//! The reference supports 11 ICAO curves (ids 8-18). This Rust port -//! currently implements only **NIST P-256 (id 12)** — the only curve marked -//! `is_supported = true` in [`domain_parameter`]. Other ids return -//! [`ECDHPaceError::UnsupportedCurve`]; add additional back-ends as needed. -//! -//! PACE-GM mapped generator formula (ICAO 9303 p11 §4.3.3.3.1 - ECDH): +//! Handles PACE key agreement using ECDH-GM mapped generator formula: //! ```text //! G' = s · G + H //! ``` @@ -13,16 +8,8 @@ //! shared-secret point derived from our private key and the other party's //! public key. -use elliptic_curve::{ - group::Group, - ops::Reduce, - sec1::{FromSec1Point, ToSec1Point}, - Generate, NonZeroScalar, PrimeField, -}; use num_bigint::BigUint; -use p256::{ - AffinePoint, NistP256, ProjectivePoint, PublicKey, Scalar, Sec1Point as EncodedPoint, SecretKey, -}; +use p256::elliptic_curve::Generate; use rand::rand_core::UnwrapErr; use rand::{rngs::StdRng, rngs::SysRng, Rng, SeedableRng}; use thiserror::Error; @@ -30,13 +17,11 @@ use thiserror::Error; use crate::proto::domain_parameter; use crate::proto::public_key_pace::PublicKeyPace; -/// ICAO domain-parameter id for NIST P-256 (the only curve supported by this -/// Rust port today). +/// ICAO domain-parameter id for NIST P-256. +#[cfg(test)] pub const NIST_P256_ID: u32 = 12; /// Error returned by [`ECDHPace`] operations. -/// -/// Consolidates the `ECDHPaceError` and `ECDHBasicAgreementPACEError`. #[derive(Debug, Error, PartialEq, Eq)] pub enum ECDHPaceError { #[error("Domain parameter with id {0} does not exist.")] @@ -61,244 +46,658 @@ pub enum ECDHPaceError { InvalidCoordinate(#[from] crate::proto::public_key_pace::PublicKeyPaceError), } -// --------------------------------------------------------------------------- -// ECDHPace -// --------------------------------------------------------------------------- +macro_rules! impl_curve_ops { + (modern, $struct_name:ident, $curve_crate:ident, $curve_type:ty, $secret_key:ty, $scalar:ty, $projective:ty, $affine:ty, $coord_len:expr, $order_str:expr) => { + #[derive(Debug)] + pub struct $struct_name { + priv_key: Option<$secret_key>, + pub_key: Option<$curve_crate::elliptic_curve::PublicKey<$curve_type>>, + ephemeral_priv: Option<$scalar>, + ephemeral_pub: Option<$projective>, + ephemeral_generator: Option<$projective>, + } -/// ECDH PACE engine for NIST P-256. Holds a main key pair plus an optional -/// ephemeral one used during PACE-GM mapping. -#[derive(Debug)] -pub struct ECDHPace { - priv_key: Option, - pub_key: Option, - ephemeral_priv: Option, - ephemeral_pub: Option, - /// Mapped generator used with the ephemeral scalar (PACE-GM). - ephemeral_generator: Option, -} + impl $struct_name { + pub fn new() -> Self { + Self { + priv_key: None, + pub_key: None, + ephemeral_priv: None, + ephemeral_pub: None, + ephemeral_generator: None, + } + } -impl ECDHPace { - /// Constructs an ECDH PACE engine for the given ICAO domain-parameter id. - /// - /// # Errors - /// - [`ECDHPaceError::UnknownId`] if `id` is not listed in the ICAO table. - /// - [`ECDHPaceError::UnsupportedCurve`] if `id` is listed but this port - /// does not yet back it. - pub fn new(id: u32) -> Result { - if domain_parameter::get(id).is_none() { - return Err(ECDHPaceError::UnknownId(id)); + fn scalar_from_bytes(&self, bytes: &[u8]) -> $scalar { + #[allow(unused_imports)] + use $curve_crate::elliptic_curve::bigint::Encoding; + let modulus_bytes = + <$curve_type as $curve_crate::elliptic_curve::Curve>::ORDER.to_be_bytes(); + let n = BigUint::from_bytes_be(modulus_bytes.as_ref()); + let val = BigUint::from_bytes_be(bytes); + let reduced = val % &n; + let r_bytes = reduced.to_bytes_be(); + let mut field_bytes = + <$curve_crate::elliptic_curve::FieldBytes<$curve_type>>::default(); + let len = field_bytes.len(); + if r_bytes.len() <= len { + field_bytes[len - r_bytes.len()..].copy_from_slice(&r_bytes); + } else { + field_bytes.copy_from_slice(&r_bytes[r_bytes.len() - len..]); + } + <$scalar as $curve_crate::elliptic_curve::ff::PrimeField>::from_repr(field_bytes) + .unwrap() + } + + pub fn generate_key_pair( + &mut self, + seed32: Option<&[u8]>, + ) -> Result<(), ECDHPaceError> { + let sk = match seed32 { + None => <$secret_key>::generate(), + Some(s) if s.len() == 32 => { + let mut seed_arr = [0u8; 32]; + seed_arr.copy_from_slice(s); + let mut rng = StdRng::from_seed(seed_arr); + let mut bytes = + <$curve_crate::elliptic_curve::FieldBytes<$curve_type>>::default(); + loop { + rng.fill_bytes(&mut bytes[..]); + if let Ok(sk) = <$secret_key>::from_slice(&bytes) { + break sk; + } + } + } + Some(_) => return Err(ECDHPaceError::InvalidSeedLen), + }; + self.pub_key = Some(sk.public_key()); + self.priv_key = Some(sk); + Ok(()) + } + + pub fn get_pub_key(&self) -> Result { + let pk = self.pub_key.as_ref().ok_or(ECDHPaceError::NoPublicKey)?; + self.point_to_pubkey_pace(pk.to_projective()) + } + + pub fn get_pub_key_ephemeral(&self) -> Result { + let pk = self + .ephemeral_pub + .as_ref() + .ok_or(ECDHPaceError::NoEphemeralPublicKey)?; + self.point_to_pubkey_pace(*pk) + } + + pub fn map_and_generate_ephemeral( + &mut self, + other_pub_key: &PublicKeyPace, + nonce: &[u8], + seed32: Option<&[u8]>, + ) -> Result<(), ECDHPaceError> { + use $curve_crate::elliptic_curve::group::Group; + + let other = self.transform_public(other_pub_key)?; + let h = self.compute_shared_point(&other)?; + let s = self.scalar_from_bytes(nonce); + let g = <$projective as Group>::generator(); + let g_prime = g * s + h; + + // Sample ephemeral scalar + let scalar = match seed32 { + None => { + let sk = <$secret_key>::generate(); + *sk.to_nonzero_scalar() + } + Some(s) if s.len() == 32 => { + let mut seed_arr = [0u8; 32]; + seed_arr.copy_from_slice(s); + let mut rng = StdRng::from_seed(seed_arr); + let mut bytes = + <$curve_crate::elliptic_curve::FieldBytes<$curve_type>>::default(); + loop { + rng.fill_bytes(&mut bytes[..]); + if let Ok(sk) = <$secret_key>::from_slice(&bytes) { + break *sk.to_nonzero_scalar(); + } + } + } + Some(_) => return Err(ECDHPaceError::InvalidSeedLen), + }; + + let pub_point = g_prime * scalar; + self.ephemeral_priv = Some(scalar); + self.ephemeral_pub = Some(pub_point); + self.ephemeral_generator = Some(g_prime); + Ok(()) + } + + pub fn get_ephemeral_shared_seed( + &self, + other_ephemeral_pub_key: &PublicKeyPace, + ) -> Result, ECDHPaceError> { + use $curve_crate::elliptic_curve::group::Group; + + let other = self.transform_public(other_ephemeral_pub_key)?; + let scalar = self + .ephemeral_priv + .as_ref() + .ok_or(ECDHPaceError::NoEphemeralPublicKey)?; + + let other_point = other.to_projective(); + if bool::from(other_point.is_identity()) { + return Err(ECDHPaceError::InfinityPoint); + } + let shared = other_point * *scalar; + if bool::from(shared.is_identity()) { + return Err(ECDHPaceError::InfinityAgreement); + } + + let pk = <$curve_crate::elliptic_curve::PublicKey<$curve_type>>::from_affine( + shared.to_affine(), + ) + .map_err(|_| ECDHPaceError::InfinityAgreement)?; + let sec1_bytes = pk.to_sec1_bytes(); + if sec1_bytes.len() != 1 + 2 * $coord_len || sec1_bytes[0] != 0x04 { + return Err(ECDHPaceError::InvalidEncoding); + } + let x_bytes = &sec1_bytes[1..1 + $coord_len]; + Ok(x_bytes.to_vec()) + } + + fn compute_shared_point( + &self, + other: &$curve_crate::elliptic_curve::PublicKey<$curve_type>, + ) -> Result<$projective, ECDHPaceError> { + use $curve_crate::elliptic_curve::group::Group; + + let sk = self.priv_key.as_ref().ok_or(ECDHPaceError::NoPublicKey)?; + let scalar = *sk.to_nonzero_scalar(); + let other_point = other.to_projective(); + if bool::from(other_point.is_identity()) { + return Err(ECDHPaceError::InfinityPoint); + } + let shared = other_point * scalar; + if bool::from(shared.is_identity()) { + return Err(ECDHPaceError::InfinityAgreement); + } + Ok(shared) + } + + fn point_to_pubkey_pace( + &self, + point: $projective, + ) -> Result { + let affine = point.to_affine(); + let pk = + <$curve_crate::elliptic_curve::PublicKey<$curve_type>>::from_affine(affine) + .map_err(|_| ECDHPaceError::InfinityPoint)?; + let sec1_bytes = pk.to_sec1_bytes(); + if sec1_bytes.len() != 1 + 2 * $coord_len || sec1_bytes[0] != 0x04 { + return Err(ECDHPaceError::InvalidEncoding); + } + let x_bytes = &sec1_bytes[1..1 + $coord_len]; + let y_bytes = &sec1_bytes[1 + $coord_len..]; + Ok(PublicKeyPace::new_ecdh_fixed( + BigUint::from_bytes_be(x_bytes), + BigUint::from_bytes_be(y_bytes), + $coord_len, + )?) + } + + fn transform_public( + &self, + pub_key: &PublicKeyPace, + ) -> Result<$curve_crate::elliptic_curve::PublicKey<$curve_type>, ECDHPaceError> { + match pub_key { + PublicKeyPace::Ecdh { x, y, .. } => { + let mut x_bytes = + <$curve_crate::elliptic_curve::FieldBytes<$curve_type>>::default(); + let mut y_bytes = + <$curve_crate::elliptic_curve::FieldBytes<$curve_type>>::default(); + let x_be = x.to_bytes_be(); + let y_be = y.to_bytes_be(); + if x_be.len() > $coord_len || y_be.len() > $coord_len { + return Err(ECDHPaceError::InvalidEncoding); + } + x_bytes[$coord_len - x_be.len()..].copy_from_slice(&x_be); + y_bytes[$coord_len - y_be.len()..].copy_from_slice(&y_be); + + let mut bytes = Vec::with_capacity(1 + 2 * $coord_len); + bytes.push(0x04); + bytes.extend_from_slice(&x_bytes); + bytes.extend_from_slice(&y_bytes); + <$curve_crate::elliptic_curve::PublicKey<$curve_type>>::from_sec1_bytes( + &bytes, + ) + .map_err(|_| ECDHPaceError::InvalidEncoding) + } + _ => Err(ECDHPaceError::InvalidEncoding), + } + } } - if id != NIST_P256_ID { - return Err(ECDHPaceError::UnsupportedCurve(id)); + }; + + (legacy, $struct_name:ident, $curve_crate:ident, $curve_type:ty, $secret_key:ty, $scalar:ty, $projective:ty, $affine:ty, $coord_len:expr, $order_str:expr) => { + #[derive(Debug)] + pub struct $struct_name { + priv_key: Option<$secret_key>, + pub_key: Option<$curve_crate::elliptic_curve::PublicKey<$curve_type>>, + ephemeral_priv: Option<$scalar>, + ephemeral_pub: Option<$projective>, + ephemeral_generator: Option<$projective>, } - Ok(Self { - priv_key: None, - pub_key: None, - ephemeral_priv: None, - ephemeral_pub: None, - ephemeral_generator: None, - }) - } - /// Generates a new main key pair using the OS RNG (`SysRng`) or a seeded - /// RNG if `seed` is provided (must be exactly 32 bytes). - pub fn generate_key_pair(&mut self, seed32: Option<&[u8]>) -> Result<(), ECDHPaceError> { - let sk = match seed32 { - None => { - let mut rng = UnwrapErr(SysRng); - SecretKey::generate_from_rng(&mut rng) + impl $struct_name { + pub fn new() -> Self { + Self { + priv_key: None, + pub_key: None, + ephemeral_priv: None, + ephemeral_pub: None, + ephemeral_generator: None, + } } - Some(s) if s.len() == 32 => { - let mut seed_arr = [0u8; 32]; - seed_arr.copy_from_slice(s); - let mut rng = StdRng::from_seed(seed_arr); - // Sample the scalar manually via rejection so the seeded path - // has stable, reproducible output independent of any RNG helper. - let scalar = loop { - let mut bytes = [0u8; 32]; - rng.fill_bytes(&mut bytes); - if let Ok(nz) = Scalar::from_repr(bytes.into()) - .into_option() - .ok_or(()) - .and_then(|s| NonZeroScalar::new(s).into_option().ok_or(())) - { - break nz; + + fn scalar_from_bytes(&self, bytes: &[u8]) -> $scalar { + #[allow(unused_imports)] + use $curve_crate::elliptic_curve::bigint::Encoding; + let modulus_bytes = + <$curve_type as $curve_crate::elliptic_curve::Curve>::ORDER.to_be_bytes(); + let n = BigUint::from_bytes_be(modulus_bytes.as_ref()); + let val = BigUint::from_bytes_be(bytes); + let reduced = val % &n; + let r_bytes = reduced.to_bytes_be(); + let mut field_bytes = + <$curve_crate::elliptic_curve::FieldBytes<$curve_type>>::default(); + let len = field_bytes.len(); + if r_bytes.len() <= len { + field_bytes[len - r_bytes.len()..].copy_from_slice(&r_bytes); + } else { + field_bytes.copy_from_slice(&r_bytes[r_bytes.len() - len..]); + } + <$scalar as $curve_crate::elliptic_curve::ff::PrimeField>::from_repr(field_bytes) + .unwrap() + } + + pub fn generate_key_pair( + &mut self, + seed32: Option<&[u8]>, + ) -> Result<(), ECDHPaceError> { + struct RngBridge<'a, R>(&'a mut R); + + impl<'a, R: rand::Rng> $curve_crate::elliptic_curve::rand_core::RngCore + for RngBridge<'a, R> + { + fn next_u32(&mut self) -> u32 { + self.0.next_u32() + } + fn next_u64(&mut self) -> u64 { + self.0.next_u64() } + fn fill_bytes(&mut self, dest: &mut [u8]) { + self.0.fill_bytes(dest) + } + fn try_fill_bytes( + &mut self, + dest: &mut [u8], + ) -> Result<(), $curve_crate::elliptic_curve::rand_core::Error> { + self.0.fill_bytes(dest); + Ok(()) + } + } + + impl<'a, R: rand::CryptoRng> $curve_crate::elliptic_curve::rand_core::CryptoRng + for RngBridge<'a, R> + { + } + + let sk = match seed32 { + None => { + let mut sys_rng = UnwrapErr(SysRng); + let mut rng = RngBridge(&mut sys_rng); + <$secret_key>::random(&mut rng) + } + Some(s) if s.len() == 32 => { + let mut seed_arr = [0u8; 32]; + seed_arr.copy_from_slice(s); + let mut rng = StdRng::from_seed(seed_arr); + let mut bytes = + <$curve_crate::elliptic_curve::FieldBytes<$curve_type>>::default(); + loop { + rng.fill_bytes(&mut bytes[..]); + if let Ok(sk) = <$secret_key>::from_slice(&bytes) { + break sk; + } + } + } + Some(_) => return Err(ECDHPaceError::InvalidSeedLen), }; - SecretKey::from(scalar) + self.pub_key = Some(sk.public_key()); + self.priv_key = Some(sk); + Ok(()) } - Some(_) => return Err(ECDHPaceError::InvalidSeedLen), - }; - self.pub_key = Some(sk.public_key()); - self.priv_key = Some(sk); - Ok(()) - } - /// Returns the main public key as a [`PublicKeyPace::Ecdh`]. - pub fn get_pub_key(&self) -> Result { - let pk = self.pub_key.as_ref().ok_or(ECDHPaceError::NoPublicKey)?; - point_to_pubkey_pace(pk.to_projective()) - } + pub fn get_pub_key(&self) -> Result { + let pk = self.pub_key.as_ref().ok_or(ECDHPaceError::NoPublicKey)?; + self.point_to_pubkey_pace(pk.to_projective()) + } - /// Returns the ephemeral public key as a [`PublicKeyPace::Ecdh`]. - pub fn get_pub_key_ephemeral(&self) -> Result { - let pk = self - .ephemeral_pub - .as_ref() - .ok_or(ECDHPaceError::NoEphemeralPublicKey)?; - point_to_pubkey_pace(*pk) - } + pub fn get_pub_key_ephemeral(&self) -> Result { + let pk = self + .ephemeral_pub + .as_ref() + .ok_or(ECDHPaceError::NoEphemeralPublicKey)?; + self.point_to_pubkey_pace(*pk) + } - /// Converts a [`PublicKeyPace::Ecdh`] into an on-curve [`PublicKey`]. - pub fn transform_public(pub_key: &PublicKeyPace) -> Result { - match pub_key { - PublicKeyPace::Ecdh { x, y, .. } => pubkey_from_xy(x, y), - _ => Err(ECDHPaceError::InvalidEncoding), - } - } + pub fn map_and_generate_ephemeral( + &mut self, + other_pub_key: &PublicKeyPace, + nonce: &[u8], + seed32: Option<&[u8]>, + ) -> Result<(), ECDHPaceError> { + use $curve_crate::elliptic_curve::group::Group; + + struct RngBridge<'a, R>(&'a mut R); + + impl<'a, R: rand::Rng> $curve_crate::elliptic_curve::rand_core::RngCore + for RngBridge<'a, R> + { + fn next_u32(&mut self) -> u32 { + self.0.next_u32() + } + fn next_u64(&mut self) -> u64 { + self.0.next_u64() + } + fn fill_bytes(&mut self, dest: &mut [u8]) { + self.0.fill_bytes(dest) + } + fn try_fill_bytes( + &mut self, + dest: &mut [u8], + ) -> Result<(), $curve_crate::elliptic_curve::rand_core::Error> { + self.0.fill_bytes(dest); + Ok(()) + } + } - /// Computes the shared secret point `P = priv · other`. P-256 has - /// cofactor `h = 1`, so no cofactor correction is needed. - pub fn get_shared_secret( - &self, - other_pub_key: &PublicKey, - ) -> Result { - let sk = self.priv_key.as_ref().ok_or(ECDHPaceError::NoPublicKey)?; - compute_shared_point(sk.to_nonzero_scalar().as_ref(), other_pub_key) - } + impl<'a, R: rand::CryptoRng> $curve_crate::elliptic_curve::rand_core::CryptoRng + for RngBridge<'a, R> + { + } - /// Ephemeral variant of [`get_shared_secret`]. - pub fn get_ephemeral_shared_secret( - &self, - other_ephemeral_pub_key: &PublicKey, - ) -> Result { - let scalar = self - .ephemeral_priv - .as_ref() - .ok_or(ECDHPaceError::NoEphemeralPublicKey)?; - compute_shared_point(scalar, other_ephemeral_pub_key) - } + let other = self.transform_public(other_pub_key)?; + let h = self.compute_shared_point(&other)?; + let s = self.scalar_from_bytes(nonce); + let g = <$projective as Group>::generator(); + let g_prime = g * s + h; + + // Sample ephemeral scalar + let scalar = match seed32 { + None => { + let mut sys_rng = UnwrapErr(SysRng); + let mut rng = RngBridge(&mut sys_rng); + let sk = <$secret_key>::random(&mut rng); + *sk.to_nonzero_scalar() + } + Some(s) if s.len() == 32 => { + let mut seed_arr = [0u8; 32]; + seed_arr.copy_from_slice(s); + let mut rng = StdRng::from_seed(seed_arr); + let mut bytes = + <$curve_crate::elliptic_curve::FieldBytes<$curve_type>>::default(); + loop { + rng.fill_bytes(&mut bytes[..]); + if let Ok(sk) = <$secret_key>::from_slice(&bytes) { + break *sk.to_nonzero_scalar(); + } + } + } + Some(_) => return Err(ECDHPaceError::InvalidSeedLen), + }; - /// Computes the PACE-GM mapped generator: - /// `G' = s · G + H`, where `H = priv · other`. - pub fn get_mapped_generator( - &self, - other_pub_key: &PublicKey, - nonce: &[u8], - ) -> Result { - let h = self.get_shared_secret(other_pub_key)?; - let s = scalar_from_bytes(nonce); - let g = ProjectivePoint::GENERATOR; - Ok(g * s + h) - } + let pub_point = g_prime * scalar; + self.ephemeral_priv = Some(scalar); + self.ephemeral_pub = Some(pub_point); + self.ephemeral_generator = Some(g_prime); + Ok(()) + } - /// Builds an ephemeral key pair using the given mapped generator and a - /// seeded / random scalar. The generator is stored so that subsequent - /// ephemeral shared-secret computations use the correct base point. - pub fn generate_ephemeral_with_custom_generator( - &mut self, - mapped_generator: ProjectivePoint, - seed32: Option<&[u8]>, - ) -> Result<(), ECDHPaceError> { - let scalar = sample_scalar(seed32)?; - let pub_point = mapped_generator * *scalar.as_ref(); - self.ephemeral_priv = Some(*scalar.as_ref()); - self.ephemeral_pub = Some(pub_point); - self.ephemeral_generator = Some(mapped_generator); - Ok(()) - } -} + pub fn get_ephemeral_shared_seed( + &self, + other_ephemeral_pub_key: &PublicKeyPace, + ) -> Result, ECDHPaceError> { + use $curve_crate::elliptic_curve::group::Group; + + let other = self.transform_public(other_ephemeral_pub_key)?; + let scalar = self + .ephemeral_priv + .as_ref() + .ok_or(ECDHPaceError::NoEphemeralPublicKey)?; + + let other_point = other.to_projective(); + if bool::from(other_point.is_identity()) { + return Err(ECDHPaceError::InfinityPoint); + } + let shared = other_point * *scalar; + if bool::from(shared.is_identity()) { + return Err(ECDHPaceError::InfinityAgreement); + } -// --------------------------------------------------------------------------- -// Helpers -// --------------------------------------------------------------------------- + let pk = <$curve_crate::elliptic_curve::PublicKey<$curve_type>>::from_affine( + shared.to_affine(), + ) + .map_err(|_| ECDHPaceError::InfinityAgreement)?; + let sec1_bytes = pk.to_sec1_bytes(); + if sec1_bytes.len() != 1 + 2 * $coord_len || sec1_bytes[0] != 0x04 { + return Err(ECDHPaceError::InvalidEncoding); + } + let x_bytes = &sec1_bytes[1..1 + $coord_len]; + Ok(x_bytes.to_vec()) + } -fn scalar_from_bytes(bytes: &[u8]) -> Scalar { - // Right-align the big-endian input into a fixed 32-byte buffer, then let - // the scalar's standard reduction wrap it modulo n (the P-256 order). - // Working on the slice directly avoids the BigUint round-trip allocation. - let mut buf = [0u8; 32]; - if bytes.len() <= 32 { - buf[32 - bytes.len()..].copy_from_slice(bytes); - } else { - // Take the least-significant 32 bytes (then reduce). Unreachable with - // well-formed PACE nonces (16 bytes), but kept defensive. - buf.copy_from_slice(&bytes[bytes.len() - 32..]); - } - >::reduce(&p256::U256::from_be_slice(&buf)) -} + fn compute_shared_point( + &self, + other: &$curve_crate::elliptic_curve::PublicKey<$curve_type>, + ) -> Result<$projective, ECDHPaceError> { + use $curve_crate::elliptic_curve::group::Group; + + let sk = self.priv_key.as_ref().ok_or(ECDHPaceError::NoPublicKey)?; + let scalar = *sk.to_nonzero_scalar(); + let other_point = other.to_projective(); + if bool::from(other_point.is_identity()) { + return Err(ECDHPaceError::InfinityPoint); + } + let shared = other_point * scalar; + if bool::from(shared.is_identity()) { + return Err(ECDHPaceError::InfinityAgreement); + } + Ok(shared) + } -fn sample_scalar(seed32: Option<&[u8]>) -> Result, ECDHPaceError> { - match seed32 { - None => { - let mut rng = UnwrapErr(SysRng); - Ok(NonZeroScalar::generate_from_rng(&mut rng)) - } - Some(s) if s.len() == 32 => { - let mut seed_arr = [0u8; 32]; - seed_arr.copy_from_slice(s); - let mut rng = StdRng::from_seed(seed_arr); - loop { - let mut bytes = [0u8; 32]; - rng.fill_bytes(&mut bytes); - if let Some(s) = Scalar::from_repr(bytes.into()).into_option() { - if let Some(nz) = NonZeroScalar::new(s).into_option() { - return Ok(nz); + fn point_to_pubkey_pace( + &self, + point: $projective, + ) -> Result { + let affine = point.to_affine(); + let pk = + <$curve_crate::elliptic_curve::PublicKey<$curve_type>>::from_affine(affine) + .map_err(|_| ECDHPaceError::InfinityPoint)?; + let sec1_bytes = pk.to_sec1_bytes(); + if sec1_bytes.len() != 1 + 2 * $coord_len || sec1_bytes[0] != 0x04 { + return Err(ECDHPaceError::InvalidEncoding); + } + let x_bytes = &sec1_bytes[1..1 + $coord_len]; + let y_bytes = &sec1_bytes[1 + $coord_len..]; + Ok(PublicKeyPace::new_ecdh_fixed( + BigUint::from_bytes_be(x_bytes), + BigUint::from_bytes_be(y_bytes), + $coord_len, + )?) + } + + fn transform_public( + &self, + pub_key: &PublicKeyPace, + ) -> Result<$curve_crate::elliptic_curve::PublicKey<$curve_type>, ECDHPaceError> { + match pub_key { + PublicKeyPace::Ecdh { x, y, .. } => { + let mut x_bytes = + <$curve_crate::elliptic_curve::FieldBytes<$curve_type>>::default(); + let mut y_bytes = + <$curve_crate::elliptic_curve::FieldBytes<$curve_type>>::default(); + let x_be = x.to_bytes_be(); + let y_be = y.to_bytes_be(); + if x_be.len() > $coord_len || y_be.len() > $coord_len { + return Err(ECDHPaceError::InvalidEncoding); + } + x_bytes[$coord_len - x_be.len()..].copy_from_slice(&x_be); + y_bytes[$coord_len - y_be.len()..].copy_from_slice(&y_be); + + let mut bytes = Vec::with_capacity(1 + 2 * $coord_len); + bytes.push(0x04); + bytes.extend_from_slice(&x_bytes); + bytes.extend_from_slice(&y_bytes); + <$curve_crate::elliptic_curve::PublicKey<$curve_type>>::from_sec1_bytes( + &bytes, + ) + .map_err(|_| ECDHPaceError::InvalidEncoding) } + _ => Err(ECDHPaceError::InvalidEncoding), } } } - Some(_) => Err(ECDHPaceError::InvalidSeedLen), - } + }; +} + +impl_curve_ops!( + modern, + NistP256Engine, + p256, + p256::NistP256, + p256::SecretKey, + p256::Scalar, + p256::ProjectivePoint, + p256::AffinePoint, + 32, + b"115792089210356248762485316520336594248464673199859591410292408985888941275213" +); +impl_curve_ops!( + modern, + BrainpoolP256r1Engine, + bp256, + bp256::r1::BrainpoolP256r1, + bp256::r1::SecretKey, + bp256::r1::Scalar, + bp256::r1::ProjectivePoint, + bp256::r1::AffinePoint, + 32, + b"115792089237316195423570985008687907852837564279074904382605163141518161494337" +); +impl_curve_ops!(legacy, NistP384Engine, p384, p384::NistP384, p384::SecretKey, p384::Scalar, p384::ProjectivePoint, p384::AffinePoint, 48, b"39402006196394479212279040100143613805079739270464620022646276856019566907421111663185381673890280521949313936996367"); +impl_curve_ops!(modern, BrainpoolP384r1Engine, bp384, bp384::r1::BrainpoolP384r1, bp384::r1::SecretKey, bp384::r1::Scalar, bp384::r1::ProjectivePoint, bp384::r1::AffinePoint, 48, b"39402006196394479212279040100143613805079739270464620022648719277063462947702816912384784949216075932598370503046777"); +impl_curve_ops!(legacy, NistP521Engine, p521, p521::NistP521, p521::SecretKey, p521::Scalar, p521::ProjectivePoint, p521::AffinePoint, 66, b"6864797660130609714981900799081393217269435300143305409394463459185543183397655394245057746333217197532963996371363321113864768612440380340372808892707005449"); +impl_curve_ops!( + legacy, + NistP224Engine, + p224, + p224::NistP224, + p224::SecretKey, + p224::Scalar, + p224::ProjectivePoint, + p224::AffinePoint, + 28, + b"26959946667150639794667015087019625940457807714424391721682722368061" +); + +#[derive(Debug)] +pub enum ECDHPace { + NistP256(NistP256Engine), + BrainpoolP256r1(BrainpoolP256r1Engine), + NistP384(NistP384Engine), + BrainpoolP384r1(BrainpoolP384r1Engine), + NistP521(NistP521Engine), + NistP224(NistP224Engine), } -fn compute_shared_point( - scalar: &Scalar, - other: &PublicKey, -) -> Result { - let other_point = other.to_projective(); - if bool::from(other_point.is_identity()) { - return Err(ECDHPaceError::InfinityPoint); +impl ECDHPace { + pub fn new(id: u32) -> Result { + if domain_parameter::get(id).is_none() { + return Err(ECDHPaceError::UnknownId(id)); + } + match id { + 10 => Ok(Self::NistP224(NistP224Engine::new())), + 12 => Ok(Self::NistP256(NistP256Engine::new())), + 13 => Ok(Self::BrainpoolP256r1(BrainpoolP256r1Engine::new())), + 15 => Ok(Self::NistP384(NistP384Engine::new())), + 16 => Ok(Self::BrainpoolP384r1(BrainpoolP384r1Engine::new())), + 18 => Ok(Self::NistP521(NistP521Engine::new())), + _ => Err(ECDHPaceError::UnsupportedCurve(id)), + } + } + + pub fn generate_key_pair(&mut self, seed32: Option<&[u8]>) -> Result<(), ECDHPaceError> { + match self { + Self::NistP256(e) => e.generate_key_pair(seed32), + Self::BrainpoolP256r1(e) => e.generate_key_pair(seed32), + Self::NistP384(e) => e.generate_key_pair(seed32), + Self::BrainpoolP384r1(e) => e.generate_key_pair(seed32), + Self::NistP521(e) => e.generate_key_pair(seed32), + Self::NistP224(e) => e.generate_key_pair(seed32), + } } - let shared = other_point * *scalar; - if bool::from(shared.is_identity()) { - return Err(ECDHPaceError::InfinityAgreement); + + pub fn get_pub_key(&self) -> Result { + match self { + Self::NistP256(e) => e.get_pub_key(), + Self::BrainpoolP256r1(e) => e.get_pub_key(), + Self::NistP384(e) => e.get_pub_key(), + Self::BrainpoolP384r1(e) => e.get_pub_key(), + Self::NistP521(e) => e.get_pub_key(), + Self::NistP224(e) => e.get_pub_key(), + } } - Ok(shared) -} -fn point_to_pubkey_pace(point: ProjectivePoint) -> Result { - let affine: AffinePoint = point.to_affine(); - let encoded: EncodedPoint = affine.to_sec1_point(false); - // The identity (point at infinity) has no affine coordinates; reject it - // instead of panicking on the missing X/Y. - let x_bytes = encoded.x().ok_or(ECDHPaceError::InfinityPoint)?; - let y_bytes = encoded.y().ok_or(ECDHPaceError::InfinityPoint)?; - // SEC1 uncompressed coordinates are fixed-width (32 bytes for P-256); carry - // that width so the X||Y form is always emitted at full length. - let coord_len = x_bytes.len(); - Ok(PublicKeyPace::new_ecdh_fixed( - BigUint::from_bytes_be(x_bytes), - BigUint::from_bytes_be(y_bytes), - coord_len, - )?) -} + pub fn get_pub_key_ephemeral(&self) -> Result { + match self { + Self::NistP256(e) => e.get_pub_key_ephemeral(), + Self::BrainpoolP256r1(e) => e.get_pub_key_ephemeral(), + Self::NistP384(e) => e.get_pub_key_ephemeral(), + Self::BrainpoolP384r1(e) => e.get_pub_key_ephemeral(), + Self::NistP521(e) => e.get_pub_key_ephemeral(), + Self::NistP224(e) => e.get_pub_key_ephemeral(), + } + } -fn pubkey_from_xy(x: &BigUint, y: &BigUint) -> Result { - let mut x_bytes = [0u8; 32]; - let mut y_bytes = [0u8; 32]; - let x_be = x.to_bytes_be(); - let y_be = y.to_bytes_be(); - if x_be.len() > 32 || y_be.len() > 32 { - return Err(ECDHPaceError::InvalidEncoding); + pub fn map_and_generate_ephemeral( + &mut self, + other_pub_key: &PublicKeyPace, + nonce: &[u8], + seed32: Option<&[u8]>, + ) -> Result<(), ECDHPaceError> { + match self { + Self::NistP256(e) => e.map_and_generate_ephemeral(other_pub_key, nonce, seed32), + Self::BrainpoolP256r1(e) => e.map_and_generate_ephemeral(other_pub_key, nonce, seed32), + Self::NistP384(e) => e.map_and_generate_ephemeral(other_pub_key, nonce, seed32), + Self::BrainpoolP384r1(e) => e.map_and_generate_ephemeral(other_pub_key, nonce, seed32), + Self::NistP521(e) => e.map_and_generate_ephemeral(other_pub_key, nonce, seed32), + Self::NistP224(e) => e.map_and_generate_ephemeral(other_pub_key, nonce, seed32), + } } - x_bytes[32 - x_be.len()..].copy_from_slice(&x_be); - y_bytes[32 - y_be.len()..].copy_from_slice(&y_be); - let encoded = EncodedPoint::from_affine_coordinates(&x_bytes.into(), &y_bytes.into(), false); - let affine = Option::::from(AffinePoint::from_sec1_point(&encoded)) - .ok_or(ECDHPaceError::InvalidEncoding)?; - Ok(PublicKey::from_affine(affine).map_err(|_| ECDHPaceError::InvalidEncoding)?) + pub fn get_ephemeral_shared_seed( + &self, + other_ephemeral_pub_key: &PublicKeyPace, + ) -> Result, ECDHPaceError> { + match self { + Self::NistP256(e) => e.get_ephemeral_shared_seed(other_ephemeral_pub_key), + Self::BrainpoolP256r1(e) => e.get_ephemeral_shared_seed(other_ephemeral_pub_key), + Self::NistP384(e) => e.get_ephemeral_shared_seed(other_ephemeral_pub_key), + Self::BrainpoolP384r1(e) => e.get_ephemeral_shared_seed(other_ephemeral_pub_key), + Self::NistP521(e) => e.get_ephemeral_shared_seed(other_ephemeral_pub_key), + Self::NistP224(e) => e.get_ephemeral_shared_seed(other_ephemeral_pub_key), + } + } } // --------------------------------------------------------------------------- @@ -311,7 +710,7 @@ mod tests { #[test] fn unknown_id_is_rejected() { - assert_eq!(ECDHPace::new(99).unwrap_err(), ECDHPaceError::UnknownId(99),); + assert_eq!(ECDHPace::new(99).unwrap_err(), ECDHPaceError::UnknownId(99)); } #[test] @@ -324,100 +723,59 @@ mod tests { } #[test] - fn p256_engine_constructs() { - let e = ECDHPace::new(NIST_P256_ID).unwrap(); - // No key pair generated yet, so the public key is unavailable. - assert_eq!(e.get_pub_key().unwrap_err(), ECDHPaceError::NoPublicKey); + fn engine_constructs_for_all_supported_curves() { + for id in &[10, 12, 13, 15, 16, 18] { + let e = ECDHPace::new(*id).unwrap(); + assert_eq!(e.get_pub_key().unwrap_err(), ECDHPaceError::NoPublicKey); + } } #[test] fn seeded_key_pair_is_deterministic() { let seed = [0x11u8; 32]; - let mut a = ECDHPace::new(NIST_P256_ID).unwrap(); - let mut b = ECDHPace::new(NIST_P256_ID).unwrap(); - a.generate_key_pair(Some(&seed)).unwrap(); - b.generate_key_pair(Some(&seed)).unwrap(); - let pka = a.get_pub_key().unwrap().to_bytes(); - let pkb = b.get_pub_key().unwrap().to_bytes(); - assert_eq!(pka, pkb); + for id in &[10, 12, 13, 15, 16, 18] { + let mut a = ECDHPace::new(*id).unwrap(); + let mut b = ECDHPace::new(*id).unwrap(); + a.generate_key_pair(Some(&seed)).unwrap(); + b.generate_key_pair(Some(&seed)).unwrap(); + let pka = a.get_pub_key().unwrap().to_bytes(); + let pkb = b.get_pub_key().unwrap().to_bytes(); + assert_eq!(pka, pkb); + } } #[test] fn seed_wrong_length_errors() { - let mut e = ECDHPace::new(NIST_P256_ID).unwrap(); + let mut e = ECDHPace::new(12).unwrap(); let err = e.generate_key_pair(Some(&[0u8; 16])).unwrap_err(); assert_eq!(err, ECDHPaceError::InvalidSeedLen); } #[test] - fn shared_secret_is_symmetric() { - let mut alice = ECDHPace::new(NIST_P256_ID).unwrap(); - let mut bob = ECDHPace::new(NIST_P256_ID).unwrap(); - alice.generate_key_pair(Some(&[0x01u8; 32])).unwrap(); - bob.generate_key_pair(Some(&[0x02u8; 32])).unwrap(); - - let alice_pk = alice.get_pub_key().unwrap(); - let bob_pk = bob.get_pub_key().unwrap(); - let alice_pk_ec = ECDHPace::transform_public(&alice_pk).unwrap(); - let bob_pk_ec = ECDHPace::transform_public(&bob_pk).unwrap(); - - let s_ab = alice.get_shared_secret(&bob_pk_ec).unwrap(); - let s_ba = bob.get_shared_secret(&alice_pk_ec).unwrap(); - assert_eq!(s_ab, s_ba); - } - - #[test] - fn transform_public_roundtrip() { - let mut e = ECDHPace::new(NIST_P256_ID).unwrap(); - e.generate_key_pair(Some(&[0x03u8; 32])).unwrap(); - let pk = e.get_pub_key().unwrap(); - let back = ECDHPace::transform_public(&pk).unwrap(); - // Re-serialise and compare. - let pk2 = point_to_pubkey_pace(back.to_projective()).unwrap(); - assert_eq!(pk.to_bytes(), pk2.to_bytes()); - } - - #[test] - fn transform_public_rejects_dh_input() { - let bad = PublicKeyPace::new_dh(vec![0x01, 0x02]); - assert_eq!( - ECDHPace::transform_public(&bad).unwrap_err(), - ECDHPaceError::InvalidEncoding, - ); - } - - #[test] - fn mapped_generator_is_on_curve_and_matches_formula() { - let mut alice = ECDHPace::new(NIST_P256_ID).unwrap(); - let mut bob = ECDHPace::new(NIST_P256_ID).unwrap(); - alice.generate_key_pair(Some(&[0x04u8; 32])).unwrap(); - bob.generate_key_pair(Some(&[0x05u8; 32])).unwrap(); - - let bob_pk = ECDHPace::transform_public(&bob.get_pub_key().unwrap()).unwrap(); - let nonce = hex::decode("A1A2A3A4A5A6A7A8A9AAABACADAEAFB0").unwrap(); - - let g_prime = alice.get_mapped_generator(&bob_pk, &nonce).unwrap(); - // Manual recomputation. - let h = alice.get_shared_secret(&bob_pk).unwrap(); - let s = scalar_from_bytes(&nonce); - let expected = ProjectivePoint::GENERATOR * s + h; - assert_eq!(g_prime, expected); - } - - #[test] - fn ephemeral_flow_with_mapped_generator() { - let mut alice = ECDHPace::new(NIST_P256_ID).unwrap(); - let mut bob = ECDHPace::new(NIST_P256_ID).unwrap(); - alice.generate_key_pair(Some(&[0x04u8; 32])).unwrap(); - bob.generate_key_pair(Some(&[0x05u8; 32])).unwrap(); - - let bob_pk = ECDHPace::transform_public(&bob.get_pub_key().unwrap()).unwrap(); - let g_prime = alice - .get_mapped_generator(&bob_pk, &[0xA1, 0xA2, 0xA3, 0xA4]) - .unwrap(); - alice - .generate_ephemeral_with_custom_generator(g_prime, Some(&[0x0Au8; 32])) - .unwrap(); - assert!(alice.get_pub_key_ephemeral().is_ok()); + fn shared_secret_is_symmetric_all_curves() { + for id in &[10, 12, 13, 15, 16, 18] { + let mut alice = ECDHPace::new(*id).unwrap(); + let mut bob = ECDHPace::new(*id).unwrap(); + alice.generate_key_pair(Some(&[0x01u8; 32])).unwrap(); + bob.generate_key_pair(Some(&[0x02u8; 32])).unwrap(); + + let alice_pk = alice.get_pub_key().unwrap(); + let bob_pk = bob.get_pub_key().unwrap(); + + // simulated map and ephemeral gen + let nonce = [0xAAu8; 16]; + alice + .map_and_generate_ephemeral(&bob_pk, &nonce, Some(&[0x03u8; 32])) + .unwrap(); + bob.map_and_generate_ephemeral(&alice_pk, &nonce, Some(&[0x04u8; 32])) + .unwrap(); + + let alice_eph_pk = alice.get_pub_key_ephemeral().unwrap(); + let bob_eph_pk = bob.get_pub_key_ephemeral().unwrap(); + + let seed_alice = alice.get_ephemeral_shared_seed(&bob_eph_pk).unwrap(); + let seed_bob = bob.get_ephemeral_shared_seed(&alice_eph_pk).unwrap(); + assert_eq!(seed_alice, seed_bob); + } } } diff --git a/crates/dmrtd/src/proto/pace_session.rs b/crates/dmrtd/src/proto/pace_session.rs index 887558e..c86d822 100644 --- a/crates/dmrtd/src/proto/pace_session.rs +++ b/crates/dmrtd/src/proto/pace_session.rs @@ -30,9 +30,7 @@ //! //! [`BacSession`]: crate::proto::bac_session::BacSession -use elliptic_curve::sec1::ToSec1Point; use num_bigint::BigUint; -use p256::ProjectivePoint; use thiserror::Error; use crate::crypto::crypto_utils::constant_time_eq; @@ -42,7 +40,8 @@ use crate::lds::asn1_object_identifiers::{ use crate::proto::access_key::AccessKey; use crate::proto::aes_smcipher::AesSmCipher; use crate::proto::dh_pace::{self, DHPace, DHPaceError}; -use crate::proto::ecdh_pace::{ECDHPace, ECDHPaceError, NIST_P256_ID}; +use crate::proto::domain_parameter; +use crate::proto::ecdh_pace::{ECDHPace, ECDHPaceError}; use crate::proto::iso7816::command_apdu::CommandApdu; use crate::proto::iso7816::iso7816::{cla, ins}; use crate::proto::iso7816::response_apdu::{ResponseApdu, StatusWord}; @@ -70,7 +69,7 @@ pub enum PaceSessionError { UnsupportedMapping(MappingType), #[error("PACE session: only AES cipher is supported (got {0:?})")] UnsupportedCipher(CipherAlgorithm), - #[error("PACE session: only NIST P-256 (id 12) is supported (got id {0})")] + #[error("PACE session: unsupported curve/parameter id (got id {0})")] UnsupportedCurve(u32), #[error("PACE session has no outstanding APDU to consume")] NoOutstandingApdu, @@ -144,9 +143,7 @@ impl PaceEngine { ) -> Result<(), PaceSessionError> { match self { PaceEngine::Ecdh(e) => { - let icc_pk = ECDHPace::transform_public(icc_mapping_pub)?; - let g_prime = e.get_mapped_generator(&icc_pk, nonce)?; - e.generate_ephemeral_with_custom_generator(g_prime, seed)?; + e.map_and_generate_ephemeral(icc_mapping_pub, nonce, seed)?; } PaceEngine::Dh(e) => { let g_prime = @@ -168,9 +165,8 @@ impl PaceEngine { ) -> Result, PaceSessionError> { match self { PaceEngine::Ecdh(e) => { - let icc_eph_pk = ECDHPace::transform_public(icc_ephemeral_pub)?; - let shared = e.get_ephemeral_shared_secret(&icc_eph_pk)?; - ecdh_shared_secret_x_bytes(shared) + let seed_bytes = e.get_ephemeral_shared_seed(icc_ephemeral_pub)?; + Ok(seed_bytes) } PaceEngine::Dh(e) => { let shared = @@ -289,7 +285,12 @@ impl PaceSession { if protocol.mapping_type != MappingType::Gm { return Err(PaceSessionError::UnsupportedMapping(protocol.mapping_type)); } - if parameter_id != NIST_P256_ID { + let supported = domain_parameter::get(parameter_id) + .map(|entry| { + entry.is_supported && entry.kind == domain_parameter::DomainParameterType::Ecp + }) + .unwrap_or(false); + if !supported { return Err(PaceSessionError::UnsupportedCurve(parameter_id)); } Ok(Self { @@ -565,12 +566,6 @@ impl PaceSession { } } -fn ecdh_shared_secret_x_bytes(shared: ProjectivePoint) -> Result, PaceSessionError> { - let encoded = shared.to_affine().to_sec1_point(false); - let x = encoded.x().ok_or(PaceSessionError::MissingSharedSecretX)?; - Ok(x.to_vec()) -} - // --------------------------------------------------------------------------- // Tests // --------------------------------------------------------------------------- @@ -587,6 +582,7 @@ mod tests { use crate::crypto::aes::{AesCipher, BlockCipherMode, AES_BLOCK_SIZE}; use crate::lds::tlv::Tlv; use crate::proto::can_key::CanKey; + use crate::proto::ecdh_pace::NIST_P256_ID; fn ecdh_gm_aes128_oid() -> OiePaceProtocol { OiePaceProtocol::new( @@ -679,11 +675,11 @@ mod tests { } #[test] - fn rejects_non_p256_curve() { + fn rejects_unsupported_curve() { let can = CanKey::new("123456").unwrap(); - match PaceSession::new_ecdh(can, ecdh_gm_aes128_oid(), 13) { - Err(PaceSessionError::UnsupportedCurve(13)) => {} - _ => panic!("expected UnsupportedCurve(13)"), + match PaceSession::new_ecdh(can, ecdh_gm_aes128_oid(), 8) { + Err(PaceSessionError::UnsupportedCurve(8)) => {} + _ => panic!("expected UnsupportedCurve(8)"), } } @@ -791,11 +787,7 @@ mod tests { // Chip also computes the mapped generator, then its own ephemeral. let terminal_mapping_pk = icc_mapping_pub_from_session(&session).expect("terminal's main pubkey after step 2"); - let terminal_mapping_ec = ECDHPace::transform_public(&terminal_mapping_pk).unwrap(); - let g_prime = icc - .get_mapped_generator(&terminal_mapping_ec, &nonce) - .unwrap(); - icc.generate_ephemeral_with_custom_generator(g_prime, Some(&seed_icc_eph)) + icc.map_and_generate_ephemeral(&terminal_mapping_pk, &nonce, Some(&seed_icc_eph)) .unwrap(); let icc_eph_pub = icc.get_pub_key_ephemeral().unwrap(); let mut step3_body = vec![0x04]; @@ -813,9 +805,7 @@ mod tests { // ICC computes its own token over the *terminal's* ephemeral public. let terminal_eph_pub = terminal_ephemeral_pub_from_session(&session) .expect("terminal ephemeral public after step 3"); - let terminal_eph_ec = ECDHPace::transform_public(&terminal_eph_pub).unwrap(); - let icc_shared = icc.get_ephemeral_shared_secret(&terminal_eph_ec).unwrap(); - let seed_bytes = ecdh_shared_secret_x_bytes(icc_shared).unwrap(); + let seed_bytes = icc.get_ephemeral_shared_seed(&terminal_eph_pub).unwrap(); let icc_k_mac = pace::calculate_mac_key(&protocol, &seed_bytes).unwrap(); let icc_auth_input = pace::generate_encoding_input_data(&protocol, &terminal_eph_pub); let icc_token = pace::calculate_auth_token(&protocol, &icc_auth_input, &icc_k_mac).unwrap();