ctap_types/ctap2/
make_credential.rs

1use crate::Vec;
2
3use serde::{Deserialize, Serialize};
4use serde_bytes::ByteArray;
5use serde_indexed::{DeserializeIndexed, SerializeIndexed};
6
7use super::{
8    AttestationFormatsPreference, AttestationStatement, AttestationStatementFormat,
9    AuthenticatorOptions, Error,
10};
11use crate::ctap2::credential_management::CredentialProtectionPolicy;
12use crate::webauthn::*;
13
14impl TryFrom<u8> for CredentialProtectionPolicy {
15    type Error = super::Error;
16
17    fn try_from(value: u8) -> Result<Self, Self::Error> {
18        Ok(match value {
19            1 => CredentialProtectionPolicy::Optional,
20            2 => CredentialProtectionPolicy::OptionalWithCredentialIdList,
21            3 => CredentialProtectionPolicy::Required,
22            _ => return Err(Self::Error::InvalidParameter),
23        })
24    }
25}
26
27#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
28#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
29#[non_exhaustive]
30pub struct Extensions {
31    #[serde(rename = "credProtect")]
32    #[serde(skip_serializing_if = "Option::is_none")]
33    pub cred_protect: Option<u8>,
34
35    #[serde(rename = "hmac-secret")]
36    #[serde(skip_serializing_if = "Option::is_none")]
37    pub hmac_secret: Option<bool>,
38
39    // See https://fidoalliance.org/specs/fido-v2.1-ps-20210615/fido-client-to-authenticator-protocol-v2.1-ps-20210615.html#sctn-largeBlobKey-extension
40    #[serde(rename = "largeBlobKey")]
41    #[serde(skip_serializing_if = "Option::is_none")]
42    pub large_blob_key: Option<bool>,
43
44    #[cfg(feature = "third-party-payment")]
45    #[serde(rename = "thirdPartyPayment")]
46    #[serde(skip_serializing_if = "Option::is_none")]
47    pub third_party_payment: Option<bool>,
48}
49
50#[derive(Clone, Debug, Eq, PartialEq, DeserializeIndexed)]
51#[non_exhaustive]
52#[serde_indexed(offset = 1)]
53pub struct Request<'a> {
54    pub client_data_hash: &'a serde_bytes::Bytes,
55    pub rp: PublicKeyCredentialRpEntity,
56    pub user: PublicKeyCredentialUserEntity,
57    pub pub_key_cred_params: FilteredPublicKeyCredentialParameters,
58    #[serde(skip_serializing_if = "Option::is_none")]
59    pub exclude_list: Option<Vec<PublicKeyCredentialDescriptorRef<'a>, 16>>,
60    #[serde(skip_serializing_if = "Option::is_none")]
61    pub extensions: Option<Extensions>,
62    #[serde(skip_serializing_if = "Option::is_none")]
63    pub options: Option<AuthenticatorOptions>,
64    #[serde(skip_serializing_if = "Option::is_none")]
65    pub pin_auth: Option<&'a serde_bytes::Bytes>,
66    #[serde(skip_serializing_if = "Option::is_none")]
67    pub pin_protocol: Option<u32>,
68    #[serde(skip_serializing_if = "Option::is_none")]
69    pub enterprise_attestation: Option<u32>,
70    #[serde(skip_serializing_if = "Option::is_none")]
71    pub attestation_formats_preference: Option<AttestationFormatsPreference>,
72}
73
74pub type AttestationObject = Response;
75
76pub type AuthenticatorData<'a> =
77    super::AuthenticatorData<'a, AttestedCredentialData<'a>, Extensions>;
78
79// NOTE: This is not CBOR, it has a custom encoding...
80// https://www.w3.org/TR/webauthn/#sec-attested-credential-data
81#[derive(Clone, Debug, Eq, PartialEq)]
82pub struct AttestedCredentialData<'a> {
83    pub aaguid: &'a [u8],
84    // this is where "unlimited non-resident keys" get stored
85    // TODO: Model as actual credential ID, with ser/de to bytes (format is up to authenticator)
86    pub credential_id: &'a [u8],
87    pub credential_public_key: &'a [u8],
88}
89
90impl<'a> super::SerializeAttestedCredentialData for AttestedCredentialData<'a> {
91    fn serialize(&self, buffer: &mut super::SerializedAuthenticatorData) -> Result<(), Error> {
92        // TODO: validate lengths of credential ID and credential public key
93        // 16 bytes, the aaguid
94        buffer
95            .extend_from_slice(self.aaguid)
96            .map_err(|_| Error::Other)?;
97        // byte length of credential ID as 16-bit unsigned big-endian integer.
98        let credential_id_len =
99            u16::try_from(self.credential_id.len()).map_err(|_| Error::Other)?;
100        buffer
101            .extend_from_slice(&credential_id_len.to_be_bytes())
102            .map_err(|_| Error::Other)?;
103        // raw bytes of credential ID
104        buffer
105            .extend_from_slice(self.credential_id)
106            .map_err(|_| Error::Other)?;
107        buffer
108            .extend_from_slice(self.credential_public_key)
109            .map_err(|_| Error::Other)?;
110        Ok(())
111    }
112}
113
114#[derive(Clone, Debug, Eq, PartialEq, SerializeIndexed)]
115#[non_exhaustive]
116#[serde_indexed(offset = 1)]
117pub struct Response {
118    pub fmt: AttestationStatementFormat,
119    pub auth_data: super::SerializedAuthenticatorData,
120    #[serde(skip_serializing_if = "Option::is_none")]
121    pub att_stmt: Option<AttestationStatement>,
122    #[serde(skip_serializing_if = "Option::is_none")]
123    pub ep_att: Option<bool>,
124    #[serde(skip_serializing_if = "Option::is_none")]
125    pub large_blob_key: Option<ByteArray<32>>,
126    #[serde(skip_serializing_if = "Option::is_none")]
127    pub unsigned_extension_outputs: Option<UnsignedExtensionOutputs>,
128}
129
130#[derive(Debug)]
131pub struct ResponseBuilder {
132    pub fmt: AttestationStatementFormat,
133    pub auth_data: super::SerializedAuthenticatorData,
134}
135
136impl ResponseBuilder {
137    #[inline(always)]
138    pub fn build(self) -> Response {
139        Response {
140            fmt: self.fmt,
141            auth_data: self.auth_data,
142            att_stmt: None,
143            ep_att: None,
144            large_blob_key: None,
145            unsigned_extension_outputs: None,
146        }
147    }
148}
149
150#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
151#[non_exhaustive]
152pub struct UnsignedExtensionOutputs {}
153
154#[cfg(test)]
155mod tests {
156    use super::*;
157    use serde_test::{assert_ser_tokens, Token};
158
159    #[test]
160    fn rp_entity_icon() {
161        // icon has been removed but must still be parsed
162        let cbor = b"\xa4\x01X \xcd\xcd\xcd\xcd\xcd\xcd\xcd\xcd\xcd\xcd\xcd\xcd\xcd\xcd\xcd\xcd\xcd\xcd\xcd\xcd\xcd\xcd\xcd\xcd\xcd\xcd\xcd\xcd\xcd\xcd\xcd\xcd\x02\xa2bidx0make_credential_relying_party_entity.example.comdiconohttp://icon.png\x03\xa2bidX \x1d\x1d\x1d\x1d\x1d\x1d\x1d\x1d\x1d\x1d\x1d\x1d\x1d\x1d\x1d\x1d\x1d\x1d\x1d\x1d\x1d\x1d\x1d\x1d\x1d\x1d\x1d\x1d\x1d\x1d\x1d\x1ddnamedAdam\x04\x81\xa2calg&dtypejpublic-key";
163        let _request: Request = cbor_smol::cbor_deserialize(cbor.as_slice()).unwrap();
164
165        // previously, we called it `url` and should still be able to deserialize it
166        let cbor = b"\xa4\x01X \xcd\xcd\xcd\xcd\xcd\xcd\xcd\xcd\xcd\xcd\xcd\xcd\xcd\xcd\xcd\xcd\xcd\xcd\xcd\xcd\xcd\xcd\xcd\xcd\xcd\xcd\xcd\xcd\xcd\xcd\xcd\xcd\x02\xa2bidx0make_credential_relying_party_entity.example.comcurlohttp://icon.png\x03\xa2bidX \x1d\x1d\x1d\x1d\x1d\x1d\x1d\x1d\x1d\x1d\x1d\x1d\x1d\x1d\x1d\x1d\x1d\x1d\x1d\x1d\x1d\x1d\x1d\x1d\x1d\x1d\x1d\x1d\x1d\x1d\x1d\x1ddnamedAdam\x04\x81\xa2calg&dtypejpublic-key";
167        let _request: Request = cbor_smol::cbor_deserialize(cbor.as_slice()).unwrap();
168    }
169
170    #[test]
171    fn test_serde_attestation_statement_format() {
172        let formats = [
173            (AttestationStatementFormat::None, "none"),
174            (AttestationStatementFormat::Packed, "packed"),
175        ];
176        for (format, s) in formats {
177            assert_ser_tokens(&format, &[Token::BorrowedStr(s)]);
178        }
179    }
180}