R03 — CTAP 2.1, clientPIN, credentialManagement and extensions¶
Status: NOT EXECUTED as a complete hardware procedure — T2B1 and T3B1.
Use the shared record, with separate results per required model/OS. Host/emulator output is separate evidence.
Preconditions¶
R02 passed; configured known FIDO PIN; python-fido2 from uv.lock; only one USB authenticator connected; both PIN protocols tested on each model.
Steps¶
- Copy the standalone script below into a private campaign file and run it from
the repository root with
uv run python. It uses physical HID and prompts for a hidden PIN. Approve the requested registrations/assertions on-device. - Check getInfo: FIDO_2_1, clientPin/credMgmt/largeBlobs/pinUvAuthToken options, credProtect/hmac-secret/largeBlobKey extensions. No unimplemented biometric or enterprise option may be advertised.
- Change the PIN using a standard client: old PIN fails, new PIN succeeds. Use one incorrect attempt and confirm its counter stays reduced after reconnecting. Do not exhaust retries outside R15.
- Also verify real browser use of the passkey. Retain evidence, then delete the script’s entries using the local menu.
Expected result¶
Every advertised feature executes. credProtect=3 hides credentials without UV; signatures verify; hmac-secret repeats for equal challenges and changes for a different challenge; largeBlob reads back unchanged; credentialManagement lists the RP and credentials. PIN protocols 1 and 2 pass.
Evidence to retain¶
getInfo, client versions, script output/assertions, PIN counters, browser success and deleted-entry inventory. Publish no PIN, token or hmac-secret.
Campaign HID script¶
This script body was exercised against the repository’s simulated CTAP stack
to verify python-fido2 calls; that provides no hardware result. It creates
two test credentials (one per PIN protocol) and changes largeBlobs. Copy it into
a private campaign file and run with uv run python. The device FIDO PIN must
already be configured.
import getpass
import hashlib
import os
from fido2.ctap import CtapError
from fido2.ctap2 import CredentialManagement, Ctap2
from fido2.ctap2.blob import LargeBlobs
from fido2.ctap2.pin import ClientPin, PinProtocolV1, PinProtocolV2
from fido2.hid import CtapHidDevice
# Connect only the disposable USB authenticator under test.
devices = list(CtapHidDevice.list_devices())
if len(devices) != 1:
raise SystemExit("Connect only the test feuristic, then retry")
device = devices[0]
try:
ctap = Ctap2(device)
info = ctap.get_info()
print("Versions:", info.versions, "Options:", info.options)
print("Extensions:", info.extensions)
assert "FIDO_2_1" in info.versions
for option in ("clientPin", "credMgmt", "largeBlobs", "pinUvAuthToken"):
assert info.options.get(option), option
assert {"credProtect", "hmac-secret", "largeBlobKey"} <= set(info.extensions)
pin = getpass.getpass("Test device FIDO PIN: ")
rp = "acceptance.example"
cdh = hashlib.sha256(b"feuristic R03 test").digest()
perm = ClientPin.PERMISSION
for protocol_type in (PinProtocolV1, PinProtocolV2):
protocol = protocol_type()
client = ClientPin(ctap, protocol)
def authorization(permission, rp_id=None):
token = client.get_pin_token(pin, permission, rp_id)
return protocol.authenticate(token, cdh)
att = ctap.make_credential(
cdh,
{"id": rp},
{"id": os.urandom(32), "name": "R03-test"},
[{"type": "public-key", "alg": -7}],
extensions={"credProtect": 3, "hmac-secret": True, "largeBlobKey": True},
options={"rk": True},
pin_uv_param=authorization(perm.MAKE_CREDENTIAL, rp),
pin_uv_protocol=protocol.VERSION,
)
credential = att.auth_data.credential_data
allow = [{"type": "public-key", "id": credential.credential_id}]
assert att.auth_data.extensions["credProtect"] == 3
try:
ctap.get_assertion(rp, cdh, allow)
except CtapError as error:
assert error.code == CtapError.ERR.NO_CREDENTIALS, error
else:
raise AssertionError("credProtect=3 released an assertion without UV")
def challenge(salts):
auth = authorization(perm.GET_ASSERTION, rp)
agreement, shared = client._get_shared_secret()
encrypted = protocol.encrypt(shared, salts)
extension = {
1: agreement,
2: encrypted,
3: protocol.authenticate(shared, encrypted),
4: protocol.VERSION,
}
assertion = ctap.get_assertion(
rp, cdh, allow,
extensions={"hmac-secret": extension},
pin_uv_param=auth, pin_uv_protocol=protocol.VERSION,
)
credential.public_key.verify(bytes(assertion.auth_data) + cdh, assertion.signature)
return protocol.decrypt(shared, assertion.auth_data.extensions["hmac-secret"])
first = challenge(b"A" * 32)
assert first == challenge(b"A" * 32)
assert first != challenge(b"B" * 32)
token = client.get_pin_token(pin, perm.LARGE_BLOB_WRITE)
blobs = LargeBlobs(ctap, protocol, token)
data = b"feuristic R03 largeBlob"
blobs.put_blob(att.large_blob_key, data)
assert blobs.get_blob(att.large_blob_key) == data
token = client.get_pin_token(pin, perm.CREDENTIAL_MGMT)
management = CredentialManagement(ctap, protocol, token)
assert management.enumerate_rps()
assert management.enumerate_creds(hashlib.sha256(rp.encode()).digest())
print("PIN protocol", protocol.VERSION, ": PASS; metadata", management.get_metadata())
print("R03 script completed; browser/PIN-change checks remain separate")
finally:
device.close()