sshkey_attest/
lib.rs

1//! This module contains the function that allows attestation of ssh sk keys which
2//! are created by FIDO2 devices.
3
4#![cfg_attr(docsrs, feature(doc_cfg))]
5#![deny(warnings)]
6#![warn(unused_extern_crates)]
7#![warn(missing_docs)]
8#![deny(clippy::todo)]
9#![deny(clippy::unimplemented)]
10#![deny(clippy::unwrap_used)]
11// #![deny(clippy::expect_used)]
12#![deny(clippy::panic)]
13#![deny(clippy::unreachable)]
14#![deny(clippy::await_holding_lock)]
15#![deny(clippy::needless_pass_by_value)]
16#![deny(clippy::trivially_copy_pass_by_ref)]
17
18#[macro_use]
19extern crate tracing;
20
21pub mod proto;
22
23use uuid::Uuid;
24pub use webauthn_rs_core::error::WebauthnError;
25use webauthn_rs_core::{
26    attestation::{
27        assert_packed_attest_req, validate_extension, verify_attestation_ca_chain, FidoGenCeAaguid,
28    },
29    crypto::{compute_sha256, verify_signature},
30    internals::AuthenticatorData,
31    proto::{
32        AttestationCaList, AttestationFormat, AttestationMetadata, COSEAlgorithm, COSEKey,
33        COSEKeyType, CredentialProtectionPolicy, ExtnState, ParsedAttestation,
34        ParsedAttestationData, RegisteredExtensions, Registration,
35    },
36};
37
38use nom::{
39    bytes::complete::{tag, take},
40    number::complete::be_u32,
41};
42
43use openssl::{bn, ec, nid, x509};
44
45use sshkeys::{Curve, EcdsaPublicKey, KeyType, KeyTypeKind, PublicKey, PublicKeyKind};
46
47use crate::proto::AttestedPublicKey;
48
49/// Given an attestation generated by ssh-keygen, parse and validate it's content
50/// returning an attested public key structure that contains metadata and the ssh
51/// public key.
52///
53/// You can create an attested sk key with:
54/// ```shell
55///  dd if=/dev/urandom of=/Users/username/.ssh/id_ecdsa_sk.chal bs=16 count=1
56///  ssh-keygen -t ecdsa-sk -O challenge=/Users/username/.ssh/id_ecdsa_sk.chal \\
57///    -O write-attestation=/Users/username/.ssh/id_ecdsa_sk.attest -f /Users/username/.ssh/id_ecdsa_sk
58/// ```
59///
60/// The contents of `id_ecdsa_sk.attest` and `id_ecdsa_sk.chal` correspond to the
61/// `attestation` and `challenge` parameters respectively.
62pub fn verify_fido_sk_ssh_attestation(
63    attestation: &[u8],
64    challenge: &[u8],
65    attestation_cas: &AttestationCaList,
66    danger_disable_certificate_time_checks: bool,
67) -> Result<AttestedPublicKey, WebauthnError> {
68    if attestation_cas.is_empty() {
69        return Err(WebauthnError::MissingAttestationCaList);
70    }
71
72    let alg = COSEAlgorithm::ES256;
73
74    let ssh_sk_attest = SshSkAttestation::try_from(attestation)?;
75
76    let acd = ssh_sk_attest
77        .auth_data
78        .acd
79        .as_ref()
80        .ok_or(WebauthnError::MissingAttestationCredentialData)?;
81
82    let attestation_format = AttestationFormat::Packed;
83
84    // Ssh simply uses the challenge as the client data hash.
85    let client_data_hash = compute_sha256(challenge);
86
87    trace!(?ssh_sk_attest);
88
89    let verification_data: Vec<u8> = ssh_sk_attest
90        .auth_data_bytes
91        .iter()
92        .chain(client_data_hash.iter())
93        .copied()
94        .collect();
95
96    let is_valid_signature = verify_signature(
97        alg,
98        &ssh_sk_attest.att_cert,
99        &ssh_sk_attest.sig,
100        &verification_data,
101    )?;
102
103    if !is_valid_signature {
104        return Err(WebauthnError::AttestationStatementSigInvalid);
105    }
106
107    // Verify that attestnCert meets the requirements in § 8.2.1 Packed Attestation
108    // Statement Certificate Requirements.
109    // https://w3c.github.io/webauthn/#sctn-packed-attestation-cert-requirements
110
111    assert_packed_attest_req(&ssh_sk_attest.att_cert)?;
112
113    // If attestnCert contains an extension with OID 1.3.6.1.4.1.45724.1.1.4
114    // (id-fido-gen-ce-aaguid) verify that the value of this extension matches the aaguid
115    // in authenticatorData.
116
117    validate_extension::<FidoGenCeAaguid>(&ssh_sk_attest.att_cert, &acd.aaguid)?;
118
119    // In the future if ssh changes their attest format we can provide the full chain here.
120    let att_x509 = vec![ssh_sk_attest.att_cert.clone()];
121
122    let attestation = ParsedAttestation {
123        data: ParsedAttestationData::Basic(att_x509),
124        metadata: AttestationMetadata::Packed {
125            aaguid: Uuid::from_bytes(acd.aaguid),
126        },
127    };
128
129    let ca_crt = verify_attestation_ca_chain(
130        &attestation.data,
131        attestation_cas,
132        danger_disable_certificate_time_checks,
133    )?;
134
135    // It may seem odd to unwrap the option and make this not verified at this point,
136    // but in this case because we have the ca_list and none was the result (which happens)
137    // in some cases, we need to map that through. But we need verify_attesation_ca_chain
138    // to still return these option types due to re-attestation in the future.
139    let ca_crt = ca_crt.ok_or(WebauthnError::AttestationNotVerifiable)?;
140
141    match &attestation.metadata {
142        AttestationMetadata::Packed { aaguid } | AttestationMetadata::Tpm { aaguid, .. } => {
143            // If not present, fail.
144            if !ca_crt.aaguids().contains_key(aaguid) {
145                error!(?aaguid, "aaguid not trusted by this CA");
146                return Err(WebauthnError::AttestationUntrustedAaguid);
147            }
148        }
149        _ => {
150            error!("this attestation format does not contain an aaguid and can not proceed");
151            return Err(WebauthnError::AttestationFormatMissingAaguid);
152        }
153    };
154
155    let cred_protect = match ssh_sk_attest.auth_data.extensions.cred_protect.as_ref() {
156        Some(credprotect) => {
157            if credprotect.0 == CredentialProtectionPolicy::UserVerificationRequired
158                && !ssh_sk_attest.auth_data.user_verified
159            {
160                return Err(WebauthnError::SshPublicKeyInconsistentUserVerification);
161            }
162            ExtnState::Set(credprotect.0)
163        }
164        None => ExtnState::NotRequested,
165    };
166
167    let extensions = RegisteredExtensions {
168        cred_protect,
169        ..Default::default()
170    };
171
172    // Assert that the key is not backup-eligible and not backed up.
173
174    if ssh_sk_attest.auth_data.backup_eligible || ssh_sk_attest.auth_data.backup_state {
175        error!("Fido ssh sk keys may not be backed up or backup eligible");
176        return Err(WebauthnError::SshPublicKeyBackupState);
177    }
178
179    // If attestation passes, extract the public key from the attestation.
180    //
181    // https://github.com/openssh/openssh-portable/blob/c46f6fed419167c1671e4227459e108036c760f8/ssh-sk.c#L291
182    let ck = COSEKey::try_from(&acd.credential_pk).map_err(|e| {
183        if matches!(e, WebauthnError::COSEKeyEDUnsupported) {
184            WebauthnError::SshPublicKeyEDUnsupported
185        } else {
186            e
187        }
188    })?;
189    trace!(?ck);
190
191    let pubkey = to_ssh_pubkey(&ck)?;
192
193    Ok(AttestedPublicKey {
194        pubkey,
195        extensions,
196        attestation,
197        attestation_format,
198    })
199}
200
201macro_rules! cbor_try_bytes {
202    (
203        $v:expr
204    ) => {{
205        match $v {
206            serde_cbor_2::Value::Bytes(m) => Ok(m),
207            _ => Err(WebauthnError::COSEKeyInvalidCBORValue),
208        }
209    }};
210}
211
212#[derive(Debug)]
213struct SshSkAttestation {
214    att_cert: x509::X509,
215    sig: Vec<u8>,
216    auth_data_bytes: Vec<u8>,
217    auth_data: AuthenticatorData<Registration>,
218}
219
220struct SshSkAttestationRaw<'a> {
221    // This is the x5c cbor per https://developers.yubico.com/libfido2/Manuals/fido_cred_x5c_ptr.html
222    att_cert_raw: &'a [u8],
223    // Likely a cbor slice?
224    sig_raw: &'a [u8],
225    // cbor auth data. Could just be serde slice?
226    auth_data_raw: &'a [u8],
227}
228
229impl TryFrom<&[u8]> for SshSkAttestation {
230    type Error = WebauthnError;
231
232    fn try_from(data: &[u8]) -> Result<SshSkAttestation, WebauthnError> {
233        // There doesn't seem to be much in the way of docs about the format of
234        // the ssh attestation binary, but reading the source, we see it is setup
235        // per: https://github.com/openssh/openssh-portable/blob/master/ssh-sk.c#L436
236        //
237        // Update: There are docs, but they are hard to find :(
238        // https://github.com/openssh/openssh-portable/blob/2709809fd616a0991dc18e3a58dea10fb383c3f0/PROTOCOL.u2f#L151-L174
239        let sk_raw = parse_ssh_sk_attestation(data)
240            .map_err(|e| {
241                error!(?e, "try_from parse_ssh_sk_attestation");
242                WebauthnError::ParseNOMFailure
243            })
244            // Discard the remaining bytes.
245            .map(|(_, ad)| ad)?;
246
247        // Convert raw fields to parsed ones.
248
249        let sig = sk_raw.sig_raw.to_vec();
250
251        let att_cert =
252            x509::X509::from_der(sk_raw.att_cert_raw).map_err(WebauthnError::OpenSSLError)?;
253
254        let auth_data_bytes = serde_cbor_2::from_slice(sk_raw.auth_data_raw)
255            .map_err(|e| {
256                error!(?e, "invalid auth data cbor");
257                WebauthnError::ParseNOMFailure
258            })
259            .and_then(|value| cbor_try_bytes!(value))?;
260
261        let auth_data: AuthenticatorData<Registration> =
262            AuthenticatorData::try_from(auth_data_bytes.as_slice()).map_err(|e| {
263                error!(?e, "invalid auth data structure");
264                WebauthnError::ParseNOMFailure
265            })?;
266
267        Ok(SshSkAttestation {
268            att_cert,
269            sig,
270            // Probably need auth_data raw.
271            auth_data_bytes,
272            auth_data,
273        })
274    }
275}
276
277fn parse_ssh_sk_attestation(i: &[u8]) -> nom::IResult<&[u8], SshSkAttestationRaw<'_>> {
278    // Starts with a 4 byte u32 for the len of the header.
279
280    let (i, _tag_len) = tag([0, 0, 0, 17])(i)?;
281    let (i, _tag) = tag("ssh-sk-attest-v01")(i)?;
282
283    let (i, att_cert_len) = be_u32(i)?;
284    let (i, att_cert_raw) = take(att_cert_len as usize)(i)?;
285
286    let (i, sig_len) = be_u32(i)?;
287    let (i, sig_raw) = take(sig_len as usize)(i)?;
288
289    let (i, auth_data_len) = be_u32(i)?;
290    let (i, auth_data_raw) = take(auth_data_len as usize)(i)?;
291
292    let (i, _resvd_flags) = be_u32(i)?;
293    let (i, _resvd) = be_u32(i)?;
294
295    Ok((
296        i,
297        SshSkAttestationRaw {
298            att_cert_raw,
299            sig_raw,
300            auth_data_raw,
301        },
302    ))
303}
304
305fn to_ssh_pubkey(cose: &COSEKey) -> Result<PublicKey, WebauthnError> {
306    match &cose.key {
307        COSEKeyType::EC_EC2(_ec2k) => {
308            let pubkey = cose.get_openssl_pkey()?;
309            let key = pubkey
310                .ec_key()
311                .and_then(|ec| {
312                    let mut ctx = bn::BigNumContext::new()?;
313                    let c_nid = nid::Nid::X9_62_PRIME256V1; // NIST P-256 curve
314                    let group = ec::EcGroup::from_curve_name(c_nid)?;
315
316                    ec.public_key().to_bytes(
317                        &group,
318                        ec::PointConversionForm::UNCOMPRESSED,
319                        &mut ctx,
320                    )
321                })
322                .map_err(WebauthnError::OpenSSLError)?;
323
324            let kind = PublicKeyKind::Ecdsa(EcdsaPublicKey {
325                curve: Curve::from_identifier("nistp256").map_err(|_| {
326                    error!("Invalid curve identifier");
327                    WebauthnError::SshPublicKeyInvalidCurve
328                })?,
329                key,
330                sk_application: Some("ssh:".to_string()),
331            });
332
333            Ok(PublicKey {
334                key_type: KeyType {
335                    name: "sk-ecdsa-sha2-nistp256@openssh.com",
336                    short_name: "ECDSA-SK",
337                    is_cert: false,
338                    is_sk: true,
339                    kind: KeyTypeKind::EcdsaSk,
340                    plain: "sk-ecdsa-sha2-nistp256@openssh.com",
341                },
342                kind,
343                comment: None,
344            })
345        }
346        _ => {
347            error!("ed25519 or ed448 public keys are not supported");
348            Err(WebauthnError::SshPublicKeyEDUnsupported)
349        }
350    }
351}
352
353#[cfg(test)]
354mod tests {
355    use super::{verify_fido_sk_ssh_attestation, WebauthnError};
356    use base64::{engine::general_purpose::STANDARD, Engine};
357    use webauthn_rs_core::proto::{
358        AttestationCaList, AttestationCaListBuilder, CredentialProtectionPolicy, ExtnState,
359    };
360    use webauthn_rs_device_catalog::data::yubico::YUBICO_U2F_ROOT_CA_SERIAL_457200631_PEM;
361
362    #[test]
363    fn test_ssh_ecdsa_sk_attest() {
364        let _ = tracing_subscriber::fmt::try_init();
365
366        // Create with:
367        //  dd if=/dev/urandom of=/Users/william/.ssh/id_ecdsa_sk.chal bs=16 count=1
368        //  ssh-keygen -t ecdsa-sk -O challenge=/Users/william/.ssh/id_ecdsa_sk.chal -O write-attestation=/Users/william/.ssh/id_ecdsa_sk.attest -f /Users/william/.ssh/id_ecdsa_sk
369
370        let attest = STANDARD.decode("AAAAEXNzaC1zay1hdHRlc3QtdjAxAAACwTCCAr0wggGloAMCAQICBBisRsAwDQYJKoZIhvcNAQELBQAwLjEsMCoGA1UEAxMjWXViaWNvIFUyRiBSb290IENBIFNlcmlhbCA0NTcyMDA2MzEwIBcNMTQwODAxMDAwMDAwWhgPMjA1MDA5MDQwMDAwMDBaMG4xCzAJBgNVBAYTAlNFMRIwEAYDVQQKDAlZdWJpY28gQUIxIjAgBgNVBAsMGUF1dGhlbnRpY2F0b3IgQXR0ZXN0YXRpb24xJzAlBgNVBAMMHll1YmljbyBVMkYgRUUgU2VyaWFsIDQxMzk0MzQ4ODBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABHnqOyx8SXAQYiMM0j/rYOUpMXHUg/EAvoWdaw+DlwMBtUbN1G7PyuPj8w+B6e1ivSaNTB69N7O8vpKowq7rTjqjbDBqMCIGCSsGAQQBgsQKAgQVMS4zLjYuMS40LjEuNDE0ODIuMS43MBMGCysGAQQBguUcAgEBBAQDAgUgMCEGCysGAQQBguUcAQEEBBIEEMtpSB6P90A5k+wKJymhVKgwDAYDVR0TAQH/BAIwADANBgkqhkiG9w0BAQsFAAOCAQEAl50Dl9hg+C7hXTEceW66+yL6p+CE2bq0xhu7V/PmtMGKSDe4XDxO2+SDQ/TWpdmxztqK4f7UkSkhcwWOXuHL3WvawHVXxqDo02gluhWef7WtjNr4BIaM+Q6PH4rqF8AWtVwqetSXyJT7cddT15uaSEtsN21yO5mNLh1DBr8QM7Wu+Myly7JWi2kkIm0io1irfYfkrF8uCRqnFXnzpWkJSX1y9U4GusHDtEE7ul6vlMO2TzT566Qay2rig3dtNkZTeEj+6IS93fWxuleYVM/9zrrDRAWVJ+Vt1Zj49WZxWr5DAd0ZETDmufDGQDkSU+IpgD867ydL7b/eP8u9QurWeQAAAEYwRAIgeYp6mYVsuaj0NpHps1qkGkJYroyurnuCKdSYWUCCsVgCIAhFdmhNWGG0cY5l3sZUhjmrwCHpuQ1A0QXbhuEtjM7sAAAAxljE4wYQ6KFiEVlg/h7CI+ZSnJ9LboAgDcteXDIcivHisb9FAAALNMtpSB6P90A5k+wKJymhVKgAQPQVE6m4sayalwAfqHVZBGEP32y5ju2Vo7U3k1zPFKQGLDhpA0dRHWvYbsvTPmqVzSGuxSyRW/ugWzPqsveALlSlAQIDJiABIVggQ25tmKStvyG74d5VF1nSmn9UCTaq/gkNu4mG8PTI11YiWCAMvZ7dwFsRGIN40+RbHnxDitWfGRtXV9rwTbBpG1P3XAAAAAAAAAAA")
371            .expect("Failed to decode attestation");
372
373        let challenge = STANDARD
374            .decode("VzCkpMNVYVgXHBuDP74v9A==")
375            .expect("Failed to decode attestation");
376
377        let pubkey = "sk-ecdsa-sha2-nistp256@openssh.com AAAAInNrLWVjZHNhLXNoYTItbmlzdHAyNTZAb3BlbnNzaC5jb20AAAAIbmlzdHAyNTYAAABBBENubZikrb8hu+HeVRdZ0pp/VAk2qv4JDbuJhvD0yNdWDL2e3cBbERiDeNPkWx58Q4rVnxkbV1fa8E2waRtT91wAAAAEc3NoOg== william@hostname";
378        let mut key = sshkeys::PublicKey::from_string(pubkey).unwrap();
379        // Blank the comment
380        key.comment = None;
381
382        let mut att_ca_builder = AttestationCaListBuilder::new();
383        att_ca_builder
384            .insert_device_pem(
385                YUBICO_U2F_ROOT_CA_SERIAL_457200631_PEM,
386                uuid::uuid!("cb69481e-8ff7-4039-93ec-0a2729a154a8"),
387                "yk 5 nano".to_string(),
388                Default::default(),
389            )
390            .expect("Failed to build att ca list");
391        let att_ca_list: AttestationCaList = att_ca_builder.build();
392
393        // Parse
394        let att = verify_fido_sk_ssh_attestation(
395            attest.as_slice(),
396            challenge.as_slice(),
397            &att_ca_list,
398            false,
399        )
400        .expect("Failed to parse attestation");
401
402        trace!("key {:?}", key);
403        trace!("att {:?}", att.pubkey);
404        trace!("att full {:?}", att);
405
406        // Check the supplied pubkey and the attested pubkey are the same.
407        assert_eq!(att.pubkey, key);
408
409        // Assert that cred protect isn't set.
410        assert!(matches!(
411            att.extensions.cred_protect,
412            ExtnState::NotRequested
413        ));
414    }
415
416    #[test]
417    fn test_ssh_ecdsa_sk_credprotect_attest() {
418        let _ = tracing_subscriber::fmt::try_init();
419
420        // Create with:
421        //  dd if=/dev/urandom of=/Users/william/.ssh/id_ecdsa_sk.chal bs=16 count=1
422        //  ssh-keygen -t ecdsa-sk -O verify-required -O challenge=/Users/william/.ssh/id_ecdsa_sk.chal -O write-attestation=/Users/william/.ssh/id_ecdsa_sk_uv.attest -f /Users/william/.ssh/id_ecdsa_sk_uv
423
424        let attest = STANDARD.decode("AAAAEXNzaC1zay1hdHRlc3QtdjAxAAAC8DCCAuwwggHUoAMCAQICCQCIobnFT2wgvjANBgkqhkiG9w0BAQsFADAuMSwwKgYDVQQDEyNZdWJpY28gVTJGIFJvb3QgQ0EgU2VyaWFsIDQ1NzIwMDYzMTAgFw0xNDA4MDEwMDAwMDBaGA8yMDUwMDkwNDAwMDAwMFowbzELMAkGA1UEBhMCU0UxEjAQBgNVBAoMCVl1YmljbyBBQjEiMCAGA1UECwwZQXV0aGVudGljYXRvciBBdHRlc3RhdGlvbjEoMCYGA1UEAwwfWXViaWNvIFUyRiBFRSBTZXJpYWwgMTE2OTc5MzQxNjBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABP3N+hZ2qRVyajtVRGx/tdK/YAcNNGY++kDoDODSHk4cAqXSZ7jZepIkLdQXk7JP2dD0gVMpP5WzOJpEv8J6tRejgZQwgZEwEwYKKwYBBAGCxAoNAQQFBAMFBAMwEAYJKwYBBAGCxAoMBAMCAQcwIgYJKwYBBAGCxAoCBBUxLjMuNi4xLjQuMS40MTQ4Mi4xLjcwEwYLKwYBBAGC5RwCAQEEBAMCBSAwIQYLKwYBBAGC5RwBAQQEEgQQc7sM1OUCSbicb7WURb9yCzAMBgNVHRMBAf8EAjAAMA0GCSqGSIb3DQEBCwUAA4IBAQA8JczOIP9yDzuYizYwoJwGCwmYbUWNuokPu/NOMRLKTHvRfZRhf5LRHX7WjD0kJwifo725l/O7b+G4Y+3w9a1tK00wBGCMxw3F/oGcxsn+Tg6zWQZW3HXN8Qxfb5vtnX7lK5omugUPyq7XBqiBqFi2oqHFxjPjZSYFqQLE1DxDfJVtxXysvG1q/tkTkRagkAaqLb59SitNKsSXJ14Y9aG6liaFpSL8q+BeIe6XBHZ8NGxGhZdnhOu6qzYcTpSXlYHjeUoVF2/crpnQocjl59cgarJgS2aJV/jlSWnyZVhKbq14up6YUg0UsO60+UYm5rKuxS5OvAsvgKbl+71jhxCSAAAASDBGAiEA0tN1SoFM6y25G1MAqiogh9YrxC3xXAjq5PqSLfpEiBMCIQCzlf2HkoQxzw26d/H54qG7usJxGqjI7ar5QTPTmyPiPAAAANRY0uMGEOihYhFZYP4ewiPmUpyfS26AIA3LXlwyHIrx4rG/xQAAAANzuwzU5QJJuJxvtZRFv3ILAEDSkcmqMSeSNIZeeun9OR70HsiBGZv4Z487AIxLcDGlygV+x8o0pcXuQLBt5qkyLgbjHz9AG8VnoG89Xsqc7FDNpQECAyYgASFYIMD4M0oQcZZURp0PhmabT3X+rvYak+JdnMwDTlJ/zBzZIlggrec0hNPMTEy2/BSWTiX/LCtOIuxSUAzRFG07JAwxxTyha2NyZWRQcm90ZWN0AwAAAAAAAAAA")
425            .expect("Failed to decode attestation");
426        let challenge = STANDARD
427            .decode("VzCkpMNVYVgXHBuDP74v9A==")
428            .expect("Failed to decode attestation");
429
430        let pubkey = "sk-ecdsa-sha2-nistp256@openssh.com AAAAInNrLWVjZHNhLXNoYTItbmlzdHAyNTZAb3BlbnNzaC5jb20AAAAIbmlzdHAyNTYAAABBBMD4M0oQcZZURp0PhmabT3X+rvYak+JdnMwDTlJ/zBzZrec0hNPMTEy2/BSWTiX/LCtOIuxSUAzRFG07JAwxxTwAAAAEc3NoOg== william@hostname";
431        let mut key = sshkeys::PublicKey::from_string(pubkey).unwrap();
432        // Blank the comment
433        key.comment = None;
434
435        let mut att_ca_builder = AttestationCaListBuilder::new();
436        att_ca_builder
437            .insert_device_pem(
438                YUBICO_U2F_ROOT_CA_SERIAL_457200631_PEM,
439                uuid::uuid!("73bb0cd4-e502-49b8-9c6f-b59445bf720b"),
440                "yk 5 fips".to_string(),
441                Default::default(),
442            )
443            .expect("Failed to build att ca list");
444        let att_ca_list: AttestationCaList = att_ca_builder.build();
445
446        // Parse
447        let att = verify_fido_sk_ssh_attestation(
448            attest.as_slice(),
449            challenge.as_slice(),
450            &att_ca_list,
451            false,
452        )
453        .expect("Failed to parse attestation");
454
455        trace!("key {:?}", key);
456        trace!("att {:?}", att.pubkey);
457        trace!("att full {:?}", att);
458
459        // Check the supplied pubkey and the attested pubkey are the same.
460        assert_eq!(att.pubkey, key);
461
462        // Assert that cred protect is present.
463        assert!(matches!(
464            att.extensions.cred_protect,
465            ExtnState::Set(CredentialProtectionPolicy::UserVerificationRequired)
466        ));
467    }
468
469    #[test]
470    fn test_ssh_ed25519_sk_attest() {
471        let _ = tracing_subscriber::fmt::try_init();
472
473        // Create with:
474        // dd if=/dev/urandom of=/tmp/id_ed25519_sk.chal bs=16 count=1
475        // ssh-keygen -t ed25519-sk  -O challenge=/tmp/id_ed25519_sk.chal -O write-attestation=/tmp/id_ed25519_sk.attest -f /tmp/id_ed25519_sk
476
477        let attest = STANDARD.decode("AAAAEXNzaC1zay1hdHRlc3QtdjAxAAAC8DCCAuwwggHUoAMCAQICCQCIobnFT2wgvjANBgkqhkiG9w0BAQsFADAuMSwwKgYDVQQDEyNZdWJpY28gVTJGIFJvb3QgQ0EgU2VyaWFsIDQ1NzIwMDYzMTAgFw0xNDA4MDEwMDAwMDBaGA8yMDUwMDkwNDAwMDAwMFowbzELMAkGA1UEBhMCU0UxEjAQBgNVBAoMCVl1YmljbyBBQjEiMCAGA1UECwwZQXV0aGVudGljYXRvciBBdHRlc3RhdGlvbjEoMCYGA1UEAwwfWXViaWNvIFUyRiBFRSBTZXJpYWwgMTE2OTc5MzQxNjBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABP3N+hZ2qRVyajtVRGx/tdK/YAcNNGY++kDoDODSHk4cAqXSZ7jZepIkLdQXk7JP2dD0gVMpP5WzOJpEv8J6tRejgZQwgZEwEwYKKwYBBAGCxAoNAQQFBAMFBAMwEAYJKwYBBAGCxAoMBAMCAQcwIgYJKwYBBAGCxAoCBBUxLjMuNi4xLjQuMS40MTQ4Mi4xLjcwEwYLKwYBBAGC5RwCAQEEBAMCBSAwIQYLKwYBBAGC5RwBAQQEEgQQc7sM1OUCSbicb7WURb9yCzAMBgNVHRMBAf8EAjAAMA0GCSqGSIb3DQEBCwUAA4IBAQA8JczOIP9yDzuYizYwoJwGCwmYbUWNuokPu/NOMRLKTHvRfZRhf5LRHX7WjD0kJwifo725l/O7b+G4Y+3w9a1tK00wBGCMxw3F/oGcxsn+Tg6zWQZW3HXN8Qxfb5vtnX7lK5omugUPyq7XBqiBqFi2oqHFxjPjZSYFqQLE1DxDfJVtxXysvG1q/tkTkRagkAaqLb59SitNKsSXJ14Y9aG6liaFpSL8q+BeIe6XBHZ8NGxGhZdnhOu6qzYcTpSXlYHjeUoVF2/crpnQocjl59cgarJgS2aJV/jlSWnyZVhKbq14up6YUg0UsO60+UYm5rKuxS5OvAsvgKbl+71jhxCSAAAARzBFAiEA9wvGXR0jdmlx41KiDgVnHng/u+aABcL0T7Mcla5RY1cCIG3w7FmnUCC9cN4OTsF0YIUKREVl7YZ/ULpgG9r3gbGcAAAA41jh4wYQ6KFiEVlg/h7CI+ZSnJ9LboAgDcteXDIcivHisb9FAAAAAnO7DNTlAkm4nG+1lEW/cgsAgOlyrDirl7wov1VQfV/0peGGSiOf4dfQ/MwcKRxhWA7OIEczExGaaoiNJZBKyVUnte5FWF4xz+g2yY1LA9DYizkHRyuH3V6nOqaBl56+pImD7oJA2sMGgFaK7OawkNInLrZn+kK1KwDwAuqGyraYxUwOimcyj3iO0cmnx8Kl3VsbpAEBAycgBiFYIJrDpo9OvZ479Kr/+2n9IY88++eEu1g+RqRgrNsGWyCLAAAAAAAAAAA=")
478            .expect("Failed to decode attestation");
479
480        let challenge = STANDARD
481            .decode("aAqBnywP0Vbv3SUgqmnMRQ==")
482            .expect("Failed to decode attestation");
483
484        let mut att_ca_builder = AttestationCaListBuilder::new();
485        att_ca_builder
486            .insert_device_pem(
487                YUBICO_U2F_ROOT_CA_SERIAL_457200631_PEM,
488                uuid::uuid!("73bb0cd4-e502-49b8-9c6f-b59445bf720b"),
489                "yk 5 fips".to_string(),
490                Default::default(),
491            )
492            .expect("Failed to build att ca list");
493        let att_ca_list: AttestationCaList = att_ca_builder.build();
494
495        // Parse
496        let att = verify_fido_sk_ssh_attestation(
497            attest.as_slice(),
498            challenge.as_slice(),
499            &att_ca_list,
500            false,
501        );
502
503        trace!("att full {:?}", att);
504
505        assert!(matches!(att, Err(WebauthnError::SshPublicKeyEDUnsupported)));
506
507        /*
508        trace!("key {:?}", key);
509        trace!("att {:?}", att.pubkey);
510        trace!("att full {:?}", att);
511
512        // Check the supplied pubkey and the attested pubkey are the same.
513        assert_eq!(att.pubkey, key);
514
515        // Assert that cred protect isn't set.
516        assert!(matches!(
517            att.extensions.cred_protect,
518            ExtnState::NotRequested
519        ));
520        */
521    }
522
523    #[test]
524    fn test_ssh_ecdsa_sk_reject_attest_aaguid() {
525        let _ = tracing_subscriber::fmt::try_init();
526
527        // Create with:
528        //  dd if=/dev/urandom of=/Users/william/.ssh/id_ecdsa_sk.chal bs=16 count=1
529        //  ssh-keygen -t ecdsa-sk -O challenge=/Users/william/.ssh/id_ecdsa_sk.chal -O write-attestation=/Users/william/.ssh/id_ecdsa_sk.attest -f /Users/william/.ssh/id_ecdsa_sk
530
531        let attest = STANDARD.decode("AAAAEXNzaC1zay1hdHRlc3QtdjAxAAACwTCCAr0wggGloAMCAQICBBisRsAwDQYJKoZIhvcNAQELBQAwLjEsMCoGA1UEAxMjWXViaWNvIFUyRiBSb290IENBIFNlcmlhbCA0NTcyMDA2MzEwIBcNMTQwODAxMDAwMDAwWhgPMjA1MDA5MDQwMDAwMDBaMG4xCzAJBgNVBAYTAlNFMRIwEAYDVQQKDAlZdWJpY28gQUIxIjAgBgNVBAsMGUF1dGhlbnRpY2F0b3IgQXR0ZXN0YXRpb24xJzAlBgNVBAMMHll1YmljbyBVMkYgRUUgU2VyaWFsIDQxMzk0MzQ4ODBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABHnqOyx8SXAQYiMM0j/rYOUpMXHUg/EAvoWdaw+DlwMBtUbN1G7PyuPj8w+B6e1ivSaNTB69N7O8vpKowq7rTjqjbDBqMCIGCSsGAQQBgsQKAgQVMS4zLjYuMS40LjEuNDE0ODIuMS43MBMGCysGAQQBguUcAgEBBAQDAgUgMCEGCysGAQQBguUcAQEEBBIEEMtpSB6P90A5k+wKJymhVKgwDAYDVR0TAQH/BAIwADANBgkqhkiG9w0BAQsFAAOCAQEAl50Dl9hg+C7hXTEceW66+yL6p+CE2bq0xhu7V/PmtMGKSDe4XDxO2+SDQ/TWpdmxztqK4f7UkSkhcwWOXuHL3WvawHVXxqDo02gluhWef7WtjNr4BIaM+Q6PH4rqF8AWtVwqetSXyJT7cddT15uaSEtsN21yO5mNLh1DBr8QM7Wu+Myly7JWi2kkIm0io1irfYfkrF8uCRqnFXnzpWkJSX1y9U4GusHDtEE7ul6vlMO2TzT566Qay2rig3dtNkZTeEj+6IS93fWxuleYVM/9zrrDRAWVJ+Vt1Zj49WZxWr5DAd0ZETDmufDGQDkSU+IpgD867ydL7b/eP8u9QurWeQAAAEYwRAIgeYp6mYVsuaj0NpHps1qkGkJYroyurnuCKdSYWUCCsVgCIAhFdmhNWGG0cY5l3sZUhjmrwCHpuQ1A0QXbhuEtjM7sAAAAxljE4wYQ6KFiEVlg/h7CI+ZSnJ9LboAgDcteXDIcivHisb9FAAALNMtpSB6P90A5k+wKJymhVKgAQPQVE6m4sayalwAfqHVZBGEP32y5ju2Vo7U3k1zPFKQGLDhpA0dRHWvYbsvTPmqVzSGuxSyRW/ugWzPqsveALlSlAQIDJiABIVggQ25tmKStvyG74d5VF1nSmn9UCTaq/gkNu4mG8PTI11YiWCAMvZ7dwFsRGIN40+RbHnxDitWfGRtXV9rwTbBpG1P3XAAAAAAAAAAA")
532            .expect("Failed to decode attestation");
533
534        let challenge = STANDARD
535            .decode("VzCkpMNVYVgXHBuDP74v9A==")
536            .expect("Failed to decode attestation");
537
538        // The device signature above is from a yk5nano, however we have an attestation policy
539        // to only allow the yk5 fips.
540        let mut att_ca_builder = AttestationCaListBuilder::new();
541        att_ca_builder
542            .insert_device_pem(
543                YUBICO_U2F_ROOT_CA_SERIAL_457200631_PEM,
544                uuid::uuid!("73bb0cd4-e502-49b8-9c6f-b59445bf720b"),
545                "yk 5 fips".to_string(),
546                Default::default(),
547            )
548            .expect("Failed to build att ca list");
549        let att_ca_list: AttestationCaList = att_ca_builder.build();
550
551        // Parse
552        let att = verify_fido_sk_ssh_attestation(
553            attest.as_slice(),
554            challenge.as_slice(),
555            &att_ca_list,
556            false,
557        );
558
559        trace!("att full {:?}", att);
560
561        assert!(matches!(
562            att,
563            Err(WebauthnError::AttestationUntrustedAaguid)
564        ));
565    }
566
567    #[test]
568    fn test_ssh_ecdsa_sk_reject_attest_ca() {
569        let _ = tracing_subscriber::fmt::try_init();
570
571        // Create with:
572        //  dd if=/dev/urandom of=/Users/william/.ssh/id_ecdsa_sk.chal bs=16 count=1
573        //  ssh-keygen -t ecdsa-sk -O challenge=/Users/william/.ssh/id_ecdsa_sk.chal -O write-attestation=/Users/william/.ssh/id_ecdsa_sk.attest -f /Users/william/.ssh/id_ecdsa_sk
574
575        let attest = STANDARD.decode("AAAAEXNzaC1zay1hdHRlc3QtdjAxAAADGzCCAxcwggK+oAMCAQICCQDFabHRsxYpGTAKBggqhkjOPQQDAjCBnDELMAkGA1UEBhMCQ0gxDzANBgNVBAgMBkdlbmV2YTEQMA4GA1UEBwwHVmVyc29peDEPMA0GA1UECgwGVE9LRU4yMSIwIAYDVQQLDBlBdXRoZW50aWNhdG9yIEF0dGVzdGF0aW9uMRMwEQYDVQQDDAp0b2tlbjIuY29tMSAwHgYJKoZIhvcNAQkBFhFvZmZpY2VAdG9rZW4yLmNvbTAeFw0xOTEyMDQwNzAyMjJaFw0zOTExMjkwNzAyMjJaMF4xCzAJBgNVBAYTAkNIMQ8wDQYDVQQKDAZUT0tFTjIxIjAgBgNVBAsMGUF1dGhlbnRpY2F0b3IgQXR0ZXN0YXRpb24xGjAYBgNVBAMMEW9mZmljZUB0b2tlbjIuY29tMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEC/l7QJNMxtrBu91XScVYjFlqTFza0N/9RRYWPItzgmppWvjUPwyCres27Lo3Waf7OVMdmc5ML5HB+eECnVWqg6OCASQwggEgMAkGA1UdEwQCMAAwHQYDVR0OBBYEFFCysnSFvKuvrSVyB3ToAQshmMpjMIG7BgNVHSMEgbMwgbChgaKkgZ8wgZwxCzAJBgNVBAYTAkNIMQ8wDQYDVQQIDAZHZW5ldmExEDAOBgNVBAcMB1ZlcnNvaXgxDzANBgNVBAoMBlRPS0VOMjEiMCAGA1UECwwZQXV0aGVudGljYXRvciBBdHRlc3RhdGlvbjETMBEGA1UEAwwKdG9rZW4yLmNvbTEgMB4GCSqGSIb3DQEJARYRb2ZmaWNlQHRva2VuMi5jb22CCQCv1vlqKeW5ejATBgsrBgEEAYLlHAIBAQQEAwIFIDAhBgsrBgEEAYLlHAEBBAQSBBCrMvDGIjmvu8Rw0u9OJU23MAoGCCqGSM49BAMCA0cAMEQCIGgzWHRCvlMLEPA+qAk+33KwVVyvTKnBxC7jESc0vSV1AiBXPi/VVvaIiDh0vtnBmMSP1WCUGHhY7RReYNm9cbe8swAAAEYwRAIgfxtWfDli/pqS0/DqyaXvLn5C4BNRXoHx1ofpU4WZqfICIEzUSXKUI4/DezfU9MtW3t5ua5fhgL7EoMdaXBRGmNnLAAAA5ljk4wYQ6KFiEVlg/h7CI+ZSnJ9LboAgDcteXDIcivHisb9FAAADIasy8MYiOa+7xHDS704lTbcAYCnb4hHUYvEK9Dp4gjgJer+Wtcj0GglGtd5ubraTzUc19amoIyg/+/lNKrntsFSalESwu7fNNRPjWldzr2zyueB9MyJZDXkOrkP1iK/B836pudmGcJq6vfV1Da2Bieks16UBAgMmIAEhWCAe32mzSUWbouK4KOykaK3dGczNTUoTqBjengeoL6DhyCJYIIogmo+NOwfBZgF5xEORNffCk+4dA+preNaQE9mSv506AAAAAAAAAAA=")
576            .expect("Failed to decode attestation");
577
578        let challenge = STANDARD
579            .decode("aAqBnywP0Vbv3SUgqmnMRQ==")
580            .expect("Failed to decode attestation");
581
582        // The device signature above is from a token 2 however we have an attestation policy
583        // to only allow the yk5 fips.
584
585        let mut att_ca_builder = AttestationCaListBuilder::new();
586        att_ca_builder
587            .insert_device_pem(
588                YUBICO_U2F_ROOT_CA_SERIAL_457200631_PEM,
589                uuid::uuid!("73bb0cd4-e502-49b8-9c6f-b59445bf720b"),
590                "yk 5 fips".to_string(),
591                Default::default(),
592            )
593            .expect("Failed to build att ca list");
594        let att_ca_list: AttestationCaList = att_ca_builder.build();
595
596        // Parse
597        let att = verify_fido_sk_ssh_attestation(
598            attest.as_slice(),
599            challenge.as_slice(),
600            &att_ca_list,
601            false,
602        );
603
604        trace!("att full {:?}", att);
605
606        assert!(matches!(
607            att,
608            Err(WebauthnError::AttestationChainNotTrusted(_))
609        ));
610    }
611}