From b28128d2e02cb32fc86f0344f6f4cd1587131a64 Mon Sep 17 00:00:00 2001 From: Fabian Kaczmarczyck Date: Wed, 3 Jun 2026 17:28:50 +0200 Subject: [PATCH 1/2] Reintroduces the batch attestation cert command We can call `tools/configure.py` again to set the batch attestation private key and certificate. Also the AAGUID can now be passed in, instead of being part of customization. One only really makes sense with the other anyway. --- Cargo.lock | 2 +- Cargo.toml | 6 +- build.rs | 37 --- docs/customization.md | 37 ++- docs/install.md | 8 + libraries/opensk/src/api/customization.rs | 13 -- libraries/opensk/src/api/persist.rs | 32 +++ libraries/opensk/src/api/persist/keys.rs | 4 +- libraries/opensk/src/ctap/mod.rs | 14 +- .../opensk/src/env/test/customization.rs | 9 +- src/env.rs | 19 +- src/env/vendor.rs | 211 ++++++++++++++++++ tools/configure.py | 22 +- 13 files changed, 322 insertions(+), 92 deletions(-) delete mode 100644 build.rs create mode 100644 src/env/vendor.rs 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..05532856 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 proof 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, From 1d9bce508421482f54154ec4eae1ab6c6783513d Mon Sep 17 00:00:00 2001 From: kaczmarczyck <43844792+kaczmarczyck@users.noreply.github.com> Date: Thu, 4 Jun 2026 11:04:23 +0200 Subject: [PATCH 2/2] Update docs/customization.md Co-authored-by: Julien Cretin --- docs/customization.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/customization.md b/docs/customization.md index 05532856..bcc157d0 100644 --- a/docs/customization.md +++ b/docs/customization.md @@ -48,7 +48,7 @@ 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 proof any +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: