Skip to content

Instantly share code, notes, and snippets.

@joostd
Last active October 2, 2023 14:28
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save joostd/3e21aaa3394277a07b001890d502b376 to your computer and use it in GitHub Desktop.
Save joostd/3e21aaa3394277a07b001890d502b376 to your computer and use it in GitHub Desktop.
Python 3.10+ script that shows attributes from a YubiHSM2 attestation certificate
#!/usr/bin/env python3
# NOTE:
# requires cryptography (pip install cryptography)
from cryptography import x509
from cryptography.hazmat.backends import default_backend
import sys
# NOTE: uses PEP 634: Structural Pattern Matching
# Requires Python 3.10
assert sys.version_info >= (3, 10)
# https://developers.yubico.com/YubiHSM2/Concepts/Attestation.html
class OID:
FIRMWARE_VERSION = "1.3.6.1.4.1.41482.4.1" # Octet String
SERIAL_NUMBER = "1.3.6.1.4.1.41482.4.2" # Integer
ORIGIN = "1.3.6.1.4.1.41482.4.3" # Bit String
DOMAINS = "1.3.6.1.4.1.41482.4.4" # Bit String
CAPABILITIES = "1.3.6.1.4.1.41482.4.5" # Bit String
OBJECT_ID = "1.3.6.1.4.1.41482.4.6" # Integer
LABEL = "1.3.6.1.4.1.41482.4.9" # Utf8String
# Bit String names - Least significant bit first
# https://developers.yubico.com/YubiHSM2/Concepts/Capability.html
capabilities = [
"get-opaque", "put-opaque", "put-authentication-key", "put-asymmetric-key", "generate-asymmetric-key", "sign-pkcs", "sign-pss", "sign-ecdsa",
"sign-eddsa", "decrypt-pkcs", "decrypt-oaep", "derive-ecdh", "export-wrapped", "import-wrapped", "put-wrap-key", "generate-wrap-key",
"exportable-under-wrap", "set-option", "get-option", "get-pseudo-random", "put-mac-key", "generate-hmac-key", "sign-hmac", "verify-hmac",
"get-log-entries", "sign-ssh-certificate", "get-template", "put-template", "reset-device", "decrypt-otp", "create-otp-aead", "randomize-otp-aead",
"rewrap-from-otp-aead-key", "rewrap-to-otp-aead-key", "sign-attestation-certificate", "put-otp-aead-key", "generate-otp-aead-key", "wrap-data", "unwrap-data", "delete-opaque",
"delete-authentication-key", "delete-asymmetric-key", "delete-wrap-key", "delete-hmac-key", "delete-template", "delete-otp-aead-key", "change-authentication-key", "put-symmetric-key",
"generate-symmetric-key", "delete-symmetric-key", "decrypt-ecb", "encrypt-ecb", "decrypt-cbc", "encrypt-cbc"
]
# https://github.com/Yubico/yubihsm-shell/blob/541e3fade9a6d7bef9da158774a295b6b71de103/lib/yubihsm.h#L754
origins = [
"generated", # The object was generated on the device
"imported", # The object was imported into the device
"undefined",
"undefined",
"imported_wrapped", # The object was imported into the device under wrap. This is used in combination with objects original 'origin'
]
# https://developers.yubico.com/YubiHSM2/Concepts/Domain.html
domains = [ (i+1) for i in range(16) ]
def bits(n):
return [] if n<1 else [ bool(n&1) ] + bits(n>>1)
def bitnames(n, names):
return [ name for (bit,name) in zip(bits(n), names) if bit ]
def list_extensions(cert_path):
with open(cert_path, 'rb') as cert_file:
cert_data = cert_file.read()
cert = x509.load_pem_x509_certificate(cert_data, default_backend())
print(f"Subject : { cert.subject.rfc4514_string() }")
for ext in cert.extensions:
t, l, *v = ext.value.public_bytes()
assert len(v) == l
match ext.oid.dotted_string:
case OID.FIRMWARE_VERSION:
assert t == 4 # octet string of length 3
assert len(v) == 3
print(f" firmware_version : {v[0]}.{v[1]}.{v[2]}")
case OID.SERIAL_NUMBER:
assert t == 2 # Integer of length 4
assert len(v) == 4
serial = int.from_bytes(v, "big")
print(f" serial_number : {serial} ({ hex(serial) })")
case OID.ORIGIN:
assert t == 3 # Bit string of length 2
assert len(v) == 2
num = int.from_bytes(v, byteorder='big')
origin = bitnames(num, origins)
print(f" origin : { hex(num) } {origin}")
case OID.DOMAINS:
assert t == 3 # Bit string of length 3
assert len(v) == 3
num = int.from_bytes(v, byteorder='big')
capability = bitnames(num, capabilities)
print(f" domains : { hex(num) } {bitnames(num,domains)}")
case OID.CAPABILITIES:
assert t == 3 # Bit string of length 8
assert len(v) == 9
num = int.from_bytes(v, byteorder='big')
print(f" capabilities : { hex(num) } {bitnames(num,capabilities)}")
case OID.OBJECT_ID:
assert t == 2 # Integer of length 1 TODO varlen?
assert len(v) == 1 or len(v) == 2
id = int.from_bytes(v, "big")
print(f" object ID : { hex(id) }")
case OID.LABEL:
assert t == 12 # UTF8String of variable length
assert len(v) >= 0
print(f" label: { bytes(v).decode('utf-8') }")
case _:
print(f" {ext.oid.dotted_string} : type {hex(t)} length {l} value { hex(int.from_bytes(v, 'big')) }")
certificate_path = 'cert.pem'
if len(sys.argv) > 1:
certificate_path = sys.argv[1]
list_extensions(certificate_path)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment