Skip to content

Commit 70cb6a6

Browse files
feat(webauthn): emit SubjectPublicKeyInfo from getPublicKey() (#230)
WebAuthn L3 §5.2.1.1 says `AuthenticatorAttestationResponse.getPublicKey()` returns DER-encoded SubjectPublicKeyInfo for credentials using ES256, EdDSA Ed25519 and RS256, and null for any algorithm the user agent does not implement. libwebauthn was emitting raw COSE bytes there instead, which relying parties could not feed into `SubtleCrypto` or standard X.509 parsers. A new converter produces SPKI for the WebAuthn L3 floor plus ESP256, which is equivalent to ES256 per RFC 9864. Algorithms outside the supported set return null per spec. Malformed keys for understood algorithms surface as an error. The conversion uses the `spki` and `der` crates from RustCrypto, both already in the transitive dependency tree via `p256`, so no new external runtime crates land. Adding new algorithm support later is a small per-algorithm addition. Stacked on top of #229.
1 parent 9e48445 commit 70cb6a6

4 files changed

Lines changed: 439 additions & 27 deletions

File tree

Cargo.lock

Lines changed: 15 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

libwebauthn/Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,8 @@ rand = "0.8.5"
6969
p256 = { version = "0.13.2", features = ["ecdh", "arithmetic", "serde"] }
7070
heapless = "0.7"
7171
cosey = "0.3.2"
72+
spki = { version = "0.7", default-features = false, features = ["alloc"] }
73+
der = { version = "0.7", default-features = false, features = ["alloc", "derive", "oid"] }
7274
aes = "0.8.2"
7375
hmac = "0.12.1"
7476
cbc = { version = "0.1", features = ["alloc"] }

libwebauthn/src/ops/webauthn/make_credential.rs

Lines changed: 35 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -91,9 +91,12 @@ impl WebAuthnIDLResponse for MakeCredentialResponse {
9191
.map_err(|e| ResponseSerializationError::PublicKeyError(e.to_string()))?,
9292
);
9393

94-
let public_key = Some(Base64UrlString::from(
95-
attested.credential_public_key.clone(),
96-
));
94+
// SubjectPublicKeyInfo per WebAuthn L3 §5.2.1.1. `to_spki` returns
95+
// `Ok(None)` for algorithms libwebauthn does not implement, which
96+
// surfaces as `getPublicKey() === null` to the relying party.
97+
let public_key = cose::to_spki(&attested.credential_public_key)
98+
.map_err(|e| ResponseSerializationError::PublicKeyError(e.to_string()))?
99+
.map(Base64UrlString::from);
97100

98101
// Build attestation object (CBOR map with authData, fmt, attStmt)
99102
let attestation_object_bytes = self.build_attestation_object(&authenticator_data_bytes)?;
@@ -1161,6 +1164,35 @@ mod tests {
11611164
assert!(model.response.transports.is_empty());
11621165
}
11631166

1167+
#[test]
1168+
fn test_response_emits_spki_for_es256() {
1169+
// The test fixture builds an ES256 P-256 credential, so getPublicKey()
1170+
// must return DER-encoded SubjectPublicKeyInfo per WebAuthn L3 §5.2.1.1.
1171+
// The SPKI for ES256 starts with the SEQUENCE / SEQUENCE / OID prefix
1172+
// 30 59 30 13 06 07 2A 86 48 CE 3D 02 01 (id-ecPublicKey), followed
1173+
// by the secp256r1 OID and the uncompressed point.
1174+
let response = create_test_response();
1175+
let request = create_test_request();
1176+
let model = response.to_idl_model(&request).unwrap();
1177+
1178+
let public_key_bytes = model
1179+
.response
1180+
.public_key
1181+
.expect("ES256 must produce SPKI")
1182+
.0;
1183+
assert_eq!(public_key_bytes.len(), 91, "ES256 SPKI is 91 bytes");
1184+
// SEQUENCE tag + length, then nested SEQUENCE for AlgorithmIdentifier.
1185+
assert_eq!(&public_key_bytes[..2], &[0x30, 0x59]);
1186+
// id-ecPublicKey OID: 1.2.840.10045.2.1
1187+
let id_ec_public_key = [0x06, 0x07, 0x2A, 0x86, 0x48, 0xCE, 0x3D, 0x02, 0x01];
1188+
assert!(
1189+
public_key_bytes
1190+
.windows(id_ec_public_key.len())
1191+
.any(|w| w == id_ec_public_key),
1192+
"SPKI must contain id-ecPublicKey OID"
1193+
);
1194+
}
1195+
11641196
#[test]
11651197
fn test_response_attestation_object_format() {
11661198
let response = create_test_response();

0 commit comments

Comments
 (0)