Aller au contenu

R03 — CTAP 2.1, clientPIN, credentialManagement et extensions

Statut : NON EXÉCUTÉ sur matériel — T2B1 et T3B1.

Appliquer le registre commun et conserver une ligne par modèle/OS requis. Les sorties des tests hôte/émulateur restent des preuves séparées.

Préconditions

R02 réussi ; PIN FIDO configuré et connu ; python-fido2 de uv.lock ; un seul authentificateur USB connecté ; tester les deux protocoles PIN sur chaque modèle.

Étapes

  1. Exécuter le script autonome ci-dessous depuis la racine avec uv run python, après l’avoir copié dans un fichier de la campagne. Il utilise le vrai HID et demande le PIN sans l’afficher. Approuver les créations/assertions demandées sur l’appareil.
  2. Vérifier getInfo : versions FIDO_2_1, options clientPin/credMgmt/largeBlobs/pinUvAuthToken, extensions credProtect/hmac-secret/largeBlobKey. Aucune option biométrique/enterprise non implémentée ne doit être annoncée.
  3. Répéter une gestion PIN avec changement depuis un client standard : ancien PIN refusé, nouveau accepté. Consommer un essai avec une valeur erronée et contrôler que le compteur diminue après rebranchement ; ne pas épuiser les essais hors campagne R15.
  4. Vérifier en parallèle dans un navigateur un usage réel de la passkey. Supprimer les entrées créées par le script depuis le menu après conservation des preuves.

Résultat attendu

Chaque fonction annoncée s’exécute effectivement ; credProtect=3 masque sans UV ; signature vérifiée ; hmac-secret reproductible à défi égal et différent à défi changé ; largeBlob relu identique ; credentialManagement liste le RP et ses identifiants. PIN 1 et 2 passent.

Preuves à conserver

getInfo, versions client, sorties/assertions du script, compteurs PIN avant/après, succès navigateur et liste des entrées effacées ; pas de PIN/token/hmac-secret publié.

Script HID de campagne

Le corps du script a été exécuté contre la pile CTAP simulée du dépôt pour vérifier les appels python-fido2 ; aucun résultat matériel n’en découle. Il crée deux identifiants de test, un par protocole PIN, et modifie largeBlobs. Copier ce bloc dans un fichier privé de campagne, puis lancer ce fichier avec uv run python. Le PIN FIDO doit déjà être défini sur l’appareil.

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

# Un seul authentificateur USB de test doit être branché.
devices = list(CtapHidDevice.list_devices())
if len(devices) != 1:
    raise SystemExit("Brancher uniquement le feuristic de test puis recommencer")
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("PIN FIDO de l'appareil de test : ")
    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 divulgue une assertion sans 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()