From 65e40bb0309a52d1310bf3d50c56360226885a32 Mon Sep 17 00:00:00 2001 From: Pol Henarejos Date: Wed, 26 Aug 2026 01:51:53 +0200 Subject: [PATCH] Add vault for OpenPGP and PIV Signed-off-by: Pol Henarejos --- .github/workflows/test.yml | 10 + pico-keys-sdk | 2 +- src/openpgp/key_container.c | 2 +- src/openpgp/key_container.h | 1 + src/openpgp/openpgp_vault.c | 31 ++- src/openpgp/vault.h | 1 + tests/docker_env.sh | 20 +- .../test_040_pcsc.py} | 0 .../040_extra/test_041_vault.py} | 208 +++++++++++++++--- tests/pico-openpgp/conftest.py | 21 -- tests/run-test-in-docker.sh | 6 +- 11 files changed, 229 insertions(+), 73 deletions(-) rename tests/openpgp/{040_pcsc_extra/test_pcsc_extra.py => 040_extra/test_040_pcsc.py} (100%) rename tests/{pico-openpgp/test_080_vault.py => openpgp/040_extra/test_041_vault.py} (61%) delete mode 100644 tests/pico-openpgp/conftest.py diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 0bbce21..40ab50e 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -39,5 +39,15 @@ jobs: persist-credentials: false - name: Build in container run: ./tests/build-in-docker.sh + - name: Validate Vault test secrets + env: + PICO_OPENPGP_VAULT_ENROLLMENT_JSON: ${{ secrets.PICO_OPENPGP_VAULT_ENROLLMENT_JSON }} + PICO_OPENPGP_VAULT_PASSPHRASE: ${{ secrets.PICO_OPENPGP_VAULT_PASSPHRASE }} + run: | + test -n "$PICO_OPENPGP_VAULT_ENROLLMENT_JSON" || { echo "PICO_OPENPGP_VAULT_ENROLLMENT_JSON is required." >&2; exit 1; } + test -n "$PICO_OPENPGP_VAULT_PASSPHRASE" || { echo "PICO_OPENPGP_VAULT_PASSPHRASE is required." >&2; exit 1; } - name: Start emulation and test + env: + PICO_OPENPGP_VAULT_ENROLLMENT_JSON: ${{ secrets.PICO_OPENPGP_VAULT_ENROLLMENT_JSON }} + PICO_OPENPGP_VAULT_PASSPHRASE: ${{ secrets.PICO_OPENPGP_VAULT_PASSPHRASE }} run: ./tests/run-test-in-docker.sh diff --git a/pico-keys-sdk b/pico-keys-sdk index c791c1b..7b23b54 160000 --- a/pico-keys-sdk +++ b/pico-keys-sdk @@ -1 +1 @@ -Subproject commit c791c1be12a6355d46e4f7fba4ae323f8f1654ce +Subproject commit 7b23b54d88514f8f1f2c9d52e384fc894bf497ec diff --git a/src/openpgp/key_container.c b/src/openpgp/key_container.c index 2c42334..fb8f179 100644 --- a/src/openpgp/key_container.c +++ b/src/openpgp/key_container.c @@ -60,7 +60,7 @@ static uint16_t openpgp_key_record_fid(uint16_t fid, uint8_t slot, uint16_t obje return (uint16_t)((prefix << 8) | openpgp_key_container_id(fid)); } -static bool openpgp_key_container_is_piv(uint16_t fid) { +bool openpgp_key_container_is_piv(uint16_t fid) { bool retired_range = fid >= EF_PIV_KEY_RETIRED1 && fid <= EF_PIV_KEY_RETIRED17; bool retired_reserved_gap = fid == EF_PIV_KEY_RETIRED19 || fid == EF_PIV_KEY_RETIRED20 || fid == EF_PIV_KEY_RETIRED18; bool active = fid >= EF_PIV_KEY_AUTHENTICATION && fid <= EF_PIV_KEY_CARDAUTH; diff --git a/src/openpgp/key_container.h b/src/openpgp/key_container.h index 5eef687..165c4cd 100644 --- a/src/openpgp/key_container.h +++ b/src/openpgp/key_container.h @@ -25,6 +25,7 @@ #define OPENPGP_KEY_OBJECT_PUBLIC 0x0002u bool openpgp_key_container_supported(uint16_t fid); +bool openpgp_key_container_is_piv(uint16_t fid); bool openpgp_key_container_is_marker(const file_t *file); bool openpgp_key_container_physical_fid(uint16_t fid); bool openpgp_key_container_can_create(uint16_t fid); diff --git a/src/openpgp/openpgp_vault.c b/src/openpgp/openpgp_vault.c index 79ec71f..b7a0752 100644 --- a/src/openpgp/openpgp_vault.c +++ b/src/openpgp/openpgp_vault.c @@ -137,6 +137,14 @@ int openpgp_vault_store_kvault(openpgp_vault_app_t app, const uint8_t kvault[OPE return ret; } +int openpgp_vault_clear_openpgp(void) { + if (openpgp_vault_sdk_init() != PICOKEYS_OK) { + return PICOKEYS_EXEC_ERROR; + } + int ret = picokeys_vault_delete_kvault(OPENPGP_VAULT_APP_OPENPGP); + return ret == PICOKEYS_ERR_FILE_NOT_FOUND ? PICOKEYS_OK : ret; +} + int openpgp_vault_clear_wrappers(void) { if (openpgp_vault_sdk_init() != PICOKEYS_OK) { return PICOKEYS_EXEC_ERROR; @@ -316,13 +324,11 @@ static int openpgp_vault_export(openpgp_vault_app_t app, uint16_t fid, uint8_t a } else { uint8_t intermediate[OPENPGP_VAULT_PLAIN_MAX + PICOKEYS_VAULT_BLOB_TAG_SIZE] = { 0 }; - uint8_t first[PICOKEYS_VAULT_BLOB_TAG_SIZE]; - ret = picokeys_vault_encrypt_layer(picokeys_vault_algorithm_layer(algorithm, 0), keys[0], blob + OPENPGP_VAULT_BLOB_HEADER_SIZE, blob, OPENPGP_VAULT_BLOB_HEADER_SIZE, plain, plain_len, intermediate, first); + ret = picokeys_vault_encrypt_layer(picokeys_vault_algorithm_layer(algorithm, 0), keys[0], blob + OPENPGP_VAULT_BLOB_HEADER_SIZE, blob, OPENPGP_VAULT_BLOB_HEADER_SIZE, plain, plain_len, intermediate, intermediate + plain_len); if (ret == PICOKEYS_OK) { ret = picokeys_vault_encrypt_layer(picokeys_vault_algorithm_layer(algorithm, 1), keys[1], blob + OPENPGP_VAULT_BLOB_HEADER_SIZE + PICOKEYS_VAULT_BLOB_NONCE_SIZE, blob, OPENPGP_VAULT_BLOB_HEADER_SIZE, intermediate, plain_len + PICOKEYS_VAULT_BLOB_TAG_SIZE, blob + OPENPGP_VAULT_BLOB_HEADER_SIZE + nonce_len, blob + total_len - PICOKEYS_VAULT_BLOB_TAG_SIZE); } mbedtls_platform_zeroize(intermediate, sizeof(intermediate)); - mbedtls_platform_zeroize(first, sizeof(first)); } if (ret == PICOKEYS_OK) { *blob_len = total_len; @@ -402,7 +408,22 @@ static int openpgp_vault_import(openpgp_vault_app_t app, uint16_t target_fid, co mbedtls_platform_zeroize(expected_hash, sizeof(expected_hash)); } if (ret == PICOKEYS_OK) { - ret = openpgp_key_container_store(target_fid, decoded.private_data, decoded.private_len, decoded.public_len ? decoded.public_data : NULL, decoded.public_len, true); + uint8_t current_private[OPENPGP_MAX_OBJECT_SIZE] = { 0 }; + uint8_t current_public[OPENPGP_MAX_OBJECT_SIZE] = { 0 }; + byte_buffer_t current_private_output = BYTE_BUFFER(current_private, sizeof(current_private)); + byte_buffer_t current_public_output = BYTE_BUFFER(current_public, sizeof(current_public)); + int current_ret = openpgp_key_container_read_private(target_fid, FILE_OBJECT_OPERATION_UPDATE, true, ¤t_private_output); + if (current_ret == PICOKEYS_OK && decoded.public_len > 0) { + current_ret = openpgp_key_container_read_public(target_fid, ¤t_public_output); + } + if (current_ret == PICOKEYS_OK && current_private_output.len == decoded.private_len && current_public_output.len == decoded.public_len && memcmp(current_private, decoded.private_data, decoded.private_len) == 0 && memcmp(current_public, decoded.public_data, decoded.public_len) == 0) { + ret = PICOKEYS_OK; + } + else { + ret = openpgp_key_container_store(target_fid, decoded.private_data, decoded.private_len, decoded.public_len ? decoded.public_data : NULL, decoded.public_len, true); + } + mbedtls_platform_zeroize(current_private, sizeof(current_private)); + mbedtls_platform_zeroize(current_public, sizeof(current_public)); } mbedtls_platform_zeroize(&decoded, sizeof(decoded)); mbedtls_platform_zeroize(plain, sizeof(plain)); @@ -533,7 +554,7 @@ int openpgp_vault_command(openpgp_vault_app_t app) { return SW_INCORRECT_P1P2(); } int ret = openpgp_vault_import(app, fid, apdu.data, apdu.nc); - return ret == PICOKEYS_OK ? SW_OK() : ret == PICOKEYS_NO_LOGIN ? SW_SECURITY_STATUS_NOT_SATISFIED() : SW_DATA_INVALID(); + return ret == PICOKEYS_OK ? SW_OK() : ret == PICOKEYS_NO_LOGIN ? SW_SECURITY_STATUS_NOT_SATISFIED() : ret == PICOKEYS_ERR_NO_MEMORY || ret == PICOKEYS_ERR_MEMORY_FATAL ? SW_MEMORY_FAILURE() : SW_DATA_INVALID(); } if (P1(apdu) == OPENPGP_VAULT_SUBCOMMAND_UNENROLL) { if (P2(apdu) != 0 || apdu.nc != 0) { diff --git a/src/openpgp/vault.h b/src/openpgp/vault.h index f9234d5..e432102 100644 --- a/src/openpgp/vault.h +++ b/src/openpgp/vault.h @@ -38,6 +38,7 @@ bool openpgp_vault_is_enrolled(void); bool openpgp_vault_wrapper_available(openpgp_vault_app_t app); int openpgp_vault_load_kvault(openpgp_vault_app_t app, uint8_t kvault[OPENPGP_VAULT_KEY_SIZE]); int openpgp_vault_store_kvault(openpgp_vault_app_t app, const uint8_t kvault[OPENPGP_VAULT_KEY_SIZE]); +int openpgp_vault_clear_openpgp(void); int openpgp_vault_clear_wrappers(void); bool openpgp_vault_backup_authorized(openpgp_vault_app_t app); int openpgp_vault_command(openpgp_vault_app_t app); diff --git a/tests/docker_env.sh b/tests/docker_env.sh index 6bc2af1..e551590 100644 --- a/tests/docker_env.sh +++ b/tests/docker_env.sh @@ -83,9 +83,9 @@ ${DOCKER} image build \ run_in_docker() { - ENV_ARGS="" + ENV_ARGS=() while [ "$1" == "-e" ]; do - ENV_ARGS="${ENV_ARGS} $1 $2" + ENV_ARGS+=("$1" "$2") shift 2 done @@ -95,12 +95,14 @@ run_in_docker() shift 2 fi - ${DOCKER} container run --rm \ + DOCKER_ARGS=(container run --rm \ --cap-add SYS_PTRACE \ - --volume $PWD:$PWD \ - --workdir ${WORKDIR} \ - -e MAKEFLAGS \ - ${ENV_ARGS} \ - ${DOCKER_IMAGE_TAG} \ - $@ + --volume "$PWD:$PWD" \ + --workdir "$WORKDIR" \ + -e MAKEFLAGS) + if [ "${#ENV_ARGS[@]}" -gt 0 ]; then + DOCKER_ARGS+=("${ENV_ARGS[@]}") + fi + DOCKER_ARGS+=("$DOCKER_IMAGE_TAG" "$@") + "${DOCKER}" "${DOCKER_ARGS[@]}" } diff --git a/tests/openpgp/040_pcsc_extra/test_pcsc_extra.py b/tests/openpgp/040_extra/test_040_pcsc.py similarity index 100% rename from tests/openpgp/040_pcsc_extra/test_pcsc_extra.py rename to tests/openpgp/040_extra/test_040_pcsc.py diff --git a/tests/pico-openpgp/test_080_vault.py b/tests/openpgp/040_extra/test_041_vault.py similarity index 61% rename from tests/pico-openpgp/test_080_vault.py rename to tests/openpgp/040_extra/test_041_vault.py index 85c675a..db50ea7 100644 --- a/tests/pico-openpgp/test_080_vault.py +++ b/tests/openpgp/040_extra/test_041_vault.py @@ -1,8 +1,14 @@ +import base64 +import ctypes +import ctypes.util import hashlib +import json import os import struct +from pathlib import Path import pytest +from cryptography import x509 from cryptography.exceptions import InvalidTag from cryptography.hazmat.primitives import hashes, serialization from cryptography.hazmat.primitives.asymmetric import x448 @@ -10,7 +16,7 @@ from cryptography.hazmat.primitives.ciphers.aead import AESGCM, ChaCha20Poly1305 from cryptography.hazmat.primitives.kdf.hkdf import HKDF -VAULT_MAGIC = b"PKV1" +VAULT_MAGIC = b"PKV\x01" VAULT_ID_DOMAIN = b"PicoKeys Vault ID v1" VAULT_ENROLL_INFO = b"PicoKeys Vault enrollment v1" VAULT_ID_BYTES = 32 @@ -34,12 +40,72 @@ VAULT_UNENROLL = 0x06 OPENPGP_AID = bytes.fromhex("D27600012401") PIV_AID = bytes.fromhex("A000000308") +DEFAULT_ENROLLMENT = Path.home() / ".config" / "PicoKeys" / "vault" / "enrollment-35d3ddbcebc9-Test.json" + +OPENPGP_VAULT_FINGERPRINT_TAGS = { + 1: 0xC7, + 2: 0xC8, + 3: 0xC9, + # OpenPGP does not define a fingerprint DO for the symmetric AES key. + 4: 0xC5, +} def _vault_id(kvault): return hashlib.sha256(VAULT_ID_DOMAIN + kvault).digest() +def _derive_passphrase(passphrase, salt): + try: + from cryptography.hazmat.primitives.kdf.argon2 import Argon2id + return Argon2id(salt=salt, length=32, iterations=3, lanes=4, memory_cost=64 * 1024).derive(passphrase.encode()) + except ImportError: + library_name = ctypes.util.find_library("argon2") + if not library_name: + raise RuntimeError("Argon2id support is unavailable") + library = ctypes.CDLL(library_name) + hash_function = library.argon2id_hash_raw + hash_function.argtypes = [ctypes.c_uint32, ctypes.c_uint32, ctypes.c_uint32, ctypes.c_void_p, ctypes.c_size_t, ctypes.c_void_p, ctypes.c_size_t, ctypes.c_void_p, ctypes.c_size_t] + hash_function.restype = ctypes.c_int + output = ctypes.create_string_buffer(32) + password_bytes = passphrase.encode() + result = hash_function(3, 64 * 1024, 4, password_bytes, len(password_bytes), salt, len(salt), output, 32) + if result != 0: + raise RuntimeError(f"Argon2id failed: {result}") + return output.raw + + +def _create_enrollment(passphrase, kvault, private_key, label, certificate=b""): + salt = bytes(range(16)) + nonce = bytes(range(12)) + private_bytes = private_key.private_bytes(serialization.Encoding.Raw, serialization.PrivateFormat.Raw, serialization.NoEncryption()) + plain = json.dumps({ + "version": 1, + "kvault": base64.b64encode(kvault).decode(), + "x448_private": base64.b64encode(private_bytes).decode(), + "certificate": base64.b64encode(certificate).decode(), + "label": label, + "vault_id": _vault_id(kvault).hex() + }, separators=(",", ":")).encode() + ciphertext = AESGCM(_derive_passphrase(passphrase, salt)).encrypt(nonce, plain, b"PicoKeys Kvault envelope v1") + return { + "version": 1, + "label": label, + "vault_id": _vault_id(kvault).hex(), + "salt": base64.b64encode(salt).decode(), + "nonce": base64.b64encode(nonce).decode(), + "ciphertext": base64.b64encode(ciphertext).decode() + } + + +def _open_enrollment(value, passphrase): + salt = base64.b64decode(value["salt"]) + nonce = base64.b64decode(value["nonce"]) + ciphertext = base64.b64decode(value["ciphertext"]) + plain = AESGCM(_derive_passphrase(passphrase, salt)).decrypt(nonce, ciphertext, b"PicoKeys Kvault envelope v1") + return json.loads(plain) + + def _object_hash(app, fid): return hashlib.sha256(bytes([app, fid >> 8, fid & 0xFF])).digest() @@ -136,6 +202,20 @@ def _enrollment_packet(private_key, device_public, challenge, kvault, label): return struct.pack(">H", len(certificate)) + certificate + nonce + AESGCM(session_key).encrypt(nonce, plain, info) +def _certificate_enrollment_packet(certificate, private_key, device_public, challenge, kvault, label): + certificate_public = x509.load_der_x509_certificate(certificate).public_key().public_bytes(serialization.Encoding.Raw, serialization.PublicFormat.Raw) + info = VAULT_ENROLL_INFO + challenge + certificate_public + device_public + shared = private_key.exchange(x448.X448PublicKey.from_public_bytes(device_public)) + session_key = HKDF(algorithm=hashes.SHA256(), length=32, salt=None, info=info).derive(shared) + label_bytes = label.encode() + if len(label_bytes) > 64: + raise ValueError("vault label is too long") + plain = kvault + bytes([len(label_bytes)]) + label_bytes + nonce = os.urandom(NONCE_BYTES) + encrypted = AESGCM(session_key).encrypt(nonce, plain, info) + return struct.pack(">H", len(certificate)) + certificate + nonce + encrypted + + def _reader(card): return card._OpenPGP_Card__reader @@ -175,24 +255,54 @@ def _select(card, aid): def _live_card(request): - if os.environ.get("PICO_OPENPGP_VAULT_LIVE") != "1": - pytest.skip("set PICO_OPENPGP_VAULT_LIVE=1 for live vault APDU tests") return request.getfixturevalue("card") def _live_admin_card(request): card = _live_card(request) - password = os.environ.get("PICO_OPENPGP_VAULT_PW3") - if not password: - pytest.skip("set PICO_OPENPGP_VAULT_PW3 for authenticated live vault tests") + password = os.environ.get("PICO_OPENPGP_VAULT_PW3", "12345678") _select(card, OPENPGP_AID) - try: - card.verify(3, password.encode()) - except Exception as error: - pytest.skip(f"live PW3 unavailable: {error}") + assert card.verify(3, password.encode()) return card +def _live_enrollment(card): + enrollment_json = os.environ.get("PICO_OPENPGP_VAULT_ENROLLMENT_JSON") + if enrollment_json: + try: + value = json.loads(enrollment_json) + except json.JSONDecodeError as error: + pytest.fail(f"invalid PICO_OPENPGP_VAULT_ENROLLMENT_JSON: {error}") + else: + path = Path(os.environ.get("PICO_OPENPGP_VAULT_ENROLLMENT", str(DEFAULT_ENROLLMENT))) + if not path.is_file(): + pytest.fail(f"enrollment JSON does not exist: {path}") + value = json.loads(path.read_text(encoding="utf-8")) + passphrase = os.environ.get("PICO_OPENPGP_VAULT_PASSPHRASE") or "test" + plain = _open_enrollment(value, passphrase) + kvault = base64.b64decode(plain["kvault"]) + private_key = x448.X448PrivateKey.from_private_bytes(base64.b64decode(plain["x448_private"])) + certificate = base64.b64decode(plain["certificate"]) + certificate_object = x509.load_der_x509_certificate(certificate) + certificate_public = certificate_object.public_key().public_bytes(serialization.Encoding.Raw, serialization.PublicFormat.Raw) + private_public = private_key.public_key().public_bytes(serialization.Encoding.Raw, serialization.PublicFormat.Raw) + assert certificate_public == private_public + assert _vault_id(kvault).hex() == plain["vault_id"] + + _select(card, OPENPGP_AID) + password = os.environ.get("PICO_OPENPGP_VAULT_PW3", "12345678") + assert card.verify(3, password.encode()) + response, result = _raw(card, INS_VAULT, VAULT_START_ENROLLMENT, 0) + assert result == b"\x90\x00" + assert len(response) == 56 + VAULT_ENROLL_CHALLENGE_BYTES + packet = _certificate_enrollment_packet(certificate, private_key, response[:56], response[56:], kvault, plain.get("label", "")) + vault_id, result = _raw(card, INS_VAULT, VAULT_FINISH_ENROLLMENT, 0, packet) + assert result == b"\x90\x00" + assert len(vault_id) == VAULT_ID_BYTES + assert vault_id == _vault_id(kvault) + return password + + def test_vault_id_is_deterministic_and_256_bit(): kvault = bytes(range(32)) assert _vault_id(kvault) == _vault_id(kvault) @@ -200,6 +310,29 @@ def test_vault_id_is_deterministic_and_256_bit(): assert _vault_id(kvault) != _vault_id(bytes(range(1, 33))) +def test_create_and_open_enrollment_json(): + value = _create_enrollment("correct horse", bytes(range(32)), x448.X448PrivateKey.generate(), "test vault") + plain = _open_enrollment(value, "correct horse") + assert plain["vault_id"] == value["vault_id"] + assert plain["label"] == "test vault" + assert len(base64.b64decode(plain["kvault"])) == 32 + + +def test_wrong_enrollment_passphrase_is_rejected(): + value = _create_enrollment("correct horse", os.urandom(32), x448.X448PrivateKey.generate(), "") + with pytest.raises(InvalidTag): + _open_enrollment(value, "wrong horse") + + +def test_tampered_enrollment_json_is_rejected(): + value = _create_enrollment("secret", os.urandom(32), x448.X448PrivateKey.generate(), "") + ciphertext = bytearray(base64.b64decode(value["ciphertext"])) + ciphertext[0] ^= 1 + value["ciphertext"] = base64.b64encode(ciphertext).decode() + with pytest.raises(InvalidTag): + _open_enrollment(value, "secret") + + @pytest.mark.parametrize("algorithm", ALGORITHMS) def test_pk_v1_round_trip_for_every_algorithm(algorithm): kvault = bytes(range(32)) @@ -313,8 +446,7 @@ def test_live_vault_dispatch_and_parameter_validation(request): def test_live_enrollment_begin_and_malformed_finish(request): card = _live_admin_card(request) response, status = _raw(card, INS_VAULT, VAULT_START_ENROLLMENT, 0) - if status != b"\x90\x00": - pytest.skip(f"enrollment start unavailable: {status.hex()}") + assert status == b"\x90\x00" assert len(response) == 56 + VAULT_ENROLL_CHALLENGE_BYTES _, status = _raw(card, INS_VAULT, VAULT_FINISH_ENROLLMENT, 0, b"\0") assert status != b"\x90\x00" @@ -326,39 +458,47 @@ def test_live_unknown_vault_subcommand_is_rejected(request): _expect(card, INS_VAULT, 0x07, 0, status=b"\x6A\x86") -def test_live_export_import_roundtrip(request): - if os.environ.get("PICO_OPENPGP_VAULT_ROUNDTRIP") != "1": - pytest.skip("set PICO_OPENPGP_VAULT_ROUNDTRIP=1 for the live key round-trip") - card = _live_admin_card(request) - status = _expect(card, INS_VAULT, VAULT_STATUS, 0) - if status[3] != VAULT_ID_BYTES: - pytest.skip("OpenPGP vault is not enrolled") - blobs = [] - for algorithm in ALGORITHMS: - blob, result = _raw(card, INS_VAULT, VAULT_EXPORT, 1, bytes([algorithm])) - if result != b"\x90\x00": - pytest.skip(f"OpenPGP key handle 1 is not exportable: {result.hex()}") - assert blob[ALGORITHM_OFFSET] == algorithm - blobs.append(blob) - for blob in blobs: - _, result = _raw(card, INS_VAULT, VAULT_IMPORT, 1, blob) - assert result == b"\x90\x00" +@pytest.mark.parametrize("algorithm", ALGORITHMS) +def test_live_export_import_roundtrip(request, algorithm): + card = _live_card(request) + _live_enrollment(card) + + blob, result = _raw(card, INS_VAULT, VAULT_EXPORT, 1, bytes([algorithm])) + assert result == b"\x90\x00" + assert blob[ALGORITHM_OFFSET] == algorithm + _, result = _raw(card, INS_VAULT, VAULT_IMPORT, 1, blob) + assert result == b"\x90\x00" -def test_live_unenroll_requires_explicit_opt_in(request): - if os.environ.get("PICO_OPENPGP_VAULT_DESTRUCTIVE_TESTS") != "1": - pytest.skip("set PICO_OPENPGP_VAULT_DESTRUCTIVE_TESTS=1 to erase the device vault") +@pytest.mark.parametrize("handle,fingerprint_tag", OPENPGP_VAULT_FINGERPRINT_TAGS.items()) +def test_live_export_import_roundtrip_for_every_openpgp_key(request, handle, fingerprint_tag): + card = _live_card(request) + _live_enrollment(card) + fingerprint, result = _raw(card, 0xCA, 0, fingerprint_tag) + assert result == b"\x90\x00" + blob, result = _raw(card, INS_VAULT, VAULT_EXPORT, handle) + assert result == b"\x90\x00" + assert blob[:4] == VAULT_MAGIC + _, result = _raw(card, INS_VAULT, VAULT_IMPORT, handle, blob) + assert result == b"\x90\x00" + restored_fingerprint, result = _raw(card, 0xCA, 0, fingerprint_tag) + assert result == b"\x90\x00" + assert restored_fingerprint == fingerprint + + +def test_live_unenroll(request): card = _live_admin_card(request) _, status = _raw(card, INS_VAULT, VAULT_UNENROLL, 0) assert status == b"\x90\x00" response = _expect(card, INS_VAULT, VAULT_STATUS, 0) assert response[3] == 0 + _live_enrollment(card) def test_live_piv_vault_dispatch(request): card = _live_card(request) _, status = _raw(card, 0xA4, 0x04, 0, PIV_AID) - if status != b"\x90\x00": - pytest.skip("connected card does not expose the PIV application") + assert status == b"\x90\x00" response = _expect(card, INS_VAULT, VAULT_STATUS, 0) assert len(response) >= 37 + _select(card, OPENPGP_AID) diff --git a/tests/pico-openpgp/conftest.py b/tests/pico-openpgp/conftest.py deleted file mode 100644 index c98977b..0000000 --- a/tests/pico-openpgp/conftest.py +++ /dev/null @@ -1,21 +0,0 @@ -import sys -from pathlib import Path - -import pytest - - -OPENPGP_TEST_DIR = Path(__file__).parents[1] / "openpgp" -if str(OPENPGP_TEST_DIR) not in sys.path: - sys.path.insert(0, str(OPENPGP_TEST_DIR)) - - -@pytest.fixture(scope="session") -def card(): - from card_reader import get_ccid_device - from openpgp_card import OpenPGP_Card - - reader = get_ccid_device() - openpgp_card = OpenPGP_Card(reader) - openpgp_card.cmd_select_openpgp() - yield openpgp_card - reader.ccid_power_off() diff --git a/tests/run-test-in-docker.sh b/tests/run-test-in-docker.sh index b8cbb3d..0f4b6c9 100755 --- a/tests/run-test-in-docker.sh +++ b/tests/run-test-in-docker.sh @@ -2,5 +2,7 @@ source tests/docker_env.sh run_in_docker rm -f memory.flash -run_in_docker ./tests/start-up-and-test.sh - +run_in_docker \ + -e "PICO_OPENPGP_VAULT_ENROLLMENT_JSON=${PICO_OPENPGP_VAULT_ENROLLMENT_JSON:-}" \ + -e "PICO_OPENPGP_VAULT_PASSPHRASE=${PICO_OPENPGP_VAULT_PASSPHRASE:-}" \ + ./tests/start-up-and-test.sh