Skip to content
Merged
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
90 changes: 72 additions & 18 deletions libwebauthn/src/fido.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
use byteorder::{BigEndian, ReadBytesExt, WriteBytesExt};
use cosey::PublicKey;
use serde::{
de::{DeserializeOwned, Error as DesError, Visitor},
Deserialize, Deserializer, Serialize,
Expand Down Expand Up @@ -62,7 +61,12 @@ bitflags! {
pub struct AttestedCredentialData {
pub aaguid: [u8; 16],
pub credential_id: Vec<u8>,
pub credential_public_key: PublicKey,
/// Credential public key in COSE_Key CBOR encoding (RFC 9052).
///
/// Stored verbatim so the authenticator data signature over it
/// remains valid for relying-party verification. The platform does
/// not crypto-validate this key.
pub credential_public_key: Vec<u8>,
}

impl From<&AttestedCredentialData> for Ctap2PublicKeyCredentialDescriptor {
Expand Down Expand Up @@ -120,16 +124,7 @@ where
Error::Platform(PlatformError::InvalidDeviceResponse)
})?;
res.extend(&att_data.credential_id);
let cose_encoded_public_key =
cbor::to_vec(&att_data.credential_public_key)
.map_err(|e| {
error!(
%e,
"Failed to create AuthenticatorData output vec at attested_credential.credential_public_key"
);
Error::Platform(PlatformError::InvalidDeviceResponse)
})?;
res.extend(cose_encoded_public_key);
res.extend(&att_data.credential_public_key);
}

if self.extensions.is_some() || self.flags.contains(AuthenticatorDataFlags::EXTENSION_DATA)
Expand Down Expand Up @@ -229,8 +224,20 @@ impl<'de, T: DeserializeOwned> Deserialize<'de> for AuthenticatorData<T> {
DesError::custom(format!("failed to read credential_id: {e}"))
})?;

let credential_public_key: PublicKey =
// Capture the COSE_Key bytes verbatim so the RP's
// signature check over authData stays valid. Parse
// through cbor::Value only to advance the cursor by
// exactly one CBOR item.
let cose_start = cursor.position() as usize;
let _: cbor::Value =
cbor::from_cursor(&mut cursor).map_err(DesError::custom)?;
let cose_end = cursor.position() as usize;
let credential_public_key = data
.get(cose_start..cose_end)
.ok_or_else(|| {
DesError::custom("cursor reported COSE_Key span outside authData")
})?
.to_vec();

Some(AttestedCredentialData {
aaguid,
Expand Down Expand Up @@ -273,7 +280,6 @@ impl<'de, T: DeserializeOwned> Deserialize<'de> for AuthenticatorData<T> {

#[cfg(test)]
mod tests {
use cosey::{Bytes, Ed25519PublicKey};
use serde_bytes::ByteBuf;

use crate::proto::ctap2::cbor;
Expand Down Expand Up @@ -301,9 +307,6 @@ mod tests {
];
let credential_id = vec![0x01, 0x01, 0x03, 0x03, 0x05, 0x05, 0x07, 0x07];
let pub_key_bytes = b"]\"\xff\xc5\x932x(\xd6-:1\xbb}\x8c$7\xf1&\xd4\xb4&\x02\x02\xa3\xd9\xe2\xba1\x1f\xec\xba";
let credential_public_key = cosey::PublicKey::Ed25519Key(Ed25519PublicKey {
x: Bytes::from_slice(pub_key_bytes).unwrap(),
});
/*
* A4 # map(4)
* 01 # unsigned(1) kty
Expand All @@ -321,7 +324,7 @@ mod tests {
let attested_credential = AttestedCredentialData {
aaguid,
credential_id: credential_id.clone(),
credential_public_key,
credential_public_key: cose_bytes.clone(),
};
type T = String;
let extensions: T = "test cbor serializable thing".to_string();
Expand Down Expand Up @@ -377,4 +380,55 @@ mod tests {
);
assert_eq!(extensions, auth_data_reparsed.extensions.unwrap());
}

#[test]
fn auth_data_parses_with_non_p256_credential_public_key() {
// Build a synthetic COSE_Key for RS256 (kty=3 RSA, alg=-257, n, e).
// Previous versions of libwebauthn couldn't parse authData carrying
// anything other than P-256 or Ed25519 because cosey::PublicKey is
// a closed enum. With opaque byte storage this now round-trips.
use crate::proto::ctap2::cose;
use serde_cbor_2::Value;

let rsa_cose = cbor::to_vec(&Value::Map(
[
(Value::Integer(1), Value::Integer(3)),
(Value::Integer(3), Value::Integer(-257)),
(Value::Integer(-1), Value::Bytes(vec![0xAA; 256])),
(Value::Integer(-2), Value::Bytes(vec![0x01, 0x00, 0x01])),
]
.into_iter()
.collect(),
))
.unwrap();

let rp_id_hash = [0x77u8; 32];
let aaguid = [0x42u8; 16];
let credential_id = vec![0xC1, 0xC2, 0xC3];
let attested_credential = AttestedCredentialData {
aaguid,
credential_id: credential_id.clone(),
credential_public_key: rsa_cose.clone(),
};
type T = String;
let auth_data: AuthenticatorData<T> = AuthenticatorData {
rp_id_hash,
flags: AuthenticatorDataFlags::USER_PRESENT
| AuthenticatorDataFlags::ATTESTED_CREDENTIALS,
signature_count: 1,
attested_credential: Some(attested_credential),
extensions: None,
};

let bytes = auth_data.to_response_bytes().unwrap();
let wrapped = cbor::to_vec(&ByteBuf::from(bytes)).unwrap();
let parsed: AuthenticatorData<T> = cbor::from_slice(&wrapped).unwrap();

let credential = parsed.attested_credential.expect("AT flag was set");
assert_eq!(credential.credential_public_key, rsa_cose);
assert_eq!(
cose::read_alg(&credential.credential_public_key).unwrap(),
crate::proto::ctap2::Ctap2COSEAlgorithmIdentifier::RS256
);
}
}
4 changes: 2 additions & 2 deletions libwebauthn/src/management/credential_management.rs
Original file line number Diff line number Diff line change
Expand Up @@ -169,7 +169,7 @@ where
let cred = Ctap2CredentialData::new(
unwrap_field!(resp.user),
unwrap_field!(resp.credential_id),
unwrap_field!(resp.public_key),
unwrap_field!(resp.public_key).into_bytes(),
unwrap_field!(resp.cred_protect),
resp.large_blob_key.map(|x| x.into_vec()),
);
Expand Down Expand Up @@ -202,7 +202,7 @@ where
let cred = Ctap2CredentialData::new(
unwrap_field!(resp.user),
unwrap_field!(resp.credential_id),
unwrap_field!(resp.public_key),
unwrap_field!(resp.public_key).into_bytes(),
unwrap_field!(resp.cred_protect),
resp.large_blob_key.map(|x| x.into_vec()),
);
Expand Down
2 changes: 1 addition & 1 deletion libwebauthn/src/ops/u2f.rs
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,7 @@ impl UpgradableResponse<MakeCredentialResponse, MakeCredentialRequest> for Regis
let attested_cred_data = AttestedCredentialData {
aaguid: [0u8; 16], // aaguid zeros
credential_id: self.key_handle.clone(),
credential_public_key: cose_public_key,
credential_public_key: cose_encoded_public_key,
};

// Initialize authenticatorData:
Expand Down
63 changes: 23 additions & 40 deletions libwebauthn/src/ops/webauthn/make_credential.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,8 @@ use crate::{
proto::{
ctap1::{Ctap1RegisteredKey, Ctap1Version},
ctap2::{
cbor, Ctap2AttestationStatement, Ctap2COSEAlgorithmIdentifier, Ctap2CredentialType,
Ctap2GetInfoResponse, Ctap2MakeCredentialsResponseExtensions,
cbor, cose, Ctap2AttestationStatement, Ctap2COSEAlgorithmIdentifier,
Ctap2CredentialType, Ctap2GetInfoResponse, Ctap2MakeCredentialsResponseExtensions,
Ctap2PublicKeyCredentialDescriptor, Ctap2PublicKeyCredentialRpEntity,
Ctap2PublicKeyCredentialUserEntity,
},
Expand Down Expand Up @@ -67,42 +67,33 @@ impl WebAuthnIDLResponse for MakeCredentialResponse {
&self,
request: &Self::Context,
) -> Result<Self::IdlModel, ResponseSerializationError> {
// Get credential ID from attested credential data
let credential_id_bytes = self
// The AT flag MUST be set on makeCredential responses per CTAP 2.2 §6.1.
let attested = self
.authenticator_data
.attested_credential
.as_ref()
.map(|cred| cred.credential_id.clone())
.unwrap_or_default();
.ok_or_else(|| {
ResponseSerializationError::AuthenticatorDataError(
"missing attested credential data".into(),
)
})?;

let id = base64_url::encode(&credential_id_bytes);
let raw_id = Base64UrlString::from(credential_id_bytes);
let id = base64_url::encode(&attested.credential_id);
let raw_id = Base64UrlString::from(attested.credential_id.clone());

// Serialize authenticator data
let authenticator_data_bytes = self
.authenticator_data
.to_response_bytes()
.map_err(|e| ResponseSerializationError::AuthenticatorDataError(e.to_string()))?;

// Get public key algorithm from attested credential data
let public_key_algorithm = self
.authenticator_data
.attested_credential
.as_ref()
.map(|cred| Self::get_public_key_algorithm(&cred.credential_public_key))
.unwrap_or_else(|| i64::from(Ctap2COSEAlgorithmIdentifier::ES256));
let public_key_algorithm = i64::from(
cose::read_alg(&attested.credential_public_key)
.map_err(|e| ResponseSerializationError::PublicKeyError(e.to_string()))?,
);

// Serialize public key to COSE key format
let public_key = self
.authenticator_data
.attested_credential
.as_ref()
.map(|cred| {
cbor::to_vec(&cred.credential_public_key)
.map(Base64UrlString::from)
.map_err(|e| ResponseSerializationError::PublicKeyError(e.to_string()))
})
.transpose()?;
let public_key = Some(Base64UrlString::from(
attested.credential_public_key.clone(),
));

// Build attestation object (CBOR map with authData, fmt, attStmt)
let attestation_object_bytes = self.build_attestation_object(&authenticator_data_bytes)?;
Expand Down Expand Up @@ -132,16 +123,6 @@ impl WebAuthnIDLResponse for MakeCredentialResponse {
}

impl MakeCredentialResponse {
/// Get the COSE algorithm identifier from the public key variant
fn get_public_key_algorithm(key: &cosey::PublicKey) -> i64 {
match key {
cosey::PublicKey::P256Key(_) => i64::from(Ctap2COSEAlgorithmIdentifier::ES256),
cosey::PublicKey::EcdhEsHkdf256Key(_) => -25, // ECDH-ES + HKDF-256
cosey::PublicKey::Ed25519Key(_) => i64::from(Ctap2COSEAlgorithmIdentifier::EDDSA),
cosey::PublicKey::TotpKey(_) => 0, // No standard algorithm for TOTP
}
}

fn build_attestation_object(
&self,
authenticator_data_bytes: &[u8],
Expand Down Expand Up @@ -1083,16 +1064,18 @@ mod tests {
let credential_id = vec![0x01, 0x02, 0x03, 0x04];
let aaguid = [0u8; 16];

// Create a P256 public key for testing
let public_key = cosey::PublicKey::P256Key(cosey::P256PublicKey {
// Minimal COSE_Key for a P-256 ES256 credential, used as opaque
// bytes by the test harness.
let cose_public_key = cosey::PublicKey::P256Key(cosey::P256PublicKey {
x: Bytes::from_slice(&[0u8; 32]).unwrap(),
y: Bytes::from_slice(&[0u8; 32]).unwrap(),
});
let credential_public_key = cbor::to_vec(&cose_public_key).unwrap();

let attested_credential = AttestedCredentialData {
aaguid,
credential_id,
credential_public_key: public_key,
credential_public_key,
};

let authenticator_data = AuthenticatorData {
Expand Down
Loading
Loading