diff --git a/Cargo.lock b/Cargo.lock index 252f6d67..349b9edd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -372,7 +372,7 @@ version = "0.1.0" dependencies = [ "opensk", "portable-atomic-util", - "uuid", + "sk-cbor", "wasefire", "wasefire-common", ] diff --git a/Cargo.toml b/Cargo.toml index a44ca149..a2c89275 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -36,6 +36,9 @@ features = [ path = "libraries/opensk" default-features = false +[dependencies.sk-cbor] +path = "libraries/cbor" + [features] config-command = ["opensk/config_command"] ctap1 = ["opensk/ctap1"] @@ -44,6 +47,3 @@ ed25519 = ["opensk/ed25519", "wasefire/api-crypto-ed25519"] fingerprint = ["dep:wasefire-common", "opensk/fingerprint", "wasefire/api-fingerprint-matcher"] led-1 = [] test = ["opensk/std", "wasefire/test"] - -[build-dependencies] -uuid = { version = "0.8", features = ["v4"] } diff --git a/build.rs b/build.rs deleted file mode 100644 index 2abd232b..00000000 --- a/build.rs +++ /dev/null @@ -1,37 +0,0 @@ -// Copyright 2019-2026 Google LLC -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -extern crate alloc; - -use std::env; -use std::fs::File; -use std::io::{Read, Write}; -use std::path::Path; - -use uuid::Uuid; - -fn main() { - println!("cargo:rerun-if-changed=crypto_data/aaguid.txt"); - - let out_dir = env::var_os("OUT_DIR").unwrap(); - let aaguid_bin_path = Path::new(&out_dir).join("opensk_aaguid.bin"); - - let mut aaguid_bin_file = File::create(aaguid_bin_path).unwrap(); - let mut aaguid_txt_file = File::open("crypto_data/aaguid.txt").unwrap(); - let mut content = String::new(); - aaguid_txt_file.read_to_string(&mut content).unwrap(); - content.truncate(36); - let aaguid = Uuid::parse_str(&content).unwrap(); - aaguid_bin_file.write_all(aaguid.as_bytes()).unwrap(); -} diff --git a/docs/customization.md b/docs/customization.md index 8fdf9347..bcc157d0 100644 --- a/docs/customization.md +++ b/docs/customization.md @@ -27,26 +27,39 @@ If you delete the key material, it will be randomly regenerated by `setup.sh`. If you either use the `ctap1` feature or set `use_batch_attestation` in the customization, OpenSK needs to send an attestation when you register a -credential on a website. By default, this attestation will be randomly -generated on each registration attempt. When you inject a batch attestation key, -it will instead be used. +credential on a website. By default, OpenSK uses -This attestation should be proof for a website that the security key is indeed -a hardware security key. A self-made OpenSK with a randomly generated batch -attestation key will not prove anything. In practise, a few more relying parties -can be used with OpenSK because of this trick. Also, U2F requires batch -attestation key material to work. +- an all-zero AAGUID, +- a randomly generated batch attestation key, +- an empty batch attestation certificate. -You can inject your own batch attestation key into the firmware. Tooling for -that is currently being reworked after the move to Wasefire. Before you do that, -a warning: +You can inject the above data into OpenSK with a custom vendor command. +However, this means that all registered credentials using this batch attestation +data can be correlated. For locked hardware security keys, this feature gives +relying parties proof that they speak to secure hardware. They compare the +AAGUID to those registered with the FIDO Alliance. Usually, the attestation private key is shared between a batch of at least 100,000 security keys of the same model. If you build your own OpenSK, your private key is unique to you. This makes you identifiable across registrations: Two websites could collaborate to track if registrations were attested with the same key material. If you use OpenSK beyond experimentation, please consider -carefully if you want to take this privacy risk. +carefully if you want to take the privacy risk of using the configure tool. + +The default randomly generated, ephemeral batch attestation keys, are helpful in +practise: Without a key, U2F does not work. Also a few more relying parties +accept OpenSK responses because of this trick. It is not meant to prove any +hardware security properties. + +To inject your own batch attestation key and AAGUID into the firmware, run: + +```sh +# Read the privacy warning above! +uv run tools/configure.py \ + --certificate=crypto_data/opensk_cert.pem \ + --private-key=crypto_data/opensk.key \ + --aaguid=crypto_data/aaguid.txt +``` ### Personalization diff --git a/docs/install.md b/docs/install.md index 29b58638..0951fd74 100644 --- a/docs/install.md +++ b/docs/install.md @@ -135,3 +135,11 @@ connected to R13. ```sh ./flash.sh opentitan ``` + +## Configuring the firmware + +After flashing the firmware, you can configure it. +You can use a custom AAGUID, batch attestation key and certificate. +Only perform this step if you understand the privacy implications. Read the +[certificate section in Customization](customization.md#Certificate-considerations) +to find the necessary command. diff --git a/libraries/opensk/src/api/customization.rs b/libraries/opensk/src/api/customization.rs index 4b4d1f6c..3c75797e 100644 --- a/libraries/opensk/src/api/customization.rs +++ b/libraries/opensk/src/api/customization.rs @@ -21,12 +21,7 @@ use crate::ctap::data_formats::{CredentialProtectionPolicy, EnterpriseAttestatio use alloc::string::String; use alloc::vec::Vec; -pub const AAGUID_LENGTH: usize = 16; - pub trait Customization { - /// Authenticator Attestation Global Unique Identifier - fn aaguid(&self) -> &'static [u8; AAGUID_LENGTH]; - // ########################################################################### // Constants for adjusting privacy and protection levels. // ########################################################################### @@ -298,7 +293,6 @@ pub trait Customization { #[derive(Clone)] pub struct CustomizationImpl { - pub aaguid: &'static [u8; AAGUID_LENGTH], pub allows_pin_protocol_v1: bool, pub default_cred_protect: Option, pub default_min_pin_length: u8, @@ -326,7 +320,6 @@ pub struct CustomizationImpl { } pub const DEFAULT_CUSTOMIZATION: CustomizationImpl = CustomizationImpl { - aaguid: &[0; AAGUID_LENGTH], allows_pin_protocol_v1: true, default_cred_protect: None, default_min_pin_length: 4, @@ -354,10 +347,6 @@ pub const DEFAULT_CUSTOMIZATION: CustomizationImpl = CustomizationImpl { }; impl Customization for CustomizationImpl { - fn aaguid(&self) -> &'static [u8; AAGUID_LENGTH] { - self.aaguid - } - fn allows_pin_protocol_v1(&self) -> bool { self.allows_pin_protocol_v1 } @@ -565,7 +554,6 @@ mod test { #[test] fn test_accessors() { let customization = CustomizationImpl { - aaguid: &[0; AAGUID_LENGTH], allows_pin_protocol_v1: true, default_cred_protect: None, default_min_pin_length: 4, @@ -591,7 +579,6 @@ mod test { #[cfg(feature = "fingerprint")] max_template_friendly_name: 64, }; - assert_eq!(customization.aaguid(), &[0; AAGUID_LENGTH]); assert!(customization.allows_pin_protocol_v1()); assert!(customization.default_cred_protect().is_none()); assert_eq!(customization.default_min_pin_length(), 4); diff --git a/libraries/opensk/src/api/persist.rs b/libraries/opensk/src/api/persist.rs index 559f779a..ed4d41d6 100644 --- a/libraries/opensk/src/api/persist.rs +++ b/libraries/opensk/src/api/persist.rs @@ -34,6 +34,8 @@ use enum_iterator::IntoEnumIterator; use sk_cbor as cbor; use sk_cbor::destructure_cbor_map; +pub const AAGUID_LENGTH: usize = 16; + pub type PersistIter<'a> = Box> + 'a>; pub type PersistCredentialIter<'a> = Box)>> + 'a>; pub type LargeBlobBuffer = Vec; @@ -581,6 +583,24 @@ pub trait Persist { Ok(()) } + /// Returns the programmed AAGUID, defaulting to all zeros if not set. + fn aaguid(&self) -> CtapResult<[u8; AAGUID_LENGTH]> { + match self.find(keys::AAGUID)? { + None => Ok([0; AAGUID_LENGTH]), + Some(value) if value.len() == AAGUID_LENGTH => { + let mut aaguid = [0; AAGUID_LENGTH]; + aaguid.copy_from_slice(&value); + Ok(aaguid) + } + _ => Err(Ctap2StatusCode::CTAP2_ERR_VENDOR_INTERNAL_ERROR), + } + } + + /// Sets the AAGUID. + fn set_aaguid(&mut self, aaguid: &[u8; AAGUID_LENGTH]) -> CtapResult<()> { + self.insert(keys::AAGUID, aaguid) + } + fn key_store_bytes(&self) -> CtapResult>> { let bytes = self.find(keys::KEY_STORE)?; Ok(bytes.map(|b| { @@ -871,4 +891,16 @@ mod test { }; assert_eq!(returned_attestation, expected_attestation); } + + #[test] + fn test_aaguid() { + let mut env = TestEnv::default(); + let persist = env.persist(); + + assert_eq!(persist.aaguid(), Ok([0; AAGUID_LENGTH])); + + let test_aaguid = [1; AAGUID_LENGTH]; + assert_eq!(persist.set_aaguid(&test_aaguid), Ok(())); + assert_eq!(persist.aaguid(), Ok(test_aaguid)); + } } diff --git a/libraries/opensk/src/api/persist/keys.rs b/libraries/opensk/src/api/persist/keys.rs index 6a8867d4..bdd9b052 100644 --- a/libraries/opensk/src/api/persist/keys.rs +++ b/libraries/opensk/src/api/persist/keys.rs @@ -73,8 +73,8 @@ make_partition! { /// Type of attestation used. ATTESTATION_ID = 4; - /// Used for the AAGUID before, but deprecated. - _AAGUID = 3; + /// Authenticator Attestation Global Unique Identifier. + AAGUID = 3; // This is the persistent key limit: // - When adding a (persistent) key above this message, make sure its value is smaller than diff --git a/libraries/opensk/src/ctap/mod.rs b/libraries/opensk/src/ctap/mod.rs index 8a2a091b..a36d421d 100644 --- a/libraries/opensk/src/ctap/mod.rs +++ b/libraries/opensk/src/ctap/mod.rs @@ -978,7 +978,7 @@ impl CtapState { }; let mut auth_data = self.generate_auth_data(env, &rp_id_hash, flags)?; - auth_data.extend(env.customization().aaguid()); + auth_data.extend(env.persist().aaguid()?); // The length is fixed to 0x20 or 0x80 and fits one byte. if credential_id.len() > 0xFF { return Err(Ctap2StatusCode::CTAP2_ERR_VENDOR_INTERNAL_ERROR); @@ -1390,7 +1390,7 @@ impl CtapState { String::from("credBlob"), String::from("largeBlobKey"), ]), - aaguid: *env.customization().aaguid(), + aaguid: env.persist().aaguid()?, options: Some(options), max_msg_size: Some(env.customization().max_msg_size() as u64), // The order implies preference. We favor the new V2. @@ -1536,7 +1536,7 @@ mod test { expected_credential_id_size: u8, expected_extension_cbor: &[u8], ) { - let expected_aaguid = env.customization().aaguid(); + let expected_aaguid = env.persist().aaguid().unwrap(); let signature_counter = env.persist().global_signature_counter().unwrap(); match make_credential_response.as_ref().unwrap() { ResponseData::AuthenticatorMakeCredential(make_credential_response) => { @@ -1577,6 +1577,7 @@ mod test { #[cfg(feature = "fingerprint")] fn test_get_info() { let mut env = TestEnv::default(); + let aaguid = env.persist().aaguid().unwrap(); let mut ctap_state = CtapState::::new(&mut env); let info_reponse = ctap_state.process_command(&mut env, &[0x04], DUMMY_CHANNEL); @@ -1596,7 +1597,7 @@ mod test { String::from("credBlob"), String::from("largeBlobKey"), ], - 0x03 => env.customization().aaguid(), + 0x03 => &aaguid[..], 0x04 => cbor_map_options! { "ep" => env.customization().enterprise_attestation_mode().map(|_| false), "rk" => true, @@ -1643,6 +1644,7 @@ mod test { #[cfg(not(feature = "fingerprint"))] fn test_get_info() { let mut env = TestEnv::default(); + let aaguid = env.persist().aaguid().unwrap(); let mut ctap_state = CtapState::::new(&mut env); let info_reponse = ctap_state.process_command(&mut env, &[0x04], DUMMY_CHANNEL); @@ -1662,7 +1664,7 @@ mod test { String::from("credBlob"), String::from("largeBlobKey"), ], - 0x03 => env.customization().aaguid(), + 0x03 => &aaguid[..], 0x04 => cbor_map_options! { "ep" => env.customization().enterprise_attestation_mode().map(|_| false), "rk" => true, @@ -1783,7 +1785,7 @@ mod test { match make_credential_response { ResponseData::AuthenticatorMakeCredential(make_credential_response) => { let auth_data = make_credential_response.auth_data; - let offset = 37 + env.customization().aaguid().len(); + let offset = 37 + env.persist().aaguid().unwrap().len(); assert_eq!(auth_data[offset], 0x00); assert_eq!(auth_data[offset + 1] as usize, CBOR_CREDENTIAL_ID_SIZE); auth_data[offset + 2..offset + 2 + CBOR_CREDENTIAL_ID_SIZE].to_vec() diff --git a/libraries/opensk/src/env/test/customization.rs b/libraries/opensk/src/env/test/customization.rs index fd7d5976..9e3f012f 100644 --- a/libraries/opensk/src/env/test/customization.rs +++ b/libraries/opensk/src/env/test/customization.rs @@ -12,13 +12,12 @@ // See the License for the specific language governing permissions and // limitations under the License. -use crate::api::customization::{AAGUID_LENGTH, Customization, CustomizationImpl}; +use crate::api::customization::{Customization, CustomizationImpl}; use crate::ctap::data_formats::{CredentialProtectionPolicy, EnterpriseAttestationMode}; use alloc::string::String; use alloc::vec::Vec; pub struct TestCustomization { - aaguid: &'static [u8; AAGUID_LENGTH], allows_pin_protocol_v1: bool, default_cred_protect: Option, default_min_pin_length: u8, @@ -63,10 +62,6 @@ impl TestCustomization { } impl Customization for TestCustomization { - fn aaguid(&self) -> &'static [u8; AAGUID_LENGTH] { - self.aaguid - } - fn allows_pin_protocol_v1(&self) -> bool { self.allows_pin_protocol_v1 } @@ -159,7 +154,6 @@ impl Customization for TestCustomization { impl From for TestCustomization { fn from(c: CustomizationImpl) -> Self { let CustomizationImpl { - aaguid, allows_pin_protocol_v1, default_cred_protect, default_min_pin_length, @@ -197,7 +191,6 @@ impl From for TestCustomization { .collect::>(); Self { - aaguid, allows_pin_protocol_v1, default_cred_protect, default_min_pin_length, diff --git a/src/env.rs b/src/env.rs index 1c54855f..7881130b 100644 --- a/src/env.rs +++ b/src/env.rs @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -use opensk::api::customization::{AAGUID_LENGTH, CustomizationImpl, DEFAULT_CUSTOMIZATION}; +use opensk::api::customization::{CustomizationImpl, DEFAULT_CUSTOMIZATION}; use opensk::ctap::status_code::Ctap2StatusCode; use opensk::env::Env; use wasefire::Error; @@ -26,17 +26,12 @@ pub(crate) mod hid_connection; pub(crate) mod persist; mod rng; mod user_presence; +mod vendor; mod write; -pub const AAGUID: &[u8; AAGUID_LENGTH] = - include_bytes!(concat!(env!("OUT_DIR"), "/opensk_aaguid.bin")); - pub(crate) fn init() -> WasefireEnv { WasefireEnv { - customization: CustomizationImpl { - aaguid: AAGUID, - ..DEFAULT_CUSTOMIZATION - }, + customization: DEFAULT_CUSTOMIZATION, user_presence: user_presence::init(), #[cfg(feature = "fingerprint")] fingerprint: fingerprint::init(), @@ -104,6 +99,14 @@ impl Env for WasefireEnv { // TODO: The applet needs to know if the platform did a cold boot. false } + + fn process_vendor_command( + &mut self, + bytes: &[u8], + channel: opensk::ctap::Channel, + ) -> Option> { + vendor::process_vendor_command(self, bytes, channel) + } } impl opensk::api::key_store::Helper for WasefireEnv {} diff --git a/src/env/vendor.rs b/src/env/vendor.rs new file mode 100644 index 00000000..9357571b --- /dev/null +++ b/src/env/vendor.rs @@ -0,0 +1,211 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use crate::env::WasefireEnv; +use alloc::vec; +use alloc::vec::Vec; +use core::convert::TryFrom; +use opensk::api::persist::{AAGUID_LENGTH, Attestation, AttestationId, Persist}; +use opensk::ctap::data_formats::{extract_byte_string, extract_map}; +use opensk::ctap::status_code::{Ctap2StatusCode, CtapResult}; +use opensk::ctap::{Channel, cbor_read, cbor_write}; +use opensk::env::Env; +use sk_cbor::{Value, cbor_map_options, destructure_cbor_map}; + +const VENDOR_COMMAND_CONFIGURE: u8 = 0x40; + +pub fn process_vendor_command( + env: &mut WasefireEnv, + bytes: &[u8], + channel: Channel, +) -> Option> { + if bytes.is_empty() || bytes[0] != VENDOR_COMMAND_CONFIGURE { + return None; + } + process_cbor(env, &bytes[1..], channel).unwrap_or_else(|e| Some(vec![e as u8])) +} + +fn process_cbor( + env: &mut WasefireEnv, + cbor_bytes: &[u8], + channel: Channel, +) -> CtapResult>> { + let decoded_cbor = cbor_read(cbor_bytes)?; + let params = VendorConfigureParameters::try_from(decoded_cbor)?; + let response = process_vendor_configure(env, params, channel)?; + Ok(Some(encode_cbor(response.into()))) +} + +fn encode_cbor(value: Value) -> Vec { + let mut response_vec = vec![Ctap2StatusCode::CTAP2_OK as u8]; + if cbor_write(value, &mut response_vec).is_err() { + vec![Ctap2StatusCode::CTAP2_ERR_VENDOR_INTERNAL_ERROR as u8] + } else { + response_vec + } +} + +#[derive(Debug, PartialEq, Eq)] +pub struct VendorConfigureParameters { + pub aaguid: Option<[u8; AAGUID_LENGTH]>, + pub attestation_material: Option, +} + +impl TryFrom for VendorConfigureParameters { + type Error = Ctap2StatusCode; + + fn try_from(cbor_value: Value) -> CtapResult { + destructure_cbor_map! { + let { + 0x01 => aaguid, + 0x02 => attestation_material, + } = extract_map(cbor_value)?; + } + let aaguid = aaguid.map(extract_byte_string).transpose()?; + let aaguid = aaguid + .map(<[u8; AAGUID_LENGTH]>::try_from) + .transpose() + .map_err(|_| Ctap2StatusCode::CTAP1_ERR_INVALID_PARAMETER)?; + let attestation_material = attestation_material + .map(Attestation::try_from) + .transpose()?; + Ok(VendorConfigureParameters { + aaguid, + attestation_material, + }) + } +} + +#[derive(Debug, PartialEq, Eq)] +pub struct VendorConfigureResponse { + pub cert_programmed: bool, + pub pkey_programmed: bool, + pub aaguid_programmed: bool, +} + +impl From for Value { + fn from(vendor_response: VendorConfigureResponse) -> Self { + let VendorConfigureResponse { + cert_programmed, + pkey_programmed, + aaguid_programmed, + } = vendor_response; + + cbor_map_options! { + 0x01 => cert_programmed, + 0x02 => pkey_programmed, + 0x03 => aaguid_programmed, + } + } +} + +fn process_vendor_configure( + env: &mut WasefireEnv, + params: VendorConfigureParameters, + channel: Channel, +) -> CtapResult { + opensk::ctap::check_user_presence(env, channel)?; + + let attestation_id = AttestationId::Batch; + let has_existing_attestation = env + .persist() + .find(opensk::api::persist::keys::ATTESTATION_ID)? + .is_some(); + let has_existing_aaguid = env + .persist() + .find(opensk::api::persist::keys::AAGUID)? + .is_some(); + + let mut response = VendorConfigureResponse { + cert_programmed: has_existing_attestation, + pkey_programmed: has_existing_attestation, + aaguid_programmed: has_existing_aaguid, + }; + + if let Some(data) = params.attestation_material + && !has_existing_attestation + { + env.persist().set_attestation(attestation_id, Some(&data))?; + response.cert_programmed = true; + response.pkey_programmed = true; + } + + if let Some(aaguid_bytes) = params.aaguid + && !has_existing_aaguid + { + env.persist().set_aaguid(&aaguid_bytes)?; + response.aaguid_programmed = true; + } + + Ok(response) +} + +#[cfg(test)] +mod test { + use super::*; + use sk_cbor::cbor_map; + + #[test] + fn test_vendor_configure_parameters() { + let dummy_aaguid = [0x77u8; 16]; + let dummy_cert = [0xddu8; 20]; + let dummy_pkey = [0x41u8; 32]; + + // Invalid aaguid length + let cbor_value = cbor_map! { + 0x01 => vec![0; 15], + }; + assert_eq!( + VendorConfigureParameters::try_from(cbor_value), + Err(Ctap2StatusCode::CTAP1_ERR_INVALID_PARAMETER) + ); + + // Valid with attestation and aaguid + let cbor_value = cbor_map! { + 0x01 => dummy_aaguid, + 0x02 => cbor_map! { + 0x01 => dummy_cert, + 0x02 => dummy_pkey, + }, + }; + assert_eq!( + VendorConfigureParameters::try_from(cbor_value), + Ok(VendorConfigureParameters { + aaguid: Some(dummy_aaguid), + attestation_material: Some(Attestation { + wrapped_private_key: dummy_pkey.to_vec(), + certificate: dummy_cert.to_vec(), + }), + }) + ); + } + + #[test] + fn test_vendor_response_into_cbor() { + let response_cbor: Value = VendorConfigureResponse { + cert_programmed: true, + pkey_programmed: false, + aaguid_programmed: true, + } + .into(); + assert_eq!( + response_cbor, + cbor_map_options! { + 0x01 => true, + 0x02 => false, + 0x03 => true, + } + ); + } +} diff --git a/tools/configure.py b/tools/configure.py index ae4f87a0..bd1796ca 100644 --- a/tools/configure.py +++ b/tools/configure.py @@ -36,7 +36,7 @@ from fido2 import ctap2 from fido2 import hid -OPENSK_VID_PID = (0x1915, 0x521F) +OPENSK_VID_PID = (0x18D1, 0x0239) OPENSK_VENDOR_CONFIGURE = 0x40 @@ -114,7 +114,16 @@ def main(args): fatal("Certificate public doesn't match with the private key.") info("Certificate is valid.") + aaguid_str = args.aaguid_file.read().strip() + try: + aaguid_obj = uuid.UUID(aaguid_str) + except ValueError: + fatal("Invalid AAGUID UUID format.") + aaguid_bytes = aaguid_obj.bytes + info(f"AAGUID to program: {aaguid_obj}") + cbor_data = {} + cbor_data[1] = aaguid_bytes cbor_data[2] = { 1: cert.public_bytes(serialization.Encoding.DER), 2: priv_key.private_numbers().private_value.to_bytes( @@ -148,10 +157,11 @@ def main(args): OPENSK_VENDOR_CONFIGURE, data=cbor_data, ) - status = {"cert": result[1], "pkey": result[2]} + status = {"cert": result[1], "pkey": result[2], "aaguid": result[3]} responses.append(status) info(f"Certificate: {'Present' if result[1] else 'Missing'}") info(f"Private Key: {'Present' if result[2] else 'Missing'}") + info(f"AAGUID: {'Present' if result[3] else 'Missing'}") except ctap.CtapError as ex: if ex.code.value == ctap.CtapError.ERR.INVALID_COMMAND: error("Failed to configure OpenSK (unsupported command).") @@ -205,6 +215,14 @@ def main(args): dest="priv_key", help=("PEM file containing the private key associated with the certificate."), ) + parser.add_argument( + "--aaguid", + type=argparse.FileType("r"), + required=True, + metavar="AAGUID_TXT_FILE", + dest="aaguid_file", + help="Path to aaguid.txt containing the UUID string.", + ) parser.add_argument( "--vendor-hid", default=False,