Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add dataclass for CHUID #628

Merged
merged 6 commits into from
Aug 28, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
73 changes: 72 additions & 1 deletion tests/test_piv.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
from ykman.piv import generate_random_management_key, parse_rfc4514_string
from ykman.piv import (
generate_random_management_key,
parse_rfc4514_string,
generate_chuid,
)

from yubikit.core import NotSupportedError, Version
from yubikit.piv import (
Expand All @@ -7,7 +11,10 @@
PIN_POLICY,
TOUCH_POLICY,
_do_check_key_support,
FascN,
Chuid,
)
from datetime import date

import pytest

Expand Down Expand Up @@ -96,3 +103,67 @@ def test_supported_algorithms(self):
_do_check_key_support(
Version(5, 7, 0), key_type, PIN_POLICY.DEFAULT, TOUCH_POLICY.DEFAULT
)


def test_fascn():
fascn = FascN(
agency_code=32,
system_code=1,
credential_number=92446,
credential_series=0,
individual_credential_issue=1,
person_identifier=1112223333,
organizational_category=1,
organizational_identifier=1223,
organization_association_category=2,
)

# https://www.idmanagement.gov/docs/pacs-tig-scepacs.pdf
# page 32
expected = bytes.fromhex("D0439458210C2C19A0846D83685A1082108CE73984108CA3FC")
assert bytes(fascn) == expected

assert FascN.from_bytes(expected) == fascn


def test_chuid():
guid = b"x" * 16
chuid = Chuid(
# Non-Federal Issuer FASC-N
fasc_n=FascN(9999, 9999, 999999, 0, 1, 0000000000, 3, 0000, 1),
guid=guid,
expiration_date=date(2030, 1, 1),
asymmetric_signature=b"",
)

expected = bytes.fromhex(
"3019d4e739da739ced39ce739d836858210842108421c84210c3eb3410787878787878787878"
"78787878787878350832303330303130313e00fe00"
)

assert bytes(chuid) == expected

assert Chuid.from_bytes(expected) == chuid


def test_chuid_deserialize():
chuid = Chuid(
buffer_length=123,
fasc_n=FascN(9999, 9999, 999999, 0, 1, 0000000000, 3, 0000, 1),
agency_code=b"1234",
organizational_identifier=b"5678",
duns=b"123456789",
guid=b"x" * 16,
expiration_date=date(2030, 1, 1),
authentication_key_map=b"1234567890",
asymmetric_signature=b"0987654321",
lrc=255,
)

assert Chuid.from_bytes(bytes(chuid)) == chuid


def test_chuid_generate():
chuid = Chuid.from_bytes(generate_chuid())
assert chuid.expiration_date == date(2030, 1, 1)
assert chuid.fasc_n.agency_code == 9999
45 changes: 39 additions & 6 deletions ykman/_cli/piv.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
PIN_POLICY,
TOUCH_POLICY,
DEFAULT_MANAGEMENT_KEY,
Chuid,
)
from yubikit.core.smartcard import ApduError, SW

Expand Down Expand Up @@ -78,6 +79,7 @@
)
from cryptography.hazmat.primitives import serialization, hashes
from cryptography.hazmat.backends import default_backend
from uuid import uuid4

import click
import datetime
Expand Down Expand Up @@ -960,6 +962,30 @@ def cert():
"""


def _update_chuid(session):
try:
chuid_data = session.get_object(OBJECT_ID.CHUID)
try:
chuid = Chuid.from_bytes(chuid_data)
except ValueError:
logger.debug("Leaving unparsable CHUID as-is")
return
if chuid.asymmetric_signature:
# Signed CHUID, leave it alone
logger.debug("Leaving signed CHUID as-is")
return
chuid.guid = uuid4().bytes
chuid_data = bytes(chuid)
logger.debug("Updating CHUID GUID")
except ApduError as e:
if e.sw == SW.FILE_NOT_FOUND:
logger.debug("Generating new CHUID")
chuid_data = generate_chuid()
else:
raise
session.put_object(OBJECT_ID.CHUID, chuid_data)


@cert.command("import")
@click.pass_context
@click_management_key_option
Expand Down Expand Up @@ -1054,7 +1080,7 @@ def do_verify():
_verify_pin_if_needed(ctx, session, do_verify, pin)

session.put_certificate(slot, cert_to_import, compress)
session.put_object(OBJECT_ID.CHUID, generate_chuid())
_update_chuid(session)
click.echo(f"Certificate imported into slot {slot.name}")


Expand Down Expand Up @@ -1094,7 +1120,9 @@ def export_certificate(ctx, format, slot, certificate):
@click_management_key_option
@click_pin_option
@click_slot_argument
@click.argument("public-key", type=click.File("rb"), metavar="PUBLIC-KEY")
@click.argument(
"public-key", type=click.File("rb"), metavar="PUBLIC-KEY", required=False
)
@click.option(
"-s",
"--subject",
Expand Down Expand Up @@ -1138,8 +1166,13 @@ def generate_certificate(
except NotSupportedError:
timeout = 1.0

data = public_key.read()
public_key = serialization.load_pem_public_key(data, default_backend())
if public_key:
data = public_key.read()
public_key = serialization.load_pem_public_key(data, default_backend())
elif session.version < (5, 4, 0):
raise CliFail("PUBLIC-KEY required for YubiKey prior to 5.4.")
else:
public_key = session.get_slot_metadata(slot).public_key

now = datetime.datetime.now(datetime.timezone.utc)
valid_to = now + datetime.timedelta(days=valid_days)
Expand All @@ -1157,7 +1190,7 @@ def generate_certificate(
session, slot, public_key, subject, now, valid_to, hash_algorithm
)
session.put_certificate(slot, cert)
session.put_object(OBJECT_ID.CHUID, generate_chuid())
_update_chuid(session)
click.echo(f"Certificate generated in slot {slot.name}.")
except ApduError:
raise CliFail("Certificate generation failed.")
Expand Down Expand Up @@ -1244,7 +1277,7 @@ def delete_certificate(ctx, management_key, pin, slot):
session = ctx.obj["session"]
_ensure_authenticated(ctx, pin, management_key)
session.delete_certificate(slot)
session.put_object(OBJECT_ID.CHUID, generate_chuid())
_update_chuid(session)
click.echo(f"Certificate in slot {slot.name} deleted.")


Expand Down
28 changes: 13 additions & 15 deletions ykman/piv.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@
ALGORITHM,
TAG_LRC,
SlotMetadata,
FascN,
Chuid,
)
from .util import display_serial

Expand All @@ -48,7 +50,8 @@
from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC
from cryptography.hazmat.backends import default_backend
from cryptography.x509.oid import NameOID
from datetime import datetime
from datetime import datetime, date
from uuid import uuid4
import logging
import struct
import os
Expand Down Expand Up @@ -468,23 +471,18 @@ def check_key(

def generate_chuid() -> bytes:
"""Generate a CHUID (Cardholder Unique Identifier)."""
# Non-Federal Issuer FASC-N
# [9999-9999-999999-0-1-0000000000300001]
FASC_N = (
b"\xd4\xe7\x39\xda\x73\x9c\xed\x39\xce\x73\x9d\x83\x68"
+ b"\x58\x21\x08\x42\x10\x84\x21\xc8\x42\x10\xc3\xeb"
)
# Expires on: 2030-01-01
EXPIRY = b"\x32\x30\x33\x30\x30\x31\x30\x31"

return (
Tlv(0x30, FASC_N)
+ Tlv(0x34, os.urandom(16))
+ Tlv(0x35, EXPIRY)
+ Tlv(0x3E)
+ Tlv(TAG_LRC)
chuid = Chuid(
# Non-Federal Issuer FASC-N
fasc_n=FascN(9999, 9999, 999999, 0, 1, 0000000000, 3, 0000, 1),
guid=uuid4().bytes,
# Expires on: 2030-01-01
expiration_date=date(2030, 1, 1),
asymmetric_signature=b"",
)

return bytes(chuid)


def generate_ccc() -> bytes:
"""Generate a CCC (Card Capability Container)."""
Expand Down
Loading