Add vault for OpenPGP and PIV

Signed-off-by: Pol Henarejos <pol.henarejos@cttc.es>
This commit is contained in:
Pol Henarejos
2026-08-26 01:51:53 +02:00
parent 71384b3b47
commit 65e40bb030
11 changed files with 229 additions and 73 deletions
+10
View File
@@ -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
+1 -1
View File
@@ -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;
+1
View File
@@ -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);
+26 -5
View File
@@ -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, &current_private_output);
if (current_ret == PICOKEYS_OK && decoded.public_len > 0) {
current_ret = openpgp_key_container_read_public(target_fid, &current_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) {
+1
View File
@@ -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);
+11 -9
View File
@@ -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[@]}"
}
@@ -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)
-21
View File
@@ -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()
+4 -2
View File
@@ -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