Files
Skey/tools/gen_attestation.py
T
ckochandClaude Opus 5.5 6c7e6ab5f5 Skey: a FIDO2 security key on the Raspberry Pi Pico
Firmware for the Pico / Pico 2 that acts as a USB FIDO2 authenticator:
CTAP2 and U2F, passkeys, BOOTSEL as the touch button, and clientPIN with
PIN/UV auth protocols 1 and 2. A host simulator runs the same core so the
python-fido2 test suite exercises it without hardware.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 15:07:38 -04:00

104 lines
3.7 KiB
Python

#!/usr/bin/env python3
"""Creates the batch attestation key and certificate and emits them as C source.
The key and certificate are kept in --key-dir and reused, so rebuilding keeps
the same attestation identity. Delete that directory to start over.
"""
import argparse
import datetime
import pathlib
import uuid
from cryptography import x509
from cryptography.hazmat.primitives import hashes, serialization
from cryptography.hazmat.primitives.asymmetric import ec
from cryptography.x509.oid import NameOID
AAGUID = uuid.UUID("225e7977-54f1-4d15-8e35-295b11658d5c")
OID_FIDO_AAGUID = x509.ObjectIdentifier("1.3.6.1.4.1.45724.1.1.4")
def create(key_path: pathlib.Path, cert_path: pathlib.Path) -> None:
key = ec.generate_private_key(ec.SECP256R1())
# Subject fields required for packed attestation certificates (WebAuthn 8.2.1).
name = x509.Name(
[
x509.NameAttribute(NameOID.COUNTRY_NAME, "US"),
x509.NameAttribute(NameOID.ORGANIZATION_NAME, "Skey DIY"),
x509.NameAttribute(NameOID.ORGANIZATIONAL_UNIT_NAME, "Authenticator Attestation"),
x509.NameAttribute(NameOID.COMMON_NAME, "Skey Pico Attestation"),
]
)
now = datetime.datetime.now(datetime.timezone.utc)
cert = (
x509.CertificateBuilder()
.subject_name(name)
.issuer_name(name)
.public_key(key.public_key())
.serial_number(x509.random_serial_number())
.not_valid_before(now - datetime.timedelta(days=1))
.not_valid_after(datetime.datetime(2049, 12, 31, tzinfo=datetime.timezone.utc))
.add_extension(x509.BasicConstraints(ca=False, path_length=None), critical=True)
.add_extension(x509.UnrecognizedExtension(OID_FIDO_AAGUID, b"\x04\x10" + AAGUID.bytes), critical=False)
.sign(key, hashes.SHA256())
)
key_path.parent.mkdir(parents=True, exist_ok=True)
key_path.write_bytes(
key.private_bytes(
serialization.Encoding.PEM,
serialization.PrivateFormat.PKCS8,
serialization.NoEncryption(),
)
)
key_path.chmod(0o600)
cert_path.write_bytes(cert.public_bytes(serialization.Encoding.DER))
def cert_aaguid(cert_der: bytes) -> bytes:
cert = x509.load_der_x509_certificate(cert_der)
try:
return cert.extensions.get_extension_for_oid(OID_FIDO_AAGUID).value.value[2:]
except x509.ExtensionNotFound:
return b""
def c_bytes(data: bytes) -> str:
rows = [", ".join(f"0x{b:02x}" for b in data[i : i + 12]) for i in range(0, len(data), 12)]
return "{\n " + ",\n ".join(rows) + ",\n}"
def main() -> None:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--key-dir", type=pathlib.Path, required=True)
parser.add_argument("--out", type=pathlib.Path, required=True)
args = parser.parse_args()
key_path = args.key_dir / "attestation_key.pem"
cert_path = args.key_dir / "attestation_cert.der"
if not key_path.exists() or not cert_path.exists() or cert_aaguid(cert_path.read_bytes()) != AAGUID.bytes:
create(key_path, cert_path)
key = serialization.load_pem_private_key(key_path.read_bytes(), password=None)
private_value = key.private_numbers().private_value.to_bytes(32, "big")
cert_der = cert_path.read_bytes()
args.out.parent.mkdir(parents=True, exist_ok=True)
args.out.write_text(
f"""// Generated by tools/gen_attestation.py. Do not edit.
#include "attestation.h"
const uint8_t skey_aaguid[16] = {c_bytes(AAGUID.bytes)};
const uint8_t attestation_key[32] = {c_bytes(private_value)};
const uint8_t attestation_cert[] = {c_bytes(cert_der)};
const size_t attestation_cert_len = sizeof(attestation_cert);
"""
)
if __name__ == "__main__":
main()