From 660fdc953c984ad4a8a953dc3142cd6639026662 Mon Sep 17 00:00:00 2001 From: Alfie Fresta Date: Sat, 20 Jun 2026 12:29:32 +0100 Subject: [PATCH] chore(api): mark growable public response and spec types non_exhaustive Add #[non_exhaustive] to CTAP2 command, transport, attestation and biometric enums plus the get-info, make-credential, get-assertion, bio-enrollment, credential-management, client-pin and large-blobs response structs so adding fields or variants stays backward compatible. Seal the test-only dummy() constructors out of the public API. --- .../src/ops/webauthn/make_credential.rs | 3 ++- libwebauthn/src/proto/ctap1/model.rs | 2 +- libwebauthn/src/proto/ctap2/model.rs | 26 +++++++++++++++++-- .../src/proto/ctap2/model/bio_enrollment.rs | 3 +++ .../src/proto/ctap2/model/client_pin.rs | 1 + .../ctap2/model/credential_management.rs | 1 + .../src/proto/ctap2/model/get_assertion.rs | 3 +++ libwebauthn/src/proto/ctap2/model/get_info.rs | 1 + .../src/proto/ctap2/model/large_blobs.rs | 1 + .../src/proto/ctap2/model/make_credential.rs | 2 ++ 10 files changed, 39 insertions(+), 4 deletions(-) diff --git a/libwebauthn/src/ops/webauthn/make_credential.rs b/libwebauthn/src/ops/webauthn/make_credential.rs index fc4cc4c3..b78e95ae 100644 --- a/libwebauthn/src/ops/webauthn/make_credential.rs +++ b/libwebauthn/src/ops/webauthn/make_credential.rs @@ -652,7 +652,8 @@ pub struct MakeCredentialsRequestExtensions { pub type MakeCredentialsResponseExtensions = Ctap2MakeCredentialsResponseExtensions; impl MakeCredentialRequest { - pub fn dummy() -> Self { + #[cfg(test)] + pub(crate) fn dummy() -> Self { Self { challenge: Vec::new(), origin: "example.org".to_owned(), diff --git a/libwebauthn/src/proto/ctap1/model.rs b/libwebauthn/src/proto/ctap1/model.rs index fd1d08d5..d10fcee4 100644 --- a/libwebauthn/src/proto/ctap1/model.rs +++ b/libwebauthn/src/proto/ctap1/model.rs @@ -86,7 +86,7 @@ impl Ctap1RegisterRequest { } } - pub fn dummy(timeout: Duration) -> Self { + pub(crate) fn dummy(timeout: Duration) -> Self { Ctap1RegisterRequest { version: Ctap1Version::U2fV2, app_id_hash: vec![0; 32], diff --git a/libwebauthn/src/proto/ctap2/model.rs b/libwebauthn/src/proto/ctap2/model.rs index d52643fd..3fde3cf0 100644 --- a/libwebauthn/src/proto/ctap2/model.rs +++ b/libwebauthn/src/proto/ctap2/model.rs @@ -50,8 +50,29 @@ pub use credential_management::{ mod large_blobs; pub use large_blobs::{Ctap2LargeBlobsRequest, Ctap2LargeBlobsResponse}; +/// CTAP2 command codes; `#[non_exhaustive]` so consumers handle unknown variants. +/// +/// ```compile_fail +/// use libwebauthn::proto::ctap2::Ctap2CommandCode; +/// let code = Ctap2CommandCode::AuthenticatorGetInfo; +/// let _value: u8 = match code { +/// Ctap2CommandCode::AuthenticatorMakeCredential => 0x01, +/// Ctap2CommandCode::AuthenticatorGetAssertion => 0x02, +/// Ctap2CommandCode::AuthenticatorGetInfo => 0x04, +/// Ctap2CommandCode::AuthenticatorClientPin => 0x06, +/// Ctap2CommandCode::AuthenticatorGetNextAssertion => 0x08, +/// Ctap2CommandCode::AuthenticatorBioEnrollment => 0x09, +/// Ctap2CommandCode::AuthenticatorBioEnrollmentPreview => 0x40, +/// Ctap2CommandCode::AuthenticatorCredentialManagement => 0x0A, +/// Ctap2CommandCode::AuthenticatorCredentialManagementPreview => 0x41, +/// Ctap2CommandCode::AuthenticatorSelection => 0x0B, +/// Ctap2CommandCode::AuthenticatorLargeBlobs => 0x0C, +/// Ctap2CommandCode::AuthenticatorConfig => 0x0D, +/// }; +/// ``` #[derive(Debug, IntoPrimitive, TryFromPrimitive, Copy, Clone, PartialEq, Serialize_repr)] #[repr(u8)] +#[non_exhaustive] pub enum Ctap2CommandCode { AuthenticatorMakeCredential = 0x01, AuthenticatorGetAssertion = 0x02, @@ -78,7 +99,7 @@ pub struct Ctap2PublicKeyCredentialRpEntity { } impl Ctap2PublicKeyCredentialRpEntity { - pub fn dummy() -> Self { + pub(crate) fn dummy() -> Self { Self { id: String::from(".dummy"), name: Some(String::from(".dummy")), @@ -109,7 +130,7 @@ pub struct Ctap2PublicKeyCredentialUserEntity { } impl Ctap2PublicKeyCredentialUserEntity { - pub fn dummy() -> Self { + pub(crate) fn dummy() -> Self { Self { id: ByteBuf::from([1]), name: Some(String::from("dummy")), @@ -150,6 +171,7 @@ pub enum Ctap2PublicKeyCredentialType { /// AuthenticatorTransport from a credential descriptor. Unknown values are kept in `Other` so they pass through unchanged. #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] #[serde(from = "String", into = "String")] +#[non_exhaustive] pub enum Ctap2Transport { Ble, Nfc, diff --git a/libwebauthn/src/proto/ctap2/model/bio_enrollment.rs b/libwebauthn/src/proto/ctap2/model/bio_enrollment.rs index b7191973..b1ff9744 100644 --- a/libwebauthn/src/proto/ctap2/model/bio_enrollment.rs +++ b/libwebauthn/src/proto/ctap2/model/bio_enrollment.rs @@ -68,6 +68,7 @@ pub struct Ctap2BioEnrollmentParams { } #[derive(Debug, Default, Clone, DeserializeIndexed)] +#[non_exhaustive] pub struct Ctap2BioEnrollmentResponse { // modality (0x01) Unsigned Integer Optional The user verification modality. #[serde(skip_serializing_if = "Option::is_none")] @@ -145,12 +146,14 @@ pub enum Ctap2LastEnrollmentSampleStatus { #[repr(u64)] #[derive(Debug, Clone, FromPrimitive, PartialEq, Serialize_repr, Deserialize_repr)] +#[non_exhaustive] pub enum Ctap2BioEnrollmentModality { Fingerprint = 0x01, // Fingerprint was too high. } #[repr(u64)] #[derive(Debug, Clone, FromPrimitive, PartialEq, Serialize_repr, Deserialize_repr)] +#[non_exhaustive] pub enum Ctap2BioEnrollmentFingerprintKind { Touch = 0x01, Swipe = 0x02, diff --git a/libwebauthn/src/proto/ctap2/model/client_pin.rs b/libwebauthn/src/proto/ctap2/model/client_pin.rs index 80edad87..55f03ba1 100644 --- a/libwebauthn/src/proto/ctap2/model/client_pin.rs +++ b/libwebauthn/src/proto/ctap2/model/client_pin.rs @@ -224,6 +224,7 @@ pub enum Ctap2PinUvAuthProtocolCommand { #[cfg_attr(test, derive(SerializeIndexed))] #[derive(Debug, Clone, Default, DeserializeIndexed)] +#[non_exhaustive] pub struct Ctap2ClientPinResponse { /// keyAgreement (0x01) #[serde(skip_serializing_if = "Option::is_none")] diff --git a/libwebauthn/src/proto/ctap2/model/credential_management.rs b/libwebauthn/src/proto/ctap2/model/credential_management.rs index ea2a05d6..ae053c07 100644 --- a/libwebauthn/src/proto/ctap2/model/credential_management.rs +++ b/libwebauthn/src/proto/ctap2/model/credential_management.rs @@ -89,6 +89,7 @@ pub struct Ctap2CredentialManagementParams { } #[derive(Debug, Default, Clone, DeserializeIndexed)] +#[non_exhaustive] pub struct Ctap2CredentialManagementResponse { // existingResidentCredentialsCount (0x01) Unsigned Integer Number of existing discoverable credentials present on the authenticator. #[serde(skip_serializing_if = "Option::is_none")] diff --git a/libwebauthn/src/proto/ctap2/model/get_assertion.rs b/libwebauthn/src/proto/ctap2/model/get_assertion.rs index 08c60fa6..b378f6d3 100644 --- a/libwebauthn/src/proto/ctap2/model/get_assertion.rs +++ b/libwebauthn/src/proto/ctap2/model/get_assertion.rs @@ -92,6 +92,7 @@ pub struct AppleAnonymousAttestationStmt { #[derive(Debug, Clone, Deserialize, Serialize)] #[serde(untagged)] +#[non_exhaustive] pub enum Ctap2AttestationStatement { PackedOrAndroid(PackedAttestationStmt), Tpm(TpmAttestationStmt), @@ -512,6 +513,7 @@ pub struct CalculatedHMACGetSecretInput { } #[derive(Debug, Clone, DeserializeIndexed)] +#[non_exhaustive] pub struct Ctap2GetAssertionResponse { #[serde(skip_serializing_if = "Option::is_none")] #[serde(index = 0x01)] @@ -687,6 +689,7 @@ impl Ctap2GetAssertionResponse { #[derive(Debug, Default, Clone, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] +#[non_exhaustive] pub struct Ctap2GetAssertionResponseExtensions { // Stored credBlob #[serde(default, skip_serializing_if = "Option::is_none", with = "serde_bytes")] diff --git a/libwebauthn/src/proto/ctap2/model/get_info.rs b/libwebauthn/src/proto/ctap2/model/get_info.rs index b1f61fc9..1c4aeff0 100644 --- a/libwebauthn/src/proto/ctap2/model/get_info.rs +++ b/libwebauthn/src/proto/ctap2/model/get_info.rs @@ -10,6 +10,7 @@ use super::{Ctap2CredentialType, Ctap2UserVerificationOperation}; #[cfg_attr(test, derive(SerializeIndexed))] #[derive(Debug, Clone, DeserializeIndexed, Default)] +#[non_exhaustive] pub struct Ctap2GetInfoResponse { /// versions (0x01) #[serde(index = 0x01)] diff --git a/libwebauthn/src/proto/ctap2/model/large_blobs.rs b/libwebauthn/src/proto/ctap2/model/large_blobs.rs index ec8d121e..1bf25232 100644 --- a/libwebauthn/src/proto/ctap2/model/large_blobs.rs +++ b/libwebauthn/src/proto/ctap2/model/large_blobs.rs @@ -88,6 +88,7 @@ impl Ctap2LargeBlobsRequest { #[cfg_attr(test, derive(SerializeIndexed))] #[derive(Debug, Default, Clone, DeserializeIndexed)] +#[non_exhaustive] pub struct Ctap2LargeBlobsResponse { #[serde(skip_serializing_if = "Option::is_none")] #[serde(index = 0x01)] diff --git a/libwebauthn/src/proto/ctap2/model/make_credential.rs b/libwebauthn/src/proto/ctap2/model/make_credential.rs index bb715926..429b94ea 100644 --- a/libwebauthn/src/proto/ctap2/model/make_credential.rs +++ b/libwebauthn/src/proto/ctap2/model/make_credential.rs @@ -357,6 +357,7 @@ impl Ctap2MakeCredentialsRequestExtensions { } #[derive(Debug, Clone, DeserializeIndexed)] +#[non_exhaustive] pub struct Ctap2MakeCredentialResponse { #[serde(index = 0x01)] pub format: String, @@ -469,6 +470,7 @@ impl Ctap2UserVerifiableRequest for Ctap2MakeCredentialRequest { #[derive(Debug, Default, Clone, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] +#[non_exhaustive] pub struct Ctap2MakeCredentialsResponseExtensions { // If storing credBlob was successful #[serde(default, skip_serializing_if = "Option::is_none")]