tests.rs (18981B)
1 use super::{ 2 super::{ 3 super::{ 4 AggErr, 5 request::{AsciiDomain, RpId}, 6 }, 7 auth::{AuthenticatorData, NonDiscoverableAuthenticatorAssertion}, 8 }, 9 AttestationFormat, AttestationObject, AuthDataContainer as _, AuthExtOutput as _, 10 AuthTransports, AuthenticatorAttestation, AuthenticatorExtensionOutput, 11 AuthenticatorExtensionOutputErr, Backup, CborSuccess, CredentialProtectionPolicy, 12 FourToSixtyThree, FromCbor as _, HmacSecret, Sig, UncompressedPubKey, 13 cbor::{ 14 BYTES, BYTES_INFO_24, MAP_1, MAP_2, MAP_3, MAP_4, SIMPLE_FALSE, SIMPLE_TRUE, TEXT_11, 15 TEXT_12, TEXT_14, 16 }, 17 }; 18 use ed25519_dalek::Verifier as _; 19 use p256::ecdsa::{DerSignature as P256Sig, SigningKey as P256Key}; 20 use rsa::sha2::{Digest as _, Sha256}; 21 #[expect(clippy::panic, reason = "OK in tests")] 22 #[expect( 23 clippy::arithmetic_side_effects, 24 clippy::indexing_slicing, 25 clippy::missing_asserts_for_indexing, 26 reason = "comments justifies correctness" 27 )] 28 fn hex_decode<const N: usize>(input: &[u8; N]) -> Vec<u8> { 29 /// Value to subtract from a lowercase hex digit. 30 const LOWER_OFFSET: u8 = b'a' - 10; 31 assert_eq!( 32 N & 1, 33 0, 34 "hex_decode must be passed a reference to an array of even length" 35 ); 36 let mut data = Vec::with_capacity(N >> 1); 37 input.chunks_exact(2).fold((), |(), byte| { 38 // `byte.len() == 2`. 39 let mut hex = byte[0]; 40 let val = match hex { 41 // `Won't underflow`. 42 b'0'..=b'9' => hex - b'0', 43 // `Won't underflow`. 44 b'a'..=b'f' => hex - LOWER_OFFSET, 45 _ => panic!("hex_decode must be passed a valid lowercase hexadecimal array"), 46 } << 4u8; 47 // `byte.len() == 2`. 48 hex = byte[1]; 49 data.push( 50 val | match hex { 51 // `Won't underflow`. 52 b'0'..=b'9' => hex - b'0', 53 // `Won't underflow`. 54 b'a'..=b'f' => hex - LOWER_OFFSET, 55 _ => panic!("hex_decode must be passed a valid lowercase hexadecimal array"), 56 }, 57 ); 58 }); 59 data 60 } 61 /// <https://pr-preview.s3.amazonaws.com/w3c/webauthn/pull/2209.html#sctn-test-vectors-none-es256> 62 #[expect( 63 clippy::panic_in_result_fn, 64 clippy::unwrap_in_result, 65 clippy::unwrap_used, 66 reason = "OK in tests" 67 )] 68 #[test] 69 fn es256_test_vector() -> Result<(), AggErr> { 70 let rp_id = RpId::Domain(AsciiDomain::try_from("example.org".to_owned())?); 71 let credential_private_key = 72 hex_decode(b"6e68e7a58484a3264f66b77f5d6dc5bc36a47085b615c9727ab334e8c369c2ee"); 73 let aaguid = hex_decode(b"8446ccb9ab1db374750b2367ff6f3a1f"); 74 let credential_id = 75 hex_decode(b"f91f391db4c9b2fde0ea70189cba3fb63f579ba6122b33ad94ff3ec330084be4"); 76 let client_data_json = hex_decode(b"7b2274797065223a22776562617574686e2e637265617465222c226368616c6c656e6765223a22414d4d507434557878475453746e63647134313759447742466938767049612d7077386f4f755657345441222c226f726967696e223a2268747470733a2f2f6578616d706c652e6f7267222c2263726f73734f726967696e223a66616c73652c22657874726144617461223a22636c69656e74446174614a534f4e206d617920626520657874656e6465642077697468206164646974696f6e616c206669656c647320696e20746865206675747572652c207375636820617320746869733a20426b5165446a646354427258426941774a544c4535513d3d227d"); 77 let attestation_object = hex_decode(b"a363666d74646e6f6e656761747453746d74a068617574684461746158a4bfabc37432958b063360d3ad6461c9c4735ae7f8edd46592a5e0f01452b2e4b559000000008446ccb9ab1db374750b2367ff6f3a1f0020f91f391db4c9b2fde0ea70189cba3fb63f579ba6122b33ad94ff3ec330084be4a5010203262001215820afefa16f97ca9b2d23eb86ccb64098d20db90856062eb249c33a9b672f26df61225820930a56b87a2fca66334b03458abf879717c12cc68ed73290af2e2664796b9220"); 78 let key = *P256Key::from_slice(credential_private_key.as_slice()) 79 .unwrap() 80 .verifying_key(); 81 let enc_key = key.to_sec1_point(false); 82 let auth_attest = 83 AuthenticatorAttestation::new(client_data_json, attestation_object, AuthTransports(0)); 84 let att_obj = 85 AttestationObject::from_data(auth_attest.attestation_object_and_c_data_hash.as_slice())?; 86 assert_eq!( 87 aaguid, 88 att_obj.data.auth_data.attested_credential_data.aaguid.0 89 ); 90 assert_eq!( 91 credential_id, 92 att_obj 93 .data 94 .auth_data 95 .attested_credential_data 96 .credential_id 97 .0 98 ); 99 assert!( 100 matches!(att_obj.data.auth_data.attested_credential_data.credential_public_key, UncompressedPubKey::P256(pub_key) if **enc_key.x().unwrap() == *pub_key.0 && **enc_key.y().unwrap() == *pub_key.1) 101 ); 102 assert_eq!( 103 *att_obj.data.auth_data.rp_id_hash, 104 *Sha256::digest(rp_id.as_ref()) 105 ); 106 assert!(att_obj.data.auth_data.flags.user_present); 107 assert!(matches!(att_obj.data.attestation, AttestationFormat::None)); 108 let authenticator_data = 109 hex_decode(b"bfabc37432958b063360d3ad6461c9c4735ae7f8edd46592a5e0f01452b2e4b51900000000"); 110 let client_data_json_2 = hex_decode(b"7b2274797065223a22776562617574686e2e676574222c226368616c6c656e6765223a224f63446e55685158756c5455506f334a5558543049393770767a7a59425039745a63685879617630314167222c226f726967696e223a2268747470733a2f2f6578616d706c652e6f7267222c2263726f73734f726967696e223a66616c73657d"); 111 let signature = hex_decode(b"3046022100f50a4e2e4409249c4a853ba361282f09841df4dd4547a13a87780218deffcd380221008480ac0f0b93538174f575bf11a1dd5d78c6e486013f937295ea13653e331e87"); 112 let auth_assertion = NonDiscoverableAuthenticatorAssertion::<1>::without_user( 113 client_data_json_2, 114 authenticator_data, 115 signature, 116 ); 117 let auth_data = AuthenticatorData::try_from(auth_assertion.authenticator_data())?; 118 assert_eq!(*auth_data.rp_id_hash(), *Sha256::digest(rp_id.as_ref())); 119 assert!(auth_data.flags().user_present); 120 assert!(match att_obj.data.auth_data.flags.backup { 121 Backup::NotEligible => matches!(auth_data.flags().backup, Backup::NotEligible), 122 Backup::Eligible => !matches!(auth_data.flags().backup, Backup::NotEligible), 123 Backup::Exists => matches!(auth_data.flags().backup, Backup::Exists), 124 }); 125 let sig = P256Sig::from_bytes(auth_assertion.signature()).unwrap(); 126 let mut msg = auth_assertion.authenticator_data().to_owned(); 127 msg.extend_from_slice(&Sha256::digest(auth_assertion.client_data_json())); 128 key.verify(msg.as_slice(), &sig).unwrap(); 129 Ok(()) 130 } 131 /// <https://pr-preview.s3.amazonaws.com/w3c/webauthn/pull/2209.html#sctn-test-vectors-packed-self-es256> 132 #[expect( 133 clippy::panic_in_result_fn, 134 clippy::unwrap_in_result, 135 clippy::unwrap_used, 136 reason = "OK in tests" 137 )] 138 #[expect(clippy::indexing_slicing, reason = "comment justifies correctness")] 139 #[test] 140 fn es256_self_attest_test_vector() -> Result<(), AggErr> { 141 let rp_id = RpId::Domain(AsciiDomain::try_from("example.org".to_owned())?); 142 let credential_private_key = 143 hex_decode(b"b4bbfa5d68e1693b6ef5a19a0e60ef7ee2cbcac81f7fec7006ac3a21e0c5116a"); 144 let aaguid = hex_decode(b"df850e09db6afbdfab51697791506cfc"); 145 let credential_id = 146 hex_decode(b"455ef34e2043a87db3d4afeb39bbcb6cc32df9347c789a865ecdca129cbef58c"); 147 let client_data_json = hex_decode(b"7b2274797065223a22776562617574686e2e637265617465222c226368616c6c656e6765223a2265476e4374334c55745936366b336a506a796e6962506b31716e666644616966715a774c33417032392d55222c226f726967696e223a2268747470733a2f2f6578616d706c652e6f7267222c2263726f73734f726967696e223a66616c73652c22657874726144617461223a22636c69656e74446174614a534f4e206d617920626520657874656e6465642077697468206164646974696f6e616c206669656c647320696e20746865206675747572652c207375636820617320746869733a205539685458764b453255526b4d6e625f3078594856673d3d227d"); 148 let attestation_object = hex_decode(b"a363666d74667061636b65646761747453746d74a263616c67266373696758483046022100ae045923ded832b844cae4d5fc864277c0dc114ad713e271af0f0d371bd3ac540221009077a088ed51a673951ad3ba2673d5029bab65b64f4ea67b234321f86fcfac5d68617574684461746158a4bfabc37432958b063360d3ad6461c9c4735ae7f8edd46592a5e0f01452b2e4b55d00000000df850e09db6afbdfab51697791506cfc0020455ef34e2043a87db3d4afeb39bbcb6cc32df9347c789a865ecdca129cbef58ca5010203262001215820eb151c8176b225cc651559fecf07af450fd85802046656b34c18f6cf193843c5225820927b8aa427a2be1b8834d233a2d34f61f13bfd44119c325d5896e183fee484f2"); 149 let key = *P256Key::from_slice(credential_private_key.as_slice()) 150 .unwrap() 151 .verifying_key(); 152 let enc_key = key.to_sec1_point(false); 153 let auth_attest = 154 AuthenticatorAttestation::new(client_data_json, attestation_object, AuthTransports(0)); 155 let (att_obj, auth_idx) = AttestationObject::parse_data(auth_attest.attestation_object())?; 156 assert_eq!(aaguid, att_obj.auth_data.attested_credential_data.aaguid.0); 157 assert_eq!( 158 credential_id, 159 att_obj.auth_data.attested_credential_data.credential_id.0 160 ); 161 assert!( 162 matches!(att_obj.auth_data.attested_credential_data.credential_public_key, UncompressedPubKey::P256(pub_key) if **enc_key.x().unwrap() == *pub_key.0 && **enc_key.y().unwrap() == *pub_key.1) 163 ); 164 assert_eq!( 165 *att_obj.auth_data.rp_id_hash, 166 *Sha256::digest(rp_id.as_ref()) 167 ); 168 assert!(att_obj.auth_data.flags.user_present); 169 assert!(match att_obj.attestation { 170 AttestationFormat::None => false, 171 AttestationFormat::Packed(attest) => { 172 match attest.signature { 173 Sig::MlDsa87(_) 174 | Sig::MlDsa65(_) 175 | Sig::MlDsa44(_) 176 | Sig::Ed25519(_) 177 | Sig::P384(_) 178 | Sig::Rs256(_) => false, 179 Sig::P256(sig) => { 180 let s = P256Sig::from_bytes(sig).unwrap(); 181 key.verify( 182 // Won't `panic` since `auth_idx` is returned from `AttestationObject::parse_data`. 183 &auth_attest.attestation_object_and_c_data_hash[auth_idx..], 184 &s, 185 ) 186 .is_ok() 187 } 188 } 189 } 190 }); 191 let authenticator_data = 192 hex_decode(b"bfabc37432958b063360d3ad6461c9c4735ae7f8edd46592a5e0f01452b2e4b50900000000"); 193 let client_data_json_2 = hex_decode(b"7b2274797065223a22776562617574686e2e676574222c226368616c6c656e6765223a225248696843784e534e493352594d45314f7731476d3132786e726b634a5f6666707637546e2d4a71386773222c226f726967696e223a2268747470733a2f2f6578616d706c652e6f7267222c2263726f73734f726967696e223a66616c73652c22657874726144617461223a22636c69656e74446174614a534f4e206d617920626520657874656e6465642077697468206164646974696f6e616c206669656c647320696e20746865206675747572652c207375636820617320746869733a206754623533727a36456853576f6d58477a696d4331513d3d227d"); 194 let signature = hex_decode(b"3044022076691be76a8618976d9803c4cdc9b97d34a7af37e3bdc894a2bf54f040ffae850220448033a015296ffb09a762efd0d719a55346941e17e91ebf64c60d439d0b9744"); 195 let auth_assertion = NonDiscoverableAuthenticatorAssertion::<1>::without_user( 196 client_data_json_2, 197 authenticator_data, 198 signature, 199 ); 200 let auth_data = AuthenticatorData::try_from(auth_assertion.authenticator_data())?; 201 assert_eq!(*auth_data.rp_id_hash(), *Sha256::digest(rp_id.as_ref())); 202 assert!(auth_data.flags().user_present); 203 assert!(match att_obj.auth_data.flags.backup { 204 Backup::NotEligible => matches!(auth_data.flags().backup, Backup::NotEligible), 205 Backup::Eligible | Backup::Exists => 206 !matches!(auth_data.flags().backup, Backup::NotEligible), 207 }); 208 let sig = P256Sig::from_bytes(auth_assertion.signature()).unwrap(); 209 let mut msg = auth_assertion.authenticator_data().to_owned(); 210 msg.extend_from_slice(&Sha256::digest(auth_assertion.client_data_json())); 211 key.verify(msg.as_slice(), &sig).unwrap(); 212 Ok(()) 213 } 214 struct AuthExtOptions<'a> { 215 cred_protect: Option<u8>, 216 hmac_secret: Option<bool>, 217 min_pin_length: Option<u8>, 218 hmac_secret_mc: Option<&'a [u8]>, 219 } 220 #[expect( 221 clippy::panic, 222 clippy::unreachable, 223 reason = "want to crash when there is a bug" 224 )] 225 #[expect( 226 clippy::arithmetic_side_effects, 227 clippy::as_conversions, 228 clippy::cast_possible_truncation, 229 reason = "comments justify correctness" 230 )] 231 fn generate_auth_extensions(opts: &AuthExtOptions<'_>) -> Vec<u8> { 232 // Maxes at 4, so addition is clearly free from overflow. 233 let map_len = u8::from(opts.cred_protect.is_some()) 234 + u8::from(opts.hmac_secret.is_some()) 235 + u8::from(opts.min_pin_length.is_some()) 236 + u8::from(opts.hmac_secret_mc.is_some()); 237 let header = match map_len { 238 0 => return Vec::new(), 239 1 => MAP_1, 240 2 => MAP_2, 241 3 => MAP_3, 242 4 => MAP_4, 243 _ => unreachable!("bug"), 244 }; 245 let mut cbor = Vec::with_capacity(128); 246 cbor.push(header); 247 if let Some(protect) = opts.cred_protect { 248 cbor.push(TEXT_11); 249 cbor.extend_from_slice(b"credProtect".as_slice()); 250 if protect >= 24 { 251 cbor.push(24); 252 } 253 cbor.push(protect); 254 } 255 if let Some(hmac) = opts.hmac_secret { 256 cbor.push(TEXT_11); 257 cbor.extend_from_slice(b"hmac-secret".as_slice()); 258 cbor.push(if hmac { SIMPLE_TRUE } else { SIMPLE_FALSE }); 259 } 260 if let Some(pin) = opts.min_pin_length { 261 cbor.push(TEXT_12); 262 cbor.extend_from_slice(b"minPinLength".as_slice()); 263 if pin >= 24 { 264 cbor.push(24); 265 } 266 cbor.push(pin); 267 } 268 if let Some(mc) = opts.hmac_secret_mc { 269 cbor.push(TEXT_14); 270 cbor.extend_from_slice(b"hmac-secret-mc".as_slice()); 271 match mc.len() { 272 len @ ..=23 => { 273 // `as` is clearly OK. 274 cbor.push(BYTES | len as u8); 275 } 276 len @ 24..=255 => { 277 cbor.push(BYTES_INFO_24); 278 // `as` is clearly OK. 279 cbor.push(len as u8); 280 } 281 _ => panic!( 282 "AuthExtOptions does not allow hmac_secret_mc to have length greater than 255" 283 ), 284 } 285 cbor.extend_from_slice(mc); 286 } 287 cbor 288 } 289 #[expect(clippy::panic_in_result_fn, reason = "not a problem for a test")] 290 #[expect(clippy::shadow_unrelated, reason = "struct destructuring is prefered")] 291 #[expect(clippy::too_many_lines, reason = "a lot to test")] 292 #[test] 293 fn auth_ext() -> Result<(), AuthenticatorExtensionOutputErr> { 294 let mut opts = generate_auth_extensions(&AuthExtOptions { 295 cred_protect: None, 296 hmac_secret: None, 297 min_pin_length: None, 298 hmac_secret_mc: None, 299 }); 300 let CborSuccess { value, remaining } = 301 AuthenticatorExtensionOutput::from_cbor(opts.as_slice())?; 302 assert!(remaining.is_empty()); 303 assert!(value.missing()); 304 opts = generate_auth_extensions(&AuthExtOptions { 305 cred_protect: None, 306 hmac_secret: None, 307 min_pin_length: None, 308 hmac_secret_mc: Some([0; 48].as_slice()), 309 }); 310 assert!( 311 AuthenticatorExtensionOutput::from_cbor(opts.as_slice()).map_or_else( 312 |e| matches!(e, AuthenticatorExtensionOutputErr::Missing), 313 |_| false, 314 ) 315 ); 316 opts = generate_auth_extensions(&AuthExtOptions { 317 cred_protect: None, 318 hmac_secret: Some(true), 319 min_pin_length: None, 320 hmac_secret_mc: Some([0; 48].as_slice()), 321 }); 322 let CborSuccess { value, remaining } = 323 AuthenticatorExtensionOutput::from_cbor(opts.as_slice())?; 324 assert!(remaining.is_empty()); 325 assert!( 326 matches!(value.cred_protect, CredentialProtectionPolicy::None) 327 && matches!(value.hmac_secret, HmacSecret::One) 328 && value.min_pin_length.is_none() 329 ); 330 opts = generate_auth_extensions(&AuthExtOptions { 331 cred_protect: None, 332 hmac_secret: Some(false), 333 min_pin_length: None, 334 hmac_secret_mc: Some([0; 48].as_slice()), 335 }); 336 assert!( 337 AuthenticatorExtensionOutput::from_cbor(opts.as_slice()).map_or_else( 338 |e| matches!(e, AuthenticatorExtensionOutputErr::Missing), 339 |_| false, 340 ) 341 ); 342 opts = generate_auth_extensions(&AuthExtOptions { 343 cred_protect: None, 344 hmac_secret: Some(true), 345 min_pin_length: None, 346 hmac_secret_mc: Some([0; 49].as_slice()), 347 }); 348 assert!( 349 AuthenticatorExtensionOutput::from_cbor(opts.as_slice()).map_or_else( 350 |e| matches!(e, AuthenticatorExtensionOutputErr::HmacSecretMcValue), 351 |_| false, 352 ) 353 ); 354 opts = generate_auth_extensions(&AuthExtOptions { 355 cred_protect: None, 356 hmac_secret: Some(true), 357 min_pin_length: None, 358 hmac_secret_mc: Some([0; 23].as_slice()), 359 }); 360 assert!( 361 AuthenticatorExtensionOutput::from_cbor(opts.as_slice()).map_or_else( 362 |e| matches!(e, AuthenticatorExtensionOutputErr::HmacSecretMcType), 363 |_| false, 364 ) 365 ); 366 opts = generate_auth_extensions(&AuthExtOptions { 367 cred_protect: Some(1), 368 hmac_secret: Some(true), 369 min_pin_length: Some(5), 370 hmac_secret_mc: Some([0; 48].as_slice()), 371 }); 372 let CborSuccess { value, remaining } = 373 AuthenticatorExtensionOutput::from_cbor(opts.as_slice())?; 374 assert!(remaining.is_empty()); 375 assert!( 376 matches!( 377 value.cred_protect, 378 CredentialProtectionPolicy::UserVerificationOptional 379 ) && matches!(value.hmac_secret, HmacSecret::One) 380 && value 381 .min_pin_length 382 .is_some_and(|pin| pin == FourToSixtyThree::Five) 383 ); 384 opts = generate_auth_extensions(&AuthExtOptions { 385 cred_protect: Some(0), 386 hmac_secret: None, 387 min_pin_length: None, 388 hmac_secret_mc: None, 389 }); 390 assert!( 391 AuthenticatorExtensionOutput::from_cbor(opts.as_slice()).map_or_else( 392 |e| matches!(e, AuthenticatorExtensionOutputErr::CredProtectValue), 393 |_| false, 394 ) 395 ); 396 opts = generate_auth_extensions(&AuthExtOptions { 397 cred_protect: None, 398 hmac_secret: None, 399 min_pin_length: Some(3), 400 hmac_secret_mc: None, 401 }); 402 assert!( 403 AuthenticatorExtensionOutput::from_cbor(opts.as_slice()).map_or_else( 404 |e| matches!(e, AuthenticatorExtensionOutputErr::MinPinLengthValue), 405 |_| false, 406 ) 407 ); 408 opts = generate_auth_extensions(&AuthExtOptions { 409 cred_protect: None, 410 hmac_secret: None, 411 min_pin_length: Some(64), 412 hmac_secret_mc: None, 413 }); 414 assert!( 415 AuthenticatorExtensionOutput::from_cbor(opts.as_slice()).map_or_else( 416 |e| matches!(e, AuthenticatorExtensionOutputErr::MinPinLengthValue), 417 |_| false, 418 ) 419 ); 420 Ok(()) 421 }