diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b8b07977..0fe53a1d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -67,3 +67,84 @@ jobs: name: cypress-ui-test-captures ${{ matrix.os }} ${{ matrix.rust }} path: target/ui/ if-no-files-found: ignore + + pykmip-test: + name: pykmip-test + runs-on: ubuntu-18.04 + strategy: + matrix: + rust: [1.47.0, stable, beta] + features: ["hsm", "hsm,hsm-tests-kmip"] + steps: + - name: Checkout repository + uses: actions/checkout@v1 + + - name: Install Rust + uses: hecrj/setup-rust-action@v1 + with: + rust-version: ${{ matrix.rust }} + + - uses: actions/setup-python@v2 + with: + python-version: '3.x' + + - name: Install PyKMIP + uses: BSFishy/pip-action@v1 + with: + packages: pykmip + + - name: Compile the tests + run: | + cargo build --tests --no-default-features --features ${{ matrix.features }} + + - name: Run the tests against the PyKMIP server + run: | + cd test-resources/pykmip + python run-server.py & + sleep 5s + openssl s_client -connect 127.0.0.1:5696 || true + cd - + cargo test --no-default-features --features ${{ matrix.features }} -- --test-threads=1 2>&1 + + - name: Dump the PyKMIP log + if: always() + working-directory: test-resources/pykmip + run: | + ls -la + cat server.log + + softhsm2-test: + name: softhsm2-test + runs-on: ubuntu-20.04 + strategy: + matrix: + rust: [1.47.0, stable, beta] + features: ["hsm,hsm-tests-pkcs11"] + steps: + - name: Checkout repository + uses: actions/checkout@v1 + + - name: Install Rust + uses: hecrj/setup-rust-action@v1 + with: + rust-version: ${{ matrix.rust }} + + - name: Install SoftHSM2 + run: | + sudo apt update + sudo apt install -y softhsm2 + sudo usermod -aG softhsm $(whoami) + sg softhsm -c "softhsm2-util --init-token --slot 0 --label \"My token 1\" --pin 1234 --so-pin 1234" + + - name: Compile the tests + run: | + cargo build --tests --no-default-features --features ${{ matrix.features }} + + - name: Run the tests against SoftHSM2 + run: | + sg softhsm -c "cargo test --no-default-features --features ${{ matrix.features }} -- --test-threads=1 2>&1" + + - name: Dump the SoftHSM2 log + if: always() + run: | + cat /var/log/syslog \ No newline at end of file diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml index 8ee45804..0f39e68d 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/e2e.yml @@ -151,7 +151,7 @@ jobs: - name: Dump diagnostics on failure if: failure() - working-directory: ./rpki-deploy/terraform/krill-e2e-test/run_on_do + working-directory: ./rpki-deploy/terraform/krill-e2e-test/run_on_localhost run: | set -x terraform output docker_env_vars diff --git a/Cargo.lock b/Cargo.lock index 56d5430c..724c5cca 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -82,6 +82,17 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cdb031dd78e28731d87d56cc8ffef4a8f36ca26c38fe2de700543e627f8a464a" +[[package]] +name = "backoff" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fe17f59a06fe8b87a6fc8bf53bb70b3aba76d7685f432487a68cd5552853625" +dependencies = [ + "getrandom 0.2.3", + "instant", + "rand 0.8.4", +] + [[package]] name = "backtrace" version = "0.3.61" @@ -288,6 +299,28 @@ dependencies = [ "subtle", ] +[[package]] +name = "cryptoki" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "503aa2bd88796da9bc6baf2c47696da40f135721b3d6680c7c6cee0b7d1f7a59" +dependencies = [ + "cryptoki-sys", + "derivative", + "libloading", + "log", +] + +[[package]] +name = "cryptoki-sys" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aec220169d3b1705b54bce57e459873828e5c3bf0e25b96e5f2142acbbb71dda" +dependencies = [ + "libloading", + "target-lexicon", +] + [[package]] name = "ctrlc" version = "3.2.1" @@ -298,6 +331,17 @@ dependencies = [ "winapi", ] +[[package]] +name = "derivative" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fcc3dd5e9e9c0b295d6e1e4d811fb6f157d5ffd784b8d202fc62eac8035a770b" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "deunicode" version = "0.4.3" @@ -364,6 +408,28 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "enum-display-derive" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f16ef37b2a9b242295d61a154ee91ae884afff6b8b933b486b12481cc58310ca" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "enum-flags" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3682d2328e61f5529088a02cd20bb0a9aeaeeeb2f26597436dd7d75d1340f8f5" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "error-chain" version = "0.11.0" @@ -777,10 +843,45 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "kmip-protocol" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c232c81ccbf10282ec2830fbd81f7f002e2548e4c169aada86c6b444bcfa89e3" +dependencies = [ + "cfg-if", + "enum-display-derive", + "enum-flags", + "kmip-ttlv", + "log", + "maybe-async", + "openssl", + "rustc_version 0.4.0", + "serde", + "serde_bytes", + "serde_derive", + "trait-set", +] + +[[package]] +name = "kmip-ttlv" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "99f11aebbf4a381db6ea786b4148d6e03285c0e58bd778d5f407b4b5ef8b57c2" +dependencies = [ + "cfg-if", + "hex", + "maybe-async", + "rustc_version 0.4.0", + "serde", + "trait-set", +] + [[package]] name = "krill" version = "0.9.5-dev" dependencies = [ + "backoff", "base64 0.13.0", "basic-cookies", "bcder", @@ -788,6 +889,7 @@ dependencies = [ "chrono", "clap", "clokwerk", + "cryptoki", "ctrlc", "fern", "futures", @@ -796,18 +898,21 @@ dependencies = [ "hyper", "intervaltree", "jmespatch", + "kmip-protocol", "libc", "libflate", "log", + "once_cell", "openidconnect", "openssl", "oso", + "r2d2", "rand 0.8.4", "regex", "reqwest", "rpassword", "rpki", - "rustc_version", + "rustc_version 0.2.3", "scrypt", "serde", "serde_json", @@ -886,6 +991,16 @@ dependencies = [ "rle-decode-fast", ] +[[package]] +name = "libloading" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "afe203d669ec979b7128619bae5a63b7b42e9203c1b29146079ee05e2f604b52" +dependencies = [ + "cfg-if", + "winapi", +] + [[package]] name = "lock_api" version = "0.4.5" @@ -925,6 +1040,17 @@ version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a3e378b66a060d48947b590737b30a1be76706c8dd7b8ba0f2fe3989c68a853f" +[[package]] +name = "maybe-async" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6007f9dad048e0a224f27ca599d669fca8cfa0dac804725aab542b2eb032bce6" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "memchr" version = "2.4.1" @@ -1334,6 +1460,17 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "r2d2" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "545c5bc2b880973c9c10e4067418407a0ccaa3091781d1671d46eb35107cb26f" +dependencies = [ + "log", + "parking_lot", + "scheduled-thread-pool", +] + [[package]] name = "rand" version = "0.7.3" @@ -1565,7 +1702,16 @@ version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "138e3e0acb6c9fb258b19b67cb8abd63c00679d2851805ea151465464fe9030a" dependencies = [ - "semver", + "semver 0.9.0", +] + +[[package]] +name = "rustc_version" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366" +dependencies = [ + "semver 1.0.4", ] [[package]] @@ -1612,6 +1758,15 @@ dependencies = [ "winapi", ] +[[package]] +name = "scheduled-thread-pool" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc6f74fd1204073fa02d5d5d68bec8021be4c38690b61264b2fdb48083d0e7d7" +dependencies = [ + "parking_lot", +] + [[package]] name = "scopeguard" version = "1.1.0" @@ -1672,6 +1827,12 @@ dependencies = [ "semver-parser", ] +[[package]] +name = "semver" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "568a8e6258aa33c13358f81fd834adb854c6f7c9468520910a9b1e8fac068012" + [[package]] name = "semver-parser" version = "0.7.0" @@ -1697,6 +1858,15 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_bytes" +version = "0.11.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16ae07dd2f88a366f15bd0632ba725227018c69a1c8550a927324f8eb8368bb9" +dependencies = [ + "serde", +] + [[package]] name = "serde_derive" version = "1.0.130" @@ -1853,6 +2023,12 @@ dependencies = [ "time", ] +[[package]] +name = "target-lexicon" +version = "0.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9bffcddbc2458fa3e6058414599e3c838a022abae82e5c67b4f7f80298d5bff" + [[package]] name = "tempfile" version = "3.2.0" @@ -2118,6 +2294,17 @@ dependencies = [ "tracing-serde", ] +[[package]] +name = "trait-set" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "875c4c873cc824e362fa9a9419ffa59807244824275a44ad06fec9684fff08f2" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "try-lock" version = "0.2.3" diff --git a/Cargo.toml b/Cargo.toml index d173651e..0b2d3a0b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -21,6 +21,7 @@ exclude = [ build = "build.rs" [dependencies] +backoff = { version = "0.3.0", optional = true } base64 = "^0.13" basic-cookies = { version = "^0.1", optional = true } bcder = "0.6.1" @@ -35,11 +36,15 @@ hex = "^0.4" hyper = { version = "^0.14", features = ["server"] } intervaltree = "0.2.6" jmespatch = { version = "^0.3", features = ["sync"], optional = true } +kmip = { version = "0.4.1", package = "kmip-protocol", features = ["tls-with-openssl"], optional = true } libflate = "^1" log = "^0.4" +once_cell = { version = "^1.7.2", optional = true } openidconnect = { version = "^2.0.0", optional = true, default_features = false } openssl = { version = "^0.10", features = ["v110"] } oso = { version = "^0.12", optional = true, default_features = false } +cryptoki = { version = "^0.3", optional = true } +r2d2 = { version = "0.8.9", optional = true } rand = "^0.8" regex = { version = "^1.4", optional = true, default_features = false, features = ["std"] } reqwest = { version = "0.11", features = ["json"] } @@ -57,6 +62,13 @@ urlparse = { version = "^0.7", optional = true } uuid = { version = "^0.8", features = [ "v4"] } xml-rs = "^0.8" +# Disable compiler optimizations for the pkcs11 crate because otherwise with a release build the +# `pReserved = ptr::null_mut()` assignment done by `CK_C_INITIALIZE_ARGS::default()` appears to be optimized out. This +# causes SoftHSMv2 to fail with error CKR_ARGUMENTS_BAD and to log to syslog "SoftHSM.cpp(436): pReserved must be set to +# NULL_PTR". Disabling optimizations for the pkcs11 crate "solves" this problem. +# [profile.release.package.pkcs11] +# opt-level = 0 + [target.'cfg(unix)'.dependencies] libc = "^0.2" syslog = "^4.0" @@ -66,14 +78,20 @@ rustc_version = "0.2.3" [features] default = [ "multi-user" ] -rta = [] -aspa = [] -multi-user = [ "basic-cookies", "jmespatch/sync", "regex", "oso", "openidconnect", "rpassword", "scrypt", "unicode-normalization", "urlparse" ] -ui-tests = [] extra-debug = [ "rpki/extra-debug" ] +multi-user = [ "basic-cookies", "jmespatch/sync", "regex", "oso", "openidconnect", "rpassword", "scrypt", "unicode-normalization", "urlparse" ] static-openssl = [ "openssl/vendored" ] + +# Preview features - not ready for production use +aspa = [] +rta = [] +hsm = ["backoff", "kmip", "once_cell", "cryptoki", "r2d2"] + +# Internal features - not for external use all-except-ui-tests = [ "multi-user", "rta", "static-openssl", "aspa" ] -hsm = [] +ui-tests = [] +hsm-tests-kmip = [ "hsm" ] +hsm-tests-pkcs11 = [ "hsm" ] # Make sure that Krill crashes on panics, rather than losing threads and # limping on in a bad state. @@ -81,7 +99,7 @@ hsm = [] panic = "abort" [dev-dependencies] -# for user management +# For user management tiny_http = { version = "^0.8", features = ["ssl"] } ctrlc = "^3.1" @@ -177,7 +195,7 @@ assets = [ post_install_script = "/usr/share/krill/rpm/postinst $*" post_uninstall_script = "/usr/share/krill/rpm/postrm $*" -# ensure that the useradd tool is present by depending on its package +# Ensure that the useradd tool is present by depending on its package [package.metadata.generate-rpm.requires] shadow-utils = "*" diff --git a/Dockerfile b/Dockerfile index c00829d9..2b6e8b64 100644 --- a/Dockerfile +++ b/Dockerfile @@ -8,6 +8,7 @@ ARG BASE_IMG=alpine:3.13 # -- stage 1: build krill and krillc # FROM ${BASE_IMG} AS build +ARG CARGO_ARGS= RUN apk add rust cargo openssl-dev @@ -18,7 +19,7 @@ COPY . . # multiplexing. This seems to help with various "spurious network error" # warnings when Cargo attempts to fetch from crates.io when building this # image on Docker Hub and GitHub Actions build machines. -RUN CARGO_HTTP_MULTIPLEXING=false cargo build --target x86_64-alpine-linux-musl --release --locked +RUN CARGO_HTTP_MULTIPLEXING=false cargo build --target x86_64-alpine-linux-musl --release --locked $CARGO_ARGS # # -- stage 2: create an image containing just the binaries, configs & diff --git a/defaults/krill-hsm.conf b/defaults/krill-hsm.conf new file mode 100644 index 00000000..8b7853d0 --- /dev/null +++ b/defaults/krill-hsm.conf @@ -0,0 +1,275 @@ +###################################################################################### +# # +# ----==== SIGNER CONFIGURATION ====---- # +# # +# The settings below can be used to configure the signer used by Krill. # +# # +###################################################################################### + + +# Signers +# ------- +# +# A signer is a cryptographic token, either hardware or software, local or remote, +# that can create RSA public/private key pairs and can sign data with the private key. +# +# Supported signer types +# ---------------------- +# +# Krill supports three types of signer: +# +# - OpenSSL based: Uses the OpenSSL library installed on the host O/S. On older +# operating systems it might be that a newer version of OpenSSL than is supported +# by the host O/S has been compiled into Krill itself and will be used instead. +# +# - PKCS#11 based: Uses a PKCS#11 v2.20 conformant library file from the filesystem. +# How the library handles the requests on behalf of Krill is library specific. A +# library such as SoftHSMv2 contains all of the code needed to handle the request +# and stores generated keys on the host filesystem. Libraries provided by well +# known HSM vendors will dispatch requests to one or a cluster of hardware +# security modules connected either physically or by network connection to the +# host on which Krill is running. +# +# - KMIP based: Makes TLS encrypted TCP connections to an operator specified server +# running a KMIP v1.2 conformant service. +# +# Key creation policy +# ------------------- +# +# Krill creates keys at different times for different purposes. Some keys are fixed +# such as the identity key for the RFC 8183 defined provisioning protocol, others can +# be rolled (e.g. the keys used for RPKI CA certificates in resource classes) and +# still others are one-off keys (e.g. keys used for EE certificates in CMS) that are +# discarded after use. +# +# Signer roles +# ------------ +# +# Signers can be assigned to roles to implement the desired policy. Roles are assigned +# by setting the following top level configuration file settings: +# +# - default_signer: The signer will be used to generate new long-term key pairs. +# Only one signer may be designated as the default. If only one signer is defined +# it will be the default. If more than one signer is defined one must be +# explicitly set as the default. +# +# - one_off_signer: The signer will be used to generate, sign with and destroy +# one-off key pairs. Only one signer may be designated as the oneoff signer. When +# not specified an OpenSSL signer will be used for this. +# +# These settings must be set to the name of a single signer, e.g.: +# +# default_signer = "My signer" +# +# [[signers]] +# type = "OpenSSL" +# name = "My signer" +# +# Required capabiliites +# --------------------- +# +# When Krill first connects to a new signer it will verify that the signer meets its +# requirements. In particular it will require the signer to generate an RSA key pair +# and to demonstrate that it can sign data correctly using the generated private key. +# +# Config file settings +# -------------------- +# +# At a minimum the "name" and "type" must be specified for a signer. +# +# One optional setting can also be set for all signers: +# +# - signer_probe_retry_seconds: When initially connecting to the signer on first use +# after Krill startup, wait at least N seconds between attempts to connect and +# test the signer for compatibility with Krill. Defaults to 30 seconds. +# +# The remaining details that must be supplied to configure a signer vary by signer +# type and by specific implementation. For example an OpenSSL signer doesn't require +# a path to a library file to load, while a PKCS#11 signer does, and one PKCS#11 +# vendor may require login by PIN code while another might allow operations to be +# performed with external PIN entry or no PIN entry at all. +# +# Default configuration +# --------------------- +# +# The default configuration is equivalent to: +# +# [[signers]] +# type = "OpenSSL" +# name = "Default OpenSSL signer" +# +# Changing the configuration +# -------------------------- +# +# The number, type, order, settings, names of signers can be changed at any time. +# Krill will apply the changes when next restarted. Via the use of identity key +# based signer binding Krill will still find the keys that it has created as long as +# the same backend is connected to, irrespective of name or connection details, and +# that the identity key in the signer has not been deleted. +# +# Warning about removing an in-use signer +# --------------------------------------- +# +# Removing a signer that owns keys that Krill is still using will prevent Krill from +# accessing those keys! +# +# Example configuration +# --------------------- +# +# Below is an example configuration. This example defines many signers but normally +# one would define only a single signer, or two signers if migrating from one signer +# to another. +# +# default_signer = "SoftHSMv2 via PKCS#11" +# +# [[signers]] +# type = "OpenSSL" +# name = "Signer 1" +# +# [[signers]] +# type = "OpenSSL" +# name = "Signer 2" +# keys_path = "/tmp/keys" +# +# [[signers]] +# type = "PKCS#11" +# name = "Kryptus via PKCS#11" +# lib_path = "/usr/local/lib/kryptus/libknetpkcs11_64/libkNETPKCS11.so" +# user_pin = "xxxxxx" +# slot = 313129207 +# +# [[signers]] +# type = "PKCS#11" +# name = "SoftHSMv2 via PKCS#11" +# lib_path = "/usr/local/lib/softhsm/libsofthsm2.so" +# user_pin = "xxxx" +# slot = 0x12a9f8f7 +# +# [[signers]] +# type = "KMIP" +# name = "Kryptus via KMIP" +# host = "my.hsm.example.com" +# port = 5696 +# server_ca_cert_path = "/path/to/some/ca.pem" +# username = "user1" +# password = "xxxxxx" + + +# OpenSSL signer configuration +# ---------------------------- +# +# This signer uses the operating system provided OpenSSL library (or on older +# operating systems it may use a modern version of the OpenSSL library compiled into +# Krill itself) to generate keys, to sign data using them and to generate random +# values. Keys are persisted as files on disk in a dedicated directory. +# +# Key Value Type Default Req'd Description +# ==================================================================================== +# keys_path path string "$datadir/keys" No The directory in which key files +# should be created. +# + + +# PKCS#11 signer configuration +# +# Krill interacts with a PKCS#11 v2.20 compatible cryptographic device via the Cryptoki +# interface which involves loading a library file from disk at runtime to which all +# cryptographic operations will be delegated. The library will in turn communicate +# with the actual cryptographic device. +# +# Note: The PKCS#11 library is not part of Krill nor is it supplied with Krill. Please +# consult the documentation for your PKCS#11 compatible cryptographic device to learn +# where you can find the .so library file and how to set up and configure it. For +# example when using SoftHSMv2 the library is commonly available at filesystem path +# /usr/lib/softhsm/libsofthsm2.so. +# +# Key Value Type Default Req'd Description +# ==================================================================================== +# lib_path path string None Yes The path to the .so dynamic library +# file to load. +# slot integer or None Yes An integer PKCS#11 "slot" ID or a +# string string "slot" label. Can also be +# given in hexadecimal, e.g. 0x12AB. +# When a label is given Krill will +# inspect all available slots and use +# the first slot whose label matches. +# ------------------------------------------------------------------------------------ +# user_pin string None No The pin or password or secret value +# used to authenticate with the +# PKCS#11 provider. The format varies +# by provider, SoftHSMv2 uses numeric +# PINs such as "12345" while AWS +# CloudHSM expects this to be in the +# form "username:password". +# login boolean True No Whether the signer must be logged in +# to before performing other +# operations. +# ------------------------------------------------------------------------------------ +# retry_seconds integer 2 No Wait N seconds before retrying a +# failed request. +# backoff_multiplier float 1.5 No How much longer to wait before retry +# N+1 compared to retry N. +# max_retry_seconds integer 30 No Stop retrying after N seconds. + + +# KMIP signer configuration +# +# Krill interacts with a KMIP v1.2 compatible cryptographic device via the TCP+TTVL +# protocol. This requires knowing the hostname, port number, and details required to +# authenticate with the provider. +# +# Key Value Type Default Req'd Description +# ==================================================================================== +# host string None Yes The domain name or IP address to +# connect to. +# port integer 5696 No The port number to connect to. +# ------------------------------------------------------------------------------------ +# insecure boolean false No If true, do not verify the servers +# TLS certificate. +# force boolean false No If true, ignore server claims that +# it lacks functionality that we +# require. For example PyKMIP 0.10.0 +# says it doesn't support operation +# ModifyAttribute but sending a +# modify attribute request succeeds. +# ------------------------------------------------------------------------------------ +# server_cert_path File system paths to certificate +# string None No files (in PEM format) for verifying +# server_ca_cert_path the identity of the server. +# string None No +# ------------------------------------------------------------------------------------ +# client_cert_path File system paths to certificate and +# string None No key files (in PEM format) for +# client_cert_private_key_path proving our identity to the server. +# string None No +# ------------------------------------------------------------------------------------ +# username string None No Credentials for authenticating with +# password string None No the server. +# ------------------------------------------------------------------------------------ +# retry_seconds integer 2 No Wait N seconds before retrying a +# failed request. +# backoff_multiplier float 1.5 No How much longer to wait before retry +# N+1 compared to retry N. +# max_retry_seconds integer 30 No Stop retrying after N seconds. +# ------------------------------------------------------------------------------------ +# connect_timeout_seconds Wait at most N seconds to make a TCP +# integer 5 No connection to the KMIP server. +# read_timeout_seconds Wait at most N seconds for more +# integer 5 No response bytes to be received from +# the KMIP server. +# write_timeout_seconds Wait at most N seconds to write more +# integer 5 No request bytes to the connection to +# the KMIP server. +# max_use_seconds integer 60*30 No Don't use an idle connection to the +# KMIP server if it has been connected +# for at least N seconds. +# max_idle_seconds integer 60*10 No Close open connections to the KMIP +# server if not used in the last N +# seconds. +# ------------------------------------------------------------------------------------ +# max_connections integer 5 No The maximum number of concurrent +# connections to permit to the server. +# max_response_bytes integer 64*1024 No The maximum number of response bytes +# to accept from the KMIP server, or +# otherwise treat the request as +# failed. diff --git a/doc/development/hsm/architecture.md b/doc/development/hsm/architecture.md index 76a8d2d8..49fe72a4 100644 --- a/doc/development/hsm/architecture.md +++ b/doc/development/hsm/architecture.md @@ -1,22 +1,223 @@ # HSM: Architecture -## Why create new KMIP crates? +## Terminology -No well or actively maintained Rust support for KMIP with sufficient functionality for Krill -existed at the time of writing. +- `Signer` trait: A trait defined by the `rpki` Rust crate, used by functions offered by the `rpki` Rust crate, and implemented by Krill. Defines an interface for creation and deletion of key pairs, lookup of and signing of data by a key known to the `Signer`, and generation of random byte sequences. -The closest candidate, https://github.com/visa/kmip, was used to explore KMIP support in the -Krill HSM prototype code. However, it was decided to create our own KMIP library because the -visa crate: +- `Signer` implementation: Until now Krill had a single implementation, `OpenSslSigner`. The `HSM` feature adds two new signers to Krill: `Pkcs11Signer` and `KmipSigner`. - - Was not published on crates.io. - - Had only rudimentary error reporting (as it targets no-std environments). - - Lacks documentation. - - Lacked TCP+TLS client support. - - Lacked support for KMIP operations that Krill requires. - - Did not appear to be actively maintained or intended for use by others. +- `Signer`: An instance of an implementation of most of the `Signer` trait (everything except random number generation). Krill can be configured to create multiple instances of the same `Signer` trait implementation, each using a different configuration. When referring to a Signer this is what is usually being referred to. -## Why not add KMIP code to Krill directly? +- `Signer` backend: 3rd party logic and storage, usually running outside Krill either on the same host or remotely, that works with keys on behalf of Krill. The backend details vary per signer implementation and configuration. -The Krill code base is already large enough and slow enough to compile. KMIP support may also -be of interest to others. It thus seemed a good candidate for separation from Krill itself. \ No newline at end of file +- `KeyIdentifier`: The SHA-1 hash of the bits of the binary DER encoding of the `SubjectPublicKey` field of the X.509 ASN.1 `SubjectPublicKeyInfo` data structure. It uniquely (or the likelyhood of collisions is sufficiently low that it can be considered unique) identifies a public/private key pair, e.g. the private key that was used to sign a certificate. + +## Signer backends + +The introduction of HSM support greatly expands the kinds of issues Krill can encounter and complexities it must handle when working with signing keys. + +- The backend for the existing `OpenSslSigner` implementation is the OpenSSL library (either provided by the host O/S provided or embedded into Krill at compilation time) with key material being stored on the local file system and identified by `KeyIdentifier`. + +- The backend for the new `Pkcs11Signer` implementation is a composite of logic provided by a 3rd party library loaded at runtime by Krill from a file on the host filesystem specified by the Krill configuration, and any services and storage used by that library. + + The PKCS#11 library MUST implement the "Cryptoki" interface defined by the **_stateful_** PKCS#11 v2.20 specification ([HTML](https://www.cryptsoft.com/pkcs11doc/v220/), [PDF](https://www.cryptsoft.com/pkcs11doc/STANDARD/pkcs-11v2-20.pdf)), _"the most widely used version of the PKCS#11 standard"_ (according to [Cryptsoft](https://www.cryptsoft.com/pkcs11doc/)). The Krill process invokes functions loaded into its process space from the configured PKCS#11 library. + + The PKCS#11 interface is a synchronous design meaning that Krill has to invoke the functions and wait for them to complete. The specification does not include any capability to poll for completion of a previously started task. It is also stateful such that Krill must open and close sessions with the interface and that key identifiers used in a session for a given key will be different to the identifiers used for the same key in a later session (the specification says _"A particular object on a token does not necessarily have a handle which is fixed for the lifetime of the object"_). Krill therefore labels keys and stores a mapping from `KeyIdentifier` to label so that it can later lookup the session specific key identifier in order to work with the key. + + Implementations vary signficantly in their design. Examples include [SoftHSMv2](https://github.com/opendnssec/SoftHSMv2) which uses logic provided by the library and local file system storage for keys, or the [Yubico SDK PKCS#11 library](https://www.yubico.com/press-releases/yubico-introduces-open-source-yubihsm-sdk-for-securing-infrastructures-and-hardware-private-key-storage/) which communicates via HTTP(S) with a "Connector" daemon which in turn communicates with the cryptographic device, to the [AWS CloudHSM PKCS#11 Client SDK 5 library](https://docs.aws.amazon.com/cloudhsm/latest/userguide/pkcs11-library.html) that makes outbound TCP/IP connections to a cluster of servers running on, and storing key data in, the Amazon Web Services cloud. + +- The backend for the new `KmipSigner` implementation is a 3rd party service that offers an interface compatible with the _stateless_ [Key Management Interoperability Protocol (KMIP) v1.2](http://docs.oasis-open.org/kmip/spec/v1.2/os/kmip-spec-v1.2-os.html) specification. + + Krill establishes one or more TCP/IP connections to the offered interface. Data exchanged with the interface is protected by TLS encryption and is encoded according to the TTLV binary protocol defined by the KMIP specification. + + KMIP is primarily a synchronous design, i.e. Krill must wait for requests to complete, Krill does not poll to see if a previously started task has since completed. Key identifiers issued by KMIP are persistent and unchanging. Krill stores a mapping from `KeyIdentifier` to KMIP key identifier so that it can work with the keys again later. + + Connections are kept alive for a period to reduce the overhead and delay that would otherwise be incurred when processing several KMIP requests in quick succession. Krill will attempt to reconnect if it encounters difficulty in communicating with the KMIP compatible service. + + KMIP supports batching of several requests together and some limited provision for referencing the output of a previous request in a subsequent request in the same batch, but Krill doesn't use this capability at present. + + +## The all important `KeyIdentifier` + +The design revolves in many ways around the Krill `KeyIdentifier` which uniquely identifies a particular signing key pair. + +> **A note about the uniqueness of the KeyIdentifier** +> +> It is theoretically possible for more than one `Signer` backend to possess a copy of the same key pair identified by the same `KeyIdentifier`, e.g. if the key pair were extracted from one backend and imported to another, or if one backend instance is part of a cluster of instances with access to the same data or where one instance is a spare kept (reasonably) in sync with a primary or if multiple instances were restored from the same backup data. However, for a given piece of data the same signature will be generated by each backend that signs the data using a key identified by the same key pair. For a given `KeyIdentifier` Krill will use the `Signer` that it noted as owning the key (which under-the-hood) + +The `KeyIdentifier` is used in many places by Krill. While theoretically the `Signer` interface permits individual implementations to designate their own key identifier, the type of identifer used by Krills implementations cannot be changed. Standards require that the `KeyIdentifier` be recorded in certificates that Krill generates and works with. When working with a certificate the `KeyIdentifier` is the only information available to Krill to identify the related key, nor should code in such parts of Krill be extended to to know about the internals of how keys are actually stored and identified in order to overcome this limitation. + +The `KeyIdentifier` is passed to implementers of the `rpki` crate `Signer` trait meaning in turn that `Signer` implementations must be able to locate the key that is associated with the `KeyIdentifier`. For the existing `OpenSslSigner` this isn't a problem as the keys are stored on disk using the `KeyIdentifier` as the file name. For other signers a mapping has to be maintained from `KeyIdentifier` to owning `Signer` and from `KeyIdentifier` to implementation specific key identifier. + +## Relating `KeyIdentifier` to RFC terms used in the PKCS#11 and KMIP specifications + +When reading the KMIP and PKCS#11 specifications various RFC defined terms are used which are relevant to our need to relate HSM keys to the Krill `KeyIdentifier`. It is useful to understand how terminology used by the code in Krill relates to the terms defined in the related specifications and RFCs. + +We can trace some of the relationships as follows: + +- The [`rpki::PublicKey::key_identifier()`](https://docs.rs/rpki/0.5.0/rpki/crypto/keys/struct.PublicKey.html#method.key_identifier) function uses the [`bcder::BitString::octets_slice()`](https://docs.rs/bcder/0.6.0/bcder/string/struct.BitString.html#method.octet_slice) function to obtain the inner [`bcder::BitString::bits`](https://docs.rs/bcder/0.6.0/src/bcder/string/bit.rs.html#64-70) subfield of the [`rpki::PublicKey::bits`](https://docs.rs/rpki/0.5.0/src/rpki/crypto/keys.rs.html#87-90) field and then invokes the [`ring::digest::digest()`](https://docs.rs/ring/0.16.20/ring/digest/fn.digest.html) function to SHA-1 hash it. The resulting value is the Krill `KeyIdentifier`. + +- SHA-1 is mandated by [section 3 Asymmetric Key Pair Formats of RFC 7935](https://datatracker.ietf.org/doc/html/rfc7935#section-3) which says _"The RSA key pairs used to compute the signatures MUST have a 2048-bit modulus and a public exponent (e) of 65,537"_ (more below on RSA modulus and public exponent for why this is relevant). + +- Assuming that `length` is implied and `tag` is not included then the `bits` to be hashed correspond to the `value of the BIT STRING` mentioned in [section 4.2.1.2 "Subject Key Identifier" of RFC 5280](https://datatracker.ietf.org/doc/html/rfc5280#section-4.2.1.2) when it states that _"The **keyIdentifier** is composed of the 160-bit **SHA-1** hash of the value of the **BIT STRING** subjectPublicKey (excluding the tag, length, and number of unused bits)"_. + +- The `bcder::BitString::bits` inner subfield of the `rpki::PublicKey::bits` field is an encoded form of the public key format defined in [section 3.1 Public Key Format of RFC 7935](https://datatracker.ietf.org/doc/html/rfc7935#section-3.1) and _"subjectPublicKey"_ defined in [Appendix A.1 of RFC 5280](https://datatracker.ietf.org/doc/html/rfc5280#appendix-A.1): + + ```rust + Rust rpki::PublicKey RFC 7935 RFC 5280 Appendix A.1 + =============================== ================ ============================================= + pub struct PublicKey { SubjectPublicKeyInfo ::= SEQUENCE { + algorithm: PublicKeyFormat, algorithm algorithm AlgorithmIdentifier, + bits: BitString, subjectPublicKey subjectPublicKey BIT STRING } + } + ``` + +- [Section 3.1 Public Key Format of RFC 7935](https://datatracker.ietf.org/doc/html/rfc7935#section-3.1) defines `subjectPublicKey` as _"RSAPublicKey MUST be used to encode the certificate's subjectPublicKey field, as specified in [RFC4055]"_ and [section 1.2 RSA Public Keys of RFC 4055](https://datatracker.ietf.org/doc/html/rfc4055#section-1.2) states: + + > The RSA public key MUST be encoded using the type RSAPublicKey type: + > ``` + > RSAPublicKey ::= SEQUENCE { + > modulus INTEGER, -- n + > publicExponent INTEGER } -- e + > ``` + > Here, the modulus is the modulus n, and publicExponent is the public + > exponent e. The DER encoded RSAPublicKey is carried in the + > subjectPublicKey BIT STRING within the subject public key + > information. + +Understanding these structures, meanings and relationships is important when considering how to get the desired information out of a PKC#11 or KMIP compliant HSM and why the Krill HSM supporting code is able to derive the `KeyIdentifier` from RSA modulus and public exponent values. + +References: +- [PKCS#11 v2.20](https://www.cryptsoft.com/pkcs11doc/STANDARD/pkcs-11v2-20.pdf): Cryptographic Token Interface Standard +- [KMIP v1.2](http://docs.oasis-open.org/kmip/spec/v1.2/os/kmip-spec-v1.2-os.html): Key Management Interoperability Protocol Specification Version 1.2 +- [RFC 4055](https://datatracker.ietf.org/doc/html/rfc4055): Additional Algorithms and Identifiers for RSA Cryptography for use in the Internet X.509 Public Key Infrastructure Certificate and Certificate Revocation List (CRL) Profile +- [RFC 5280](https://datatracker.ietf.org/doc/html/rfc5280): Internet X.509 Public Key Infrastructure Certificate and Certificate Revocation List (CRL) Profile +- [RFC 7935](https://datatracker.ietf.org/doc/html/rfc7935): The Profile for Algorithms and Key Sizes for Use in the Resource Public Key Infrastructure + + +## Decision log + +- Roll our own Rust KMIP library as no actively maintained Rust support for KMIP with sufficient functionality for Krill existed at the time of writing. + + The best candidate I found for use instead of rolling our own was the https://github.com/visa/kmip crate. This crate was used to explore KMIP support in the Krill HSM prototype. However, I considered it insufficient for use in final Krill HSM support because: + + - It lacked the ability to execute some KMIP operations needed by Krill (e.g. `Destroy`, `ModifyAttribute`, `RngRetrieve`, `Sign`). + - It did not offer any TCP/IP TLS client capability, only byte level (de)serialization. + - It did not have any tests of its own functionality. + - It did not have any tests showing conformance with the KMIP specification. + - It was not published on https://crates.io/ and thus a new version of Krill that depended on it would not be releasable to https://crates.io/ unless we embedded the `visa/kmip` crate code inside Krill. + - Error reporting was quite basic (presumably because it targets no-std environments which limits its ability to construct abitrary complex error messages). + - There was no indication that the crate was intended for use by others nor any advertisement of support or potential for support. + - There was no indication of activity at the time of writing (no releases, issues or pull requests) since the initial release in July 2020. + +- Don't include the KMIP code in the main Krill code base as it is orthogonal to and independently useful outside of + Krill, and the Krill code base is already quite large & slow to compile. + +- Support multiple concurrently active signers for use cases such as: + + - Rollover to a new signer (creation of new keys with the new signer while continued use of keys created by the previous signer). + + - Generation of one-off keys using OpenSSL even when using a separate signer for creation of other keys, as doing this with an HSM can require multiple potentially slow requests (create, activate, sign, deactivate, destroy, potentially each being a network round trip plus relatively slow execution of operations compared to local OpenSSL) and because the security benefits of an HSM are not thought to be necessary for one-off signing keys. + +- Maintain mappings of Krill `KeyIdentifier` to signer identifier (so that we can dispatch signing requests to the + correct signer) and `KeyIdentifier` to signer specific key identifiers (so that we can instruct the signer to work with the correct key). + + Initially it was hoped that this would not be needed as keys could be tagged on creation or maybe even created with a supplied primary identifier, but in testing with actual PKCS#11 and KMIP providers it was discovered to be necessary. + + For example the KMIP specification says that the key Unique Identifier _"SHALL be assigned by the key management system at creation or registration time, and then SHALL NOT be changed or deleted before the object is destroyed"_ and thus cannot be modified after creation to be the `KeyIdentifier` nor at the time of writing did the AWS CloudHSM support the PKCS#11 `C_SetAttributeValue` function that would be needed to apply some sort of identifier or label to the key after creation. + + Instead some other identifier must be stored with the key in another attribute at key creation time. For PKCS#11 the `CKA_ID` attribute was chosen for this as `CKA_LABEL` was thought to be better used for storing a descriptive label to be shown by HSM client tooling. For KMIP the `Name` attribute is used. + +- Support generation of the `KeyIdentifier` from component parts (RSA modulus and exponent) for cases where the signer + doesn't support (or guarantee support for) exposing that itself but does provides access to the component parts which can be used to reconstitute it. + + For example the PKCS#11 v2.20 specification defines the `CKA_HASH_OF_SUBJECT_PUBLIC_KEY` key attribute which might be used to to obtain the `KeyIdentifier` for a created key, but the AWS CloudHSM PKCS#11 implementation didn't support it at the time of writing. + + There is also the v2.40 definied `CKA_PUBLIC_KEY_INFO` key attribute (_"DER-encoding of the SubjectPublicKeyInfo (see above) for the public key contained in this certificate (default empty)"_) whose value could be deconstructed to obtain the `KeyIdentifier` (see the notes above the relationship between `SubjectPublicKeyInfo` and `KeyIdentifier`), but it is an optional field which is allowed to be empty (and at least with SoftHSMv2 at the time of writing was always empty) and the [AWS CloudHSM list of supported PKCS#11 attributes](https://docs.aws.amazon.com/cloudhsm/latest/userguide/pkcs11-attributes.html) didn't inlcude it) and would not anyway be supported by clients that implement an earlier version of the PKCS#11 specification. + + In theory PKCS#11 clients support locating a key by its `CKA_MODULUS` and `CKA_PUBLIC_EXPONENT` but KMIP doesn't support this, though should support locating a key by its "digest". + +- Don't fail to start Krill if a signer backend is not reachable or lacks required capabilities. Preventing Krill from + operating won't fix the problem and prevents Krill from doing anything else useful with keys from other signers or + offering its API or UI. + +- Be robust in case of network delays and errors and problems in external signing services. Retry requests that fail due to issues potentially caused by transient network degradation. Re-use TCP+TLS sessions to avoid costly TCP+TLS setup and teardown costs per request to the signer service. + +## Design + +Old: Prior to the addition of HSM support there was only ever a single Signer and control flow looked like this: + +``` +Krill calling code -> KrillSigner -> OpenSslSigner +``` + +New: With the addition of HSM support there may be multiple concurrently active Signers and the control flow becomes this: + +``` + Creates signers + Pending Signers: [SignerProvider, SignerProvider, ...] + : | +Krill calling code -> KrillSigner -> SignerRouter + | + where: + Ready Signers: [SignerProvider, SignerProvider, ...] + SignerProvider is one of: | ^ ^ + - KmipSigner + SignerMapper ------+---------------+ + - OpenSslSigner | + - Pkcs11Signer + AggregateStore +``` + +- `KrillSigner` remains the central interface between Krill and the signer backends,, handling config file parsing and initial signer creation, conversion of error types and providing higher level functions that make use of the underlying signers. `KrillSigner` delegates signer registration and dispatch to `SignerRouter`. + +- `SignerRouter` uses an instance of `SignerMapper` to record which signer backends exist and which keys they possess. Signers use the same `SignerMapper` instance to register the keys as their own and to register the mapping between Krill `KeyIdentifier` and signer backend specific internal identifier(s). + +- `SignerRouter` registers/binds and dispatches to signers. Signers start in the pending set and are not yet usable. Registration and binding are the process of probing a singer backend to establish if we can connect to it and if so if it is usable. New signers are registered by creating an identity key inside it and recording it in the `SignerMapper`. Later invocations of Krill will verify the identity of the signer (and thus which `SignerMapper` ID relates to it and which keys it possesses) using this identity key. A signer is moved to the ready set once it has been successfully probed and registered/bound and its `SignerMapper` ID has been determined and communicated to it. Signers that fail to be probed or are discovered to be unusable are dropped from the pending set without being added to the ready set. + +- `SignerRouter` identifies the appropriate signer for a given request. Signer selection happens in one of two ways: + - For requests relating to an existing key the request is routed to the signer that owns the key, as identified by the `SignerMapper`. + - For all other requests the signer is selected based on its assigned roles, e.g. default signer or one-off signer roles can be assigned to specific signers, as defined by `SignerFlags`. + + Actual dispatch is delegated to an instance of `SignerProvider` because enum based dispatch is noisy and the "Provider" enum dispatch pattern was alrady established in the multi-user auth code. + +- `SignerProvider` dispatchses requests using enum based dispatching. This approach was chosen over use of Rust traits due to complexities associated with traits (e.g. async traits are not yet officially supported) and the lack of requirement to support an arbitrary number of as yet unseen implementations of some trait. We know exactly how many different signer implementations we need to support: OpenSSL soft signer, PKCS#11 based signer and KMIP based signer. + +## Signer mapper stored data example + +From `/signers//snapshot.json`: +```json +{ + "id": "a32392f6-da5d-4341-98c1-cecdb0c12416", + "version": 9, + "signer_name": "Pkcs11Signer - No config file name available yet", + "signer_info": "PKCS#11 Signer [token: My token 1 (model: SoftHSM v2, vendor: SoftHSM project), slot: 2146913893, server: SoftHSM (Cryptoki v2.6), library: libsofthsm2.so]", + "signer_identity": { + "public_key": "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAwbJR0aHy3tFzXhs/lWlCfWpFH7xFk+HRyGA3GYTIsUBwwu2uS9QAZXb8X3NjPLSvw+0Cfcy9n7yHC6B4pQda13bxVSvshC1f4mUc/iXWtMLg4x/F2fJNcTGVW3DbmPWKMXQVeBboFMwF+FZll7EgMsvmiZXn6qeoLm5hfcjihqb88k+HUuMvsGcz939jOIirxv8xP6jT/vJGDxidgoBSIBL3AUSbjh0WEopAzGX8Z+nNvaAPtanhWEB7n0mktyTis4G+GPh1N0pOT5pGxEPf7BnOb1gnPTbb0sTz7M64vkXkxCL69Yxlz3c3MxW4zncwKmGwXo4cmEpN8CTWC5ORbQIDAQAB", + "private_key_internal_id": "32b25442dbf26971ffa556d2415810f80e3139d2" + }, + "keys": { + "422A26E19291F094B5182FC993DF32B14682D9D2": "b740d70d1e6ad8ebc91d7011753cdb43b76b719b", + "87F9B52806A0E7B3812D87DDCC943FF51C8749B6": "03332ef8050c393a42395c4e55aba64c3d64c953", + "7858DEB28A517BF21F2B2D540A904FDB22615B76": "d322f65668fc7821c18f037237facce403a331df", + "63CDB21953376A88588990E77F423B4832A03F5A": "61f0c63e9288aed063fdfe8ca7482cd62544fc70", + "AB41906ECA2F59D7A8DC76DADF46F63062FE765B": "bc49cd8983ddc3177b47a87d19365d75cb4ecfc6", + "B83D0D7B9B8264F2170EA21C6CECE54A4D9F4549": "a76440202b5ab2113b4b9cdca199b5b38f23308e", + "395B687CB0BCAAE074A299BB08DA82911A83D971": "c0c0b61047dec3514d9f9ce19c0253c0c381bb3a", + "05770B676863B9459A19480B283E025DF0AD96BC": "a9a7bd64a9c48a112c639910b5528feede52dff9" + } +} +``` + +Here we see an example of a PKCS#11 signer using the SoftHSMv2 PKCS#11 library. The snapshot includes the set of keys created by this signer with both their Krill `KeyIdentifier` and the CKA_ID stored with the key in SoftHSMv2. We also see the identity details needed to confirm that the backend is indeed the owner of these keys. + +## Crate dependencies + +- Loading and interfacing with PKCS#11 libraries is handled by the [`pkcs11`](https://crates.io/crates/pkcs11) crate. +- Communicating with KMIP servers is handled by the NLnet Labs [`kmip-protocol`](https://crates.io/crates/kmip-protocol) crate. +- Connection pooling is handled by the [`r2d2`](https://crates.io/crates/r2d2) crate. +- Retry and backoff is handled by the [`backoff`](https://crates.io/crates/backoff) crate. +- KMIP TLS is handled by the (already used) [`openssl`](https://crates.io/crates/openssl) crate rather than the [`rustls`](https://crates.io/crates/rustls) crate as the latter may impose stricter limits on outbound connectivity to HSMs than can be pragmatically expected to work in all customer environments. + +## Known issues + +- Ideally signers would be Rust async and async changes to the `rpki` crate were designed. At the time of writing however, supporting async signing would require deeper changes in core Krill code that we will leave for now as it's not yet clear whether or not blocking signers with potentially slow backends is really a problem or not. +- TBD: Switch to `tokio-native-tls` to keep the benefits of and control of local O/S native TLS providers and avoid the + 'modern security' limitations of `rustls` while switching to an `async` model. +- There are no timeouts on PKCS#11 operations. diff --git a/doc/development/hsm/connectivity.md b/doc/development/hsm/connectivity.md new file mode 100644 index 00000000..980f9e2e --- /dev/null +++ b/doc/development/hsm/connectivity.md @@ -0,0 +1,60 @@ +# HSM: Connectivity + +Unlike the original OpenSSL based signer in Krill, the PKCS#11 and KMIP signers connect to +cryptographic token systems that exist outside of Krill. That connection can fail, either +from the start or at some point while Krill is running. + +As stated this issue is common to the PKCS#11 and KMIP signers. As much as possible the +overlap in connection handling functionality has been extracted into common code that is +shared by both signer implementations. Some of the code however is very similar but not quite +the same and is thus "duplicated" in both signers but follows the same pattern. + +## Executing a request via a "connection" + +For KMIP a request to the backend literally involves a TCP connection to the KMIP server. +For PKCS#11 a network connection may or may not be made, Krill has no way of knowing, Krill +only invokes a PKCS#11 library function, but conceptually a connection is still being made +from Krill to the backend. + +Both implementations have a `fn with_conn(&self, desc: &str, do_something_with_conn: F)` +function. This function obtains (and sets up if necessary) a connection to the backend and +then passes the "connection" instance to a callback which uses it to execute a request. + +Both implementations implement (in the same way but with almost duplicate code) a retry with +backoff strategy if the request fails and the failure is transient rather than permanent. The +logic to decide which types of failure are transient and which are permanent, and the concept +of what a "connection" is, are specific to each implementation. + +## Probe status + +Both implementations share the concept of probe "status" and of a backend moving +from one status to another. A backend must be successfully probed before it can be used. +The code for this **is** factored out to the `StatefulProbe` type +defined in the `probe.rs` module. However, a key function defined in the module, +`fn status(&self, probe: F)`, takes a callback for which the PKCS#11 and KMIP signers each have +their own specific implementation. + +A "probe" represents the idea of something used to see if a signer backend is reachable, +usable & ready. As such a `StatefulProbe` is an enum which can be in one of three states: `Probing`, `Unusable` or `Usable`. + +Initially a probe is in the `Probing` state which carries with it the details necessary to +establish an initial connection to the signer backend. Whenever the `fn status()` of a probe +in `Probing` status is queried it will "send" a probe and if necessary based on the result of +the probe will move to either the `Usable` or `Unusable` state. + +When in the `Probing` state a new "probe" will only be sent at most every N seconds. In between +"probes" a call to `fn status()` will return the last known state of the probe. This is to avoid +repeated rapid attempts to re-probe the signer backend when it has already been determined that +it is not yet available/ready but might soon become so. Probes are only "sent" during the probing +status, they are not sent once the probe moves to the (un)usable status. + +A probe in `Unusable` state cannot now or ever be used during the lifetime of the Krill process. +It has probed the signer backend and found it permanently lacking. + +A probe in `Usable` state carries with it the details necessary to communicate with the backend +via an established connection and/or to (re)establish connections as necessary, and contains +some metadata describing the backend being connected to which was determined when moving from +the `Probing` to the `Usable` state. + +Note that probe status never moves backwards in the lifetime of a single Krill process from (un)usable +to probing. diff --git a/doc/development/hsm/overview.md b/doc/development/hsm/overview.md index 2cc05342..0c407220 100644 --- a/doc/development/hsm/overview.md +++ b/doc/development/hsm/overview.md @@ -5,26 +5,23 @@ The feature adds (or will add) the following to Krill: - Pluggable signers: - Three "plugins": - - Host file-based OpenSSL key management & signing - - [PKCS#11](https://www.cryptsoft.com/pkcs11doc/) dynamic library support - - [Key Management Interoperability Protocol (KMIP)](https://www.oasis-open.org/committees/tc_home.php?wg_abbrev=kmip) support - - Partial support for KMIP 1.0-1.2 (signing was introduced in 1.2) + - Host file-based OpenSSL signer + - [PKCS#11](https://www.cryptsoft.com/pkcs11doc/) v2.20+ dynamic library support + - [Key Management Interoperability Protocol (KMIP)](https://www.oasis-open.org/committees/tc_home.php?wg_abbrev=kmip) v1.2+ support - TTLV over TLS only, no XML/JSON over HTTPS - Key ownership tracking: - To sign with a key the request must be delegated to the correct signer. - Therefore we must keep track of which signer "owns" each key. - - Random value generation fallback support: - - Not all PKCS#11 or KMIP compatible devices support generating random values. - - Fallback in such cases to the OpenSSL signer (or to a user specified signer?) + - Signer tracking: + - To delegate a request to the correct signer we must know which signer "instance" corresponds to which signer + "configuration". - The concept of signing with specific signers for specific purposes: - - Signer for one-time keys (avoid slow HSMs for keys that don't need the security guarantees an HSM provides) + - Signer for one-off keys (avoid slow HSMs for keys that don't need the security guarantees an HSM provides) - Signer for new keys - Signer for existing keys (based on key tracking as mentioned above) - - Signer for key rollover - - Signer for fallback random value generation The feature also adds two new Rust crates for KMIP support: - https://github.com/NLnetLabs/kmip-protocol (https://crates.io/crates/kmip-protocol) @@ -38,4 +35,5 @@ The feature also adds two new Rust crates for KMIP support: The feature only lightly touches the core RPKI related code in Krill in order to dispatch signing related requests to the correct signer. The main source code components impacted by this feature are: - - ``src/daemon/crypto/signing.rs`` + - ``src/daemon/crypto/signing/`` + - ``src/daemon/config.rs`` diff --git a/doc/development/hsm/readme.md b/doc/development/hsm/readme.md index b125fee2..05cfd504 100644 --- a/doc/development/hsm/readme.md +++ b/doc/development/hsm/readme.md @@ -11,4 +11,5 @@ Further reading: - [Overview](./overview.md) - [Requirements](./requirements.md) -- [Architecture](./architecture.md) \ No newline at end of file +- [Architecture](./architecture.md) +- [Connectivity](./connectivity.md) \ No newline at end of file diff --git a/doc/development/hsm/requirements.md b/doc/development/hsm/requirements.md index bf2576c2..5fdedc40 100644 --- a/doc/development/hsm/requirements.md +++ b/doc/development/hsm/requirements.md @@ -4,4 +4,10 @@ The primary initial requirements that influenced the architecture were: - Support for key creation, deletion, signing using HSMs via two standard protocols: - [PKCS#11](https://www.cryptsoft.com/pkcs11doc/) - - [Key Management Interoperability Protocol (KMIP)](https://www.oasis-open.org/committees/tc_home.php?wg_abbrev=kmip) \ No newline at end of file + - [Key Management Interoperability Protocol (KMIP)](https://www.oasis-open.org/committees/tc_home.php?wg_abbrev=kmip) + +Further analysis extended & refined these requirements such that: + - Krill `Signer` implementations also support random number generation, so possibly support that via HSMs too. + - PKCS#11 v2.20 is the most widely deployed version thus we should target that. + - KMIP didn't support signing until v1.2 so target that. + - KMIP v1.0 supported only TCP+TLS+TTLV where TTLV is a custom binary wire format defined by the KMIP specification. Later KMIP versions added support for HTTPS, XML and JSON, but only TCP+TLS+TTLV is mandatory for all KMIP servers. Therefore we should support TCP+TLS+TTLV. diff --git a/src/cli/client.rs b/src/cli/client.rs index 4a9bb8a8..2cda4a9b 100644 --- a/src/cli/client.rs +++ b/src/cli/client.rs @@ -493,6 +493,7 @@ impl KrillClient { fn init_config(&self, details: KrillInitDetails) -> Result { let defaults = include_str!("../../defaults/krill.conf"); let multi_add_on = include_str!("../../defaults/krill-multi-user.conf"); + let hsm_add_on = include_str!("../../defaults/krill-hsm.conf"); let mut config = defaults.to_string(); config = config.replace("### admin_token =", &format!("admin_token = \"{}\"", self.token)); @@ -518,8 +519,13 @@ impl KrillClient { config.push_str(multi_add_on); } - let c: Config = toml::from_slice(config.as_ref()).map_err(Error::init)?; - c.verify().map_err(Error::init)?; + if details.hsm() { + config.push_str("\n\n\n"); + config.push_str(hsm_add_on); + } + + let mut c: Config = toml::from_slice(config.as_ref()).map_err(Error::init)?; + c.process().map_err(Error::init)?; Ok(ApiResponse::GenericBody(config)) } diff --git a/src/cli/options.rs b/src/cli/options.rs index 77a4b7a5..5fb52f8e 100644 --- a/src/cli/options.rs +++ b/src/cli/options.rs @@ -2440,6 +2440,7 @@ pub struct KrillInitDetails { data_dir: Option, log_file: Option, multi_user: bool, + hsm: bool, } impl KrillInitDetails { @@ -2448,6 +2449,7 @@ impl KrillInitDetails { data_dir: None, log_file: None, multi_user: true, + hsm: false, } } @@ -2470,6 +2472,10 @@ impl KrillInitDetails { pub fn multi_user(&self) -> bool { self.multi_user } + + pub fn hsm(&self) -> bool { + self.hsm + } } impl Default for KrillInitDetails { @@ -2478,6 +2484,7 @@ impl Default for KrillInitDetails { data_dir: None, log_file: None, multi_user: false, + hsm: false, } } } diff --git a/src/commons/api/ca.rs b/src/commons/api/ca.rs index 094a1ed4..ceaaf867 100644 --- a/src/commons/api/ca.rs +++ b/src/commons/api/ca.rs @@ -2664,11 +2664,9 @@ impl ResourceSetError { #[cfg(test)] mod test { use bytes::Bytes; + use rpki::repository::crypto::PublicKeyFormat; - use rpki::repository::crypto::{signer::Signer, PublicKeyFormat}; - - use crate::commons::util::softsigner::OpenSslSigner; - use crate::test; + use crate::{commons::crypto::OpenSslSigner, test}; use super::*; @@ -2705,7 +2703,7 @@ mod test { #[test] fn mft_uri() { test::test_under_tmp(|d| { - let signer = OpenSslSigner::build(&d).unwrap(); + let signer = OpenSslSigner::build(&d, "dummy", None).unwrap(); let key_id = signer.create_key(PublicKeyFormat::Rsa).unwrap(); let pub_key = signer.get_key_info(&key_id).unwrap(); diff --git a/src/commons/crypto/cert.rs b/src/commons/crypto/cert.rs index 4f2ca8f8..4eec7f78 100644 --- a/src/commons/crypto/cert.rs +++ b/src/commons/crypto/cert.rs @@ -741,8 +741,9 @@ impl From<&PublicKey> for IdExtensions { #[cfg(test)] pub mod tests { + use std::time::Duration; - use crate::test::*; + use crate::{commons::crypto::KrillSignerBuilder, daemon::config::ConfigDefaults, test::*}; use super::*; @@ -756,7 +757,10 @@ pub mod tests { #[test] fn should_create_self_signed_ta_id_cert() { test_under_tmp(|d| { - let s = KrillSigner::build(&d).unwrap(); + let signers = ConfigDefaults::signers(); + let s = KrillSignerBuilder::new(&d, Duration::from_secs(1), &signers) + .build() + .unwrap(); let key_id = s.create_key().unwrap(); let id_cert = IdCertBuilder::new_ta_id_cert(&key_id, &s).unwrap(); diff --git a/src/commons/crypto/cms.rs b/src/commons/crypto/cms.rs index 16910eea..37d4210c 100644 --- a/src/commons/crypto/cms.rs +++ b/src/commons/crypto/cms.rs @@ -509,10 +509,12 @@ impl CrlNumber { #[cfg(test)] mod tests { - use std::str::FromStr; + use std::{str::FromStr, time::Duration}; use crate::commons::api::Handle; + use crate::commons::crypto::KrillSignerBuilder; use crate::commons::remote::rfc6492::Message; + use crate::daemon::config::ConfigDefaults; use crate::test::test_under_tmp; use super::*; @@ -545,7 +547,10 @@ mod tests { #[test] fn should_create_crl_for_protocol() { test_under_tmp(|d| { - let s = KrillSigner::build(&d).unwrap(); + let signers = ConfigDefaults::signers(); + let s = KrillSignerBuilder::new(&d, Duration::from_secs(1), &signers) + .build() + .unwrap(); let key_id = s.create_key().unwrap(); let key_info = s.get_key_info(&key_id).unwrap(); @@ -557,7 +562,10 @@ mod tests { #[test] fn should_create_signed_publication_message() { test_under_tmp(|d| { - let s = KrillSigner::build(&d).unwrap(); + let signers = ConfigDefaults::signers(); + let s = KrillSignerBuilder::new(&d, Duration::from_secs(1), &signers) + .build() + .unwrap(); let key_id = s.create_key().unwrap(); let id_cert = IdCertBuilder::new_ta_id_cert(&key_id, &s).unwrap(); diff --git a/src/commons/crypto/signing.rs b/src/commons/crypto/signing.rs deleted file mode 100644 index 0a2f4c43..00000000 --- a/src/commons/crypto/signing.rs +++ /dev/null @@ -1,582 +0,0 @@ -//! Support for signing mft, crl, certificates, roas.. -//! Common objects for TAs and CAs -use std::{ - ops::Deref, - sync::{Arc, RwLock}, - {convert::TryFrom, path::Path}, -}; - -use bcder::Captured; -use bytes::Bytes; - -use rpki::{ - repository::{ - aspa::{Aspa, AspaBuilder}, - cert::{Cert, KeyUsage, Overclaim, TbsCert}, - crl::{Crl, CrlEntry, TbsCertList}, - crypto::{DigestAlgorithm, KeyIdentifier, PublicKey, PublicKeyFormat, Signature, SignatureAlgorithm, Signer}, - csr::Csr, - manifest::{FileAndHash, Manifest, ManifestContent}, - roa::{Roa, RoaBuilder}, - rta, - sigobj::SignedObjectBuilder, - x509::{Name, Serial, Time, Validity}, - }, - uri, -}; - -#[cfg(feature = "hsm")] -use crate::commons::util::dummysigner::DummySigner; - -use crate::{ - commons::{ - api::{IssuedCert, RcvdCert, ReplacedObject, RepoInfo, RequestResourceLimit, ResourceSet}, - crypto::{self, CryptoResult}, - error::Error, - util::{softsigner::OpenSslSigner, AllowedUri}, - KrillResult, - }, - daemon::ca::CertifiedKey, -}; - -//------------ Signer -------------------------------------------------------- - -#[derive(Clone, Debug)] -enum SignerProvider { - OpenSsl(OpenSslSigner), - - #[cfg(feature = "hsm")] - #[allow(dead_code)] - Dummy(DummySigner), -} - -impl SignerProvider { - pub fn create_key(&mut self) -> CryptoResult { - match self { - SignerProvider::OpenSsl(signer) => signer.create_key(PublicKeyFormat::Rsa), - #[cfg(feature = "hsm")] - SignerProvider::Dummy(signer) => signer.create_key(PublicKeyFormat::Rsa), - } - .map_err(crypto::Error::signer) - } - - pub fn destroy_key(&mut self, key_id: &KeyIdentifier) -> CryptoResult<()> { - match self { - SignerProvider::OpenSsl(signer) => signer.destroy_key(key_id), - #[cfg(feature = "hsm")] - SignerProvider::Dummy(signer) => signer.destroy_key(key_id), - } - .map_err(crypto::Error::key_error) - } - - pub fn get_key_info(&self, key_id: &KeyIdentifier) -> CryptoResult { - match self { - SignerProvider::OpenSsl(signer) => signer.get_key_info(key_id), - #[cfg(feature = "hsm")] - SignerProvider::Dummy(signer) => signer.get_key_info(key_id), - } - .map_err(crypto::Error::key_error) - } - - pub fn random_serial(&self) -> CryptoResult { - match self { - SignerProvider::OpenSsl(signer) => Serial::random(signer.deref()), - #[cfg(feature = "hsm")] - SignerProvider::Dummy(signer) => Serial::random(signer.deref()), - } - .map_err(crypto::Error::signer) - } - - pub fn sign + ?Sized>( - &self, - key_id: &KeyIdentifier, - sig_alg: SignatureAlgorithm, - data: &D, - ) -> CryptoResult { - match self { - SignerProvider::OpenSsl(signer) => signer.sign(key_id, sig_alg, data), - #[cfg(feature = "hsm")] - SignerProvider::Dummy(signer) => signer.sign(key_id, sig_alg, data), - } - .map_err(crypto::Error::signing) - } - - pub fn sign_one_off + ?Sized>( - &self, - sig_alg: SignatureAlgorithm, - data: &D, - ) -> CryptoResult<(Signature, PublicKey)> { - match self { - SignerProvider::OpenSsl(signer) => signer.sign_one_off(sig_alg, data), - #[cfg(feature = "hsm")] - SignerProvider::Dummy(signer) => signer.sign_one_off(sig_alg, data), - } - .map_err(crypto::Error::signer) - } - - pub fn sign_csr(&self, base_repo: &RepoInfo, name_space: &str, key: &KeyIdentifier) -> CryptoResult { - fn func(signer: &T, base_repo: &RepoInfo, name_space: &str, key: &KeyIdentifier) -> CryptoResult - where - T: Signer, - { - let pub_key = signer.get_key_info(key).map_err(crypto::Error::key_error)?; - Csr::construct( - signer.deref(), - key, - &base_repo.ca_repository(name_space).join(&[]).unwrap(), // force trailing slash - &base_repo.rpki_manifest(name_space, &pub_key.key_identifier()), - Some(&base_repo.rpki_notify()), - ) - .map_err(crypto::Error::signing) - } - - let enc = match self { - SignerProvider::OpenSsl(signer) => func(signer.deref(), base_repo, name_space, key), - #[cfg(feature = "hsm")] - SignerProvider::Dummy(signer) => func(signer.deref(), base_repo, name_space, key), - }?; - - Ok(Csr::decode(enc.as_slice())?) - } - - pub fn sign_cert(&self, tbs: TbsCert, key_id: &KeyIdentifier) -> CryptoResult { - match self { - SignerProvider::OpenSsl(signer) => tbs.into_cert(signer.deref(), key_id), - #[cfg(feature = "hsm")] - SignerProvider::Dummy(signer) => tbs.into_cert(signer.deref(), key_id), - } - .map_err(crypto::Error::signing) - } - - pub fn sign_crl(&self, tbs: TbsCertList>, key_id: &KeyIdentifier) -> CryptoResult { - match self { - SignerProvider::OpenSsl(signer) => tbs.into_crl(signer.deref(), key_id), - #[cfg(feature = "hsm")] - SignerProvider::Dummy(signer) => tbs.into_crl(signer.deref(), key_id), - } - .map_err(crypto::Error::signing) - } - - pub fn sign_manifest( - &self, - content: ManifestContent, - builder: SignedObjectBuilder, - key_id: &KeyIdentifier, - ) -> CryptoResult { - match self { - SignerProvider::OpenSsl(signer) => content.into_manifest(builder, signer.deref(), key_id), - #[cfg(feature = "hsm")] - SignerProvider::Dummy(signer) => content.into_manifest(builder, signer.deref(), key_id), - } - .map_err(crypto::Error::signing) - } - - pub fn sign_roa( - &self, - roa_builder: RoaBuilder, - object_builder: SignedObjectBuilder, - key_id: &KeyIdentifier, - ) -> CryptoResult { - match self { - SignerProvider::OpenSsl(signer) => roa_builder.finalize(object_builder, signer.deref(), key_id), - #[cfg(feature = "hsm")] - SignerProvider::Dummy(signer) => roa_builder.finalize(object_builder, signer.deref(), key_id), - } - .map_err(crypto::Error::signing) - } - - pub fn sign_aspa( - &self, - aspa_builder: AspaBuilder, - object_builder: SignedObjectBuilder, - key_id: &KeyIdentifier, - ) -> CryptoResult { - match self { - SignerProvider::OpenSsl(signer) => aspa_builder.finalize(object_builder, signer.deref(), key_id), - #[cfg(feature = "hsm")] - SignerProvider::Dummy(signer) => aspa_builder.finalize(object_builder, signer.deref(), key_id), - } - .map_err(crypto::Error::signing) - } - - pub fn sign_rta(&self, rta_builder: &mut rta::RtaBuilder, ee: Cert) -> CryptoResult<()> { - let key = ee.subject_key_identifier(); - rta_builder.push_cert(ee); - - match self { - SignerProvider::OpenSsl(signer) => rta_builder.sign(signer.deref(), &key, None, None), - #[cfg(feature = "hsm")] - SignerProvider::Dummy(signer) => rta_builder.sign(signer.deref(), &key, None, None), - } - .map_err(crypto::Error::signing) - } -} - -#[derive(Clone, Debug)] -pub struct KrillSigner { - // KrillSigner chooses which signer to use when. The noise of handling the enum based dispatch is handled by the - // SignerProvider type defined above, patterned after the existing AuthProvider enum based approach. - // - // Use Arc references so that we can use refer to the same signer instance more than once if that signer should be - // used for multiple purposes, e.g. as both general_signer and one_off_signer in this case. - // - // Use an RwLock because the Signer trait from the rpki-rs crate uses &mut self for create_key() and destroy_key() - // operations. In future we might move the responsibility for locking into the signer so that it can lock only what - // actually needs to be locked raet - - // The general signer is used for all signing operations except one off signing. - general_signer: Arc>, - - // As the security of a HSM isn't needed for one off keys, and HSMs are slow, by default this should be an instance - // of OpenSslSigner. However, if users think the perceived extra security is warranted let them use a different - // Signer for one off keys if that's what they want. - one_off_signer: Arc>, -} - -impl KrillSigner { - pub fn build(work_dir: &Path) -> KrillResult { - // The types of signer to initialize, the details needed to initialize them and the intended purpose for each - // signer (e.g. signer for past keys, currently used signer, signer to use for a key roll, etc.) should come - // from the configuration file. KrillSigner should combine that input its own rules, e.g. to dispatch a signing - // request to the correct signer we will need to determine which signer possesses the signing key, and the - // signer to use to create a new key depends on whether the key is one-off or not and whether or not it is - // being created for a key roll. For now the capability for different signers for different purposes exists but - // is not yet used. - - let openssl_signer = OpenSslSigner::build(work_dir)?; - let openssl_signer = Arc::new(RwLock::new(SignerProvider::OpenSsl(openssl_signer))); - let general_signer = openssl_signer.clone(); - let one_off_signer = openssl_signer; - Ok(KrillSigner { - general_signer, - one_off_signer, - }) - } - - pub fn create_key(&self) -> CryptoResult { - self.general_signer.write().unwrap().create_key() - } - - pub fn destroy_key(&self, key_id: &KeyIdentifier) -> CryptoResult<()> { - self.general_signer.write().unwrap().destroy_key(key_id) - } - - pub fn get_key_info(&self, key_id: &KeyIdentifier) -> CryptoResult { - self.general_signer.read().unwrap().get_key_info(key_id) - } - - pub fn random_serial(&self) -> CryptoResult { - self.general_signer.read().unwrap().random_serial() - } - - pub fn sign + ?Sized>(&self, key_id: &KeyIdentifier, data: &D) -> CryptoResult { - self.general_signer - .read() - .unwrap() - .sign(key_id, SignatureAlgorithm::default(), data) - } - - pub fn sign_one_off + ?Sized>(&self, data: &D) -> CryptoResult<(Signature, PublicKey)> { - self.one_off_signer - .read() - .unwrap() - .sign_one_off(SignatureAlgorithm::default(), data) - } - - pub fn sign_csr(&self, base_repo: &RepoInfo, name_space: &str, key: &KeyIdentifier) -> CryptoResult { - self.general_signer.read().unwrap().sign_csr(base_repo, name_space, key) - } - - pub fn sign_cert(&self, tbs: TbsCert, key_id: &KeyIdentifier) -> CryptoResult { - self.general_signer.read().unwrap().sign_cert(tbs, key_id) - } - - pub fn sign_crl(&self, tbs: TbsCertList>, key_id: &KeyIdentifier) -> CryptoResult { - self.general_signer.read().unwrap().sign_crl(tbs, key_id) - } - - pub fn sign_manifest( - &self, - content: ManifestContent, - builder: SignedObjectBuilder, - key_id: &KeyIdentifier, - ) -> CryptoResult { - self.general_signer - .read() - .unwrap() - .sign_manifest(content, builder, key_id) - } - - pub fn sign_roa( - &self, - roa_builder: RoaBuilder, - object_builder: SignedObjectBuilder, - key_id: &KeyIdentifier, - ) -> CryptoResult { - self.general_signer - .read() - .unwrap() - .sign_roa(roa_builder, object_builder, key_id) - } - - pub fn sign_aspa( - &self, - aspa_builder: AspaBuilder, - object_builder: SignedObjectBuilder, - key_id: &KeyIdentifier, - ) -> CryptoResult { - self.general_signer - .read() - .unwrap() - .sign_aspa(aspa_builder, object_builder, key_id) - } - - pub fn sign_rta(&self, rta_builder: &mut rta::RtaBuilder, ee: Cert) -> CryptoResult<()> { - self.general_signer.read().unwrap().sign_rta(rta_builder, ee) - } -} - -// //------------ Signer -------------------------------------------------------- -// -// pub trait Signer: crypto::Signer + Clone + Sized + Sync + Send + 'static {} -// impl + Clone + Sized + Sync + Send + 'static> Signer for T {} - -//------------ CsrInfo ------------------------------------------------------- - -pub type CaRepository = uri::Rsync; -pub type RpkiManifest = uri::Rsync; -pub type RpkiNotify = uri::Https; - -pub struct CsrInfo { - ca_repository: CaRepository, - rpki_manifest: RpkiManifest, - rpki_notify: Option, - key: PublicKey, -} - -impl CsrInfo { - pub fn new( - ca_repository: CaRepository, - rpki_manifest: RpkiManifest, - rpki_notify: Option, - key: PublicKey, - ) -> Self { - CsrInfo { - ca_repository, - rpki_manifest, - rpki_notify, - key, - } - } - - pub fn global_uris(&self) -> bool { - self.ca_repository.seems_global_uri() - && self.rpki_manifest.seems_global_uri() - && self - .rpki_notify - .as_ref() - .map(|uri| uri.seems_global_uri()) - .unwrap_or_else(|| true) - } - - pub fn unpack(self) -> (CaRepository, RpkiManifest, Option, PublicKey) { - (self.ca_repository, self.rpki_manifest, self.rpki_notify, self.key) - } - - pub fn key_id(&self) -> KeyIdentifier { - self.key.key_identifier() - } -} - -impl TryFrom<&Csr> for CsrInfo { - type Error = Error; - - fn try_from(csr: &Csr) -> KrillResult { - csr.validate().map_err(|_| Error::invalid_csr("invalid signature"))?; - let ca_repository = csr - .ca_repository() - .cloned() - .ok_or_else(|| Error::invalid_csr("missing ca repository"))?; - let rpki_manifest = csr - .rpki_manifest() - .cloned() - .ok_or_else(|| Error::invalid_csr("missing rpki manifest"))?; - let rpki_notify = csr.rpki_notify().cloned(); - let key = csr.public_key().clone(); - Ok(CsrInfo { - ca_repository, - rpki_manifest, - rpki_notify, - key, - }) - } -} - -impl From<&Cert> for CsrInfo { - fn from(issued: &Cert) -> Self { - let ca_repository = issued.ca_repository().cloned().unwrap(); - let rpki_manifest = issued.rpki_manifest().cloned().unwrap(); - let rpki_notify = issued.rpki_notify().cloned(); - let key = issued.subject_public_key_info().clone(); - CsrInfo { - ca_repository, - rpki_manifest, - rpki_notify, - key, - } - } -} - -//------------ CaSignSupport ------------------------------------------------- - -/// Support signing by CAs -pub struct SignSupport; - -impl SignSupport { - /// Create an IssuedCert - pub fn make_issued_cert( - csr: CsrInfo, - resources: &ResourceSet, - limit: RequestResourceLimit, - replaces: Option, - signing_key: &CertifiedKey, - weeks: i64, - signer: &KrillSigner, - ) -> KrillResult { - let signing_cert = signing_key.incoming_cert(); - let resources = resources.apply_limit(&limit)?; - if !signing_cert.resources().contains(&resources) { - return Err(Error::MissingResources); - } - - let validity = Self::sign_validity_weeks(weeks); - let request = CertRequest::Ca(csr, validity); - - let tbs = Self::make_tbs_cert(&resources, signing_cert, request, signer)?; - let cert = signer.sign_cert(tbs, signing_key.key_id())?; - - let cert_uri = signing_cert.uri_for_object(&cert); - - Ok(IssuedCert::new(cert_uri, limit, resources, cert, replaces)) - } - - /// Create an EE certificate for use in ResourceTaggedAttestations. - /// Note that for RPKI signed objects such as ROAs and Manifests, the - /// EE certificate is created by the rpki.rs library instead. - pub fn make_rta_ee_cert( - resources: &ResourceSet, - signing_key: &CertifiedKey, - validity: Validity, - pub_key: PublicKey, - signer: &KrillSigner, - ) -> KrillResult { - let signing_cert = signing_key.incoming_cert(); - let request = CertRequest::Ee(pub_key, validity); - let tbs = Self::make_tbs_cert(resources, signing_cert, request, signer)?; - - let cert = signer.sign_cert(tbs, signing_key.key_id())?; - Ok(cert) - } - - fn make_tbs_cert( - resources: &ResourceSet, - signing_cert: &RcvdCert, - request: CertRequest, - signer: &KrillSigner, - ) -> KrillResult { - let serial = signer.random_serial()?; - let issuer = signing_cert.cert().subject().clone(); - - let validity = match &request { - CertRequest::Ca(_, validity) => *validity, - CertRequest::Ee(_, validity) => *validity, - }; - - let pub_key = match &request { - CertRequest::Ca(info, _) => info.key.clone(), - CertRequest::Ee(key, _) => key.clone(), - }; - - let subject = Some(Name::from_pub_key(&pub_key)); - - let key_usage = match &request { - CertRequest::Ca(_, _) => KeyUsage::Ca, - CertRequest::Ee(_, _) => KeyUsage::Ee, - }; - - let overclaim = Overclaim::Refuse; - - let mut cert = TbsCert::new(serial, issuer, validity, subject, pub_key, key_usage, overclaim); - - let asns = resources.to_as_resources(); - if asns.is_inherited() || !asns.to_blocks().unwrap().is_empty() { - cert.set_as_resources(asns); - } - - let ipv4 = resources.to_ip_resources_v4(); - if ipv4.is_inherited() || !ipv4.to_blocks().unwrap().is_empty() { - cert.set_v4_resources(ipv4); - } - - let ipv6 = resources.to_ip_resources_v6(); - if ipv6.is_inherited() || !ipv6.to_blocks().unwrap().is_empty() { - cert.set_v6_resources(ipv6); - } - - cert.set_authority_key_identifier(Some(signing_cert.cert().subject_key_identifier())); - cert.set_ca_issuer(Some(signing_cert.uri().clone())); - cert.set_crl_uri(Some(signing_cert.crl_uri())); - - match request { - CertRequest::Ca(csr, _) => { - let (ca_repository, rpki_manifest, rpki_notify, _pub_key) = csr.unpack(); - cert.set_basic_ca(Some(true)); - cert.set_ca_repository(Some(ca_repository)); - cert.set_rpki_manifest(Some(rpki_manifest)); - cert.set_rpki_notify(rpki_notify); - } - CertRequest::Ee(_, _) => { - // cert.set_signed_object() ?? - } - } - - Ok(cert) - } - - /// Returns a validity period from 5 minutes ago (in case of NTP mess-up), to - /// X weeks from now. - pub fn sign_validity_weeks(weeks: i64) -> Validity { - let from = Time::five_minutes_ago(); - let until = Time::now() + chrono::Duration::weeks(weeks); - Validity::new(from, until) - } - - pub fn sign_validity_days(days: i64) -> Validity { - let from = Time::five_minutes_ago(); - let until = Time::now() + chrono::Duration::days(days); - Validity::new(from, until) - } -} - -#[allow(clippy::large_enum_variant)] -enum CertRequest { - Ca(CsrInfo, Validity), - Ee(PublicKey, Validity), -} - -trait ManifestEntry { - fn mft_bytes(&self) -> Bytes; - fn mft_hash(&self) -> Bytes { - let digest = DigestAlgorithm::default().digest(self.mft_bytes().as_ref()); - Bytes::copy_from_slice(digest.as_ref()) - } - fn mft_entry(&self, name: &str) -> FileAndHash { - FileAndHash::new(Bytes::copy_from_slice(name.as_bytes()), self.mft_hash()) - } -} - -impl ManifestEntry for Crl { - fn mft_bytes(&self) -> Bytes { - self.to_captured().into_bytes() - } -} diff --git a/src/commons/crypto/signing/dispatch/error.rs b/src/commons/crypto/signing/dispatch/error.rs new file mode 100644 index 00000000..8c7fee00 --- /dev/null +++ b/src/commons/crypto/signing/dispatch/error.rs @@ -0,0 +1,30 @@ +pub(super) struct ErrorString(String); + +impl std::ops::Deref for ErrorString { + type Target = String; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl ErrorString { + pub fn new(displayable: T) -> Self { + ErrorString(format!("{}", displayable)) + } +} + +impl From for String { + fn from(err: ErrorString) -> Self { + err.0 + } +} + +impl From for ErrorString +where + T: std::fmt::Display, +{ + fn from(err: T) -> Self { + ErrorString::new(err) + } +} diff --git a/src/commons/crypto/signing/dispatch/krillsigner.rs b/src/commons/crypto/signing/dispatch/krillsigner.rs new file mode 100644 index 00000000..7384eacf --- /dev/null +++ b/src/commons/crypto/signing/dispatch/krillsigner.rs @@ -0,0 +1,681 @@ +use std::{path::Path, sync::Arc, time::Duration}; + +use rpki::repository::{ + aspa::{Aspa, AspaBuilder}, + cert::TbsCert, + crl::{CrlEntry, TbsCertList}, + crypto::{KeyIdentifier, PublicKey, PublicKeyFormat, Signature, SignatureAlgorithm, Signer}, + manifest::ManifestContent, + roa::RoaBuilder, + rta, + sigobj::SignedObjectBuilder, + x509::Serial, + Cert, Crl, Csr, Manifest, Roa, +}; + +use crate::{ + commons::{ + api::RepoInfo, + crypto::{ + self, + dispatch::{ + signerinfo::SignerMapper, + signerprovider::{SignerFlags, SignerProvider}, + signerrouter::SignerRouter, + }, + CryptoResult, OpenSslSigner, + }, + error::Error, + KrillResult, + }, + daemon::config::{SignerConfig, SignerType}, +}; + +#[cfg(feature = "hsm")] +use crate::commons::{ + api::Handle, + crypto::signers::{kmip::KmipSigner, pkcs11::Pkcs11Signer}, +}; + +#[cfg(feature = "hsm")] +use std::collections::HashMap; + +/// High level signing interface between Krill and the [SignerRouter]. +/// +/// KrillSigner: +/// - Delegates Signer management and dispatch to [SignerRouter]. +/// - Maps Result to KrillResult. +/// - Directs signers to use the RPKI standard key format (RSA). +/// - Directs signers to use the RPKI standard signature algorithm (RSA PKCS #1 v1.5 with SHA-256). +/// - Offers additional high level functions compared to the [Signer] trait. +/// +/// We delegate to [SignerRouter] because our interface differs to that of the [Signer] trait and because the code is +/// easier to read if we separate out responsibilities. +/// +/// We need dispatch to the correct [Signer] to be done by a Struct that implements the [Signer] trait itself because +/// otherwise functions elsewhere in Krill that take a [Signer] trait as input will not invoke the correct [Signer]. +/// +/// We _could_ implement the [Signer] trait in [KrillSigner] but then we would implement two almost identical but +/// subtly different interfaces in the same struct AND implement management of signers and dispatch to the correct +/// signer all in one place, and that quickly becomes harder to read, understand and maintain. + +type SignerBuilderFn = fn( + &SignerType, + SignerFlags, + &Path, + &str, + std::time::Duration, + &Option>, +) -> KrillResult; + +#[derive(Debug)] +pub struct KrillSignerBuilder<'a> { + work_dir: &'a Path, + probe_interval: Duration, + signer_configs: &'a [SignerConfig], + default_signer: Option<&'a SignerConfig>, + one_off_signer: Option<&'a SignerConfig>, +} + +impl<'a> KrillSignerBuilder<'a> { + pub fn new(work_dir: &'a Path, probe_interval: Duration, signer_configs: &'a [SignerConfig]) -> Self { + Self { + work_dir, + probe_interval, + signer_configs, + default_signer: None, + one_off_signer: None, + } + } + + pub fn with_default_signer(&'a mut self, signer_config: &'a SignerConfig) -> &'a mut Self { + self.default_signer = Some(signer_config); + self + } + + pub fn with_one_off_signer(&'a mut self, signer_config: &'a SignerConfig) -> &'a mut Self { + self.one_off_signer = Some(signer_config); + self + } + + pub fn build(&'a mut self) -> KrillResult { + if self.signer_configs.is_empty() { + return Err(Error::ConfigError("At least one signer must be defined".to_string())); + } + + if self.signer_configs.len() == 1 { + if self.default_signer.is_none() { + self.default_signer = Some(&self.signer_configs[0]); + } + if self.one_off_signer.is_none() { + self.one_off_signer = Some(&self.signer_configs[0]); + } + } + + if self.default_signer.is_none() { + return Err(Error::ConfigError("No default signer is defined".to_string())); + } + let default_signer = self.default_signer.unwrap(); + + if !self.signer_configs.contains(default_signer) { + return Err(Error::ConfigError( + "The default signer must be one of the defined signers".to_string(), + )); + } + + if self.one_off_signer.is_none() { + return Err(Error::ConfigError("No one-off signer is defined".to_string())); + } + let one_off_signer = self.one_off_signer.unwrap(); + + if !self.signer_configs.contains(one_off_signer) { + return Err(Error::ConfigError( + "The one-off signer must be one of the defined signers".to_string(), + )); + } + + KrillSigner::build( + self.work_dir, + self.probe_interval, + self.signer_configs, + default_signer, + one_off_signer, + ) + } +} + +#[derive(Debug)] +pub struct KrillSigner { + router: SignerRouter, +} + +impl KrillSigner { + fn build( + work_dir: &Path, + probe_interval: Duration, + signer_configs: &[SignerConfig], + default_signer: &SignerConfig, + one_off_signer: &SignerConfig, + ) -> KrillResult { + #[cfg(not(feature = "hsm"))] + let signer_mapper = None; + #[cfg(feature = "hsm")] + let signer_mapper = Some(Arc::new(SignerMapper::build(work_dir)?)); + let signers = Self::build_signers( + signer_builder, + work_dir, + probe_interval, + &signer_mapper, + signer_configs, + default_signer, + one_off_signer, + )?; + let router = SignerRouter::build(signer_mapper, signers)?; + Ok(KrillSigner { router }) + } + + #[cfg(feature = "hsm")] + pub fn get_mapper(&self) -> Option> { + self.router.get_mapper() + } + + #[cfg(feature = "hsm")] + pub fn get_active_signers(&self) -> HashMap> { + self.router.get_active_signers() + } + + pub fn create_key(&self) -> CryptoResult { + self.router + .create_key(PublicKeyFormat::Rsa) + .map_err(crypto::Error::signer) + } + + pub fn destroy_key(&self, key_id: &KeyIdentifier) -> CryptoResult<()> { + self.router.destroy_key(key_id).map_err(crypto::Error::key_error) + } + + pub fn get_key_info(&self, key_id: &KeyIdentifier) -> CryptoResult { + self.router.get_key_info(key_id).map_err(crypto::Error::key_error) + } + + pub fn random_serial(&self) -> CryptoResult { + Serial::random(&self.router).map_err(crypto::Error::signer) + } + + pub fn sign + ?Sized>(&self, key_id: &KeyIdentifier, data: &D) -> CryptoResult { + self.router + .sign(key_id, SignatureAlgorithm::default(), data) + .map_err(crypto::Error::signing) + } + + pub fn sign_one_off + ?Sized>(&self, data: &D) -> CryptoResult<(Signature, PublicKey)> { + self.router + .sign_one_off(SignatureAlgorithm::default(), data) + .map_err(crypto::Error::signer) + } + + pub fn sign_csr(&self, base_repo: &RepoInfo, name_space: &str, key: &KeyIdentifier) -> CryptoResult { + let pub_key = self.router.get_key_info(key).map_err(crypto::Error::key_error)?; + let enc = Csr::construct( + &self.router, + key, + &base_repo.ca_repository(name_space).join(&[]).unwrap(), // force trailing slash + &base_repo.rpki_manifest(name_space, &pub_key.key_identifier()), + Some(&base_repo.rpki_notify()), + ) + .map_err(crypto::Error::signing)?; + Ok(Csr::decode(enc.as_slice())?) + } + + pub fn sign_cert(&self, tbs: TbsCert, key_id: &KeyIdentifier) -> CryptoResult { + tbs.into_cert(&self.router, key_id).map_err(crypto::Error::signing) + } + + pub fn sign_crl(&self, tbs: TbsCertList>, key_id: &KeyIdentifier) -> CryptoResult { + tbs.into_crl(&self.router, key_id).map_err(crypto::Error::signing) + } + + pub fn sign_manifest( + &self, + content: ManifestContent, + builder: SignedObjectBuilder, + key_id: &KeyIdentifier, + ) -> CryptoResult { + content + .into_manifest(builder, &self.router, key_id) + .map_err(crypto::Error::signing) + } + + pub fn sign_roa( + &self, + roa_builder: RoaBuilder, + object_builder: SignedObjectBuilder, + key_id: &KeyIdentifier, + ) -> CryptoResult { + roa_builder + .finalize(object_builder, &self.router, key_id) + .map_err(crypto::Error::signing) + } + + pub fn sign_aspa( + &self, + aspa_builder: AspaBuilder, + object_builder: SignedObjectBuilder, + key_id: &KeyIdentifier, + ) -> CryptoResult { + aspa_builder + .finalize(object_builder, &self.router, key_id) + .map_err(crypto::Error::signing) + } + + pub fn sign_rta(&self, rta_builder: &mut rta::RtaBuilder, ee: Cert) -> CryptoResult<()> { + let key = ee.subject_key_identifier(); + rta_builder.push_cert(ee); + rta_builder + .sign(&self.router, &key, None, None) + .map_err(crypto::Error::signing) + } +} + +impl KrillSigner { + fn build_signers( + signer_builder: SignerBuilderFn, + work_dir: &Path, + probe_interval: std::time::Duration, + mapper: &Option>, + configs: &[SignerConfig], + default_signer: &SignerConfig, + one_off_signer: &SignerConfig, + ) -> KrillResult> { + // There must always be at least one signer + if configs.is_empty() { + return Err(Error::signer( + "Internal error: At least one signer config must be provided", + )); + } + + // Instantiate each configured signer + let mut signers = Vec::new(); + for config in configs.iter() { + let flags = SignerFlags::new(config.name == default_signer.name, config.name == one_off_signer.name); + + info!( + "Configuring signer '{}' (type: {}, {})", + config.name, config.signer_type, flags + ); + + let signer = (signer_builder)( + &config.signer_type, + flags, + work_dir, + &config.name, + probe_interval, + mapper, + )?; + + signers.push(signer); + } + + Ok(signers) + } +} + +fn signer_builder( + r#type: &SignerType, + flags: SignerFlags, + work_dir: &Path, + name: &str, + probe_interval: Duration, + mapper: &Option>, +) -> KrillResult { + match r#type { + SignerType::OpenSsl(conf) => { + let data_dir = if let Some(ref path) = conf.keys_path { + path.as_path() + } else { + work_dir + }; + + let signer = OpenSslSigner::build(data_dir, name, mapper.clone())?; + + Ok(SignerProvider::OpenSsl(flags, signer)) + } + #[cfg(feature = "hsm")] + SignerType::Pkcs11(conf) => { + let signer = Pkcs11Signer::build(name, &conf, probe_interval, mapper.as_ref().unwrap().clone())?; + Ok(SignerProvider::Pkcs11(flags, signer)) + } + #[cfg(feature = "hsm")] + SignerType::Kmip(conf) => { + let signer = KmipSigner::build(name, &conf, probe_interval, mapper.as_ref().unwrap().clone())?; + Ok(SignerProvider::Kmip(flags, signer)) + } + } +} + +//------------ Tests --------------------------------------------------------- + +#[cfg(all( + test, + feature = "hsm", + not(any(feature = "hsm-tests-kmip", feature = "hsm-tests-pkcs11")) +))] +pub mod tests { + use std::{path::PathBuf, time::Duration}; + + use crate::{ + commons::crypto::signers::mocksigner::{MockSigner, MockSignerCallCounts}, + daemon::config::Config, + test, + }; + + use super::*; + + /// A signer builder fn that builds MockSigner instances instead of real signer instances. + /// Used to test KrillSigner::build_signers(). + fn mock_signer_builder( + r#type: &SignerType, + flags: SignerFlags, + _work_dir: &Path, + name: &str, + _probe_interval: Duration, + mapper: &Option>, + ) -> KrillResult { + let call_counts = Arc::new(MockSignerCallCounts::new()); + let mut mock_signer = MockSigner::new(name, mapper.as_ref().unwrap().clone(), call_counts.clone(), None, None); + mock_signer.set_info(&format!("mock {} signer", r#type)); + Ok(SignerProvider::Mock(flags, mock_signer)) + } + + /// Create a Krill Config object from a Krill config file text fragment. + fn config_fragment_to_config_object(fragment: &str) -> Result { + let mut config_str = r#"admin_token = "***""#.to_string(); + config_str.push_str(fragment); + toml::from_str(&config_str) + } + + fn build_krill_signer_from_config( + signers_config_fragment: &str, + work_dir: &PathBuf, + mapper: Arc, + ) -> KrillResult> { + let mut config = config_fragment_to_config_object(signers_config_fragment).unwrap(); + config.process().map_err(|err| Error::ConfigError(err.to_string()))?; + let mapper = Some(mapper); + let probe_interval = std::time::Duration::from_secs(1); + KrillSigner::build_signers( + mock_signer_builder, + work_dir, + probe_interval, + &mapper, + &config.signers, + config.default_signer(), + config.one_off_signer(), + ) + } + + fn assert_signer_name(signer: &SignerProvider, expected: &str) { + assert_eq!(signer.get_name(), expected); + } + + fn assert_signer_type(signer: &SignerProvider, expected: &str) { + assert_eq!(signer.get_info().unwrap(), format!("mock {} signer", expected)); + } + + fn assert_signer_name_and_type(signer: &SignerProvider, expected: &str) { + assert_signer_name(signer, expected); + assert_signer_type(signer, expected); + } + + fn assert_signer_flags(signer: &SignerProvider, expected_default: bool, expected_one_off: bool) { + assert_eq!(expected_default, signer.is_default_signer()); + assert_eq!(expected_one_off, signer.is_one_off_signer()); + } + + /// Prior to the addition of HSM support Krill had no notion of configurable signers. Instead it always created a + /// single OpenSSL signer that was used for all signing related operations (i.e. key creation, deletion, signing, + /// one-off signing and random number generation). With HSM support enabled, if no signers are defined in the Krill + /// configuration file the behaviour should be the same as it was before HSM support was added. + #[test] + pub fn no_signers_equals_one_openssl_signer_for_backward_compatibility() { + test::test_under_tmp(|d| { + let mapper = Arc::new(SignerMapper::build(&d).unwrap()); + let signers = build_krill_signer_from_config("", &d, mapper).unwrap(); + assert_eq!(signers.len(), 1); + let signer = &signers[0]; + assert_signer_name(signer, "Default OpenSSL signer"); + assert_signer_type(signer, "OpenSSL"); + assert_signer_flags(signer, true, true); + }); + } + + #[test] + pub fn signer_name_is_respected() { + test::test_under_tmp(|d| { + let mapper = Arc::new(SignerMapper::build(&d).unwrap()); + let signers_config_fragment = r#" + [[signers]] + type = "OpenSSL" + name = "Some test name" + "#; + let signers = build_krill_signer_from_config(signers_config_fragment, &d, mapper).unwrap(); + assert_eq!(signers.len(), 1); + let signer = &signers[0]; + assert_signer_name(signer, "Some test name"); + assert_signer_type(signer, "OpenSSL"); + assert_signer_flags(signer, true, true); + }); + } + + /// To make it easier for the operator we don't want them to have to manually remember to mark a single OpenSSL + /// signer configuration as the default one, it should just automatically be the default signer and should in fact + /// be used for all signing related operations, i.e. one-off signing and random number generation as well as the + /// key creation, deletion and signing operations handled by the default signer. + #[test] + pub fn single_openssl_signer_is_made_the_default_all_signer() { + test::test_under_tmp(|d| { + let mapper = Arc::new(SignerMapper::build(&d).unwrap()); + let signers_config_fragment = r#" + [[signers]] + type = "OpenSSL" + name = "OpenSSL" + "#; + let signers = build_krill_signer_from_config(signers_config_fragment, &d, mapper).unwrap(); + assert_eq!(signers.len(), 1); + let signer = &signers[0]; + assert_signer_name_and_type(signer, "OpenSSL"); + assert_signer_flags(signer, true, true); + }); + } + + #[test] + pub fn create_openssl_signer_for_one_off_signing() { + test::test_under_tmp(|d| { + let mapper = Arc::new(SignerMapper::build(&d).unwrap()); + + let signer_config_fragment = r#" + [[signers]] + type = "KMIP" + name = "KMIP" + host = "dummy host" + "#; + let signers = build_krill_signer_from_config(signer_config_fragment, &d, mapper.clone()).unwrap(); + assert_eq!(signers.len(), 2); + let signer = &signers[0]; + assert_signer_name_and_type(signer, "KMIP"); + assert_signer_flags(signer, true, false); + + let signer = &signers[1]; + assert_signer_name(signer, "OpenSSL one-off signer"); + assert_signer_type(signer, "OpenSSL"); + assert_signer_flags(signer, false, true); + + // --- + + let signer_config_fragment = r#" + [[signers]] + type = "PKCS#11" + name = "PKCS#11" + lib_path = "dummy" + slot = "dummy slot" + "#; + let signers = build_krill_signer_from_config(signer_config_fragment, &d, mapper).unwrap(); + assert_eq!(signers.len(), 2); + let signer = &signers[0]; + assert_signer_name_and_type(signer, "PKCS#11"); + assert_signer_flags(signer, true, false); + + let signer = &signers[1]; + assert_signer_name(signer, "OpenSSL one-off signer"); + assert_signer_type(signer, "OpenSSL"); + assert_signer_flags(signer, false, true); + }); + } + + #[test] + pub fn one_off_signer_is_respected() { + test::test_under_tmp(|d| { + let mapper = Arc::new(SignerMapper::build(&d).unwrap()); + + let signer_config_fragment = r#" + one_off_signer = "KMIP" + + [[signers]] + type = "KMIP" + name = "KMIP" + host = "dummy host" + "#; + let signers = build_krill_signer_from_config(signer_config_fragment, &d, mapper.clone()).unwrap(); + assert_eq!(signers.len(), 1); + let signer = &signers[0]; + assert_signer_name_and_type(signer, "KMIP"); + assert_signer_flags(signer, true, true); + + // --- + + let signer_config_fragment = r#" + one_off_signer = "PKCS#11" + + [[signers]] + type = "PKCS#11" + name = "PKCS#11" + lib_path = "dummy" + slot = "dummy slot" + "#; + let signers = build_krill_signer_from_config(signer_config_fragment, &d, mapper).unwrap(); + assert_eq!(signers.len(), 1); + let signer = &signers[0]; + assert_signer_name_and_type(signer, "PKCS#11"); + assert_signer_flags(signer, true, true); + }); + } + + #[test] + pub fn default_signer_is_respected() { + test::test_under_tmp(|d| { + let mapper = Arc::new(SignerMapper::build(&d).unwrap()); + + let signer_config_fragment = r#" + default_signer = "Signer 2" + + [[signers]] + type = "OpenSSL" + name = "Signer 1" + + [[signers]] + type = "KMIP" + name = "Signer 2" + host = "dummy host" + "#; + let signers = build_krill_signer_from_config(signer_config_fragment, &d, mapper.clone()).unwrap(); + assert_eq!(signers.len(), 2); + + let signer = &signers[0]; + assert_signer_type(signer, "OpenSSL"); + assert_signer_name(signer, "Signer 1"); + assert_signer_flags(signer, false, true); + + let signer = &signers[1]; + assert_signer_type(signer, "KMIP"); + assert_signer_name(signer, "Signer 2"); + assert_signer_flags(signer, true, false); + }); + } + + #[test] + pub fn default_signer_and_one_off_signer_are_respected() { + test::test_under_tmp(|d| { + let mapper = Arc::new(SignerMapper::build(&d).unwrap()); + + let signer_config_fragment = r#" + default_signer = "Signer 2" + one_off_signer = "Signer 2" + + [[signers]] + type = "OpenSSL" + name = "Signer 1" # unused / historic signer + + [[signers]] + type = "KMIP" + name = "Signer 2" # default and one off signer + host = "dummy host" + "#; + let signers = build_krill_signer_from_config(signer_config_fragment, &d, mapper.clone()).unwrap(); + assert_eq!(signers.len(), 2); + + let signer = &signers[0]; + assert_signer_type(signer, "OpenSSL"); + assert_signer_name(signer, "Signer 1"); + assert_signer_flags(signer, false, false); + + let signer = &signers[1]; + assert_signer_type(signer, "KMIP"); + assert_signer_name(signer, "Signer 2"); + assert_signer_flags(signer, true, true); + }); + } + + #[test] + pub fn historic_signers_are_permitted() { + test::test_under_tmp(|d| { + let mapper = Arc::new(SignerMapper::build(&d).unwrap()); + + let signer_config_fragment = r#" + default_signer = "Signer 2" + one_off_signer = "Signer 3" + + [[signers]] + type = "OpenSSL" + name = "Signer 1" # historic signer only used with previously created keys + + [[signers]] + type = "KMIP" + name = "Signer 2" # default signer for new keys + host = "dummy host" + + [[signers]] + type = "PKCS#11" + name = "Signer 3" # one off signer + lib_path = "dummy" + slot = "dummy slot" + "#; + let signers = build_krill_signer_from_config(signer_config_fragment, &d, mapper.clone()).unwrap(); + assert_eq!(signers.len(), 3); + + let signer = &signers[0]; + assert_signer_type(signer, "OpenSSL"); + assert_signer_name(signer, "Signer 1"); + assert_signer_flags(signer, false, false); + + let signer = &signers[1]; + assert_signer_type(signer, "KMIP"); + assert_signer_name(signer, "Signer 2"); + assert_signer_flags(signer, true, false); + + let signer = &signers[2]; + assert_signer_type(signer, "PKCS#11"); + assert_signer_name(signer, "Signer 3"); + assert_signer_flags(signer, false, true); + }); + } +} diff --git a/src/commons/crypto/signing/dispatch/mod.rs b/src/commons/crypto/signing/dispatch/mod.rs new file mode 100644 index 00000000..8d46285f --- /dev/null +++ b/src/commons/crypto/signing/dispatch/mod.rs @@ -0,0 +1,9 @@ +pub mod krillsigner; + +pub mod signerinfo; + +pub mod signerprovider; + +pub mod signerrouter; + +pub mod error; diff --git a/src/commons/crypto/signing/dispatch/signerinfo.rs b/src/commons/crypto/signing/dispatch/signerinfo.rs new file mode 100644 index 00000000..ae6bcd19 --- /dev/null +++ b/src/commons/crypto/signing/dispatch/signerinfo.rs @@ -0,0 +1,463 @@ +//! An event sourcing aggregate store for capturing information about signer backends and set of keys they possess. + +use std::{collections::HashMap, fmt, path::Path, str::FromStr}; + +use rpki::repository::crypto::{KeyIdentifier, PublicKey}; + +use crate::{ + commons::{ + actor::Actor, + api::{CommandSummary, Handle}, + error::Error, + eventsourcing::{Aggregate, AggregateStore, CommandDetails, SentCommand, StoredEvent, WithStorableDetails}, + util::ext_serde, + KrillResult, + }, + constants::{ACTOR_DEF_KRILL, SIGNERS_DIR}, +}; + +//------------ InitSignerInfoEvent ----------------------------------------------------------------------------- +type InitSignerInfoEvent = StoredEvent; + +impl InitSignerInfoEvent { + pub fn init( + id: &Handle, + signer_name: &str, + signer_info: &str, + public_key: &PublicKey, + private_key_internal_id: &str, + ) -> Self { + StoredEvent::new( + id, + 0, + InitSignerInfoDetails { + signer_name: signer_name.to_string(), + signer_info: signer_info.to_string(), + signer_identity: SignerIdentity { + public_key: public_key.clone(), + private_key_internal_id: private_key_internal_id.to_string(), + }, + }, + ) + } +} + +#[derive(Clone, Deserialize, Eq, PartialEq, Serialize)] +struct InitSignerInfoDetails { + pub signer_name: String, + pub signer_info: String, + pub signer_identity: SignerIdentity, +} + +impl fmt::Display for InitSignerInfoDetails { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "Signer info initialized with name '{}'", self.signer_name) + } +} + +//------------ SignerInfoEvent --------------------------------------------------------------------------------- +type SignerInfoEvent = StoredEvent; + +#[derive(Clone, Deserialize, Eq, PartialEq, Serialize)] +enum SignerInfoEventDetails { + KeyAdded(KeyIdentifier, String), + KeyRemoved(KeyIdentifier), + SignerNameChanged(String), + SignerInfoChanged(String), +} + +impl SignerInfoEvent { + pub fn key_added(si: &SignerInfo, key_id: KeyIdentifier, internal_key_id: String) -> Self { + StoredEvent::new( + si.id(), + si.version, + SignerInfoEventDetails::KeyAdded(key_id, internal_key_id), + ) + } + + pub fn key_removed(si: &SignerInfo, key_id: KeyIdentifier) -> Self { + StoredEvent::new(si.id(), si.version, SignerInfoEventDetails::KeyRemoved(key_id)) + } + + pub fn signer_name_changed(si: &SignerInfo, signer_name: String) -> Self { + StoredEvent::new( + si.id(), + si.version, + SignerInfoEventDetails::SignerNameChanged(signer_name), + ) + } + + pub fn signer_info_changed(si: &SignerInfo, signer_info: String) -> Self { + StoredEvent::new( + si.id(), + si.version, + SignerInfoEventDetails::SignerInfoChanged(signer_info), + ) + } +} + +impl fmt::Display for SignerInfoEventDetails { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + SignerInfoEventDetails::KeyAdded(key_id, internal_key_id) => write!( + f, + "added key with key id '{}' and internal key id '{}'", + key_id, internal_key_id + ), + SignerInfoEventDetails::KeyRemoved(key_id) => write!(f, "removed key with key id '{}'", key_id), + SignerInfoEventDetails::SignerNameChanged(signer_name) => { + write!(f, "signer name changed to '{}'", signer_name) + } + SignerInfoEventDetails::SignerInfoChanged(signer_info) => { + write!(f, "signer info changed to '{}'", signer_info) + } + } + } +} + +//------------ SignerInfoCommand ---------------------------------------------------------------------------------- + +type SignerInfoCommand = SentCommand; + +#[derive(Clone, Deserialize, Eq, PartialEq, Serialize)] +enum SignerInfoCommandDetails { + AddKey(KeyIdentifier, String), + RemoveKey(KeyIdentifier), + ChangeSignerName(String), + ChangeSignerInfo(String), +} + +impl fmt::Display for SignerInfoCommandDetails { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + SignerInfoCommandDetails::AddKey(key_id, internal_key_id) => write!( + f, + "Add key with key id '{}' and internal key id '{}'", + key_id, internal_key_id + ), + SignerInfoCommandDetails::RemoveKey(key_id) => write!(f, "Remove key with key id '{}'", key_id), + SignerInfoCommandDetails::ChangeSignerName(signer_name) => { + write!(f, "Change signer name to '{}'", signer_name) + } + SignerInfoCommandDetails::ChangeSignerInfo(signer_info) => { + write!(f, "Change signer info to '{}'", signer_info) + } + } + } +} + +impl WithStorableDetails for SignerInfoCommandDetails { + fn summary(&self) -> CommandSummary { + match self { + SignerInfoCommandDetails::AddKey(key_id, internal_key_id) => CommandSummary::new("signer-add-key", &self) + .with_arg("key_id", key_id) + .with_arg("internal_key_id", internal_key_id), + SignerInfoCommandDetails::RemoveKey(key_id) => { + CommandSummary::new("signer-remove-key", &self).with_arg("key_id", key_id) + } + SignerInfoCommandDetails::ChangeSignerName(signer_name) => { + CommandSummary::new("signer-change-name", &self).with_arg("signer_name", signer_name) + } + SignerInfoCommandDetails::ChangeSignerInfo(signer_info) => { + CommandSummary::new("signer-change-info", &self).with_arg("signer_info", signer_info) + } + } + } +} + +impl CommandDetails for SignerInfoCommandDetails { + type Event = SignerInfoEvent; + type StorableDetails = Self; + + fn store(&self) -> Self::StorableDetails { + self.clone() + } +} + +impl SignerInfoCommand { + pub fn add_key(id: &Handle, version: Option, key_id: &KeyIdentifier, internal_key_id: &str) -> Self { + let details = SignerInfoCommandDetails::AddKey(*key_id, internal_key_id.to_string()); + let actor = Actor::test_from_def(ACTOR_DEF_KRILL); + Self::new(id, version, details, &actor) + } + + pub fn remove_key(id: &Handle, version: Option, key_id: &KeyIdentifier) -> Self { + let details = SignerInfoCommandDetails::RemoveKey(*key_id); + let actor = Actor::test_from_def(ACTOR_DEF_KRILL); + Self::new(id, version, details, &actor) + } + + pub fn change_signer_name(id: &Handle, version: Option, signer_name: &str) -> Self { + let details = SignerInfoCommandDetails::ChangeSignerName(signer_name.to_string()); + let actor = Actor::test_from_def(ACTOR_DEF_KRILL); + Self::new(id, version, details, &actor) + } + + pub fn change_signer_info(id: &Handle, version: Option, signer_info: &str) -> Self { + let details = SignerInfoCommandDetails::ChangeSignerInfo(signer_info.to_string()); + let actor = Actor::test_from_def(ACTOR_DEF_KRILL); + Self::new(id, version, details, &actor) + } +} + +//------------ SignerInfo ----------------------------------------------------------------------------------------- + +#[derive(Clone, Deserialize, Eq, PartialEq, Serialize)] +struct SignerIdentity { + /// An X.509 Subject Public Key Info public key that can be used to verify the identity of the signer. + #[serde( + deserialize_with = "ext_serde::de_public_key", + serialize_with = "ext_serde::ser_public_key" + )] + public_key: PublicKey, + + /// The internal signer backend specific identifier for the corresponding private key. + private_key_internal_id: String, +} + +/// SignerInfo defines the set of keys created in a particular signer backend and the identity of that backend. +/// +#[derive(Clone, Deserialize, Serialize)] +struct SignerInfo { + /// The id is needed when generating events. + id: Handle, + + /// The version of for this particular SignerInfo. Versions are incremented whenever events are applied. They are + /// used to store those and apply events in the correct sequence, as well as to detect concurrency issues when a + /// command is sent. + version: u64, + + /// An operator assigned human readable name for this signer. + signer_name: String, + + /// Information about the signer backend being used. + signer_info: String, + + /// Details needed to confirm the identity of the signer backend. + signer_identity: SignerIdentity, + + /// The keys that the signer possesses identified by their Krill KeyIdentifier and their corresponding signer + /// specific internal identifier. + keys: HashMap, +} + +impl SignerInfo { + pub fn id(&self) -> &Handle { + &self.id + } +} + +impl Aggregate for SignerInfo { + type Command = SignerInfoCommand; + type StorableCommandDetails = SignerInfoCommandDetails; + type Event = SignerInfoEvent; + type InitEvent = InitSignerInfoEvent; + type Error = Error; + + fn init(event: InitSignerInfoEvent) -> Result { + let (id, _version, init) = event.unpack(); + Ok(SignerInfo { + id, + version: 1, + signer_name: init.signer_name, + signer_info: init.signer_info, + signer_identity: init.signer_identity, + keys: HashMap::new(), + }) + } + + fn version(&self) -> u64 { + self.version + } + + fn apply(&mut self, event: SignerInfoEvent) { + match event.into_details() { + SignerInfoEventDetails::KeyAdded(key_id, internal_key_id) => { + self.keys.insert(key_id, internal_key_id); + } + SignerInfoEventDetails::KeyRemoved(key_id) => { + let _ = self.keys.remove(&key_id); + } + SignerInfoEventDetails::SignerNameChanged(signer_name) => { + self.signer_name = signer_name; + } + SignerInfoEventDetails::SignerInfoChanged(signer_info) => { + self.signer_info = signer_info; + } + } + self.version += 1; + } + + fn process_command(&self, command: Self::Command) -> Result, Self::Error> { + match command.into_details() { + SignerInfoCommandDetails::AddKey(key_id, internal_key_id) => { + let event = SignerInfoEvent::key_added(self, key_id, internal_key_id); + Ok(vec![event]) + } + SignerInfoCommandDetails::RemoveKey(key_id) => { + let event = SignerInfoEvent::key_removed(self, key_id); + Ok(vec![event]) + } + SignerInfoCommandDetails::ChangeSignerName(signer_name) => { + if signer_name != self.signer_name { + let event = SignerInfoEvent::signer_name_changed(self, signer_name); + Ok(vec![event]) + } else { + Ok(vec![]) + } + } + SignerInfoCommandDetails::ChangeSignerInfo(signer_info) => { + if signer_info != self.signer_info { + let event = SignerInfoEvent::signer_info_changed(self, signer_info); + Ok(vec![event]) + } else { + Ok(vec![]) + } + } + } + } +} + +pub struct SignerMapper { + store: AggregateStore, +} + +impl std::fmt::Debug for SignerMapper { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("SignerMapper").finish() + } +} + +impl SignerMapper { + /// Build a SignerMapper that will read/write its data in a subdirectory of the given work dir. + pub fn build(work_dir: &Path) -> KrillResult { + let store = AggregateStore::::disk(work_dir, SIGNERS_DIR)?; + Ok(SignerMapper { store }) + } + + /// Record the existence of a new signer. + /// + /// A signer has several properties, some fixed, some modifiable. The handle and public key are fixed at signer + /// creation time while the name and info strings can be changed later. + /// + /// - The handle is an unchanging identifier that will uniquely identify the signer in the mapper store. Each signer + /// in the store is required to have a unique handle. The meaning/content of the handle is opaque to the store. + /// Do not use a human readable string as the handle because you may cause confusion if the value has some meaning + /// that is later found to be false or misleading and can then no longer be changed. Instead use the 'name' + /// argument to assign human readable identifier that may need to be changed later. + /// + /// - The public key is an unchanging public key that can be used to verify that a given signer in the mapper store + /// corresponds to a particular signer backend. Verification is done by asking the signer backend to sign a + /// challenge and verifying that the produced signature corresponds to the stored public key. If verification is + /// successful it means that we expect the signer backend to possess the keys attributed to it in the signer + /// store. + /// + /// - The name is an operator defined string that is expected to come from the Krill configuration file and which + /// is intended to be a useful friendly human readable identifier to be displayed in the UI or in CLI output or + /// included in log or error messages. The name can be changed later by calling `change_signer_name()`. + /// + /// - The info string is intended to contain details retrieved from the signer backend that describe useful, + /// interesting and/or identifying properties of the backend. The info string can be changed later by calling + /// `change_signer_info()`. This could be useful for example if the signer backend retains its content but is + /// upgraded to a newer version, we can then update the info string in the signer store and the upgrade will be + /// visible in the history of the store. + pub fn add_signer( + &self, + signer_name: &str, + signer_info: &str, + public_key: &PublicKey, + private_key_internal_id: &str, + ) -> KrillResult { + let signer_handle = Handle::from_str(&uuid::Uuid::new_v4().to_string()).map_err(|err| { + Error::SignerError(format!( + "Generated UUID is not a valid signer handle: {}", + err.to_string() + )) + })?; + + let init = InitSignerInfoEvent::init( + &signer_handle, + signer_name, + signer_info, + public_key, + private_key_internal_id, + ); + self.store.add(init)?; + Ok(signer_handle) + } + + pub fn _remove_signer(&self, signer_handle: &Handle) -> KrillResult<()> { + self.store.drop_aggregate(signer_handle)?; + Ok(()) + } + + pub fn get_signer_name(&self, signer_handle: &Handle) -> KrillResult { + Ok(self.store.get_latest(signer_handle)?.signer_name.clone()) + } + + pub fn change_signer_name(&self, signer_handle: &Handle, signer_name: &str) -> KrillResult<()> { + let cmd = SignerInfoCommand::change_signer_name(signer_handle, None, signer_name); + self.store.command(cmd)?; + Ok(()) + } + + pub fn get_signer_public_key(&self, signer_handle: &Handle) -> KrillResult { + Ok(self.store.get_latest(signer_handle)?.signer_identity.public_key.clone()) + } + + pub fn get_signer_private_key_internal_id(&self, signer_handle: &Handle) -> KrillResult { + Ok(self + .store + .get_latest(signer_handle)? + .signer_identity + .private_key_internal_id + .clone()) + } + + pub fn change_signer_info(&self, signer_handle: &Handle, signer_info: &str) -> KrillResult<()> { + let cmd = SignerInfoCommand::change_signer_info(signer_handle, None, signer_info); + self.store.command(cmd)?; + Ok(()) + } + + /// Record the owner of a Krill key and its corresponding signer specific internal id. + pub fn add_key(&self, signer_handle: &Handle, key_id: &KeyIdentifier, internal_key_id: &str) -> KrillResult<()> { + let cmd = SignerInfoCommand::add_key(signer_handle, None, key_id, internal_key_id); + self.store.command(cmd)?; + Ok(()) + } + + pub fn remove_key(&self, signer_handle: &Handle, key_id: &KeyIdentifier) -> KrillResult<()> { + let cmd = SignerInfoCommand::remove_key(signer_handle, None, key_id); + self.store.command(cmd)?; + Ok(()) + } + + /// Retrieve the signer specific internal id corresponding to the given Krill key. + pub fn get_key(&self, signer_handle: &Handle, key_id: &KeyIdentifier) -> KrillResult { + self.store + .get_latest(signer_handle)? + .keys + .get(key_id) + .cloned() + .ok_or_else(|| Error::SignerError(format!("Key with key id '{}' not found", key_id))) + } + + /// Get the complete set of known signer handles. + pub fn get_signer_handles(&self) -> KrillResult> { + self.store.list().map_err(Error::AggregateStoreError) + } + + /// Get the handle of the signer that possesses the given Krill key, if any. + pub fn get_signer_for_key(&self, key_id: &KeyIdentifier) -> KrillResult { + // Look for the key id in the key set of each set. Not very efficient but can be improved upon later if + // needed, e.g. by creating on startup and maintaining an in-memory map of KeyIdentifier to signer Handles. + for signer_handle in self.store.list()? { + let signer_info = self.store.get_latest(&signer_handle)?; + if signer_info.keys.contains_key(key_id) { + return Ok(signer_handle); + } + } + Err(Error::SignerError(format!("No signer owns key id '{}'", key_id))) + } +} diff --git a/src/commons/crypto/signing/dispatch/signerprovider.rs b/src/commons/crypto/signing/dispatch/signerprovider.rs new file mode 100644 index 00000000..273be031 --- /dev/null +++ b/src/commons/crypto/signing/dispatch/signerprovider.rs @@ -0,0 +1,241 @@ +use rpki::repository::crypto::{ + signer::KeyError, KeyIdentifier, PublicKey, PublicKeyFormat, Signature, SignatureAlgorithm, SigningError, +}; + +use crate::commons::{ + api::Handle, + crypto::signers::{error::SignerError, softsigner::OpenSslSigner}, +}; + +#[cfg(all(test, feature = "hsm"))] +use crate::commons::crypto::signers::mocksigner::MockSigner; + +#[cfg(feature = "hsm")] +use crate::commons::crypto::signers::{kmip::KmipSigner, pkcs11::Pkcs11Signer}; + +//------------ SignerProvider ------------------------------------------------ + +#[derive(Debug)] +pub struct SignerFlags { + pub is_default_signer: bool, + pub is_one_off_signer: bool, +} + +impl std::fmt::Display for SignerFlags { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_fmt(format_args!( + "default: {}, one_off: {}", + self.is_default_signer, self.is_one_off_signer + )) + } +} + +impl Default for SignerFlags { + fn default() -> Self { + // Making round trips to an HSM to create, sign with and destroy one-off signing keys can be slow, and doesn't + // benefit from the protection afforded by storing keys in the HSM. Therefore by default we don't use the + // default signer for one-off signing but instead expect an OpenSSL signer to be created for this purpose. + Self { + is_default_signer: true, + is_one_off_signer: false, + } + } +} + +impl SignerFlags { + pub fn new(is_default_signer: bool, is_one_off_signer: bool) -> Self { + Self { + is_default_signer, + is_one_off_signer, + } + } +} + +/// Dispatchers Signer requests to a particular implementation of the Signer trait. +/// +/// Named and modelled after the similar AuthProvider concept that already exists in Krill. +#[allow(dead_code)] // Needed as we currently only ever construct one variant +#[derive(Debug)] +pub enum SignerProvider { + OpenSsl(SignerFlags, OpenSslSigner), + + #[cfg(feature = "hsm")] + Kmip(SignerFlags, KmipSigner), + + #[cfg(feature = "hsm")] + Pkcs11(SignerFlags, Pkcs11Signer), + + #[cfg(all(test, feature = "hsm"))] + Mock(SignerFlags, MockSigner), +} + +impl SignerProvider { + pub fn is_default_signer(&self) -> bool { + match self { + SignerProvider::OpenSsl(flags, _) => flags.is_default_signer, + #[cfg(feature = "hsm")] + SignerProvider::Kmip(flags, _) => flags.is_default_signer, + #[cfg(feature = "hsm")] + SignerProvider::Pkcs11(flags, _) => flags.is_default_signer, + #[cfg(all(test, feature = "hsm"))] + SignerProvider::Mock(flags, _) => flags.is_default_signer, + } + } + + pub fn is_one_off_signer(&self) -> bool { + match self { + SignerProvider::OpenSsl(flags, _) => flags.is_one_off_signer, + #[cfg(feature = "hsm")] + SignerProvider::Kmip(flags, _) => flags.is_one_off_signer, + #[cfg(feature = "hsm")] + SignerProvider::Pkcs11(flags, _) => flags.is_one_off_signer, + #[cfg(all(test, feature = "hsm"))] + SignerProvider::Mock(flags, _) => flags.is_one_off_signer, + } + } + + pub fn create_registration_key(&self) -> Result<(PublicKey, String), SignerError> { + match self { + SignerProvider::OpenSsl(_, signer) => signer.create_registration_key(), + #[cfg(feature = "hsm")] + SignerProvider::Kmip(_, signer) => signer.create_registration_key(), + #[cfg(feature = "hsm")] + SignerProvider::Pkcs11(_, signer) => signer.create_registration_key(), + #[cfg(all(test, feature = "hsm"))] + SignerProvider::Mock(_, signer) => signer.create_registration_key(), + } + } + + pub fn sign_registration_challenge + ?Sized>( + &self, + signer_private_key_id: &str, + challenge: &D, + ) -> Result { + match self { + SignerProvider::OpenSsl(_, signer) => signer.sign_registration_challenge(signer_private_key_id, challenge), + #[cfg(feature = "hsm")] + SignerProvider::Kmip(_, signer) => signer.sign_registration_challenge(signer_private_key_id, challenge), + #[cfg(feature = "hsm")] + SignerProvider::Pkcs11(_, signer) => signer.sign_registration_challenge(signer_private_key_id, challenge), + #[cfg(all(test, feature = "hsm"))] + SignerProvider::Mock(_, signer) => signer.sign_registration_challenge(signer_private_key_id, challenge), + } + } + + pub fn set_handle(&self, handle: Handle) { + match self { + SignerProvider::OpenSsl(_, signer) => signer.set_handle(handle), + #[cfg(feature = "hsm")] + SignerProvider::Kmip(_, signer) => signer.set_handle(handle), + #[cfg(feature = "hsm")] + SignerProvider::Pkcs11(_, signer) => signer.set_handle(handle), + #[cfg(all(test, feature = "hsm"))] + SignerProvider::Mock(_, signer) => signer.set_handle(handle), + } + } + + pub fn get_name(&self) -> &str { + match self { + SignerProvider::OpenSsl(_, signer) => signer.get_name(), + #[cfg(feature = "hsm")] + SignerProvider::Kmip(_, signer) => signer.get_name(), + #[cfg(feature = "hsm")] + SignerProvider::Pkcs11(_, signer) => signer.get_name(), + #[cfg(all(test, feature = "hsm"))] + SignerProvider::Mock(_, signer) => signer.get_name(), + } + } + + pub fn get_info(&self) -> Option { + match self { + SignerProvider::OpenSsl(_, signer) => signer.get_info(), + #[cfg(feature = "hsm")] + SignerProvider::Kmip(_, signer) => signer.get_info(), + #[cfg(feature = "hsm")] + SignerProvider::Pkcs11(_, signer) => signer.get_info(), + #[cfg(all(test, feature = "hsm"))] + SignerProvider::Mock(_, signer) => signer.get_info(), + } + } + + #[cfg(all(test, feature = "hsm"))] + pub fn wipe_all_keys(&self) { + match self { + SignerProvider::Mock(_, signer) => signer.wipe_all_keys(), + _ => { /* NOOP */ } + } + } +} + +// Implement the functions defined by the `Signer` trait because `SignerRouter` expects to invoke them, but as the +// dispatching is not trait based we don't actually have to implement the `Signer` trait. +impl SignerProvider { + pub fn create_key(&self, algorithm: PublicKeyFormat) -> Result { + match self { + SignerProvider::OpenSsl(_, signer) => signer.create_key(algorithm), + #[cfg(feature = "hsm")] + SignerProvider::Kmip(_, signer) => signer.create_key(algorithm), + #[cfg(feature = "hsm")] + SignerProvider::Pkcs11(_, signer) => signer.create_key(algorithm), + #[cfg(all(test, feature = "hsm"))] + SignerProvider::Mock(_, signer) => signer.create_key(algorithm), + } + } + + pub fn get_key_info(&self, key: &KeyIdentifier) -> Result> { + match self { + SignerProvider::OpenSsl(_, signer) => signer.get_key_info(key), + #[cfg(feature = "hsm")] + SignerProvider::Kmip(_, signer) => signer.get_key_info(key), + #[cfg(feature = "hsm")] + SignerProvider::Pkcs11(_, signer) => signer.get_key_info(key), + #[cfg(all(test, feature = "hsm"))] + SignerProvider::Mock(_, signer) => signer.get_key_info(key), + } + } + + pub fn destroy_key(&self, key: &KeyIdentifier) -> Result<(), KeyError> { + match self { + SignerProvider::OpenSsl(_, signer) => signer.destroy_key(key), + #[cfg(feature = "hsm")] + SignerProvider::Kmip(_, signer) => signer.destroy_key(key), + #[cfg(feature = "hsm")] + SignerProvider::Pkcs11(_, signer) => signer.destroy_key(key), + #[cfg(all(test, feature = "hsm"))] + SignerProvider::Mock(_, signer) => signer.destroy_key(key), + } + } + + pub fn sign + ?Sized>( + &self, + key: &KeyIdentifier, + algorithm: SignatureAlgorithm, + data: &D, + ) -> Result> { + match self { + SignerProvider::OpenSsl(_, signer) => signer.sign(key, algorithm, data), + #[cfg(feature = "hsm")] + SignerProvider::Kmip(_, signer) => signer.sign(key, algorithm, data), + #[cfg(feature = "hsm")] + SignerProvider::Pkcs11(_, signer) => signer.sign(key, algorithm, data), + #[cfg(all(test, feature = "hsm"))] + SignerProvider::Mock(_, signer) => signer.sign(key, algorithm, data), + } + } + + pub fn sign_one_off + ?Sized>( + &self, + algorithm: SignatureAlgorithm, + data: &D, + ) -> Result<(Signature, PublicKey), SignerError> { + match self { + SignerProvider::OpenSsl(_, signer) => signer.sign_one_off(algorithm, data), + #[cfg(feature = "hsm")] + SignerProvider::Kmip(_, signer) => signer.sign_one_off(algorithm, data), + #[cfg(feature = "hsm")] + SignerProvider::Pkcs11(_, signer) => signer.sign_one_off(algorithm, data), + #[cfg(all(test, feature = "hsm"))] + SignerProvider::Mock(_, signer) => signer.sign_one_off(algorithm, data), + } + } +} diff --git a/src/commons/crypto/signing/dispatch/signerrouter.rs b/src/commons/crypto/signing/dispatch/signerrouter.rs new file mode 100644 index 00000000..b8532ad9 --- /dev/null +++ b/src/commons/crypto/signing/dispatch/signerrouter.rs @@ -0,0 +1,913 @@ +use std::sync::Arc; +use std::{collections::HashMap, sync::RwLock}; + +use rpki::repository::crypto::{ + signer::KeyError, KeyIdentifier, PublicKey, PublicKeyFormat, Signature, SignatureAlgorithm, Signer, SigningError, +}; + +use crate::commons::{ + api::Handle, + crypto::{ + dispatch::{signerinfo::SignerMapper, signerprovider::SignerProvider}, + signers::error::SignerError, + }, + error::Error, + KrillResult, +}; + +#[cfg(feature = "hsm")] +use crate::commons::crypto::dispatch::error::ErrorString; + +/// Manages multiple Signers and routes requests to the appropriate Signer. +/// +/// SignerRouter: +/// - Creates the appropriate [Signer] implementations according to configuration. +/// - Handles registration of [Signer] instances with the [SignerMapper]. +/// - Dispatches requests to the correct [Signer] instance, either because the request specified a [KeyIdentifier] +/// which is owned by a particular [Signer] instance, or because the kind of request dictates the kind of [Signer] +/// that should handle it (e.g. one-off signing may be handled by a different [Signer] than handles new key +/// creation). +/// +/// Note: If the `hsm` feature is not enabled all requests are routed to an instance of the [OpenSslSigner] for +/// backward compatibility with the behaviour of Krill before the introduction of the feature and the [SignerMapper] is +/// not created. +/// +/// To avoid the complexities of dynamic dispatch in Rust we use enum based dispatch instead, as we know at compile time +/// which implementations of the [Signer] trait exist. The code noise caused by doing enum based dispatch is wrapped up +/// in the [SignerProvider] struct so we can focus on the business logic here instead. +/// +/// [SignerProvider] instances are wrapped in [Arc] so that we can "assign" the same signer to multiple different +/// "roles" (default signer, one-off signer, etc). +/// +/// Additional complexity is introduced by the need to wrap the [Signer]s in a lock due to the use of `&mut` by the +/// [Signer] trait on the `create_key()` and `destroy_key()` functions. The latest, not yet released, version of the +/// `rpki-rs` crate which defines the [Signer] trait removes the `&mut` from the trait and so we will be able to remove +/// these locks and instead use interior mutability inside the [Signer] implementations as appropriate/necessary rather +/// than lock the entire [Signer]. Even if that is released we will not make those changes in the current code however +/// as that will introduce too many changes in one PR. See https://github.com/NLnetLabs/rpki-rs/issues/161 and +/// https://github.com/NLnetLabs/rpki-rs/pull/162 for more information. +/// +/// Further, a signer may not be available at the time we wish to use it, perhaps it is down or being slow or a network +/// or configuration issue prevents us connecting to it at that time. Signers are therefore maintained in two distinct +/// sets: pending and active. Signers start in the pending set and are promoted to the active set once we are able to +/// verify that we can connect to and use them and determine which [SignerMapper] [Handle] they should be assigned. +#[derive(Debug)] +pub struct SignerRouter { + /// The signer to use for creating new keys. + /// + /// Exceptions: + /// - One-off signing keys are NOT created by the default signer. See `one_off_signer` below. + /// - Random numbers are always generated using OpenSSL. + default_signer: Arc, + + /// The signer to create, sign with and destroy a one-off key. + /// + /// As the security of a HSM isn't needed for one-off keys, and HSMs are slow, by default this should be an instance + /// of [OpenSslSigner]. However, if users think the perceived extra security is warranted let them use a different + /// signer for one-off keys if that's what they want. + one_off_signer: Arc, + + /// A mechanism for identifying the signer [Handle] that owns the key with a particular [KeyIdentifier]. + /// + /// Used to route requests to the signer that possesses the key. If a key was created using a signer that is no + /// longer present in the config file then the [SignerMapper] may return a [Handle] which is not present in the + /// `active_signers` set (see below) and thus for which we thus have no way of using the key. + /// + /// Conversely, if a key was deleted from the signer/HSM by an external entity without our knowledge then the + /// [SignerMapper] may return a [Handle] for a signer which no longer possesses the key. + /// + /// A reference to the [SignerMapper] is also given to each [Signer] so that it can register the mapping of newly + /// created keys by their [KeyIdentifier] to their [Signer] implementation specific internal key identifier, and + /// in reverse to lookup the internal key identifier from A given [KeyIdentifier]. + signer_mapper: Option>, + + /// A lookup table for resolving a signer [Handle] to its associated [SignerProvider] instance. + /// + /// Used for any operation which must be routed to the signer that owns the key, e.g. key deletion and signing + /// (except one-off signing). First the [] + /// + /// If a signer was used in the past to create a key but that signer is no longer present in the Krill config file + /// it will not be present in this map and will thus not be usable. While we could keep a record of connection + /// details for used signers even once they are removed from the config file we don't do that, the operator must + /// ensure correct connection details are present in the config file. There are multiple reasons for this: + /// connection details likely include secrets such as client certificates, keys, usernames and passwords; an + /// operator may no longer wish to or have the right to use a particular signer/HSM; once we support multi-node + /// deployment connection details to the signer/HSM may vary from one node to another so there is no single correct + /// set of connection details to store in the history, e.g. if the HSM is clustered and each Krill node uses its + /// nearest/same subnet HSM instance which has a different IP address from the HSM instance used by another Krill + /// node in another subnet). + /// + /// This lookup table includes at least the default and one off signers and may also include other signers defined + /// in the config file which were used to create keys in the past which are still in use. + /// + /// [SignerProvider] instances are moved to this set from the `pending_signers` set once we are able to confirm that + /// we can connect to them and can identify the correct signer [Handle] used by the [SignerMapper] to associate with + /// keys created by that signer. + active_signers: RwLock>>, + + /// The set of [SignerProvider] instances that are configured but not yet confirmed to be usable. All signers start + /// off in this set and are moved to the `active_signers` set as soon as we are able to confirm them. See + /// `active_signers` above. + pending_signers: RwLock>>, +} + +impl SignerRouter { + pub fn build(signer_mapper: Option>, mut signers: Vec) -> KrillResult { + // Keep a mapping of signer mapper handle to signer provider. Fill it in as and when signers become ready at + // which point their signer mapper handle will be known. + let active_signers = RwLock::new(HashMap::new()); + + // One and only one signer should be the default. The default signer is used for operations that don't concern + // an existing key, i.e. key creation and one-off signing. + // Create the signers + let mut default_signer: Option> = None; + let mut one_off_signer: Option> = None; + let mut all_signers = Vec::new(); + + for signer in signers.drain(..) { + let signer = Arc::new(signer); + if signer.is_default_signer() { + Self::set_once(&mut default_signer, signer.clone()) + .map_err(|_| Error::ConfigError("There must only be one default signer".to_string()))?; + } else { + if signer.is_one_off_signer() { + Self::set_once(&mut one_off_signer, signer.clone()) + .map_err(|_| Error::ConfigError("There must only be one one-off signer".to_string()))?; + } + } + all_signers.push(signer.clone()); + } + + let default_signer = default_signer.unwrap(); + let pending_signers = RwLock::new(all_signers); + + Ok(SignerRouter { + default_signer: default_signer.clone(), + one_off_signer: one_off_signer.unwrap_or_else(|| default_signer.clone()), + active_signers, + pending_signers, + signer_mapper, + }) + } + + pub fn get_mapper(&self) -> Option> { + self.signer_mapper.clone() + } + + pub fn get_active_signers(&self) -> HashMap> { + self.active_signers.read().unwrap().clone() + } + + /// Locate the [SignerProvider] that owns a given [KeyIdentifier], if the signer is active. + /// + /// If the signer that owns the key has not yet been promoted from the pending set to the active set or if no + /// the key was not created by us or was not registered with the [SignerMapper] then this lookup will fail with + /// [SignerError::KeyNotFound]. + fn get_signer_for_key(&self, key_id: &KeyIdentifier) -> Result, SignerError> { + match &self.signer_mapper { + None => Ok(self.default_signer.clone()), + Some(mapper) => { + // Get the signer handle for the key + let signer_handle = mapper + .get_signer_for_key(key_id) + .map_err(|_| SignerError::KeyNotFound)?; + + // Get the SignerProvider for the handle, if the signer is active + let signer = self.active_signers.read().unwrap().get(&signer_handle).cloned(); + + signer.ok_or(SignerError::KeyNotFound) + } + } + } + + fn set_once(to_be_set: &mut Option>, new_value: Arc) -> Result<(), ()> { + let old_value = to_be_set.replace(new_value); + if old_value.is_some() { + Err(()) + } else { + Ok(()) + } + } +} + +/// When the "hsm" feature is enabled we can no longer assume that signers are immediately and always available as was +/// the case without the "hsm" feature when only the OpenSslSigner was supported. We therefore keep created signers on +/// standby in a "pending" set until we can verify that they are reachable and usable and can determine which +/// [SignerMapper] [Handle] to assign to them. +/// +/// The Krill configuration file defines named signers with a type (openssl, kmip or pkcs#11) and type specific +/// settings (key dir path, hostname, port number, TLS certificate paths, username, password, slot id, etc) and assigns +/// signers one or more roles (default signer or one-off signer) either explicitly or by default. +/// +/// Keys created using signers in a previous Krill process MUST have been registered by the signer with the +/// [SignerMapper] to indicate that the signer owns/possesses the key, and how to map from the [KeyIdentifier] to any +/// internal signer specific key id. When a new Krill process starts it will need to know for any given [KeyIdentifier] +/// which signer that was created should be used to work with the key. Rather than rely on operator supplied signer +/// names being stable or requiring operators to also maintain a stable signer id in the config, we instead "bind" the +/// signer backend to the signer handle that owns the set of keys stored in the [SignerMapper]. +/// +/// Binding is done by asking the signer on first use to create a new key pair for which we save the public key and the +/// signer specific internal private key identifier and combine them into a unique [Handle] for use by the signer with +/// the [SignerMapper]. We also store some metadata about the signer backend with the [Handle] in the [SignerMapper] +/// which allows us to see if the configuration and/or backend properties change over time. +/// +/// On subsequent bindings we determine which signer maps to which [SignerMapper] [Handle] by extracting the private key +/// signer specific internal id from the [Handle] and asking each signer to sign a challenge using that key. We then +/// verify the signature using the saved public key. If the signer doesn't know the internal private key id or produces +/// an incorrect signature we know that the signer doesn't possess the binding key and thus likely isn't the signer we +/// should go to for the keys mapped to the [SignerMapper] [Handle] corresponding to the binding key. +/// +/// By binding this way we both verify that the signer is usable (at least for key pair creation and signing) and that +/// we are using a signer that should have the keys we expect it to possess. +/// +#[cfg(feature = "hsm")] +enum IdentifyResult { + Unavailable, + Corrupt, + Identified(Handle), + Unusable, + Unidentified, +} + +#[cfg(feature = "hsm")] +enum RegisterResult { + NotReady, + ReadyVerified(Handle), + ReadyUnusable, +} + +#[cfg(not(feature = "hsm"))] +impl SignerRouter { + fn bind_ready_signers(&self) {} +} + +#[cfg(feature = "hsm")] +impl SignerRouter { + /// Check for and bind any ready signers. + /// + /// This function should return as quickly as possible. Newly bound signers will be moved from the pending set to + /// the active set and be available immediately for use by the caller. + /// + /// This function should be invoked prior to attempting a signing operation so that the required signer is ready to + /// handle the request. On error we log but do not return an error to the caller because the signer required by the + /// caller may have been previously bound and this binding error may relate to a different signer. There's also + /// nothing the caller can do if a binding failure occurs so receiving an error wouldn't be useful. + /// + /// If all signers have either already been bound or deemed to be permanently broken then this function will return + /// immediately. In cases of temporary connectivity issues the signer handling code may deem it worth trying again + /// but in such cases should implement retry and backoff such that not every attempt to use the signer is blocked + /// trying to connect to the backend. Instead most attempts to use a temporarily unavailable signer should fail + /// very quickly because the signer handling code is "sleeping" between binding attempts. + fn bind_ready_signers(&self) { + if let Err(err) = self.do_ready_signer_binding() { + error!("Internal error: Unable to bind ready signers: {}", err); + } + } + + /// Attempt to bind pending signers. + fn do_ready_signer_binding(&self) -> Result<(), String> { + let num_pending_signers = self.pending_signers.read().unwrap().len(); + if num_pending_signers > 0 { + trace!("Attempting to bind {} pending signers", num_pending_signers); + + // Fetch the handle of every signer previously created in the [SignerMapper] to see if any of the pending + // signers is actually one of these or is a new signer that we haven't seen before. + let candidate_handles = self.get_candidate_signer_handles()?; + trace!("{} signers were previously registered", candidate_handles.len()); + + // Block until we can get a write lock on the set of pending_signers as we will hopefully remove one or + // more items from the set. Standard practice in Krill is to panic if a lock cannot be obtained. + let mut pending_signers = self.pending_signers.write().unwrap(); + + let mut abort_flag = false; + + // For each pending signer see if we can verify it and if so move it from the pending set to the active set. + pending_signers.retain(|signer_provider| -> bool { + if abort_flag { + return true; + } + + let signer_name = signer_provider.get_name().to_string(); + + // See if this is a known signer that whose signature matches the public key stored in the + // [SignerMapper] for the signer. + self.identify_signer(signer_provider, &candidate_handles) + .and_then(|verify_result| match verify_result { + IdentifyResult::Unavailable => { + // Signer isn't ready yet, leave it in the pending set and try again next time. + trace!("Signer '{}' is unavailable", signer_name); + Ok(true) + } + IdentifyResult::Identified(signer_handle) => { + // Signer is ready and verified, add it to the active set. + self.active_signers + .write() + .unwrap() + .insert(signer_handle, signer_provider.clone()); + info!("Signer '{}' is ready for use", signer_name); + // And remove it from the pending set + Ok(false) + } + IdentifyResult::Unidentified => { + // Signer is ready and new, register it and move it to the active set + self.register_new_signer(signer_provider).and_then( + |register_result| match register_result { + RegisterResult::NotReady => { + // Strange, it was ready just now when we verified it ... leave it in the + // pending set and try again next time. + trace!("Signer '{}' is not ready", signer_name); + Ok(true) + } + RegisterResult::ReadyVerified(signer_handle) => { + // Signer is ready and verified, add it to the active set. + self.active_signers + .write() + .unwrap() + .insert(signer_handle, signer_provider.clone()); + info!("Signer '{}' is ready for use", signer_name); + // And remove it from the pending set + Ok(false) + } + RegisterResult::ReadyUnusable => { + // Signer registration failed, remove it from the pending set + warn!("Signer '{}' could not be registered: signer is not usable", signer_name); + Ok(false) + } + }, + ) + } + IdentifyResult::Unusable => { + // Signer is ready and unusable, remove it from the pending set + warn!("Signer '{}' could not be identified: signer is not usable", signer_name); + Ok(false) + } + IdentifyResult::Corrupt => { + // This case should never happen as this variant is handled in the called code + Err(ErrorString::new("Internal error: invalid handle")) + } + }) + .unwrap_or_else(|err| { + error!("Signer '{}' could not be bound: {}. Aborting.", signer_name, *err); + abort_flag = true; + true + }) + }); + } + + Ok(()) + } + + /// Retrieves the set of signer handles known to the signer mapper. + fn get_candidate_signer_handles(&self) -> Result, String> { + // TODO: Filter out already bound signers? + Ok(self + .signer_mapper + .as_ref() + .unwrap() + .get_signer_handles() + .map_err(|err| format!("Failed to get signer handles: {}", err))?) + } + + /// Checks if the signer identity can be shown to match one of the known signer public keys. + fn identify_signer( + &self, + signer_provider: &Arc, + candidate_handles: &[Handle], + ) -> Result { + let config_signer_name = signer_provider.get_name().to_string(); + + // First try any candidate handle whose signer name matches the name of the signer provider then fall back to + // trying other candidate handles, as perhaps the signer was renamed in the config file and no longer matches by + // name but can still be matched by verifying a new signing signature with the stored public key of the other + // candidate handles. + let mut ordered_candidate_handles = Vec::new(); + for candidate_handle in candidate_handles { + let stored_signer_name = self.signer_mapper.as_ref().unwrap().get_signer_name(candidate_handle)?; + if stored_signer_name == config_signer_name { + ordered_candidate_handles.insert(0, candidate_handle); + } else { + ordered_candidate_handles.push(candidate_handle); + } + } + + for candidate_handle in ordered_candidate_handles { + let res = self.is_signer_identified_by_handle(signer_provider, candidate_handle)?; + match res { + IdentifyResult::Unidentified => { + // Signer was contacted and no errors were encountered but it doesn't know the key encoded in the + // given handle. Try again with the next handle. + continue; + } + IdentifyResult::Corrupt => { + // The candidate handle or signer public key is invalid so no key could be extracted to present to + // the signer. Try again with the next handle. + continue; + } + IdentifyResult::Unavailable | IdentifyResult::Unusable | IdentifyResult::Identified(_) => { + // No need to try the next candidate key, let the caller process the result. + return Ok(res); + } + } + } + + // No errors occurred while contacting the signer but it doesn't know any of our candidate keys so this must be + // a new signer that should be registered. + Ok(IdentifyResult::Unidentified) + } + + /// Checks if the signer identity matches the signer public key associated with a given signer handle. + /// + /// To match the signer backend must have access to a key whose signer internal key ID matches one we stored when + /// the signer was previously registered, and when used to sign a challenge the signature must match the public + /// key we have on record (also stored when the signer was previously registered). + fn is_signer_identified_by_handle( + &self, + signer_provider: &Arc, + candidate_handle: &Handle, + ) -> Result { + let handle_name = self.signer_mapper.as_ref().unwrap().get_signer_name(candidate_handle)?; + let signer_name = signer_provider.get_name().to_string(); + trace!( + "Attempting to identify signer '{}' using identity key stored for signer '{}'", + signer_name, + handle_name + ); + + let public_key = match self + .signer_mapper + .as_ref() + .unwrap() + .get_signer_public_key(candidate_handle) + { + Ok(res) => Ok(res), + Err(err) => match err { + crate::commons::error::Error::SignerError(err) => { + error!( + "Internal error: Identity public key for signer '{}' is invalid: {}", + handle_name, err + ); + return Ok(IdentifyResult::Corrupt); + } + err => Err(err), + }, + }?; + + let signer_private_key_id = self + .signer_mapper + .as_ref() + .unwrap() + .get_signer_private_key_internal_id(candidate_handle)?; + + let challenge = "Krill signer verification challenge".as_bytes(); + let signature = match signer_provider.sign_registration_challenge(&signer_private_key_id, challenge) { + Err(SignerError::TemporarilyUnavailable) => { + debug!("Signer '{}' could not be contacted", signer_name); + return Ok(IdentifyResult::Unavailable); + } + Err(SignerError::KeyNotFound) => { + debug!( + "Signer '{}' not matched: private key id '{}' not found", + signer_name, signer_private_key_id + ); + return Ok(IdentifyResult::Unidentified); + } + Err(err) => { + error!("Signer '{}' is unusable: {}", signer_name, err); + return Ok(IdentifyResult::Unusable); + } + Ok(res) => res, + }; + + if public_key.verify(challenge, &signature).is_ok() { + debug!("Signer '{}' is ready and known, binding", signer_name); + let signer_info = signer_provider.get_info().unwrap_or("No signer info".to_string()); + + signer_provider.set_handle(candidate_handle.clone()); + + if let Err(err) = self + .signer_mapper + .as_ref() + .unwrap() + .change_signer_name(candidate_handle, &signer_name) + { + // This is unexpected and perhaps indicative of a deeper problem but log and keep going. + error!( + "Internal error: Failed to change name of signer to '{}': {}", + signer_name, err + ); + } + if let Err(err) = self + .signer_mapper + .as_ref() + .unwrap() + .change_signer_info(candidate_handle, &signer_info) + { + // This is unexpected and perhaps indicative of a deeper problem but log and keep going. + error!( + "Internal error: Failed to change info for signer '{}' to '{}': {}", + signer_name, signer_info, err + ); + } + + debug!( + "Signer '{}' bound to signer mapper handle '{}'", + signer_name, candidate_handle + ); + } else { + debug!( + "Signer '{}' not matched: incorrect signature created with private key '{}'", + signer_name, signer_private_key_id + ); + } + + Ok(IdentifyResult::Identified(candidate_handle.clone())) + } + + /// Register a signer backend so that we can identify it later. + /// + /// Registration creates a key pair in the signer backend and stores the signer specific internal ID of the created + /// private key and the content of the created public key. Registration also verifies that the signer is able to + /// sign using the newly created private key such that the created signature matches the created public key. + fn register_new_signer(&self, signer_provider: &Arc) -> Result { + let signer_name = signer_provider.get_name().to_string(); + + trace!("Attempting to register signer '{}'", signer_name); + + let (public_key, signer_private_key_id) = match signer_provider.create_registration_key() { + Err(SignerError::TemporarilyUnavailable) => return Ok(RegisterResult::NotReady), + Err(_) => return Ok(RegisterResult::ReadyUnusable), + Ok(res) => res, + }; + + let challenge = "Krill signer verification challenge".as_bytes(); + let signature = match signer_provider.sign_registration_challenge(&signer_private_key_id, challenge) { + Err(SignerError::TemporarilyUnavailable) => return Ok(RegisterResult::NotReady), + Err(_) => return Ok(RegisterResult::ReadyUnusable), + Ok(res) => res, + }; + + if !public_key.verify(challenge, &signature).is_ok() { + error!("Signer '{}' challenge signature is invalid", signer_name); + return Ok(RegisterResult::ReadyUnusable); + } + + debug!("Signer '{}' is ready and new, binding", signer_name); + + let signer_info = signer_provider.get_info().unwrap_or("No signer info".to_string()); + + let signer_handle = self.signer_mapper.as_ref().unwrap().add_signer( + &signer_name, + &signer_info, + &public_key, + &signer_private_key_id, + )?; + + signer_provider.set_handle(signer_handle.clone()); + + debug!("Signer '{}' bound to signer handle '{}'", signer_name, signer_handle); + Ok(RegisterResult::ReadyVerified(signer_handle)) + } +} + +impl Signer for SignerRouter { + type KeyId = KeyIdentifier; + type Error = SignerError; + + fn create_key(&self, algorithm: PublicKeyFormat) -> Result { + self.bind_ready_signers(); + self.default_signer.create_key(algorithm) + } + + fn get_key_info(&self, key_id: &KeyIdentifier) -> Result> { + self.bind_ready_signers(); + self.get_signer_for_key(key_id)?.get_key_info(key_id) + } + + fn destroy_key(&self, key_id: &KeyIdentifier) -> Result<(), KeyError> { + self.bind_ready_signers(); + self.get_signer_for_key(key_id)?.destroy_key(key_id) + } + + fn sign + ?Sized>( + &self, + key_id: &KeyIdentifier, + algorithm: SignatureAlgorithm, + data: &D, + ) -> Result> { + self.bind_ready_signers(); + self.get_signer_for_key(key_id)?.sign(key_id, algorithm, data) + } + + fn sign_one_off + ?Sized>( + &self, + algorithm: SignatureAlgorithm, + data: &D, + ) -> Result<(Signature, PublicKey), Self::Error> { + self.bind_ready_signers(); + self.one_off_signer.sign_one_off(algorithm, data) + } + + fn rand(&self, target: &mut [u8]) -> Result<(), Self::Error> { + self.bind_ready_signers(); + openssl::rand::rand_bytes(target).map_err(SignerError::OpenSslError) + } +} + +#[cfg(all(test, feature = "hsm"))] +pub mod tests { + use crate::{ + commons::crypto::{ + dispatch::signerprovider::SignerFlags, + signers::mocksigner::{ + CreateRegistrationKeyErrorCb, FnIdx, MockSigner, MockSignerCallCounts, SignRegistrationChallengeErrorCb, + }, + }, + test, + }; + + use super::*; + + fn create_signer_router(all_signers: &[Arc], signer_mapper: Arc) -> SignerRouter { + SignerRouter { + default_signer: all_signers[0].clone(), + one_off_signer: all_signers[0].clone(), + signer_mapper: Some(signer_mapper.clone()), + active_signers: RwLock::new(HashMap::new()), + pending_signers: RwLock::new(all_signers.to_vec()), + } + } + + #[test] + pub fn verify_that_a_usable_signer_is_registered_and_can_be_used() { + test::test_under_tmp(|d| { + #[allow(non_snake_case)] + let DEF_SIG_ALG = SignatureAlgorithm::default(); + + // Build a mock signer that is contactable and usable for the SignerRouter + let call_counts = Arc::new(MockSignerCallCounts::new()); + let signer_mapper = Arc::new(SignerMapper::build(&d).unwrap()); + let mock_signer = MockSigner::new("mock signer", signer_mapper.clone(), call_counts.clone(), None, None); + let mock_signer = Arc::new(SignerProvider::Mock(SignerFlags::default(), mock_signer)); + + // Create a SignerRouter that uses the mock signer with the mock signer starting in the pending signer set. + let router = create_signer_router(&[mock_signer.clone()], signer_mapper.clone()); + + // No signers have been registered with the SignerMapper yet + assert_eq!(0, signer_mapper.get_signer_handles().unwrap().len()); + + // Verify that initially none of the functions in the mock signer have been called + assert_eq!(0, call_counts.get(FnIdx::CreateRegistrationKey)); + assert_eq!(0, call_counts.get(FnIdx::SignRegistrationChallenge)); + assert_eq!(0, call_counts.get(FnIdx::GetInfo)); + assert_eq!(0, call_counts.get(FnIdx::SetHandle)); + assert_eq!(0, call_counts.get(FnIdx::CreateKey)); + assert_eq!(0, call_counts.get(FnIdx::Sign)); + assert_eq!(0, call_counts.get(FnIdx::DestroyKey)); + + // Try to use the SignerRouter to generate a random value. This should cause the SignerRouter to contact + // the mock signer, ask it to create a registration key, verify that it can sign correctly with that key, + // assign a signer mapper handle to the signer, then check for random number generation support and finally + // actually generate the random number. + let mut out_buf: [u8; 1] = [0; 1]; + router.rand(&mut out_buf).unwrap(); + assert_eq!(1, call_counts.get(FnIdx::CreateRegistrationKey)); + assert_eq!(1, call_counts.get(FnIdx::SignRegistrationChallenge)); + assert_eq!(1, call_counts.get(FnIdx::GetInfo)); + assert_eq!(1, call_counts.get(FnIdx::SetHandle)); + + // One signer has been registered with the SignerMapper now + assert_eq!(1, signer_mapper.get_signer_handles().unwrap().len()); + + // Ask for another random number. This time none of the registration steps should be performed as the signer + // is already registered and active. + router.rand(&mut out_buf).unwrap(); + + // Check that we can create a new key with the mock signer via the SignerRouter and that the key gets + // registered with the signer mapper. + let key_identifier = router.create_key(PublicKeyFormat::Rsa).unwrap(); + assert!(signer_mapper.get_signer_for_key(&key_identifier).is_ok()); + assert_eq!(1, call_counts.get(FnIdx::CreateKey)); + + // Check that we can sign with the SignerRouter using the Krill key identifier. The SignerRouter should + // discover from the SignerMapper that the key belongs to the mock signer and so dispatch the signing + // request to the mock signer. + router.sign(&key_identifier, DEF_SIG_ALG, &out_buf).unwrap(); + assert_eq!(1, call_counts.get(FnIdx::Sign)); + + // Throw the SignerRouter away and create a new one. This is like restarting Krill. Keep the mock signer as + // otherwise we will lose its in-memory private key store. Keep the SignerMapper as the mock signer is + // using it, and because destroying it and recreating it would just be like forcing it to re-read it's saved + // state from disk (and we're not trying to test the AggregateStore here anyway!). + let router = create_signer_router(&[mock_signer.clone()], signer_mapper.clone()); + + // Try to use the SignerRouter to sign again. This time around the SignerMapper should find the existing + // signer in its records and only ask the signer to sign the registration challenge, but not ask it to + // create a registration key. + router.sign(&key_identifier, DEF_SIG_ALG, &out_buf).unwrap(); + assert_eq!(1, call_counts.get(FnIdx::CreateRegistrationKey)); + assert_eq!(2, call_counts.get(FnIdx::SignRegistrationChallenge)); + assert_eq!(2, call_counts.get(FnIdx::GetInfo)); + assert_eq!(2, call_counts.get(FnIdx::SetHandle)); + assert_eq!(2, call_counts.get(FnIdx::Sign)); + + // Now delete the key and verify that we can no longer sign with it. + router.destroy_key(&key_identifier).unwrap(); + assert_eq!(1, call_counts.get(FnIdx::DestroyKey)); + + let err = router.sign(&key_identifier, SignatureAlgorithm::default(), &out_buf); + // TODO: Should this error from the SignerRouter actually be SigningError::KeyNotFound instead of + // SigningError::Signer(SignerError::KeyNotFound)? + assert!(matches!(err, Err(SigningError::Signer(SignerError::KeyNotFound)))); + + // The Sign call count is still 2 because the SignerRouter fails to determine which signer owns the key + // and fails. + assert_eq!(2, call_counts.get(FnIdx::Sign)); + + // Now ask the mock signer to forget its registration key. After this the SignerRouter should fail to + // verify it and require it to register anew. + mock_signer.wipe_all_keys(); + + // The mock signer still works for the moment because the SignerRouter doesn't do registration again as + // it thinks it still has an active signer. + let key_identifier = router.create_key(PublicKeyFormat::Rsa).unwrap(); + router.sign(&key_identifier, DEF_SIG_ALG, &out_buf).unwrap(); + + assert_eq!(1, call_counts.get(FnIdx::CreateRegistrationKey)); + assert_eq!(2, call_counts.get(FnIdx::SignRegistrationChallenge)); + assert_eq!(2, call_counts.get(FnIdx::CreateKey)); + assert_eq!(3, call_counts.get(FnIdx::Sign)); + + // Throw away the SignerRouter again, thereby forcing the mock signer to be in the pending set again + // instead of the ready set. Now the SignerRouter should register the mock signer again and we should end + // up with a second signer in the SignerMapper as the ability to identify the first one has been lost + // (because above we instructed the mock signer to wipe all its keys). As the SignerMapper contains an + // existing signer the call count to sign_registration_challenge() in the mock signer will actually + // increase twice because the SignerRouter will first challenge it to prove that it is the already + // known signer. Without the identity key however the mock signer fails this identity check and is + // registered again (and then sign challenged again, hence the double increment). + let router = create_signer_router(&[mock_signer.clone()], signer_mapper.clone()); + + let err = router.sign(&key_identifier, DEF_SIG_ALG, &out_buf); + assert!(matches!(err, Err(SigningError::Signer(SignerError::KeyNotFound)))); + + assert_eq!(2, call_counts.get(FnIdx::CreateRegistrationKey)); + assert_eq!(4, call_counts.get(FnIdx::SignRegistrationChallenge)); + assert_eq!(3, call_counts.get(FnIdx::GetInfo)); + assert_eq!(3, call_counts.get(FnIdx::SetHandle)); + assert_eq!(3, call_counts.get(FnIdx::Sign)); + + // Two signers have been registered with the SignerMapper by this point, one of which is now orphaned as + // the keys that it knows about refer to a signer backend that is no longer able to prove that it is the + // owner of these keys (because its identity key was deleted in the signer backend). Thus the SignerRouter + // doesn't know which signer to forward requests to in order to work with the keys owned by the orphaned + // signer. + assert_eq!(2, signer_mapper.get_signer_handles().unwrap().len()); + }); + } + + #[test] + pub fn verify_that_unusable_signers_are_neither_registered_nor_retried() { + fn perm_unusable(_: &MockSignerCallCounts) -> Result<(), SignerError> { + Err(SignerError::PermanentlyUnusable) + } + + fn internal_error(_: &MockSignerCallCounts) -> Result<(), SignerError> { + Err(SignerError::Other("internal error".to_string())) + } + + fn temp_unavail(_: &MockSignerCallCounts) -> Result<(), SignerError> { + Err(SignerError::TemporarilyUnavailable) + } + + fn create_broken_signer( + signer_mapper: Arc, + call_counts: Arc, + create_registration_key_error_cb: Option, + sign_registration_challenge_error_cb: Option, + ) -> Arc { + Arc::new(SignerProvider::Mock( + SignerFlags::default(), + MockSigner::new( + "broken mock signer", + signer_mapper, + call_counts, + create_registration_key_error_cb, + sign_registration_challenge_error_cb, + ), + )) + } + + fn create_broken_signers(sm: Arc, cc: Arc) -> Vec> { + let mut broken_signers = Vec::new(); + broken_signers.push(create_broken_signer(sm.clone(), cc.clone(), Some(perm_unusable), None)); + broken_signers.push(create_broken_signer(sm.clone(), cc.clone(), Some(internal_error), None)); + broken_signers.push(create_broken_signer(sm.clone(), cc.clone(), Some(temp_unavail), None)); + broken_signers.push(create_broken_signer(sm.clone(), cc.clone(), None, Some(perm_unusable))); + broken_signers.push(create_broken_signer(sm.clone(), cc.clone(), None, Some(internal_error))); + broken_signers.push(create_broken_signer(sm.clone(), cc.clone(), None, Some(temp_unavail))); + broken_signers + } + + test::test_under_tmp(|d| { + let call_counts = Arc::new(MockSignerCallCounts::new()); + let signer_mapper = Arc::new(SignerMapper::build(&d).unwrap()); + let broken_signers = create_broken_signers(signer_mapper.clone(), call_counts.clone()); + + // Create a SignerRouter that has access to all of the broken signers + let router = create_signer_router(broken_signers.as_slice(), signer_mapper.clone()); + + // No signers have been registered with the SignerMapper yet + assert_eq!(0, signer_mapper.get_signer_handles().unwrap().len()); + + let mut rand_out: [u8; 1] = [0; 1]; + + // Try to use the SignerRouter to generate a random value. This should cause the SignerRouter to contact + // all of the mock signers, asking them to create a registration key, and if that succeeds to then verify + // that the signer can sign correctly with that key. None of the broken signers will succeed at these steps + // and so the counter of registered signers will remain at zero. + router.rand(&mut rand_out).unwrap(); + + // The number of attempts to register a signer should have increased by the number of signers. + // Half of the signers should fail at the registration step, the other half at the challenge signing step. + // So the number of signers that we succeeded in moving out of the pending set to the active set and + // registering with the signer mapper should be zero. + assert_eq!(6, call_counts.get(FnIdx::CreateRegistrationKey)); + assert_eq!(3, call_counts.get(FnIdx::SignRegistrationChallenge)); + assert_eq!(0, signer_mapper.get_signer_handles().unwrap().len()); + + // + // Try again. + // + router.rand(&mut rand_out).unwrap(); + + // The signers that were permanently unusable at registration should not be tried again. + assert_eq!(6 + 2, call_counts.get(FnIdx::CreateRegistrationKey)); + + // The signers that were permanently unusable at challenge signing should not be tried again. + assert_eq!(3 + 1, call_counts.get(FnIdx::SignRegistrationChallenge)); + + // And the end result should be that no signers were registered with the signer mapper. + assert_eq!(0, signer_mapper.get_signer_handles().unwrap().len()); + }); + } + + #[test] + pub fn verify_that_temporarily_unavailable_signers_are_registered_when_available() { + fn temp_unavail(call_counts: &MockSignerCallCounts) -> Result<(), SignerError> { + if call_counts.get(FnIdx::CreateRegistrationKey) == 1 { + // Fail the first time registration is attempted + Err(SignerError::TemporarilyUnavailable) + } else { + // Succeed on subsequent attempts + Ok(()) + } + } + + test::test_under_tmp(|d| { + let call_counts = Arc::new(MockSignerCallCounts::new()); + let signer_mapper = Arc::new(SignerMapper::build(&d).unwrap()); + + let temp_unavail_signer = Arc::new(SignerProvider::Mock( + SignerFlags::default(), + MockSigner::new( + "mock temporararily unavailable signer", + signer_mapper.clone(), + call_counts.clone(), + Some(temp_unavail), + None, + ), + )); + + // Create a SignerRouter that uses the mock signer with the mock signer starting in the pending signer set. + let router = create_signer_router(&[temp_unavail_signer], signer_mapper.clone()); + + // No signers have been registered with the SignerMapper yet + assert_eq!(0, signer_mapper.get_signer_handles().unwrap().len()); + + let mut rand_out: [u8; 1] = [0; 1]; + + // Try to use the SignerRouter to generate a random value. This should cause the SignerRouter to contact + // the mock signer, ask it to create a registration key, verify that it can sign correctly with that key, + // assign a signer mapper handle to the signer, then check for random number generation support and finally + // actually generate the random number. This should fail the first time due to the logic imlpemented by the + // temp_avail() function above. + router.rand(&mut rand_out).unwrap(); + + // The number of attempts to register a signer should have increased by one. + assert_eq!(1, call_counts.get(FnIdx::CreateRegistrationKey)); + assert_eq!(0, call_counts.get(FnIdx::SignRegistrationChallenge)); + assert_eq!(0, signer_mapper.get_signer_handles().unwrap().len()); + + // + // Try again. We should succeed the second time due to the logic imlpemented by the temp_avail() function + // above. + // + router.rand(&mut rand_out).unwrap(); + + // We should be all green now + assert_eq!(2, call_counts.get(FnIdx::CreateRegistrationKey)); + assert_eq!(1, call_counts.get(FnIdx::SignRegistrationChallenge)); + assert_eq!(1, signer_mapper.get_signer_handles().unwrap().len()); + }); + } +} diff --git a/src/commons/crypto/signing/misc.rs b/src/commons/crypto/signing/misc.rs new file mode 100644 index 00000000..d2213548 --- /dev/null +++ b/src/commons/crypto/signing/misc.rs @@ -0,0 +1,267 @@ +//! Support for signing mft, crl, certificates, roas.. +//! Common objects for TAs and CAs + +use std::convert::TryFrom; + +use bytes::Bytes; +use rpki::{ + repository::{ + cert::{KeyUsage, Overclaim, TbsCert}, + crypto::{DigestAlgorithm, KeyIdentifier, PublicKey}, + manifest::FileAndHash, + x509::{Name, Time, Validity}, + Cert, Crl, Csr, + }, + uri, +}; + +use crate::{ + commons::{ + api::{IssuedCert, RcvdCert, ReplacedObject, RequestResourceLimit, ResourceSet}, + crypto::KrillSigner, + error::Error, + util::AllowedUri, + KrillResult, + }, + daemon::ca::CertifiedKey, +}; + +//------------ CsrInfo ------------------------------------------------------- + +pub type CaRepository = uri::Rsync; +pub type RpkiManifest = uri::Rsync; +pub type RpkiNotify = uri::Https; + +pub struct CsrInfo { + ca_repository: CaRepository, + rpki_manifest: RpkiManifest, + rpki_notify: Option, + key: PublicKey, +} + +impl CsrInfo { + pub fn new( + ca_repository: CaRepository, + rpki_manifest: RpkiManifest, + rpki_notify: Option, + key: PublicKey, + ) -> Self { + CsrInfo { + ca_repository, + rpki_manifest, + rpki_notify, + key, + } + } + + pub fn global_uris(&self) -> bool { + self.ca_repository.seems_global_uri() + && self.rpki_manifest.seems_global_uri() + && self + .rpki_notify + .as_ref() + .map(|uri| uri.seems_global_uri()) + .unwrap_or_else(|| true) + } + + pub fn unpack(self) -> (CaRepository, RpkiManifest, Option, PublicKey) { + (self.ca_repository, self.rpki_manifest, self.rpki_notify, self.key) + } + + pub fn key_id(&self) -> KeyIdentifier { + self.key.key_identifier() + } +} + +impl TryFrom<&Csr> for CsrInfo { + type Error = Error; + + fn try_from(csr: &Csr) -> KrillResult { + csr.validate().map_err(|_| Error::invalid_csr("invalid signature"))?; + let ca_repository = csr + .ca_repository() + .cloned() + .ok_or_else(|| Error::invalid_csr("missing ca repository"))?; + let rpki_manifest = csr + .rpki_manifest() + .cloned() + .ok_or_else(|| Error::invalid_csr("missing rpki manifest"))?; + let rpki_notify = csr.rpki_notify().cloned(); + let key = csr.public_key().clone(); + Ok(CsrInfo { + ca_repository, + rpki_manifest, + rpki_notify, + key, + }) + } +} + +impl From<&Cert> for CsrInfo { + fn from(issued: &Cert) -> Self { + let ca_repository = issued.ca_repository().cloned().unwrap(); + let rpki_manifest = issued.rpki_manifest().cloned().unwrap(); + let rpki_notify = issued.rpki_notify().cloned(); + let key = issued.subject_public_key_info().clone(); + CsrInfo { + ca_repository, + rpki_manifest, + rpki_notify, + key, + } + } +} + +//------------ CaSignSupport ------------------------------------------------- + +/// Support signing by CAs +pub struct SignSupport; + +impl SignSupport { + /// Create an IssuedCert + pub fn make_issued_cert( + csr: CsrInfo, + resources: &ResourceSet, + limit: RequestResourceLimit, + replaces: Option, + signing_key: &CertifiedKey, + weeks: i64, + signer: &KrillSigner, + ) -> KrillResult { + let signing_cert = signing_key.incoming_cert(); + let resources = resources.apply_limit(&limit)?; + if !signing_cert.resources().contains(&resources) { + return Err(Error::MissingResources); + } + + let validity = Self::sign_validity_weeks(weeks); + let request = CertRequest::Ca(csr, validity); + + let tbs = Self::make_tbs_cert(&resources, signing_cert, request, signer)?; + let cert = signer.sign_cert(tbs, signing_key.key_id())?; + + let cert_uri = signing_cert.uri_for_object(&cert); + + Ok(IssuedCert::new(cert_uri, limit, resources, cert, replaces)) + } + + /// Create an EE certificate for use in ResourceTaggedAttestations. + /// Note that for RPKI signed objects such as ROAs and Manifests, the + /// EE certificate is created by the rpki.rs library instead. + pub fn make_rta_ee_cert( + resources: &ResourceSet, + signing_key: &CertifiedKey, + validity: Validity, + pub_key: PublicKey, + signer: &KrillSigner, + ) -> KrillResult { + let signing_cert = signing_key.incoming_cert(); + let request = CertRequest::Ee(pub_key, validity); + let tbs = Self::make_tbs_cert(resources, signing_cert, request, signer)?; + + let cert = signer.sign_cert(tbs, signing_key.key_id())?; + Ok(cert) + } + + fn make_tbs_cert( + resources: &ResourceSet, + signing_cert: &RcvdCert, + request: CertRequest, + signer: &KrillSigner, + ) -> KrillResult { + let serial = signer.random_serial()?; + let issuer = signing_cert.cert().subject().clone(); + + let validity = match &request { + CertRequest::Ca(_, validity) => *validity, + CertRequest::Ee(_, validity) => *validity, + }; + + let pub_key = match &request { + CertRequest::Ca(info, _) => info.key.clone(), + CertRequest::Ee(key, _) => key.clone(), + }; + + let subject = Some(Name::from_pub_key(&pub_key)); + + let key_usage = match &request { + CertRequest::Ca(_, _) => KeyUsage::Ca, + CertRequest::Ee(_, _) => KeyUsage::Ee, + }; + + let overclaim = Overclaim::Refuse; + + let mut cert = TbsCert::new(serial, issuer, validity, subject, pub_key, key_usage, overclaim); + + let asns = resources.to_as_resources(); + if asns.is_inherited() || !asns.to_blocks().unwrap().is_empty() { + cert.set_as_resources(asns); + } + + let ipv4 = resources.to_ip_resources_v4(); + if ipv4.is_inherited() || !ipv4.to_blocks().unwrap().is_empty() { + cert.set_v4_resources(ipv4); + } + + let ipv6 = resources.to_ip_resources_v6(); + if ipv6.is_inherited() || !ipv6.to_blocks().unwrap().is_empty() { + cert.set_v6_resources(ipv6); + } + + cert.set_authority_key_identifier(Some(signing_cert.cert().subject_key_identifier())); + cert.set_ca_issuer(Some(signing_cert.uri().clone())); + cert.set_crl_uri(Some(signing_cert.crl_uri())); + + match request { + CertRequest::Ca(csr, _) => { + let (ca_repository, rpki_manifest, rpki_notify, _pub_key) = csr.unpack(); + cert.set_basic_ca(Some(true)); + cert.set_ca_repository(Some(ca_repository)); + cert.set_rpki_manifest(Some(rpki_manifest)); + cert.set_rpki_notify(rpki_notify); + } + CertRequest::Ee(_, _) => { + // cert.set_signed_object() ?? + } + } + + Ok(cert) + } + + /// Returns a validity period from 5 minutes ago (in case of NTP mess-up), to + /// X weeks from now. + pub fn sign_validity_weeks(weeks: i64) -> Validity { + let from = Time::five_minutes_ago(); + let until = Time::now() + chrono::Duration::weeks(weeks); + Validity::new(from, until) + } + + pub fn sign_validity_days(days: i64) -> Validity { + let from = Time::five_minutes_ago(); + let until = Time::now() + chrono::Duration::days(days); + Validity::new(from, until) + } +} + +#[allow(clippy::large_enum_variant)] +enum CertRequest { + Ca(CsrInfo, Validity), + Ee(PublicKey, Validity), +} + +trait ManifestEntry { + fn mft_bytes(&self) -> Bytes; + fn mft_hash(&self) -> Bytes { + let digest = DigestAlgorithm::default().digest(self.mft_bytes().as_ref()); + Bytes::copy_from_slice(digest.as_ref()) + } + fn mft_entry(&self, name: &str) -> FileAndHash { + FileAndHash::new(Bytes::copy_from_slice(name.as_bytes()), self.mft_hash()) + } +} + +impl ManifestEntry for Crl { + fn mft_bytes(&self) -> Bytes { + self.to_captured().into_bytes() + } +} diff --git a/src/commons/crypto/signing/mod.rs b/src/commons/crypto/signing/mod.rs new file mode 100644 index 00000000..28791aea --- /dev/null +++ b/src/commons/crypto/signing/mod.rs @@ -0,0 +1,19 @@ +pub mod dispatch; + +pub(super) mod signers; + +mod misc; + +pub use dispatch::krillsigner::{KrillSigner, KrillSignerBuilder}; +pub use signers::error::SignerError; +pub use signers::softsigner::OpenSslSigner; + +#[cfg(feature = "hsm")] +pub use signers::kmip::signer::KmipSignerConfig; + +#[cfg(feature = "hsm")] +pub use signers::pkcs11::signer::{Pkcs11SignerConfig, SlotIdOrLabel}; + +pub use signers::softsigner::OpenSslSignerConfig; + +pub use misc::*; diff --git a/src/commons/crypto/signing/signers/error.rs b/src/commons/crypto/signing/signers/error.rs new file mode 100644 index 00000000..fcb5dba7 --- /dev/null +++ b/src/commons/crypto/signing/signers/error.rs @@ -0,0 +1,56 @@ +use std::{fmt, path::PathBuf}; + +use openssl::error::ErrorStack; + +use crate::commons::error::KrillIoError; + +#[derive(Debug)] +pub enum SignerError { + DecodeError, + InvalidWorkDir(PathBuf), + IoError(KrillIoError), + JsonError(serde_json::Error), + KeyNotFound, + KmipError(String), + OpenSslError(ErrorStack), + Other(String), + PermanentlyUnusable, + Pkcs11Error(String), + TemporarilyUnavailable, +} + +impl fmt::Display for SignerError { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + SignerError::DecodeError => write!(f, "Could not decode key"), + SignerError::InvalidWorkDir(path) => write!(f, "Invalid base path: {}", path.to_string_lossy()), + SignerError::IoError(e) => e.fmt(f), + SignerError::JsonError(e) => write!(f, "Could not decode public key info: {}", e), + SignerError::KeyNotFound => write!(f, "Could not find key"), + SignerError::KmipError(e) => write!(f, "KMIP Error: {}", e), + SignerError::OpenSslError(e) => write!(f, "OpenSSL Error: {}", e), + SignerError::Other(e) => write!(f, "Signer error: {}", e), + SignerError::PermanentlyUnusable => write!(f, "Signer is unusable"), + SignerError::Pkcs11Error(e) => write!(f, "PKCS#11 Error: {}", e), + SignerError::TemporarilyUnavailable => write!(f, "Signer is unavailable"), + } + } +} + +impl From for SignerError { + fn from(e: ErrorStack) -> Self { + SignerError::OpenSslError(e) + } +} + +impl From for SignerError { + fn from(e: serde_json::Error) -> Self { + SignerError::JsonError(e) + } +} + +impl From for SignerError { + fn from(e: KrillIoError) -> Self { + SignerError::IoError(e) + } +} diff --git a/src/commons/crypto/signing/signers/kmip/connpool.rs b/src/commons/crypto/signing/signers/kmip/connpool.rs new file mode 100644 index 00000000..00b5dc50 --- /dev/null +++ b/src/commons/crypto/signing/signers/kmip/connpool.rs @@ -0,0 +1,130 @@ +/// KMIP TLS connection pool +/// +/// Used to: +/// - Avoid repeated TCP connection setup and TLS session establishment for mutiple KMIP requests made close together +/// in time. +/// - Handle loss of connectivity by re-creating the connection when an existing connection is considered to be +/// "broken" at the network level. +use std::{sync::Arc, time::Duration}; + +use kmip::client::ConnectionSettings; + +use crate::commons::crypto::signers::{error::SignerError, kmip::signer::KmipTlsClient}; + +/// Manages KMIP TCP + TLS connection creation. +/// +/// Uses the [r2d2] crate to manage a pool of connections. +/// +/// [r2d2]: https://crates.io/crates/r2d2/ +#[derive(Debug)] +pub struct ConnectionManager { + conn_settings: Arc, +} + +impl ConnectionManager { + /// Create a pool of up-to N TCP + TLS connections to the KMIP server. + #[rustfmt::skip] + pub fn create_connection_pool( + conn_settings: Arc, + max_response_bytes: u32, + max_life_time: Duration, + max_idle_time: Duration, + ) -> Result, SignerError> { + let max_life_time = Some(max_life_time); + let max_idle_time = Some(max_idle_time); + + let pool = r2d2::Pool::builder() + // Don't pre-create idle connections to the KMIP server + .min_idle(Some(0)) + + // Create at most this many concurrent connections to the KMIP server + .max_size(max_response_bytes) + + // Don't verify that a connection is usable when fetching it from the pool (as doing so requires sending a + // request to the server and we might as well just try the actual request that we want the connection for) + .test_on_check_out(false) + + // Don't use the default logging behaviour as `[ERROR] [r2d2] Server error: ...` is a bit confusing for end + // users who shouldn't know or care that we use the r2d2 crate. + .error_handler(Box::new(ErrorLoggingHandler)) + + // Don't keep using the same connection for longer than around N minutes (unless in use in which case it + // will wait until the connection is returned to the pool before closing it) - maybe long held connections + // would run into problems with some firewalls. + .max_lifetime(max_life_time) + + // Don't keep connections open that were not used in the last N minutes. + .idle_timeout(max_idle_time) + + // Don't wait longer than N seconds for a new connection to be established, instead try again to connect. + .connection_timeout(conn_settings.connect_timeout.unwrap_or(Duration::from_secs(30))) + + // Use our connection manager to create connections in the pool and to verify their health + .build(ConnectionManager { conn_settings })?; + + Ok(pool) + } + + /// Connect using the given connection settings to a KMIP server. + /// + /// This function creates a new connection to the server. The connection is NOT taken from the connection pool. + pub fn connect_one_off(settings: &ConnectionSettings) -> Result { + let conn = kmip::client::tls::openssl::connect(settings)?; + Ok(conn) + } +} + +impl r2d2::ManageConnection for ConnectionManager { + type Connection = KmipTlsClient; + + type Error = kmip::client::Error; + + /// Establishes a KMIP server connection which will be added to the connection pool. + fn connect(&self) -> Result { + Self::connect_one_off(&self.conn_settings) + } + + /// This function is never used because the [r2d2] `test_on_check_out` flag is set to false when the connection + /// pool is created. + /// + /// [r2d2]: https://crates.io/crates/r2d2/ + fn is_valid(&self, _conn: &mut Self::Connection) -> Result<(), Self::Error> { + unreachable!() + } + + /// Quickly verify if an existing connection is broken. + /// + /// Used to discard and re-create connections that encounter multiple connection related errors. + fn has_broken(&self, conn: &mut Self::Connection) -> bool { + conn.connection_error_count() > 1 + } +} + +/// A Krill specific [r2d2] error logging handler. +/// +/// Logs connection pool related connection error messages using the format `"[] Pool error: ..."` instead of +/// the default [r2d2] `"[ERROR] [r2d2] Server error: ..."` format. Assumes that the logging framework will include the +/// logging module context in the logged message, i.e. `xxx::kmip::xxx` and thus we don't need to mention KMIP in the +/// logged message content. +/// +/// Rationale: +/// - The use of the [r2d2] crate is an internal detail which of no use to end users consulting the logs and which we +/// may change at any time. +/// - Krill should be the one to determine the appropriate level to log a connection issue at, not [r2d2]. +#[derive(Debug)] +struct ErrorLoggingHandler; + +impl r2d2::HandleError for ErrorLoggingHandler +where + E: std::fmt::Display, +{ + fn handle_error(&self, err: E) { + warn!("Pool error: {}", err) + } +} + +impl From for SignerError { + fn from(err: r2d2::Error) -> Self { + SignerError::KmipError(format!("{}", err)) + } +} diff --git a/src/commons/crypto/signing/signers/kmip/mod.rs b/src/commons/crypto/signing/signers/kmip/mod.rs new file mode 100644 index 00000000..4f7c4028 --- /dev/null +++ b/src/commons/crypto/signing/signers/kmip/mod.rs @@ -0,0 +1,19 @@ +//! Support for signing things using an external KMIP compliant cryptographic token. +//! +//! Currently only intended for sanity checking the use of KMIP with Krill by running as the only signer in place of +//! the usual [OpenSslSigner]. Assumes that the KMIP server is a [PyKMIP] instance that is created for and destroyed +//! after the Krill tests have run. Uses hard-coded connection details and in-memory storage of key identifiers issued +//! by the KMIP server. +//! +//! The current implementation splits the KMIP signer into four Rust modules: +//! - `connpool`: Connection pooling related functionality. +//! - `internal`: KMIP server interaction, including probing and retry/backoff logic. +//! - `keymap`: In-memory mapping of `KeyIdentifier` to KMIP key identifiers. +//! - `signer`: The public signer trait implementation. Delegates to `internal`. +//! +//! [OpenSslSigner]: crate::commons::util::softsigner::OpenSslSigner +//! [PyKMIP]: https://github.com/OpenKMIP/PyKMIP +pub mod connpool; +pub mod signer; + +pub use signer::KmipSigner; diff --git a/src/commons/crypto/signing/signers/kmip/signer.rs b/src/commons/crypto/signing/signers/kmip/signer.rs new file mode 100644 index 00000000..17d6d165 --- /dev/null +++ b/src/commons/crypto/signing/signers/kmip/signer.rs @@ -0,0 +1,986 @@ +use std::{ + convert::{TryFrom, TryInto}, + net::TcpStream, + ops::Deref, + path::PathBuf, + sync::{Arc, RwLock}, + time::Duration, +}; + +use backoff::ExponentialBackoff; +use bcder::encode::{PrimitiveContent, Values}; +use bytes::Bytes; +use kmip::{ + client::{Client, ClientCertificate}, + types::{ + common::{KeyMaterial, ObjectType, Operation}, + response::ManagedObject, + }, +}; +use openssl::ssl::SslStream; +use r2d2::PooledConnection; +use rpki::repository::crypto::{ + signer::KeyError, KeyIdentifier, PublicKey, PublicKeyFormat, Signature, SignatureAlgorithm, SigningError, +}; + +use crate::commons::{ + api::{Handle, Timestamp}, + crypto::{ + dispatch::signerinfo::SignerMapper, + signers::{ + kmip::connpool::ConnectionManager, + probe::{ProbeError, ProbeStatus, StatefulProbe}, + util, + }, + SignerError, + }, + error::KrillIoError, +}; + +//------------ Types and constants ------------------------------------------------------------------------------------ + +/// A KMIP client that uses a specific TLS and TCP stream implementation. Currently set to [SslStream] from the +/// [openssl] crate. This will be a different type if we switch to different TCP and/or TLS implementations or to an +/// async implementation, but the client interface will remain the same. +pub type KmipTlsClient = Client>; + +#[derive(Clone, Debug, Deserialize, PartialEq)] +pub struct KmipSignerConfig { + pub host: String, + + #[serde(default = "KmipSignerConfig::default_kmip_port")] + pub port: u16, + + #[serde(default)] + pub insecure: bool, + + #[serde(default)] + pub force: bool, + + #[serde(default)] + pub server_cert_path: Option, + + #[serde(default)] + pub server_ca_cert_path: Option, + + #[serde(default)] + pub client_cert_path: Option, + + #[serde(default)] + pub client_cert_private_key_path: Option, + + #[serde(default)] + pub username: Option, + + #[serde(default)] + pub password: Option, + + #[serde(default = "KmipSignerConfig::default_retry_seconds")] + pub retry_seconds: u64, + + #[serde(default = "KmipSignerConfig::default_backoff_multiplier")] + pub backoff_multiplier: f64, + + #[serde(default = "KmipSignerConfig::default_max_retry_seconds")] + pub max_retry_seconds: u64, + + #[serde(default = "KmipSignerConfig::default_connect_timeout_seconds")] + pub connect_timeout_seconds: u64, + + #[serde(default = "KmipSignerConfig::default_read_timeout_seconds")] + pub read_timeout_seconds: u64, + + #[serde(default = "KmipSignerConfig::default_write_timeout_seconds")] + pub write_timeout_seconds: u64, + + #[serde(default = "KmipSignerConfig::default_max_lifetime_seconds")] + pub max_lifetime_seconds: u64, + + #[serde(default = "KmipSignerConfig::default_max_idle_seconds")] + pub max_idle_seconds: u64, + + #[serde(default = "KmipSignerConfig::default_max_connections")] + pub max_connections: u32, + + #[serde(default = "KmipSignerConfig::default_max_response_bytes")] + pub max_response_bytes: u32, +} + +impl KmipSignerConfig { + pub fn default_kmip_port() -> u16 { + // From: http://docs.oasis-open.org/kmip/profiles/v1.1/os/kmip-profiles-v1.1-os.html#_Toc332820682 + // "KMIP servers using the Basic Authentication Suite SHOULD use TCP port number 5696, as assigned by IANA, to + // receive and send KMIP messages. KMIP clients using the Basic Authentication Suite MAY use the same 5696 TCP + // port number." + 5696 + } + + pub fn default_retry_seconds() -> u64 { + 2 + } + + pub fn default_backoff_multiplier() -> f64 { + 1.5 + } + + pub fn default_max_retry_seconds() -> u64 { + 30 + } + + pub fn default_connect_timeout_seconds() -> u64 { + 5 + } + + pub fn default_read_timeout_seconds() -> u64 { + 5 + } + + pub fn default_write_timeout_seconds() -> u64 { + 5 + } + + pub fn default_max_lifetime_seconds() -> u64 { + 60 * 30 + } + + pub fn default_max_idle_seconds() -> u64 { + 60 * 10 + } + + pub fn default_max_connections() -> u32 { + 5 + } + + pub fn default_max_response_bytes() -> u32 { + 64 * 1024 + } +} + +impl Eq for KmipSignerConfig {} + +#[derive(Debug)] +struct ConnectionSettings { + client: kmip::client::ConnectionSettings, + + force: bool, + + retry_interval: Duration, + + backoff_multiplier: f64, + + retry_timeout: Duration, + + lifetime_timeout: Duration, + + idle_timeout: Duration, + + max_connections: u32, +} + +impl TryFrom<&KmipSignerConfig> for ConnectionSettings { + type Error = SignerError; + + fn try_from(conf: &KmipSignerConfig) -> Result { + let host = conf.host.clone(); + let port = conf.port; + let username = conf.username.clone(); + let password = conf.password.clone(); + let insecure = conf.insecure; + let connect_timeout = Some(Duration::from_secs(conf.connect_timeout_seconds)); + let read_timeout = Some(Duration::from_secs(conf.read_timeout_seconds)); + let write_timeout = Some(Duration::from_secs(conf.write_timeout_seconds)); + let max_response_bytes = Some(conf.max_response_bytes); + + let force = conf.force; + let retry_interval = Duration::from_secs(conf.retry_seconds); + let backoff_multiplier = conf.backoff_multiplier; + let retry_timeout = Duration::from_secs(conf.max_retry_seconds); + let lifetime_timeout = Duration::from_secs(conf.max_lifetime_seconds); + let idle_timeout = Duration::from_secs(conf.max_idle_seconds); + let max_connections = conf.max_connections; + + let client_cert = match &conf.client_cert_path { + Some(cert_path) => { + let cert_bytes = read_binary_file(cert_path)?; + let key_bytes = match &conf.client_cert_private_key_path { + Some(key_path) => Some(read_binary_file(key_path)?), + None => None, + }; + Some(ClientCertificate::SeparatePem { cert_bytes, key_bytes }) + } + None => None, + }; + + let server_cert = match &conf.server_cert_path { + Some(cert_path) => Some(read_binary_file(cert_path)?), + None => None, + }; + + let ca_cert = match &conf.server_ca_cert_path { + Some(cert_path) => Some(read_binary_file(cert_path)?), + None => None, + }; + + let client = kmip::client::ConnectionSettings { + host, + port, + username, + password, + insecure, + client_cert, + server_cert, + ca_cert, + connect_timeout, + read_timeout, + write_timeout, + max_response_bytes, + }; + + Ok(ConnectionSettings { + client, + force, + retry_interval, + backoff_multiplier, + retry_timeout, + lifetime_timeout, + idle_timeout, + max_connections, + }) + } +} + +fn read_binary_file(file_path: &PathBuf) -> Result, SignerError> { + Ok(std::fs::read(file_path).map_err(|err| { + SignerError::IoError(KrillIoError::new(format!("Failed to read file '{:?}'", file_path), err)) + })?) +} + +//------------ The KMIP signer management interface ------------------------------------------------------------------- + +#[derive(Debug)] +pub struct KmipSigner { + name: String, + + handle: RwLock>, + + mapper: Arc, + + /// A probe dependent interface to the KMIP server. + server: Arc>, +} + +impl KmipSigner { + /// Creates a new instance of KmipSigner. + pub fn build( + name: &str, + conf: &KmipSignerConfig, + probe_interval: Duration, + mapper: Arc, + ) -> Result { + // Signer initialization should not block Krill startup. As such we delaying contacting the KMIP server until + // first use. The downside of this approach is that we won't detect any issues until that point. + + let server = Arc::new(StatefulProbe::new( + name.to_string(), + Arc::new(conf.try_into()?), + probe_interval, + )); + + let s = KmipSigner { + name: name.to_string(), + handle: RwLock::new(None), + mapper: mapper.clone(), + server, + }; + + Ok(s) + } + + pub fn get_name(&self) -> &str { + &self.name + } + + pub fn set_handle(&self, handle: Handle) { + let mut writable_handle = self.handle.write().unwrap(); + if writable_handle.is_some() { + panic!("Cannot set signer handle as handle is already set"); + } + *writable_handle = Some(handle); + } + + pub fn get_info(&self) -> Option { + if let Ok(status) = self.server.status(Self::probe_server) { + if let Ok(state) = status.state() { + return Some(state.conn_info.clone()); + } + } + None + } + + pub fn create_registration_key(&self) -> Result<(PublicKey, String), SignerError> { + let (public_key, kmip_key_pair_ids) = self.build_key(PublicKeyFormat::Rsa)?; + let internal_key_id = kmip_key_pair_ids.private_key_id.to_string(); + Ok((public_key, internal_key_id)) + } + + pub fn sign_registration_challenge + ?Sized>( + &self, + signer_private_key_id: &str, + challenge: &D, + ) -> Result { + self.sign_with_key(signer_private_key_id, SignatureAlgorithm::default(), challenge.as_ref()) + } +} + +//------------ Probe based server access ------------------------------------------------------------------------------ + +/// The details needed to interact with a usable KMIP server. +#[derive(Clone, Debug)] +struct UsableServerState { + /// A pool of TCP + TLS clients for connecting to the KMIP server + pool: r2d2::Pool, + + conn_info: String, + + retry_interval: Duration, + + backoff_multiplier: f64, + + retry_timeout: Duration, +} + +impl UsableServerState { + pub fn new( + pool: r2d2::Pool, + conn_info: String, + retry_interval: Duration, + backoff_multiplier: f64, + retry_timeout: Duration, + ) -> UsableServerState { + UsableServerState { + pool, + conn_info, + retry_interval, + backoff_multiplier, + retry_timeout, + } + } + + pub fn get_connection(&self) -> Result, SignerError> { + let conn = self.pool.get()?; + Ok(conn) + } +} + +impl KmipSigner { + /// Verify if the configured KMIP server is contactable and supports the required capabilities. + fn probe_server( + name: String, + status: &ProbeStatus, + ) -> Result> { + let conn_settings: Arc = status.config()?; + debug!( + "[{}] Probing server at {}:{}", + name, conn_settings.client.host, conn_settings.client.port + ); + + // Attempt a one-off connection to check if we should abort due to a configuration error (e.g. unusable + // certificate) that will never work, and to determine the capabilities of the server (which may affect our + // behaviour). + let conn = ConnectionManager::connect_one_off(&conn_settings.client).map_err(|err| { + match err { + // Fatal error + kmip::client::Error::ConfigurationError(err) => { + error!("Failed to connect KMIP server: Configuration error: {}", err); + ProbeError::CompletedUnusable + } + + // I/O error attempting to contact the server or a problem on an internal problem at the server, not + // necessarily fatal or a reason to abort creating the pool. + kmip::client::Error::ServerError(err) => { + error!("Failed to connect KMIP server: Server error: {}", err); + ProbeError::CallbackFailed(SignerError::KmipError(format!("Failed to connect to server: {}", err))) + } + + // Impossible errors: we didn't yet try to send a request or receive a response + kmip::client::Error::SerializeError(err) + | kmip::client::Error::RequestWriteError(err) + | kmip::client::Error::ResponseReadError(err) + | kmip::client::Error::DeserializeError(err) + | kmip::client::Error::InternalError(err) + | kmip::client::Error::Unknown(err) + | kmip::client::Error::ItemNotFound(err) => { + error!("Failed to connect KMIP server: Unexpected error: {}", err); + ProbeError::CompletedUnusable + } + + other => { + error!("Failed to connect KMIP server: Unexpected error: {}", other); + ProbeError::CompletedUnusable + } + } + })?; + + // We managed to establish a TCP+TLS connection to the KMIP server. Send it a Query request to discover how + // it calls itself and which KMIP operations it supports. + let server_properties = conn + .query() + .map_err(|err| ProbeError::CallbackFailed(SignerError::KmipError(err.to_string())))?; + let supported_operations = server_properties.operations.unwrap_or_default(); + + // Check whether or not the KMIP operations that we require are supported by the server + let mut unsupported_operations = Vec::new(); + for required_op in &[ + Operation::CreateKeyPair, + Operation::Activate, + Operation::Sign, + Operation::Revoke, + Operation::Destroy, + Operation::Get, + Operation::ModifyAttribute, + ] { + if !supported_operations.contains(required_op) { + unsupported_operations.push(required_op.to_string()); + } + } + + // Warn about and (optionally) fail due to the lack of any unsupported operations. + if !unsupported_operations.is_empty() { + // Hard fail due to unsupported operations, unless our configuration tells us to try using this server + // anyway. For example, PyKMIP 0.10.0 does not include the ModifyAttribute operation in the set of + // supported operations even though it does support it. Without this flag we would not be able to use + // PyKMIP with Krill! + if conn_settings.force { + warn!( + "[{}] Ignoring KMIP server lacking support for one or more required operations: {}", + name, + unsupported_operations.join(",") + ); + } else { + error!( + "[{}] KMIP server lacks support for one or more required operations: {}", + name, + unsupported_operations.join(",") + ); + return Err(ProbeError::CompletedUnusable); + } + } + + // Switch from probing the server to using it. + // ------------------------------------------- + + let server_identification = server_properties.vendor_identification.unwrap_or("Unknown".into()); + + // Success! We can use this server. Announce it and switch our status to KmipSignerStatus::Usable. + info!( + "[{}] Using KMIP server '{}' at {}:{}", + name, server_identification, conn_settings.client.host, conn_settings.client.port + ); + + let conn_info = format!( + "KMIP Signer [vendor: {}, host: {}, port: {}]", + server_identification, conn_settings.client.host, conn_settings.client.port + ); + let pool = ConnectionManager::create_connection_pool( + Arc::new(conn_settings.client.clone()), + conn_settings.client.max_response_bytes.unwrap(), + conn_settings.lifetime_timeout, + conn_settings.idle_timeout, + )?; + let state = UsableServerState::new( + pool, + conn_info, + conn_settings.retry_interval, + conn_settings.backoff_multiplier, + conn_settings.retry_timeout, + ); + + Ok(state) + } +} + +//------------ Connection related functions --------------------------------------------------------------------------- + +impl KmipSigner { + /// Get a connection to the KMIP server from the pool, if the server is usable. + fn connect(&self) -> Result, SignerError> { + let conn = self.server.status(Self::probe_server)?.state()?.get_connection()?; + Ok(conn) + } + + /// Perform some operation using a KMIP server pool connection. + /// + /// Fails if the KMIP server is not [KmipSignerStatus::Usable]. If the operation fails due to a transient + /// connection error, retry with backoff upto a defined retry limit. + fn with_conn(&self, desc: &str, do_something_with_conn: F) -> Result + where + F: FnOnce(&KmipTlsClient) -> Result + Copy, + { + // Define a notify callback to customize messages written to the logger + let notify = |err, next: std::time::Duration| { + warn!("{} failed, retrying in {} seconds: {}", desc, next.as_secs(), err); + }; + + // Define an operation to (re)try + let op = || { + // First get a (possibly already existing) connection from the pool + let conn = self.connect()?; + + // Next, try to execute the callers operation using the connection. If it fails, examine the cause of + // failure to determine if it should be a hard-fail (no more retries) or if we should try again. + Ok((do_something_with_conn)(conn.deref()).map_err(retry_on_connection_error)?) + }; + + // Don't even bother going round the retry loop if we haven't yet successfully connected to the KMIP server + // and verified its capabilities: + let status = self.server.status(Self::probe_server)?; + let state = status.state()?; + + // Define the backoff policy to use + let backoff_policy = ExponentialBackoff { + initial_interval: state.retry_interval, + multiplier: state.backoff_multiplier, + max_elapsed_time: Some(state.retry_timeout), + ..Default::default() + }; + + // Try (and retry if needed) the requested operation. + Ok(backoff::retry_notify(backoff_policy, op, notify)?) + } +} + +/// The status of a key. +/// +/// KMIP servers require that a key be activated before it can be used for signing and be inactive (revoked) before it +/// can be deleted. +#[derive(Debug, PartialEq)] +pub(super) enum KeyStatus { + /// The key is inactive. + Inactive, + + /// The key was activated. + Active, +} + +pub(super) struct KmipKeyPairIds { + pub public_key_id: String, + pub private_key_id: String, +} + +//------------ High level helper functions for use by the public Signer interface implementation ---------------------- + +impl KmipSigner { + /// Remember that the given KMIP public and private key pair IDs correspond to the given KeyIdentifier. + pub(super) fn remember_kmip_key_ids( + &self, + key_id: &KeyIdentifier, + kmip_key_ids: KmipKeyPairIds, + ) -> Result<(), SignerError> { + // TODO: Don't assume colons cannot appear in HSM key ids. + let internal_key_id = format!("{}:{}", kmip_key_ids.public_key_id, kmip_key_ids.private_key_id); + + let readable_handle = self.handle.read().unwrap(); + let signer_handle = readable_handle.as_ref().ok_or(SignerError::Other( + "KMIP: Failed to record signer key: Signer handle not set".to_string(), + ))?; + self.mapper + .add_key(signer_handle, key_id, &internal_key_id) + .map_err(|err| SignerError::KmipError(format!("Failed to record signer key: {}", err)))?; + + Ok(()) + } + + /// Given a KeyIdentifier lookup the corresponding KMIP public and private key pair IDs. + pub(super) fn lookup_kmip_key_ids(&self, key_id: &KeyIdentifier) -> Result> { + // split_once isn't available until Rust 1.52 + pub fn split_once<'a>(s: &'a str, delimiter: char) -> Option<(&'a str, &'a str)> { + let (start, end) = s.split_at(s.find(delimiter)?); + Some((&start[..=(start.len() - 1)], &end[1..])) + } + + let readable_handle = self.handle.read().unwrap(); + let signer_handle = readable_handle.as_ref().ok_or(KeyError::KeyNotFound)?; + + let internal_key_id = self + .mapper + .get_key(signer_handle, key_id) + .map_err(|_| KeyError::KeyNotFound)?; + + let (public_key_id, private_key_id) = split_once(&internal_key_id, ':').unwrap(); + + Ok(KmipKeyPairIds { + public_key_id: public_key_id.to_string(), + private_key_id: private_key_id.to_string(), + }) + } + + /// Create a key pair in the KMIP server in the requested format and make it ready for use by Krill. + pub(super) fn build_key(&self, algorithm: PublicKeyFormat) -> Result<(PublicKey, KmipKeyPairIds), SignerError> { + // https://tools.ietf.org/html/rfc6485#section-3: Asymmetric Key Pair Formats + // "The RSA key pairs used to compute the signatures MUST have a 2048-bit + // modulus and a public exponent (e) of 65,537." + + if !matches!(algorithm, PublicKeyFormat::Rsa) { + return Err(SignerError::KmipError(format!( + "Algorithm {:?} not supported while creating key", + &algorithm + ))); + } + + // Give keys a Krill specific but random name initially. Once we have created them we can determine the SHA-1 + // of their X.509 SubjectPublicKeyInfo aka the Krill KeyIdentifier and use that in the name instead of the + // random component. + + // The name given to a key is purely for our own use, the KMIP server doesn't care about it. We give keys a + // name that clearly indicates they relate to Krill as this may be helpful to the KMIP server operator. Once + // the key is created we rename it to include its Krill KeyIdentifier (aka the SHA-1 of the X.509 + // SubjectPublicKeyInfo) so that we can relate the key back to its usage in Krill. We include the Unix seconds + // since 1970-01-01 timestamp in the name initially just as some rough at-a-glance indication of when it was + // created and to differentiate it from other keys with the same name (of which there should be none as they + // key should either be renamed after creation or should have been deleted at some point). + let prefix = format!("krill_new_key_{}", Timestamp::now()); + let private_key_name = format!("{}_priv", prefix); + let public_key_name = format!("{}_pub", prefix); + + // Create the RSA key pair + let kmip_key_pair_ids = self.create_rsa_key_pair(private_key_name, public_key_name)?; + + // Prepare the new keys for use, and attempt to destroy them if anything goes wrong + let public_key = self + .prepare_keypair_for_use(&kmip_key_pair_ids.private_key_id, &kmip_key_pair_ids.public_key_id) + .or_else(|err| { + let _ = self.destroy_key_pair(&kmip_key_pair_ids, KeyStatus::Inactive); + Err(SignerError::KmipError(err.to_string())) + })?; + + Ok((public_key, kmip_key_pair_ids)) + } + + /// Create an RSA key pair in the KMIP server. + fn create_rsa_key_pair( + &self, + private_key_name: String, + public_key_name: String, + ) -> Result { + let (private_key_id, public_key_id) = self.with_conn("create key pair", |conn| { + conn.create_rsa_key_pair(2048, private_key_name.clone(), public_key_name.clone()) + })?; + + let kmip_key_ids = KmipKeyPairIds { + public_key_id, + private_key_id, + }; + + Ok(kmip_key_ids) + } + + /// Make the given KMIP private and public key pair ready for use by Krill. + /// + /// Note that this function renames the created keys but this is not needed for correct functioning of Krill, it + /// is rather done to aid the KMIP server operator when administering the HSM. + /// + /// It also activates the private key. Without this the key cannot be used for signing. An alternate approach could + /// be to set the activation date of the key when creating it thereby avoiding the extra activation step, or to + /// perform the activation operation as part of a bulk request also containing the create key operation, thereby + /// reducing the number of round trips to the server. + fn prepare_keypair_for_use(&self, private_key_id: &str, public_key_id: &str) -> Result { + // Create a public key object for the public key + let public_key = self.get_public_key_from_id(&public_key_id)?; + + // Determine names for the public and private key that allow them to be related back to their usage in Krill + // TODO: Give even more helpful names to the keys such as the name of the CA they were created for? + let hex_key_id = hex::encode(public_key.key_identifier()); + let new_public_key_name = format!("krill-public-key-{}", hex_key_id); + let new_private_key_name = format!("krill-private-key-{}", hex_key_id); + + // Rename the keys to their new names + self.with_conn("rename key", |conn| { + conn.rename_key(public_key_id, new_public_key_name.clone()) + })?; + + self.with_conn("rename key", |conn| { + conn.rename_key(private_key_id, new_private_key_name.clone()) + })?; + + // Activate the private key so that it can be used for signing. Do this last otherwise if there is a problem + // with preparing the key pair for use we have to deactivate the private key before we can destroy it. + self.with_conn("activate key", |conn| conn.activate_key(&private_key_id))?; + + Ok(public_key) + } + + /// Get the RSA public bytes for the given KMIP server public key. + fn get_rsa_public_key_bytes(&self, public_key_id: &str) -> Result { + let response_payload = self.with_conn("get key", |conn| conn.get_key(public_key_id))?; + + if response_payload.object_type != ObjectType::PublicKey { + return Err(SignerError::KmipError(format!( + "Failed to get key material: unsupported object type '{:?}' returned by KMIP Get operation for public key with ID '{}'", + response_payload.object_type, public_key_id))); + } + + let key_material = match response_payload.cryptographic_object { + ManagedObject::PublicKey(public_key) => public_key.key_block.key_value.key_material, + _ => { + return Err(SignerError::KmipError(format!( + "Failed to get key material: unsupported cryptographic object type returned by KMIP Get operation for public key with ID '{}'", + public_key_id))); + } + }; + + let rsa_public_key_bytes = match key_material { + KeyMaterial::Bytes(bytes) => bytes::Bytes::from(bytes), + KeyMaterial::TransparentRSAPublicKey(pub_key) => { + util::rsa_public_key_bytes_from_parts(&pub_key.modulus, &pub_key.public_exponent)? + } + KeyMaterial::TransparentRSAPrivateKey(priv_key) => { + if let Some(public_exponent) = priv_key.public_exponent { + util::rsa_public_key_bytes_from_parts(&priv_key.modulus, &public_exponent)? + } else { + return Err(SignerError::KmipError(format!( + "Failed to get key material: missing exponent in transparent RSA private key returned by KMIP Get operation for public key with ID '{}'", + public_key_id))); + } + } + _ => { + return Err(SignerError::KmipError(format!( + "Failed to get key material: unsupported key material type {:?} returned by KMIP Get operation for public key with ID '{}'", + key_material, public_key_id))); + } + }; + + Ok(rsa_public_key_bytes) + } + + pub(super) fn get_public_key_from_id(&self, public_key_id: &str) -> Result { + let rsa_public_key_bytes = self.get_rsa_public_key_bytes(public_key_id)?; + + let subject_public_key = bcder::BitString::new(0, rsa_public_key_bytes); + + let subject_public_key_info = + bcder::encode::sequence((PublicKeyFormat::Rsa.encode(), subject_public_key.encode())); + + let mut subject_public_key_info_source: Vec = Vec::new(); + subject_public_key_info + .write_encoded(bcder::Mode::Der, &mut subject_public_key_info_source) + .map_err(|err| { + SignerError::KmipError(format!( + "Failed to create DER encoded SubjectPublicKeyInfo from constituent parts: {}", + err + )) + })?; + + let public_key = PublicKey::decode(subject_public_key_info_source.as_slice()).map_err(|err| { + SignerError::KmipError(format!( + "Failed to create public key from the DER encoded SubjectPublicKeyInfo: {}", + err + )) + })?; + + Ok(public_key) + } + + pub(super) fn sign_with_key( + &self, + private_key_id: &str, + algorithm: SignatureAlgorithm, + data: &[u8], + ) -> Result { + if algorithm.public_key_format() != PublicKeyFormat::Rsa { + return Err(SignerError::KmipError(format!( + "Algorithm '{:?}' not supported", + algorithm.public_key_format() + ))); + } + + let signed = self.with_conn("sign", |conn| conn.sign(&private_key_id, data))?; + + let sig = Signature::new(SignatureAlgorithm::default(), Bytes::from(signed.signature_data)); + + Ok(sig) + } + + pub(super) fn destroy_key_pair( + &self, + kmip_key_pair_ids: &KmipKeyPairIds, + mode: KeyStatus, + ) -> Result<(), SignerError> { + let mut res = self.with_conn("destroy key", |conn| conn.destroy_key(&kmip_key_pair_ids.public_key_id)); + + if let Err(err) = &res { + warn!( + "[{}] Failed to destroy KMIP public key '{}': {}", + self.name, &kmip_key_pair_ids.public_key_id, err + ); + } + + let mut deactivated = true; + if mode == KeyStatus::Active { + // TODO: it's unclear from the KMIP 1.2 specification if this can fail because the key is already revoked. + // If that is a possible failure scenario we should not abort here but instead continue to delete the key. + let res2 = self.with_conn("revoke key", |conn| conn.revoke_key(&kmip_key_pair_ids.private_key_id)); + + if let Err(err) = &res2 { + deactivated = false; + warn!( + "[{}] Failed to revoke KMIP private key '{}': {}", + self.name, &kmip_key_pair_ids.private_key_id, err + ); + } + + res = res.and(res2); + } + + if deactivated { + // TODO: This can fail if the key is not in the correct state, e.g. one cause can be if the key is not + // revoked. We don't expect this because we assume we know whether we activated or revoked the key or not + // but if for some reason the key exists, we think it does not require revocation but actually it does, + // then we would fail here. In such a case we could attempt to revoke and retry, but that assumes we can + // detect that specific failure scenario. + let res3 = self.with_conn("destroy key", |conn| { + conn.destroy_key(&kmip_key_pair_ids.private_key_id) + }); + + if let Err(err) = &res3 { + warn!( + "[{}] Failed to destroy KMIP private key '{}': {}", + self.name, &kmip_key_pair_ids.private_key_id, err + ); + } + + res = res.and(res3); + } + + res + } +} + +//------------ Functions required to exist by the `SignerProvider` ---------------------------------------------------- + +// Implement the functions defined by the `Signer` trait because `SignerProvider` expects to invoke them, but as the +// dispatching is not trait based we don't actually have to implement the `Signer` trait. + +impl KmipSigner { + pub fn create_key(&self, algorithm: PublicKeyFormat) -> Result { + let (key, kmip_key_pair_ids) = self.build_key(algorithm)?; + let key_id = key.key_identifier(); + self.remember_kmip_key_ids(&key_id, kmip_key_pair_ids)?; + Ok(key_id) + } + + pub fn get_key_info(&self, key_id: &KeyIdentifier) -> Result> { + let kmip_key_pair_ids = self.lookup_kmip_key_ids(key_id)?; + self.get_public_key_from_id(&kmip_key_pair_ids.public_key_id) + .map_err(|err| KeyError::Signer(err)) + } + + pub fn destroy_key(&self, key_id: &KeyIdentifier) -> Result<(), KeyError> { + let kmip_key_pair_ids = self.lookup_kmip_key_ids(key_id)?; + + let mut res = self + .destroy_key_pair(&kmip_key_pair_ids, KeyStatus::Active) + .map_err(|err| match err { + SignerError::KeyNotFound => KeyError::KeyNotFound, + _ => KeyError::Signer(err), + }); + + if let Err(err) = &res { + warn!( + "[{}] Failed to completely destroy KMIP key pair with ID {} (KMIP public key ID: {}, KMIP private key ID: {}): {}", + self.name, key_id, kmip_key_pair_ids.public_key_id, kmip_key_pair_ids.private_key_id, err + ); + } + + // remove the key from the signer mapper as well + if let Some(signer_handle) = self.handle.read().unwrap().as_ref() { + let res2 = self + .mapper + .remove_key(signer_handle, key_id) + .map_err(|err| KeyError::Signer(SignerError::Other(err.to_string()))); + + if let Err(err) = &res2 { + warn!( + "[{}] Failed to remove mapping for key with ID {}: {}", + self.name, key_id, err + ); + } + + res = res.and(res2); + } + + res + } + + pub fn sign + ?Sized>( + &self, + key_id: &KeyIdentifier, + algorithm: SignatureAlgorithm, + data: &D, + ) -> Result> { + let kmip_key_pair_ids = self.lookup_kmip_key_ids(key_id)?; + + let signature = self + .sign_with_key(&kmip_key_pair_ids.private_key_id, algorithm, data.as_ref()) + .map_err(|err| { + SigningError::Signer(SignerError::KmipError(format!( + "Signing data failed for Krill KeyIdentifier '{}' and KMIP private key id '{}': {}", + key_id, kmip_key_pair_ids.private_key_id, err + ))) + })?; + + Ok(signature) + } + + pub fn sign_one_off + ?Sized>( + &self, + algorithm: SignatureAlgorithm, + data: &D, + ) -> Result<(Signature, PublicKey), SignerError> { + // TODO: Is it possible to use a KMIP batch request to implement the create, activate, sign, deactivate, delete + // in one round-trip to the server? + let (key, kmip_key_pair_ids) = self.build_key(PublicKeyFormat::Rsa)?; + + let signature_res = self + .sign_with_key(&kmip_key_pair_ids.private_key_id, algorithm, data.as_ref()) + .map_err(|err| SignerError::KmipError(format!("One-off signing of data failed: {}", err))); + + let _ = self.destroy_key_pair(&kmip_key_pair_ids, KeyStatus::Active); + + let signature = signature_res?; + + Ok((signature, key)) + } +} + +// -------------------------------------------------------------------------------------------------------------------- +// Retry with backoff related helper impls/fns: +// -------------------------------------------------------------------------------------------------------------------- + +impl From for SignerError { + fn from(err: kmip::client::Error) -> Self { + match err { + kmip::client::Error::ItemNotFound(_) => SignerError::KeyNotFound, + _ => SignerError::KmipError(format!("Client error: {}", err)), + } + } +} + +impl From> for SignerError { + fn from(err: backoff::Error) -> Self { + match err { + backoff::Error::Permanent(err) => err, + backoff::Error::Transient(err) => err, + } + } +} + +fn retry_on_connection_error(err: kmip::client::Error) -> backoff::Error +where + E: From, +{ + if err.is_connection_error() { + backoff::Error::Transient(err.into()) + } else { + backoff::Error::Permanent(err.into()) + } +} diff --git a/src/commons/crypto/signing/signers/mocksigner.rs b/src/commons/crypto/signing/signers/mocksigner.rs new file mode 100644 index 00000000..f6e3ca03 --- /dev/null +++ b/src/commons/crypto/signing/signers/mocksigner.rs @@ -0,0 +1,266 @@ +use std::{ + collections::HashMap, + sync::{Arc, RwLock}, +}; + +use bytes::Bytes; +use openssl::{ + hash::MessageDigest, + pkey::{PKey, Private}, + rsa::Rsa, +}; +use rpki::repository::crypto::{ + signer::KeyError, KeyIdentifier, PublicKey, PublicKeyFormat, Signature, SignatureAlgorithm, SigningError, +}; + +use crate::commons::{ + api::Handle, + crypto::{dispatch::signerinfo::SignerMapper, SignerError}, +}; + +pub enum FnIdx { + CreateRegistrationKey, + SignRegistrationChallenge, + SetHandle, + GetName, + GetInfo, + CreateKey, + GetKeyInfo, + DestroyKey, + Sign, + SignOneOff, + Count, +} + +#[derive(Debug)] +pub struct MockSignerCallCounts { + call_counts: RwLock>, +} + +impl MockSignerCallCounts { + pub fn new() -> Self { + let mut call_counts = Vec::with_capacity(FnIdx::Count as usize); + call_counts.resize(FnIdx::Count as usize, 0); + + Self { + call_counts: RwLock::new(call_counts), + } + } + + pub fn get(&self, fn_idx: FnIdx) -> u32 { + self.call_counts.read().unwrap()[fn_idx as usize] + } + + pub fn inc(&self, fn_idx: FnIdx) { + self.call_counts.write().unwrap()[fn_idx as usize] += 1; + } +} + +pub type CreateRegistrationKeyErrorCb = fn(&MockSignerCallCounts) -> Result<(), SignerError>; +pub type SignRegistrationChallengeErrorCb = fn(&MockSignerCallCounts) -> Result<(), SignerError>; + +pub struct MockSigner { + name: String, + info: Option, + fn_call_counts: Arc, + handle: RwLock>, + mapper: Arc, + keys: RwLock>>, + create_registration_key_error_cb: Option, + sign_registration_challenge_error_cb: Option, +} + +impl std::fmt::Debug for MockSigner { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("MockSigner").finish() + } +} + +// test interface +impl MockSigner { + pub fn new( + name: &str, + signer_mapper: Arc, + fn_call_counts: Arc, + create_registration_key_error_cb: Option, + sign_registration_challenge_error_cb: Option, + ) -> Self { + Self { + name: name.to_string(), + info: None, + fn_call_counts, + handle: RwLock::new(None), + mapper: signer_mapper, + keys: RwLock::new(HashMap::new()), + create_registration_key_error_cb, + sign_registration_challenge_error_cb, + } + } + + fn inc_fn_call_count(&self, fn_idx: FnIdx) { + self.fn_call_counts.inc(fn_idx) + } + + pub fn set_info(&mut self, info: &str) { + self.info = Some(info.to_string()); + } + + fn build_key(&self) -> Result<(PublicKey, PKey, KeyIdentifier, String), SignerError> { + // generate a key pair + let rsa = Rsa::generate(2048)?; + let pkey = PKey::from_rsa(rsa)?; + let public_key = Self::public_key_from_pkey(&pkey).unwrap(); + let key_identifier = public_key.key_identifier(); + + // remember this private key by its "internal id" + let internal_id = key_identifier.to_string(); + self.keys.write().unwrap().insert(internal_id.clone(), pkey.clone()); + + // return the key details to the caller + Ok((public_key, pkey, key_identifier, internal_id)) + } + + fn sign_with_key + ?Sized>(pkey: &PKey, challenge: &D) -> Result { + let mut signer = ::openssl::sign::Signer::new(MessageDigest::sha256(), &pkey)?; + signer.update(challenge.as_ref())?; + let signature = Signature::new(SignatureAlgorithm::default(), Bytes::from(signer.sign_to_vec()?)); + Ok(signature) + } + + fn public_key_from_pkey(pkey: &PKey) -> Result { + let mut b = Bytes::from(pkey.rsa().unwrap().public_key_to_der().unwrap()); + PublicKey::decode(&mut b).map_err(|_| SignerError::DecodeError) + } + + fn internal_id_from_key_identifier(&self, key_identifier: &KeyIdentifier) -> Result { + let lock = self.handle.read().unwrap(); + let signer_handle = lock.as_ref().unwrap(); + self.mapper + .get_key(signer_handle, key_identifier) + .map_err(|_| SignerError::KeyNotFound) + } + + fn load_key(&self, internal_id: &str) -> Option> { + // "load" the private key from storage by its "internal id" + let keys = self.keys.read().unwrap(); + keys.get(internal_id).cloned() + } +} + +// interface expected by SignerProvider +impl MockSigner { + pub fn create_registration_key(&self) -> Result<(PublicKey, String), SignerError> { + self.inc_fn_call_count(FnIdx::CreateRegistrationKey); + if let Some(err_cb) = &self.create_registration_key_error_cb { + let _ = (err_cb)(&self.fn_call_counts)?; + } + let (public_key, _, _, internal_id) = self.build_key().unwrap(); + Ok((public_key, internal_id)) + } + + pub fn sign_registration_challenge + ?Sized>( + &self, + signer_private_key_id: &str, + challenge: &D, + ) -> Result { + self.inc_fn_call_count(FnIdx::SignRegistrationChallenge); + if let Some(err_cb) = &self.sign_registration_challenge_error_cb { + let _ = (err_cb)(&self.fn_call_counts)?; + } + let pkey = self.load_key(signer_private_key_id).ok_or(SignerError::KeyNotFound)?; + + // sign the given data using the loaded private key + let signature = Self::sign_with_key(&pkey, challenge)?; + + // return the generated signature to the caller + Ok(signature) + } + + pub fn set_handle(&self, handle: Handle) { + self.inc_fn_call_count(FnIdx::SetHandle); + // remember the handle that has been generated for us so that we can use it when registering keys with the + // signer mapper + self.handle.write().unwrap().replace(handle); + } + + pub fn get_name(&self) -> &str { + self.inc_fn_call_count(FnIdx::GetName); + &self.name + } + + pub fn get_info(&self) -> Option { + self.inc_fn_call_count(FnIdx::GetInfo); + self.info.clone() + } +} + +// Implement the functions defined by the `Signer` trait because `SignerProvider` expects to invoke them, but as the +// dispatching is not trait based we don't actually have to implement the `Signer` trait. +impl MockSigner { + pub fn create_key(&self, _algorithm: PublicKeyFormat) -> Result { + self.inc_fn_call_count(FnIdx::CreateKey); + let (_, _, key_identifier, internal_id) = self.build_key().unwrap(); + + // tell the signer mapper we own this key identifier which maps to our "internal id" + let lock = self.handle.read().unwrap(); + let signer_handle = lock.as_ref().unwrap(); + self.mapper + .add_key(signer_handle, &key_identifier, &internal_id) + .unwrap(); + + Ok(key_identifier) + } + + pub fn get_key_info(&self, key_identifier: &KeyIdentifier) -> Result> { + self.inc_fn_call_count(FnIdx::GetKeyInfo); + let internal_id = self.internal_id_from_key_identifier(key_identifier).unwrap(); + let pkey = self.load_key(&internal_id).ok_or(KeyError::KeyNotFound)?; + let public_key = Self::public_key_from_pkey(&pkey).unwrap(); + Ok(public_key) + } + + pub fn destroy_key(&self, key_id: &KeyIdentifier) -> Result<(), KeyError> { + self.inc_fn_call_count(FnIdx::DestroyKey); + let internal_id = self.internal_id_from_key_identifier(key_id).unwrap(); + let _ = self.keys.write().unwrap().remove(&internal_id); + + // remove the key from the signer mapper as well + if let Some(signer_handle) = self.handle.read().unwrap().as_ref() { + self.mapper + .remove_key(signer_handle, key_id) + .map_err(|err| KeyError::Signer(SignerError::Other(err.to_string())))?; + } + + Ok(()) + } + + pub fn sign + ?Sized>( + &self, + key_identifier: &KeyIdentifier, + _algorithm: SignatureAlgorithm, + data: &D, + ) -> Result> { + self.inc_fn_call_count(FnIdx::Sign); + let internal_id = self.internal_id_from_key_identifier(key_identifier)?; + let pkey = self.load_key(&internal_id).ok_or(SignerError::KeyNotFound)?; + Self::sign_with_key(&pkey, data).map_err(|err| SigningError::Signer(err)) + } + + pub fn sign_one_off + ?Sized>( + &self, + _algorithm: SignatureAlgorithm, + data: &D, + ) -> Result<(Signature, PublicKey), SignerError> { + self.inc_fn_call_count(FnIdx::SignOneOff); + let (public_key, pkey, _, internal_id) = self.build_key().unwrap(); + let signature = Self::sign_with_key(&pkey, data).unwrap(); + let _ = self.keys.write().unwrap().remove(&internal_id); + Ok((signature, public_key)) + } + + pub fn wipe_all_keys(&self) { + // wipe out all our keys, including the identity key used by the SignerRouter to verify that we are an + // already known signer. + self.keys.write().unwrap().clear(); + } +} diff --git a/src/commons/crypto/signing/signers/mod.rs b/src/commons/crypto/signing/signers/mod.rs new file mode 100644 index 00000000..6b2bb671 --- /dev/null +++ b/src/commons/crypto/signing/signers/mod.rs @@ -0,0 +1,18 @@ +pub mod error; + +#[cfg(feature = "hsm")] +pub mod kmip; + +#[cfg(feature = "hsm")] +pub mod pkcs11; + +pub mod softsigner; + +#[cfg(feature = "hsm")] +pub mod util; + +#[cfg(feature = "hsm")] +pub mod probe; + +#[cfg(all(test, feature = "hsm"))] +pub mod mocksigner; diff --git a/src/commons/crypto/signing/signers/pkcs11/context.rs b/src/commons/crypto/signing/signers/pkcs11/context.rs new file mode 100644 index 00000000..4e60bdc7 --- /dev/null +++ b/src/commons/crypto/signing/signers/pkcs11/context.rs @@ -0,0 +1,271 @@ +//! The PKCS#11 "Cryptoki" context. +//! +//! The term "context" isn't part of the PKCS#11 specification, it's the name given by the `pkcs11` Rust crate to the +//! root data structure that represents a loaded PKCS#11 library and gives access to the functions exported by it. +//! +//! Each PKCS#11 library must be initialized only once by a single application using it, irrespective of however many +//! threads there are within the application that use it. +//! +//! # Known issues +//! +//! There are no timeouts around the calls into the PKCS#11 context and yet we have no idea what the PKCS#11 library +//! is going to do when invoked. If it uses a TCP/IP connection to a remote service which is itself not that fast even +//! when operating normally, the invocation could block for quite a while (in computing terms at least). One possible +//! way to improve this could be to invoke the library in another thread and way a maximum amount of time in the +//! invoking thread before deciding to give up on the spawned thread that is taking too long. +use std::{ + collections::{hash_map::Entry, HashMap}, + path::Path, + sync::{Arc, Mutex, RwLock}, +}; + +use cryptoki::error::Error as Pkcs11Error; + +use cryptoki::{ + context::{CInitializeArgs, Info, Pkcs11}, + mechanism::Mechanism, + object::{Attribute, AttributeType, ObjectHandle}, + session::{Session, SessionFlags, UserType}, + slot::{Slot, SlotInfo, TokenInfo}, +}; +use once_cell::sync::OnceCell; + +use crate::commons::crypto::SignerError; + +#[derive(Debug, Clone)] +pub(super) struct ThreadSafePkcs11Context(Arc>); + +impl std::ops::Deref for ThreadSafePkcs11Context { + type Target = Arc>; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl ThreadSafePkcs11Context { + pub fn new(file_name: &str, ctx: Pkcs11) -> Self { + Self(Arc::new(RwLock::new(Pkcs11Context { + lib_file_name: file_name.to_string(), + ctx: Some(ctx), + initialized: false, + }))) + } +} + +/// To enable use cases such as migrating from one PKCS#11 provider to another we need to support loading more than one +/// PKCS#11 library at once and so need to distinguish one library from another. Prior to actually initializing the +/// library the only means we have for differentiating one from another is the file system path which the library is +/// loaded from. This may not actually be unique, it could be two copies of the same library (or two different versions +/// of the same library), or it could be some sort of symbolic link or duplicate mount or other mechanism for making two +/// file system paths point to the same underlying file. To avoid attempts to load different versions of the same +/// library we use the filename as the unique identifier rather than the entire path so that two library files at +/// different filesystem locations with the same name are not both loaded into the memory of our process at the same +/// time. +/// +/// To give access to the same loaded library from a second or subsequent caller without double loading or +/// initialization of the library we need a means of looking up the library context by filename. We use a simple +/// RwLock'd HashMap for this. +type Pkcs11ContextsByFileName = Arc>>; +static CONTEXTS: OnceCell = OnceCell::new(); + +#[derive(Debug)] +pub(super) struct Pkcs11Context { + lib_file_name: String, + + /// The Rust `pkcs11` Ctx object which gives access to the loaded library functions. + /// + /// Some(...) means that the library was successfully loaded and passed the initial checks performed by the + /// `pkcs11` crate (at the time of writing it checks that a lot of function pointers are available as expected). + /// + /// None means that we tried and failed to load the library. + ctx: Option, + + initialized: bool, +} + +impl Pkcs11Context { + /// Load the PKCS#11 library. + pub fn get_or_load(lib_path: &Path) -> Result { + // Initialize the singleton map of PKCS#11 contexts. Failure here should be impossible or else so severe that + // panicking is all we can do. + let contexts = CONTEXTS + .get_or_try_init(|| -> Result { Ok(Arc::new(RwLock::new(HashMap::new()))) }) + .unwrap(); + + // Use the file name of the library as the key into the map, if the path represents a file. + let lib_file_name = lib_path.file_name().ok_or_else(|| { + SignerError::Pkcs11Error(format!( + "Failed to load PKCS#11 library '{:?}': path does not refer to a file", + lib_path + )) + })?; + + // Get a reference to either the already loaded library, or to the result of trying to load it. + let lib_file_name = lib_file_name.to_string_lossy().to_string(); + let mut locked_contexts = contexts.write().unwrap(); + + let ctx_ref = Self::or_insert_with_key(locked_contexts.entry(lib_file_name), |file_name| { + // The library isn't yet in the map, so load it. + trace!("Loading PKCS#11 library '{:?}'", lib_path); + + let ctx = Pkcs11::new(lib_path).map_err(|err| { + SignerError::Pkcs11Error(format!("Failed to load PKCS#11 library '{:?}': {}", lib_path, err)) + })?; + + trace!("Loaded PKCS#11 library '{:?}'", lib_path); + Ok(ThreadSafePkcs11Context::new(file_name, ctx)) + })?; + + Ok(ctx_ref.clone()) + } + + /// Invoke C_Initialize in the loaded PKCS#11 library, if not already initialized. + /// We don't do this at the time of loading the library as we don't want to delay or block Krill startup. + pub fn initialize_if_not_already(&mut self) -> Result<(), SignerError> { + let _ = self.ctx.as_mut().ok_or(SignerError::Pkcs11Error(format!( + "Failed to initialize library '{}': Library is not loaded yet", + self.lib_file_name + )))?; + + if !self.initialized { + // Note: YubiHSM uses the reserved field of the initialize arguments to pass settings to the library but + // (the current version of) the `cryptoki` crate doesn't provide a way to set those if we wanted to support + // this way of configuring the PKCS#11 token. + + // TODO: add a timeout around the call to initialize? + if let Err(err) = self.initialize(CInitializeArgs::OsThreads) { + error!("Failed to initialize PKCS#11 library '{}': {}", self.lib_file_name, err); + return Err(SignerError::PermanentlyUnusable); + } + + self.initialized = true; + } + + Ok(()) + } + + // Entry::or_insert_with_key() isn't available until Rust 1.50 + fn or_insert_with_key<'a, F: FnOnce(&String) -> Result>( + e: Entry<'a, String, ThreadSafePkcs11Context>, + default: F, + ) -> Result<&'a mut ThreadSafePkcs11Context, SignerError> { + let existing_or_new_value = match e { + Entry::Occupied(entry) => entry.into_mut(), + Entry::Vacant(entry) => { + let value = default(entry.key())?; + entry.insert(value) + } + }; + + Ok(existing_or_new_value) + } +} + +//------------ Deref with logging (rather than just impl std::ops::Deref) --------------------------------------------- + +// TODO: add a timeout around Cryptoki calls? +impl Pkcs11Context { + fn logged_cryptoki_call(&self, cryptoki_call_name: &'static str, call: F) -> Result + where + F: FnOnce(&Pkcs11) -> Result, + { + trace!("{}::{}()", self.lib_file_name, cryptoki_call_name); + let res = (call)(self.ctx.as_ref().unwrap()); + if let Err(err) = &res { + error!("{}::{}() failed: {}", self.lib_file_name, cryptoki_call_name, err); + } + res + } +} + +impl Pkcs11Context { + fn initialize(&self, init_args: CInitializeArgs) -> Result<(), Pkcs11Error> { + self.logged_cryptoki_call("Initialize", |cryptoki| cryptoki.initialize(init_args)) + } + + pub fn get_info(&self) -> Result { + self.logged_cryptoki_call("GetLibraryInfo", |cryptoki| cryptoki.get_library_info()) + } + + pub fn get_slot_list(&self, token_present: bool) -> Result, Pkcs11Error> { + self.logged_cryptoki_call("GetSlotList", |cryptoki| { + if token_present { + cryptoki.get_slots_with_initialized_token() + } else { + cryptoki.get_all_slots() + } + }) + } + + pub fn get_slot_info(&self, slot: Slot) -> Result { + self.logged_cryptoki_call("GetSlotInfo", |cryptoki| cryptoki.get_slot_info(slot)) + } + + pub fn get_token_info(&self, slot: Slot) -> Result { + self.logged_cryptoki_call("GetTokenInfo", |cryptoki| cryptoki.get_token_info(slot)) + } + + pub fn open_session(&self, slot: Slot, flags: SessionFlags) -> Result { + self.logged_cryptoki_call("OpenSession", |cryptoki| cryptoki.open_session_no_callback(slot, flags)) + } + + pub fn generate_key_pair( + &self, + session: Arc>, + mechanism: &Mechanism, + public_key_template: &[Attribute], + private_key_template: &[Attribute], + ) -> Result<(ObjectHandle, ObjectHandle), Pkcs11Error> { + self.logged_cryptoki_call("GenerateKeyPair", |_| { + session + .lock() + .unwrap() + .generate_key_pair(mechanism, public_key_template, private_key_template) + }) + } + + pub fn get_attributes<'a>( + &self, + session: Arc>, + object: ObjectHandle, + template: &[AttributeType], + ) -> Result, Pkcs11Error> { + self.logged_cryptoki_call("GetAttributes", move |_| { + session.lock().unwrap().get_attributes(object, template) + }) + } + + pub fn login<'a>( + &self, + session: Arc>, + user_type: UserType, + pin: Option<&'a str>, + ) -> Result<(), Pkcs11Error> { + self.logged_cryptoki_call("Login", |_| session.lock().unwrap().login(user_type, pin)) + } + + pub fn sign( + &self, + session: Arc>, + mechanism: &Mechanism, + key: ObjectHandle, + data: &[u8], + ) -> Result, Pkcs11Error> { + self.logged_cryptoki_call("Sign", |_| session.lock().unwrap().sign(mechanism, key, data)) + } + + pub fn find_objects( + &self, + session: Arc>, + template: &[Attribute], + ) -> Result, Pkcs11Error> { + self.logged_cryptoki_call("FindObjects", |_| session.lock().unwrap().find_objects(template)) + } + + pub fn destroy_object(&self, session: Arc>, object_handle: ObjectHandle) -> Result<(), Pkcs11Error> { + self.logged_cryptoki_call("DestroyObject", |_| { + session.lock().unwrap().destroy_object(object_handle) + }) + } +} diff --git a/src/commons/crypto/signing/signers/pkcs11/mod.rs b/src/commons/crypto/signing/signers/pkcs11/mod.rs new file mode 100644 index 00000000..56c0c730 --- /dev/null +++ b/src/commons/crypto/signing/signers/pkcs11/mod.rs @@ -0,0 +1,33 @@ +pub mod context; +pub mod session; +/// # Thread safety +/// +/// From section 6.7.6 "Capabilities of sessions": +/// +/// "A consequence of the fact that a single session can, in general, perform only one operation at a time is that an +/// application should never make multiple simultaneous function calls to Cryptoki which use a common session. If +/// multiple threads of an application attempt to use a common session concurrently in this fashion, Cryptoki does +/// not define what happens. This means that if multiple threads of an application all need to use Cryptoki to access +/// a particular token, it might be appropriate for each thread to have its own session with the token, unless the +/// application can ensure by some other means (e.g., by some locking mechanism) that no sessions are ever used by +/// multiple threads simultaneously. This is true regardless of whether or not the Cryptoki library was initialized +/// in a fashion which permits safe multi-threaded access to it. Even if it is safe to access the library from +/// multiple threads simultaneously, it is still not necessarily safe to use a particular session from multiple +/// threads simultaneously."" +/// +/// # Terminology +/// +/// The PKCS#11 specification defines the term token as a "logical view of a cryptographic device" and slot as "a +/// logical reader that potentially contains a token". However, rather than refer to "tokens" we instead here refer to +/// the PKCS#11 server. This is because Krill uses the term token to refer to the token used to authenticate with the +/// Krill API. +/// +/// By using "server" the code is more consistent with the KmipSigner code, we avoid the overlap with the Krill meaning +/// of "token" and, while it may at first seem misleading because for example the SoftHSMv2 PKCS#11 library communicates +/// with a local process and not a remote server, this is no worse than the KMIP scenario when the "server" is actually +/// a locally running PyKMIP Python process, and in cases such as the AWS CloudHSM PKCS#11 library is actually +/// representative of the remote cloud server or server cluster nature of the backend being communicated with by the +/// PKCS#11 library. +pub mod signer; + +pub use signer::Pkcs11Signer; diff --git a/src/commons/crypto/signing/signers/pkcs11/session.rs b/src/commons/crypto/signing/signers/pkcs11/session.rs new file mode 100644 index 00000000..03da3911 --- /dev/null +++ b/src/commons/crypto/signing/signers/pkcs11/session.rs @@ -0,0 +1,100 @@ +use std::sync::{Arc, Mutex}; + +use cryptoki::error::Error as Pkcs11Error; +use cryptoki::mechanism::Mechanism; +use cryptoki::object::{Attribute, AttributeType, ObjectHandle}; +use cryptoki::session::{SessionFlags, UserType}; +use cryptoki::{session::Session, slot::Slot}; + +use crate::commons::crypto::signers::pkcs11::context::ThreadSafePkcs11Context; + +#[derive(Debug)] +pub(super) struct Pkcs11Session { + context: ThreadSafePkcs11Context, + + session_handle: Arc>, +} + +impl Pkcs11Session { + pub fn new(context: ThreadSafePkcs11Context, slot: Slot) -> Result { + // Section 11.6 "Session management functions" under "C_OpenSession" says: + // "For legacy reasons, the CKF_SERIAL_SESSION bit must always be set; if a call to C_OpenSession does not + // have this bit set, the call should return unsuccessfully with the error code + // CKR_PARALLEL_NOT_SUPPORTED." + // + // Note that we don't track whether or not the session logs in so that we can later logout because the spec + // we invoke C_CloseSession on drop and the spec for C_CloseSession says: + // "If this function is successful and it closes the last session between the application and the token, the + // login state of the token for the application returns to public sessions. Any new sessions to the token + // opened by the application will be either R/O Public or R/W Public sessions." + // + // In the spirit of not doing anything we don't have to do, we can keep the code simpler by not calling + // C_Logout because we don't have to. + let mut flags = SessionFlags::new(); + flags.set_serial_session(true); + flags.set_rw_session(true); + let session_handle = context.read().unwrap().open_session(slot, flags)?; + Ok(Pkcs11Session { + context, + session_handle: Arc::new(Mutex::new(session_handle)), + }) + } +} + +impl Pkcs11Session { + pub fn generate_key_pair( + &self, + mechanism: &Mechanism, + pub_template: &[Attribute], + priv_template: &[Attribute], + ) -> Result<(ObjectHandle, ObjectHandle), Pkcs11Error> { + self.context.read().unwrap().generate_key_pair( + self.session_handle.clone(), + mechanism, + pub_template, + priv_template, + ) + } + + pub fn get_attributes<'a>( + &self, + pub_handle: ObjectHandle, + pub_template: &[AttributeType], + ) -> Result, Pkcs11Error> { + self.context + .read() + .unwrap() + .get_attributes(self.session_handle.clone(), pub_handle, pub_template) + } + + pub fn login(&self, user_type: UserType, user_pin: Option<&str>) -> Result<(), Pkcs11Error> { + self.context + .read() + .unwrap() + .login(self.session_handle.clone(), user_type, user_pin) + } + + // Note: Cryptographic operations can fail if the key has CKA_ALWAYS_AUTHENTICATE set as that requires that we call + // C_Login immediately prior to calling C_SignInit, and we don't support that yet (would it ever make sense as this + // could for example require an operator to enter a pin code in a key pad on every signing moment?). + pub fn sign(&self, mechanism: &Mechanism, key: ObjectHandle, data: &[u8]) -> Result, Pkcs11Error> { + self.context + .read() + .unwrap() + .sign(self.session_handle.clone(), mechanism, key, data) + } + + pub fn find_objects(&self, template: &[Attribute]) -> Result, Pkcs11Error> { + self.context + .read() + .unwrap() + .find_objects(self.session_handle.clone(), template) + } + + pub fn destroy_object(&self, object_handle: ObjectHandle) -> Result<(), Pkcs11Error> { + self.context + .read() + .unwrap() + .destroy_object(self.session_handle.clone(), object_handle) + } +} diff --git a/src/commons/crypto/signing/signers/pkcs11/signer.rs b/src/commons/crypto/signing/signers/pkcs11/signer.rs new file mode 100644 index 00000000..9c28dcfc --- /dev/null +++ b/src/commons/crypto/signing/signers/pkcs11/signer.rs @@ -0,0 +1,1231 @@ +use std::{ + convert::{TryFrom, TryInto}, + marker::PhantomData, + path::Path, + sync::{Arc, RwLock, RwLockReadGuard}, + time::Duration, +}; + +use backoff::ExponentialBackoff; + +use bcder::encode::{PrimitiveContent, Values}; +use bytes::Bytes; +use cryptoki::{ + context::Info, + error::Error as Pkcs11Error, + mechanism::Mechanism, + object::{Attribute, AttributeType, ObjectClass, ObjectHandle}, + session::UserType, + slot::{Slot, SlotInfo, TokenInfo}, +}; +use rpki::repository::crypto::{ + signer::KeyError, KeyIdentifier, PublicKey, PublicKeyFormat, Signature, SignatureAlgorithm, SigningError, +}; + +use crate::commons::{ + api::Handle, + crypto::{ + dispatch::signerinfo::SignerMapper, + signers::{ + pkcs11::{ + context::{Pkcs11Context, ThreadSafePkcs11Context}, + session::Pkcs11Session, + }, + probe::{ProbeError, ProbeStatus, StatefulProbe}, + util, + }, + SignerError, + }, +}; + +//------------ Types and constants ------------------------------------------------------------------------------------ + +use serde::{de::Visitor, Deserialize}; + +#[derive(Clone, Debug, Deserialize, PartialEq)] +pub struct Pkcs11SignerConfig { + pub lib_path: String, + + pub user_pin: Option, + + #[serde(deserialize_with = "slot_id_or_label")] + pub slot: SlotIdOrLabel, + + #[serde(default = "Pkcs11SignerConfig::default_login")] + pub login: bool, + + #[serde(default = "Pkcs11SignerConfig::default_retry_seconds")] + pub retry_seconds: u64, + + #[serde(default = "Pkcs11SignerConfig::default_backoff_multiplier")] + pub backoff_multiplier: f64, + + #[serde(default = "Pkcs11SignerConfig::default_max_retry_seconds")] + pub max_retry_seconds: u64, +} + +impl Pkcs11SignerConfig { + pub fn default_login() -> bool { + true + } + + pub fn default_retry_seconds() -> u64 { + 2 + } + + pub fn default_backoff_multiplier() -> f64 { + 1.5 + } + + pub fn default_max_retry_seconds() -> u64 { + 30 + } +} + +impl Eq for Pkcs11SignerConfig {} + +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +pub enum LoginMode { + // The token can do cryptographic operations such as signing without requiring C_Login to be called first, and so a + // user pin is also not required. + LoginNotRequired, + + // The token requires that C_Login be called prior to performing any cryptographic operations such as signing. A + // correct user pin may be needed for the login to succeed. + LoginRequired, +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum SlotIdOrLabel { + Id(u64), + + Label(String), +} + +// Placeholder struct +#[derive(Clone, Debug)] +struct ConnectionSettings { + lib_path: String, + + // For some PKCS#11 libraries it is easy, or only possible, to connect by slot ID (rather than slot label). With + // others using labeled slots is easier (e.g. with SoftHSMv2 slot 0 has a seemingly random actual slot ID generated + // when the slot is initialized) or is dynamic (apparently the OpenDNSSec project has encountered this behaviour). + // When a slot label is supplied all available slots will be queried via `C_GetSlotList` and the slot id of the + // first slot with a matching label will be used to connect to the HSM. + slot: SlotIdOrLabel, + + // The user pin is optional, it may be possible to login without it. Quoting the PKCS#11 v2.20 specificiation for + // the C_Login operation: + // + // "If the token has a “protected authentication path”, as indicated by the CKF_PROTECTED_AUTHENTICATION_PATH + // flag in its CK_TOKEN_INFO being set, then that means that there is some way for a user to be authenticated to + // the token without having the application send a PIN through the Cryptoki library. One such possibility is that + // the user enters a PIN on a PINpad on the token itself, or on the slot device. Or the user might not even use a + // PIN—authentication could be achieved by some fingerprint-reading device, for example. To log into a token with + // a protected authentication path, the pPin parameter to C_Login should be NULL_PTR." + user_pin: Option, + + login_mode: LoginMode, + + retry_interval: Duration, + + backoff_multiplier: f64, + + retry_timeout: Duration, +} + +impl TryFrom<&Pkcs11SignerConfig> for ConnectionSettings { + type Error = SignerError; + + fn try_from(conf: &Pkcs11SignerConfig) -> Result { + let lib_path = conf.lib_path.clone(); + let slot = conf.slot.clone(); + let user_pin = conf.user_pin.clone(); + let login_mode = match conf.login { + true => LoginMode::LoginRequired, + false => LoginMode::LoginNotRequired, + }; + let retry_interval = Duration::from_secs(conf.retry_seconds); + let backoff_multiplier = conf.backoff_multiplier; + let retry_timeout = Duration::from_secs(conf.max_retry_seconds); + + Ok(ConnectionSettings { + lib_path, + slot, + user_pin, + login_mode, + retry_interval, + backoff_multiplier, + retry_timeout, + }) + } +} + +#[derive(Debug)] +pub struct Pkcs11Signer { + name: String, + + handle: RwLock>, + + mapper: Arc, + + /// A probe dependent interface to the PKCS#11 server. + server: Arc>, +} + +impl Pkcs11Signer { + /// Creates a new instance of Pkcs11Signer. + /// + /// Warning: invoking this function twice within the same process when testing with SoftHSM can lead to error + /// CKR_USER_ALREADY_LOGGED_IN. To avoid this tests should be run with `cargo test ... -- --test-threads=1`. + pub fn build( + name: &str, + conf: &Pkcs11SignerConfig, + probe_interval: Duration, + mapper: Arc, + ) -> Result { + // Signer initialization should not block Krill startup. As such we verify that we are able to load the PKCS#11 + // library don't we initialize the PKCS#11 interface yet because we don't know what it's code will do. If it + // were to block while trying to connect to a remote server it would block Krill from starting up completely. + // If the remote server is down and the library has logic to delay and retry, or lacks appropriate timeouts of + // connection attempts, we could get stuck for a while. Instead we defer initialization of the library until + // first use. The downside of this approach is that we won't detect any issues until that point. Another reason + // not to initialize the PKCS#11 library here is that if there are multiple instances of the Pkcs11Signer only + // the first of them should call the PKCS#11 C_Initialize() function as the PKCS#11 v2.20 specification states + // that "Note that exactly one call to C_Initialize should be made for each application (as opposed to one call + // for every thread, for example)". At least, for the same PKCS#11 library that is. If two instances of + // Pkcs11Signer each use a different PKCS#11 library, e.g. one uses the SoftHSMv2 library and the other uses the + // AWS CloudHSM library, presumably they both need initlaizing within the same instance of the Krill + // "application". + + let server = Arc::new(StatefulProbe::new( + name.to_string(), + Arc::new(conf.try_into()?), + probe_interval, + )); + + let s = Pkcs11Signer { + name: name.to_string(), + handle: RwLock::new(None), + mapper: mapper.clone(), + server, + }; + + Ok(s) + } + + pub fn get_name(&self) -> &str { + &self.name + } + + pub fn set_handle(&self, handle: crate::commons::api::Handle) { + let mut writable_handle = self.handle.write().unwrap(); + if writable_handle.is_some() { + panic!("Cannot set signer handle as handle is already set"); + } + *writable_handle = Some(handle); + } + + pub fn get_info(&self) -> Option { + if let Ok(status) = self.server.status(Self::probe_server) { + if let Ok(state) = status.state() { + return Some(state.conn_info.clone()); + } + } + None + } + + pub fn create_registration_key(&self) -> Result<(PublicKey, String), SignerError> { + let (public_key, _, _, internal_key_id) = self.build_key(PublicKeyFormat::Rsa)?; + Ok((public_key, internal_key_id)) + } + + pub fn sign_registration_challenge + ?Sized>( + &self, + key_id: &str, + challenge: &D, + ) -> Result { + let priv_handle = self + .find_key(key_id, ObjectClass::PRIVATE_KEY) + .map_err(|err| match err { + KeyError::KeyNotFound => SignerError::KeyNotFound, + KeyError::Signer(err) => err, + })?; + self.sign_with_key(priv_handle, SignatureAlgorithm::default(), challenge.as_ref()) + } +} + +//------------ Probe based server access ------------------------------------------------------------------------------ + +/// The details needed to interact with a usable PKCS#11 server. +#[derive(Debug)] +struct UsableServerState { + context: ThreadSafePkcs11Context, + + conn_info: String, + + slot_id: Slot, + + /// When login_mode is NOT LoginMode::LoginRequired this will be None. + /// + /// Section 11.6 "Session management functions" of the PKCS#11 v2.20 specification says: + /// "Call C_Login to log the user into the token. Since all sessions an application has with a token have a + /// shared login state, C_Login only needs to be called for one of the sessions." + /// + /// Therefore we hold a reference to the login session so that all future sessions are considered logged in. + /// The Drop impl for Pkcs11Session will log the session out if logged in. + login_session: Option, + + retry_interval: Duration, + + backoff_multiplier: f64, + + retry_timeout: Duration, +} + +impl UsableServerState { + pub fn new( + context: ThreadSafePkcs11Context, + conn_info: String, + slot_id: Slot, + login_session: Option, + retry_interval: Duration, + backoff_multiplier: f64, + retry_timeout: Duration, + ) -> UsableServerState { + UsableServerState { + context, + conn_info, + slot_id, + login_session, + retry_interval, + backoff_multiplier, + retry_timeout, + } + } + + pub fn get_connection(&self) -> Result { + Pkcs11Session::new(self.context.clone(), self.slot_id) + } +} + +impl Pkcs11Signer { + /// Verify if the configured server is contactable and supports the required capabilities. + fn probe_server( + name: String, + status: &ProbeStatus, + ) -> Result> { + // fn force_cache_flush(context: ThreadSafePkcs11Context) { + // // Finalize the PKCS#11 library so that we re-initialize it on next use, otherwise it just caches (at + // // least with SoftHSMv2 and YubiHSM) the token info and doesn't ever report the presence of the token + // // even when it becomes available. + // let _ = Arc::try_unwrap(context).unwrap().into_inner().unwrap().finalize(); + // } + + fn slot_label_eq(ctx: &RwLockReadGuard, slot: Slot, slot_label: &str) -> bool { + match ctx.get_token_info(slot) { + Ok(info) => String::from_utf8_lossy(&info.label).trim_end() == slot_label, + Err(err) => { + warn!( + "Failed to obtain token info for PKCS#11 slot id '{}': {}", + slot.id(), + err + ); + false + } + } + } + + fn find_slot_by_label( + readable_ctx: &RwLockReadGuard, + label: &str, + ) -> Result, Pkcs11Error> { + let possible_slot_id = readable_ctx + .get_slot_list(true)? + .into_iter() + .find(|&id| slot_label_eq(readable_ctx, id, label)); + Ok(possible_slot_id) + } + + fn initialize_if_needed( + conn_settings: &Arc, + ) -> Result { + let lib_path = Path::new(&conn_settings.lib_path); + let ctx = Pkcs11Context::get_or_load(&lib_path)?; + ctx.write().unwrap().initialize_if_not_already()?; + Ok(ctx) + } + + fn interrogate_token( + conn_settings: &Arc, + ctx: ThreadSafePkcs11Context, + name: &str, + lib_name: &String, + ) -> Result<(Info, Slot, SlotInfo, TokenInfo, Option), ProbeError> { + let readable_ctx = ctx.read().unwrap(); + + let cryptoki_info = readable_ctx.get_info().map_err(|err| { + error!( + "[{}] Unable to read PKCS#11 info for library '{}': {}", + name, lib_name, err + ); + ProbeError::CompletedUnusable + })?; + trace!("[{}] C_GetInfo(): {:?}", name, cryptoki_info); + + let slot = match &conn_settings.slot { + SlotIdOrLabel::Id(id) => { + match readable_ctx + .get_slot_list(false) + .map_err(|err| { + error!( + "[{}] Unable to get PKCS#11 slot list for library '{}': {}", + name, lib_name, err + ); + ProbeError::CompletedUnusable + })? + .into_iter() + .find(|&slot| slot.id() == *id) + { + Some(slot) => slot, + None => { + let err_msg = format!( + "[{}] No PKCS#11 slot found for library '{}' with id {}", + name, lib_name, id + ); + + error!("{}", err_msg); + return Err(ProbeError::CallbackFailed(SignerError::Pkcs11Error(err_msg))); + } + } + } + SlotIdOrLabel::Label(label) => { + // No slot id provided, look it up by its label instead + match find_slot_by_label(&readable_ctx, &label) { + Ok(Some(slot)) => slot, + Ok(None) => { + let err_msg = format!( + "[{}] No PKCS#11 slot found for library '{}' with label '{}'", + name, lib_name, label + ); + + error!("{}", err_msg); + return Err(ProbeError::CallbackFailed(SignerError::Pkcs11Error(err_msg))); + } + Err(err) => { + error!( + "[{}] Failed to enumerate PKCS#11 slots for library '{}': {}", + name, lib_name, err + ); + return Err(ProbeError::CompletedUnusable); + } + } + } + }; + + let slot_info = readable_ctx.get_slot_info(slot).map_err(|err| { + let err_msg = format!( + "[{}] Unable to read PKCS#11 slot info for library '{}' slot {}: {}", + name, lib_name, slot, err + ); + + error!("{}", err_msg); + ProbeError::CallbackFailed(SignerError::Pkcs11Error(err_msg)) + })?; + trace!("[{}] C_GetSlotInfo(): {:?}", name, slot_info); + + let token_info = readable_ctx.get_token_info(slot).map_err(|err| { + let err_msg = format!( + "[{}] Unable to read PKCS#11 token info for library '{}' slot {}: {}", + name, lib_name, slot, err + ); + + error!("{}", err_msg); + ProbeError::CallbackFailed(SignerError::Pkcs11Error(err_msg)) + })?; + trace!("[{}] C_GetTokenInfo(): {:?}", name, token_info); + + let user_pin = conn_settings.user_pin.clone(); + Ok((cryptoki_info, slot, slot_info, token_info, user_pin)) + } + + fn login( + session: Pkcs11Session, + login_mode: LoginMode, + user_pin: Option, + name: &str, + lib_name: &String, + slot: Slot, + ) -> Result, ProbeError> { + match login_mode { + LoginMode::LoginNotRequired => { + // Nothing to do + Ok(None) + } + LoginMode::LoginRequired => { + session.login(UserType::User, user_pin.as_deref()).map_err(|err| { + error!( + "[{}] Unable to login to PKCS#11 session for library '{}' slot {}: {}", + name, lib_name, slot, err + ); + ProbeError::CompletedUnusable + })?; + + trace!( + "[{}] Logged in to PKCS#11 session for library '{}' slot {}", + name, + lib_name, + slot, + ); + + Ok(Some(session)) + } + } + } + + let conn_settings = status.config()?; + let lib_name = &conn_settings.lib_path; + + debug!("[{}] Probing server using library '{}'", name, lib_name); + + let context = initialize_if_needed(&conn_settings).map_err(|err| { + error!( + "[{}] Unable to initialize PKCS#11 info for library '{}': {}", + name, lib_name, err + ); + ProbeError::CompletedUnusable + })?; + + let (cryptoki_info, slot, _slot_info, token_info, user_pin) = + interrogate_token(&conn_settings, context.clone(), &name, &lib_name).map_err(|err| { + if matches!(err, ProbeError::CallbackFailed(SignerError::Pkcs11Error(_))) { + // While the token is not available now, it might be later. + // force_cache_flush(context.clone()); + } + err + })?; + + let session = Pkcs11Session::new(context.clone(), slot).map_err(|err| { + error!( + "[{}] Unable to open PKCS#11 session for library '{}' slot {}: {}", + name, lib_name, slot, err + ); + ProbeError::CompletedUnusable + })?; + + // Note: We don't need to check for supported functions because the `pkcs11` Rust crate `fn new()` already + // requires that all of the functions that we need are supported. In fact it checks for so many functions I + // wonder if it might not fail on some customer deployments, but perhaps it checks only for functions required + // by the PKCS#11 specification...? + + // TODO: check for RSA key pair support? + + // Login if needed + let login_session = login(session, conn_settings.login_mode, user_pin, &name, &lib_name, slot)?; + + // Switch from probing the server to using it. + // ------------------------------------------- + + // Note: When Display'd via '{}' with format!() as is done below, the Rust `pkcs11` crate automatically trims + // trailing whitespace from Cryptoki padded strings such as the token info label, model and manufacturerID. + + let server_identification = format!( + "{} (Cryptoki v{})", + cryptoki_info.manufacturer_id(), + cryptoki_info.library_version() + ); + + let token_identification = format!( + "{} (model: {}, vendor: {})", + token_info.label(), + token_info.model(), + token_info.manufacturer_id() + ); + + info!( + "Using PKCS#11 token '{}' in slot {} of server '{}' via library '{}'", + token_identification, slot, server_identification, lib_name + ); + + let server_info = format!( + "PKCS#11 Signer [token: {}, slot: {}, server: {}, library: {}]", + token_identification, slot, server_identification, lib_name + ); + + let state = UsableServerState::new( + context, + server_info, + slot, + login_session, + conn_settings.retry_interval, + conn_settings.backoff_multiplier, + conn_settings.retry_timeout, + ); + + Ok(state) + } +} + +//------------ Connection related functions --------------------------------------------------------------------------- + +impl Pkcs11Signer { + /// Get a connection to the server, if the server is usable. + fn connect(&self) -> Result { + let conn = self.server.status(Self::probe_server)?.state()?.get_connection()?; + Ok(conn) + } + + /// Perform some operation using a PKCS#11 connection. + /// + /// Fails if the PKCS#11 server is not [Usable]. If the operation fails due to a transient connection error, retry + /// with backoff upto a defined retry limit. + fn with_conn(&self, desc: &str, mut do_something_with_conn: F) -> Result + where + F: FnMut(&Pkcs11Session) -> Result, + { + let signer_name = &self.name; + + // Define a notify callback to customize messages written to the logger + let notify = |err, next: Duration| { + warn!( + "[{}] {} failed, retrying in {} seconds: {}", + signer_name, + desc, + next.as_secs(), + err + ); + }; + + // Define an operation to (re)try + let op = || { + // First get a (possibly already existing) connection from the pool + let conn = self.connect().map_err(retry_on_transient_signer_error)?; + + // Next, try to execute the callers operation using the connection. If it fails, examine the cause of + // failure to determine if it should be a hard-fail (no more retries) or if we should try again. + Ok((do_something_with_conn)(&conn).map_err(retry_on_transient_pkcs11_error)?) + }; + + // Don't even bother going round the retry loop if we haven't yet successfully connected to the PKCS#11 server + // and verified its capabilities: + let status = self.server.status(Self::probe_server)?; + let state = status.state()?; + + // Define the backoff policy to use + let backoff_policy = ExponentialBackoff { + initial_interval: state.retry_interval, + multiplier: state.backoff_multiplier, + max_elapsed_time: Some(state.retry_timeout), + ..Default::default() + }; + + // Try (and retry if needed) the requested operation. + let res = backoff::retry_notify(backoff_policy, op, notify).or_else(|err| { + error!("[{}] {} failed, retries exhausted: {}", signer_name, desc, err); + Err(err) + })?; + + Ok(res) + } +} + +//------------ High level helper functions for use by the public Signer interface implementation ---------------------- + +impl Pkcs11Signer { + pub(super) fn remember_key_id( + &self, + key_id: &rpki::repository::crypto::KeyIdentifier, + internal_key_id: String, + ) -> Result<(), SignerError> { + let readable_handle = self.handle.read().unwrap(); + let signer_handle = readable_handle.as_ref().ok_or(SignerError::Other( + "PKCS#11: Failed to record signer key: Signer handle not set".to_string(), + ))?; + self.mapper + .add_key(signer_handle, key_id, &internal_key_id) + .map_err(|err| SignerError::Pkcs11Error(format!("Failed to record signer key: {}", err)))?; + + Ok(()) + } + + pub(super) fn lookup_key_id(&self, key_id: &KeyIdentifier) -> Result> { + let readable_handle = self.handle.read().unwrap(); + let signer_handle = readable_handle.as_ref().ok_or(KeyError::KeyNotFound)?; + + let internal_key_id = self + .mapper + .get_key(signer_handle, key_id) + .map_err(|_| KeyError::KeyNotFound)?; + + Ok(internal_key_id) + } + + pub(super) fn build_key( + &self, + algorithm: PublicKeyFormat, + ) -> Result<(PublicKey, ObjectHandle, ObjectHandle, String), SignerError> { + // https://tools.ietf.org/html/rfc6485#section-3: Asymmetric Key Pair Formats + // "The RSA key pairs used to compute the signatures MUST have a 2048-bit + // modulus and a public exponent (e) of 65,537." + + if !matches!(algorithm, PublicKeyFormat::Rsa) { + return Err(SignerError::Pkcs11Error(format!( + "Algorithm {:?} not supported while creating key", + &algorithm + ))); + } + + let mech = Mechanism::RsaPkcsKeyPairGen; + + let mut cka_id: [u8; 20] = [0; 20]; + openssl::rand::rand_bytes(&mut cka_id) + .map_err(|_| SignerError::Pkcs11Error("Internal error while generating a random number".to_string()))?; + + let mut pub_template: Vec = Vec::new(); + pub_template.push(Attribute::Id(cka_id.to_vec())); + pub_template.push(Attribute::Verify(true)); + pub_template.push(Attribute::Encrypt(false)); + pub_template.push(Attribute::Wrap(false)); + pub_template.push(Attribute::Token(true)); + pub_template.push(Attribute::Private(true)); + pub_template.push(Attribute::ModulusBits(2048.into())); + pub_template.push(Attribute::PublicExponent(vec![0x01, 0x00, 0x01])); + pub_template.push(Attribute::Label("Krill".to_string().into_bytes())); + + let mut priv_template: Vec = Vec::new(); + priv_template.push(Attribute::Id(cka_id.to_vec())); + priv_template.push(Attribute::Sign(true)); + priv_template.push(Attribute::Decrypt(false)); + priv_template.push(Attribute::Unwrap(false)); + priv_template.push(Attribute::Sensitive(true)); + priv_template.push(Attribute::Token(true)); + priv_template.push(Attribute::Private(true)); + priv_template.push(Attribute::Extractable(false)); + priv_template.push(Attribute::Label("Krill".to_string().into_bytes())); + + let (pub_handle, priv_handle) = self.with_conn("generate key pair", |conn| { + // The Krill functional test once failed under GitHub Actions with error: + // libsofthsm2.so::C_GenerateKeyPair() failed: PKCS#11: CKR_TEMPLATE_INCONSISTENT (0xd1) + // and with the underlying SoftHSM log containing this at the same timestamp: + // ObjectFile.cpp(124): The attribute does not exist: 0x00000002 + // and where the `pkcs11` Rust crate `types.rs` file defines that attribute as: + // pub const CKA_PRIVATE: CK_ATTRIBUTE_TYPE = 0x00000002; + // How can the CKA_PRIVATE attribute not exist? + // Is this a real issue or just a transient problem with SoftHSMv2? + conn.generate_key_pair(&mech, &pub_template, &priv_template) + })?; + + let public_key = self.get_public_key_from_handle(pub_handle)?; + + Ok((public_key, pub_handle, priv_handle, hex::encode(cka_id))) + } + + fn get_rsa_public_key_bytes(&self, pub_handle: ObjectHandle) -> Result { + let res = self.with_conn("get key pair parts", |conn| { + conn.get_attributes(pub_handle, &[AttributeType::Modulus, AttributeType::PublicExponent]) + })?; + + if res.len() == 2 { + if let (Attribute::Modulus(m), Attribute::PublicExponent(e)) = (&res[0], &res[1]) { + return util::rsa_public_key_bytes_from_parts(m, e); + } + } + + Err(SignerError::Pkcs11Error(format!( + "Unable to obtain modulus and public exponent for key {:?}", + pub_handle + ))) + } + + // TODO: This is almost identical to the equivalent fn in KmipSigner. Factor out the common code. + pub(super) fn get_public_key_from_handle(&self, pub_handle: ObjectHandle) -> Result { + let rsa_public_key_bytes = self.get_rsa_public_key_bytes(pub_handle)?; + + let subject_public_key = bcder::BitString::new(0, rsa_public_key_bytes); + + let subject_public_key_info = + bcder::encode::sequence((PublicKeyFormat::Rsa.encode(), subject_public_key.encode())); + + let mut subject_public_key_info_source: Vec = Vec::new(); + subject_public_key_info + .write_encoded(bcder::Mode::Der, &mut subject_public_key_info_source) + .map_err(|err| { + SignerError::Pkcs11Error(format!( + "Failed to create DER encoded SubjectPublicKeyInfo from constituent parts: {}", + err + )) + })?; + + let public_key = PublicKey::decode(subject_public_key_info_source.as_slice()).map_err(|err| { + SignerError::Pkcs11Error(format!( + "Failed to create public key from the DER encoded SubjectPublicKeyInfo: {}", + err + )) + })?; + + Ok(public_key) + } + + pub(super) fn sign_with_key( + &self, + private_key_handle: ObjectHandle, + algorithm: SignatureAlgorithm, + data: &[u8], + ) -> Result { + if algorithm.public_key_format() != PublicKeyFormat::Rsa { + return Err(SignerError::KmipError(format!( + "Algorithm '{:?}' not supported", + algorithm.public_key_format() + ))); + } + + let mechanism = Mechanism::Sha256RsaPkcs; + + // Note: The AWS CloudHSM Known Issues for the PKCS#11 Library states: + // https://docs.aws.amazon.com/cloudhsm/latest/userguide/ki-pkcs11-sdk.html#ki-pkcs11-7 + // + // Issue: You could not hash more than 16KB of data + // For larger buffers, only the first 16KB will be hashed and returned. The excess data would have been + // silently ignored. + // Resolution status: Data less than 16KB in size continues to be sent to the HSM for hashing. We have added + // capability to hash locally, in software, data between 16KB and 64KB in size. The client and the SDKs will + // explicitly fail if the data buffer is larger than 64KB. You must update your client and SDK(s) to version + // 1.1.1 or higher to benefit from the fix. + // + // TODO: if data is larger than 16KB we should hash locally and only use the HSM for signing, not for hashing. + // Should we enable this behaviour based on detection of an AWS CloudHSM or a config flag or ??? As an example, + // Oracle enables an AWS CloudHSM specific workaround by detecting a CLOUDHSM_IGNORE_CKA_MODIFIABLE_FALSE + // environment variable. + + let signature_data = self.with_conn("sign", |conn| conn.sign(&mechanism, private_key_handle, data))?; + + let sig = Signature::new(SignatureAlgorithm::default(), Bytes::from(signature_data)); + + Ok(sig) + } + + pub(super) fn find_key( + &self, + cka_id_hex_str: &str, + key_class: ObjectClass, + ) -> Result> { + let human_key_class = match key_class { + ObjectClass::PUBLIC_KEY => "public key", + ObjectClass::PRIVATE_KEY => "private key", + _ => "key", + }; + + let cka_id = hex::decode(cka_id_hex_str).map_err(|_| KeyError::Signer(SignerError::DecodeError))?; + + let results = self.with_conn("find key", |conn| { + // Find at most one result that matches the given key class (public or private) and the given PKCS#11 + // CKA_ID bytes. + + // A PKCS#11 session can have at most one active search operation at a time. A search must be initialized, + // results fetched, and then finalized, only then can the session perform another search. + conn.find_objects(&[Attribute::Class(key_class), Attribute::Id(cka_id.clone())]) + })?; + + match results.len() { + 0 => Err(KeyError::KeyNotFound), + 1 => Ok(results[0]), + _ => Err(KeyError::Signer(SignerError::Pkcs11Error(format!( + "More than one {} found with id {}", + &human_key_class, cka_id_hex_str + )))), + } + } + + pub(super) fn destroy_key_by_handle(&self, key_handle: ObjectHandle) -> Result<(), SignerError> { + trace!("[{}] Destroying key with PKCS#11 handle {}", self.name, key_handle); + self.with_conn("destroy", |conn| conn.destroy_object(key_handle)) + } +} + +//------------ Functions required to exist by the `SignerProvider` ---------------------------------------------------- + +// Implement the functions defined by the `Signer` trait because `SignerProvider` expects to invoke them, but as the +// dispatching is not trait based we don't actually have to implement the `Signer` trait. + +impl Pkcs11Signer { + pub fn create_key(&self, algorithm: PublicKeyFormat) -> Result { + let (key, _, _, internal_key_id) = self.build_key(algorithm)?; + let key_id = key.key_identifier(); + self.remember_key_id(&key_id, internal_key_id)?; + Ok(key_id) + } + + pub fn get_key_info(&self, key_id: &KeyIdentifier) -> Result> { + let internal_key_id = self.lookup_key_id(key_id)?; + let pub_handle = self.find_key(&internal_key_id, ObjectClass::PUBLIC_KEY)?; + self.get_public_key_from_handle(pub_handle) + .map_err(|err| KeyError::Signer(err)) + } + + pub fn destroy_key(&self, key_id: &KeyIdentifier) -> Result<(), KeyError> { + debug!("[{}] Destroying key pair with ID {}", self.name, key_id); + let internal_key_id = self.lookup_key_id(key_id)?; + let mut res: Result<(), KeyError> = Ok(()); + + // try deleting the public key + if let Ok(pub_handle) = self.find_key(&internal_key_id, ObjectClass::PUBLIC_KEY) { + res = self.destroy_key_by_handle(pub_handle).map_err(|err| match err { + SignerError::KeyNotFound => KeyError::KeyNotFound, + _ => KeyError::Signer(err), + }); + + if let Err(err) = &res { + warn!( + "[{}] Failed to destroy public key with ID {}: {}", + self.name, key_id, err + ); + } + } + + // try deleting the private key + if let Ok(priv_handle) = self.find_key(&internal_key_id, ObjectClass::PRIVATE_KEY) { + let res2 = self.destroy_key_by_handle(priv_handle).map_err(|err| match err { + SignerError::KeyNotFound => KeyError::KeyNotFound, + _ => KeyError::Signer(err), + }); + + if let Err(err) = &res2 { + warn!( + "[{}] Failed to destroy private key with ID {}: {}", + self.name, key_id, err + ); + } + + res = res.and(res2); + } + + // remove the key from the signer mapper as well + if let Some(signer_handle) = self.handle.read().unwrap().as_ref() { + let res3 = self + .mapper + .remove_key(signer_handle, key_id) + .map_err(|err| KeyError::Signer(SignerError::Other(err.to_string()))); + + if let Err(err) = &res3 { + warn!( + "[{}] Failed to remove mapping for key with ID {}: {}", + self.name, key_id, err + ); + } + + res = res.and(res3); + } + + res + } + + pub fn sign + ?Sized>( + &self, + key_id: &KeyIdentifier, + algorithm: SignatureAlgorithm, + data: &D, + ) -> Result> { + let internal_key_id = self.lookup_key_id(key_id)?; + let priv_handle = self + .find_key(&internal_key_id, ObjectClass::PRIVATE_KEY) + .map_err(|err| match err { + KeyError::KeyNotFound => SigningError::KeyNotFound, + KeyError::Signer(err) => SigningError::Signer(err), + })?; + + self.sign_with_key(priv_handle, algorithm, data.as_ref()) + .map_err(|err| SigningError::Signer(err)) + } + + pub fn sign_one_off + ?Sized>( + &self, + algorithm: SignatureAlgorithm, + data: &D, + ) -> Result<(Signature, PublicKey), SignerError> { + let (key, pub_handle, priv_handle, _) = self.build_key(PublicKeyFormat::Rsa)?; + + let signature_res = self + .sign_with_key(priv_handle, algorithm, data.as_ref()) + .map_err(|err| SignerError::Pkcs11Error(format!("One-off signing of data failed: {}", err))); + + let _ = self.destroy_key_by_handle(pub_handle); + let _ = self.destroy_key_by_handle(priv_handle); + + let signature = signature_res?; + + Ok((signature, key)) + } +} + +// -------------------------------------------------------------------------------------------------------------------- +// Retry with backoff related helper impls/fns: +// -------------------------------------------------------------------------------------------------------------------- + +fn retry_on_transient_pkcs11_error(err: Pkcs11Error) -> backoff::Error { + if is_transient_error(&err) { + backoff::Error::Transient(err.into()) + } else { + backoff::Error::Permanent(err.into()) + } +} + +fn retry_on_transient_signer_error(err: SignerError) -> backoff::Error { + match err { + SignerError::TemporarilyUnavailable => backoff::Error::Transient(err), + _ => backoff::Error::Permanent(err), + } +} + +fn is_transient_error(err: &Pkcs11Error) -> bool { + match err { + Pkcs11Error::NotSupported + | Pkcs11Error::NullFunctionPointer + | Pkcs11Error::LibraryLoading(_) + | Pkcs11Error::TryFromInt(_) + | Pkcs11Error::TryFromSlice(_) + | Pkcs11Error::NulError(_) + | Pkcs11Error::InvalidValue + | Pkcs11Error::PinNotSet => { + // The Rust `pkcs11` crate had a serious problem such as the loaded library not exporting a required + // function or that it was asked to initialize an already initialized library. + false + } + Pkcs11Error::Pkcs11(err) => { + // Error codes were taken from the `types` module of the Rust `pkcs11` crate. + // See section 11.1 of the PKCS#11 v2.20 specification for an explanation of each value. + // Return true only for errors which might succeed very soon after they failed. Errors which are solvable + // by an operator changing data or configuration in the HSM are not treated as transient errors as they + // are unlikely to be solved in the immediate future and thus there is no value in retrying. + // + // Causes of certain errors that might be worth documenting or suggesting as guidance to the user in the + // logs: + // + // - CKR_FUNCTION_FAILED: Can happen when the PKCS#11 library doesn't have access to its files, e.g. + // when `softhsm2-util --init-token` was run as root but Krill is run as a + // different user. + // - CKR_FUNCTION_FAILED: Can happen when the PKCS#11 library cannot find its configuration files, e.g. + // when the YubiHSM PKCS#11 library cannot find `yubihsm_pkcs11.conf` in the + // current directory and the environment variable `YUBIHSM_PKCS11_CONF` is not + // set and pointing to the correct location of the file or if the file is not + // readable by the Krill user. + // - CKR_TOKEN_NOT_PRESENT: Can happen when the YubiHSM PKCS#11 configuration file `connector` setting + // reers to a URL at which `yubihsm-connector -d` should be listening but the + // YubiHSM PKCS#11 library fails to connect to (either because the URL is + // incorrect or it is a HTTPS URL but there is a TLS failure such as invalid + // certificate or unknown signing CA etc) or some other issue such as a firewall + // or operating system restriction preventing access. + match err { + cryptoki::error::RvError::ActionProhibited => false, + cryptoki::error::RvError::ArgumentsBad => false, // resubmitting the same bad arguments will just fail again + cryptoki::error::RvError::AttributeReadOnly => false, // for attributes that are always read only retrying will not succeed + cryptoki::error::RvError::AttributeSensitive => false, + cryptoki::error::RvError::AttributeTypeInvalid => false, + cryptoki::error::RvError::AttributeValueInvalid => false, + cryptoki::error::RvError::BufferTooSmall => false, + cryptoki::error::RvError::Cancel => false, + cryptoki::error::RvError::CantLock => false, + cryptoki::error::RvError::CryptokiAlreadyInitialized => false, + cryptoki::error::RvError::CryptokiNotInitialized => false, + cryptoki::error::RvError::CurveNotSupported => false, + cryptoki::error::RvError::DataInvalid => false, + cryptoki::error::RvError::DataLenRange => false, + cryptoki::error::RvError::DeviceError => true, // some error but we don't know what so could be transient + cryptoki::error::RvError::DeviceMemory => true, // maybe the token frees up some memory such that a retry succeeds? + cryptoki::error::RvError::DeviceRemoved => true, // not present at the time the function was executed but might be later + cryptoki::error::RvError::DomainParamsInvalid => false, + cryptoki::error::RvError::EncryptedDataInvalid => false, + cryptoki::error::RvError::EncryptedDataLenRange => false, + cryptoki::error::RvError::ExceededMaxIterations => false, + cryptoki::error::RvError::FipsSelfTestFailed => false, + cryptoki::error::RvError::FunctionCanceled => false, + cryptoki::error::RvError::FunctionFailed => true, // the spec says the situation is not necessarily totally hopeless + cryptoki::error::RvError::FunctionNotParallel => false, + cryptoki::error::RvError::FunctionNotSupported => false, + cryptoki::error::RvError::FunctionRejected => false, + cryptoki::error::RvError::GeneralError => false, + cryptoki::error::RvError::HostMemory => true, + cryptoki::error::RvError::InformationSensitive => false, + cryptoki::error::RvError::KeyChanged => false, + cryptoki::error::RvError::KeyFunctionNotPermitted => false, + cryptoki::error::RvError::KeyHandleInvalid => false, + cryptoki::error::RvError::KeyIndigestible => false, + cryptoki::error::RvError::KeyNeeded => false, + cryptoki::error::RvError::KeyNotNeeded => false, + cryptoki::error::RvError::KeyNotWrappable => false, + cryptoki::error::RvError::KeySizeRange => false, + cryptoki::error::RvError::KeyTypeInconsistent => false, + cryptoki::error::RvError::KeyUnextractable => false, + cryptoki::error::RvError::LibraryLoadFailed => false, + cryptoki::error::RvError::MechanismInvalid => false, + cryptoki::error::RvError::MechanismParamInvalid => false, + cryptoki::error::RvError::MutexBad => false, // should never happen so consider it fatal? + cryptoki::error::RvError::MutexNotLocked => false, // should never happen so consider it fatal? + cryptoki::error::RvError::NeedToCreateThreads => false, + cryptoki::error::RvError::NewPinMode => false, + cryptoki::error::RvError::NextOtp => false, + cryptoki::error::RvError::NoEvent => false, + cryptoki::error::RvError::ObjectHandleInvalid => false, + cryptoki::error::RvError::OperationActive => true, // the active operation might finish thereby permitting a retry to succeed + cryptoki::error::RvError::OperationNotInitialized => false, + cryptoki::error::RvError::PinExpired => false, + cryptoki::error::RvError::PinIncorrect => false, + cryptoki::error::RvError::PinInvalid => false, + cryptoki::error::RvError::PinLenRange => false, + cryptoki::error::RvError::PinLocked => false, + cryptoki::error::RvError::PinTooWeak => false, + cryptoki::error::RvError::PublicKeyInvalid => false, + cryptoki::error::RvError::RandomNoRng => false, + cryptoki::error::RvError::RandomSeedNotSupported => false, + cryptoki::error::RvError::SavedStateInvalid => false, + cryptoki::error::RvError::SessionClosed => true, // maybe on retry we open a new session and succeed? + cryptoki::error::RvError::SessionCount => true, // if a session closes it might be possible on retry for a session open to succeed + cryptoki::error::RvError::SessionExists => false, + cryptoki::error::RvError::SessionHandleInvalid => false, + cryptoki::error::RvError::SessionParallelNotSupported => false, + cryptoki::error::RvError::SessionReadOnly => false, + cryptoki::error::RvError::SessionReadOnlyExists => true, // will succeed on retry if the conflicting SO session logs out + cryptoki::error::RvError::SessionReadWriteSoExists => true, // will succeed on retry if the conflicting SO session logs out + cryptoki::error::RvError::SignatureInvalid => false, + cryptoki::error::RvError::SignatureLenRange => false, + cryptoki::error::RvError::SlotIdInvalid => true, // maybe we tried accessing the slot just before it is created? + cryptoki::error::RvError::StateUnsaveable => true, // the spec doesn't seem to rule out this being a temporary condition + cryptoki::error::RvError::TemplateIncomplete => false, + cryptoki::error::RvError::TemplateInconsistent => false, + cryptoki::error::RvError::TokenNotPresent => true, // not present at the time the function was executed but might be later + cryptoki::error::RvError::TokenNotRecognized => false, + cryptoki::error::RvError::TokenWriteProtected => true, // maybe the right protection is a transient condition? + cryptoki::error::RvError::UnwrappingKeyHandleInvalid => false, + cryptoki::error::RvError::UnwrappingKeySizeRange => false, + cryptoki::error::RvError::UnwrappingKeyTypeInconsistent => false, + cryptoki::error::RvError::UserAlreadyLoggedIn => true, // maybe another client was is busy logging out so try again? + cryptoki::error::RvError::UserAnotherAlreadyLoggedIn => true, + cryptoki::error::RvError::UserNotLoggedIn => false, + cryptoki::error::RvError::UserPinNotInitialized => false, + cryptoki::error::RvError::UserTooManyTypes => true, // maybe some sessions are terminated while retrying permitting us to succeed? + cryptoki::error::RvError::UserTypeInvalid => false, + cryptoki::error::RvError::VendorDefined => false, + cryptoki::error::RvError::WrappedKeyInvalid => false, + cryptoki::error::RvError::WrappedKeyLenRange => false, + cryptoki::error::RvError::WrappingKeyHandleInvalid => false, + cryptoki::error::RvError::WrappingKeySizeRange => false, + cryptoki::error::RvError::WrappingKeyTypeInconsistent => false, + } + } + } +} + +impl From for SignerError { + fn from(err: Pkcs11Error) -> Self { + if is_transient_error(&err) { + error!("PKCS#11 signer unavailable: {}", err); + SignerError::TemporarilyUnavailable + } else { + SignerError::Pkcs11Error(err.to_string()) + } + } +} + +impl From> for SignerError { + fn from(err: ProbeError) -> Self { + match err { + ProbeError::WrongState => { + SignerError::Other("Internal error: probe is not in the expected state".to_string()) + } + ProbeError::AwaitingNextProbe => SignerError::TemporarilyUnavailable, + ProbeError::CompletedUnusable => SignerError::PermanentlyUnusable, + ProbeError::CallbackFailed(err) => err, + } + } +} + +impl From for ProbeError { + fn from(err: SignerError) -> Self { + ProbeError::CallbackFailed(err) + } +} + +macro_rules! integer_to_slot_id { + ($deserialize:ident, $type:ident) => { + fn $deserialize(self, v: $type) -> Result + where + E: serde::de::Error, + { + Ok(SlotIdOrLabel::Id(u64::try_from(v).map_err(|_| { + serde::de::Error::custom("not a valid PKCS#11 slot ID") + })?)) + } + }; +} + +// Based on https://serde.rs/string-or-struct.html +fn slot_id_or_label<'de, D>(deserializer: D) -> Result +where + D: serde::Deserializer<'de>, +{ + struct UintOrString(PhantomData SlotIdOrLabel>); + + impl<'de> Visitor<'de> for UintOrString { + type Value = SlotIdOrLabel; + + fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { + formatter.write_str("PKCS#11 unsigned integer slot ID or string label") + } + + fn visit_str(self, value: &str) -> Result + where + E: serde::de::Error, + { + Ok(SlotIdOrLabel::Label(value.to_string())) + } + + integer_to_slot_id!(visit_u8, u8); + integer_to_slot_id!(visit_u16, u16); + integer_to_slot_id!(visit_u32, u32); + integer_to_slot_id!(visit_u64, u64); + integer_to_slot_id!(visit_i8, i8); + integer_to_slot_id!(visit_i16, i16); + integer_to_slot_id!(visit_i32, i32); + integer_to_slot_id!(visit_i64, i64); + } + + deserializer.deserialize_any(UintOrString(PhantomData)) +} + +#[cfg(test)] +mod tes { + use super::*; + + #[test] + fn configure_using_slot_id() { + let config_str = r#" + lib_path = "dummy path" + slot = 1234 + "#; + let config: Pkcs11SignerConfig = toml::from_str(&config_str).unwrap(); + assert!(matches!(config.slot, SlotIdOrLabel::Id(1234))); + } + + #[test] + fn configure_using_slot_label() { + let config_str = r#" + lib_path = "dummy path" + slot = "well well well" + "#; + let config: Pkcs11SignerConfig = toml::from_str(&config_str).unwrap(); + let expected_label = "well well well".to_string(); + assert!(matches!(config.slot, SlotIdOrLabel::Label(label) if label == expected_label)); + } + + #[test] + fn configure_using_negative_slot_id() { + let config_str = r#" + lib_path = "dummy path" + slot = -1234 + "#; + let err = toml::from_str::(&config_str).unwrap_err(); + assert_eq!( + err.to_string(), + "not a valid PKCS#11 slot ID for key `slot` at line 3 column 20" + ) + } +} diff --git a/src/commons/crypto/signing/signers/probe.rs b/src/commons/crypto/signing/signers/probe.rs new file mode 100644 index 00000000..71ce98ee --- /dev/null +++ b/src/commons/crypto/signing/signers/probe.rs @@ -0,0 +1,363 @@ +use std::{ + marker::PhantomData, + sync::{Arc, RwLock, RwLockReadGuard}, + time::{Duration, Instant}, +}; + +#[derive(Debug)] +pub enum ProbeError { + WrongState, + AwaitingNextProbe, + CompletedUnusable, + CallbackFailed(E), +} + +/// Probe status based access to the PKCS#11 server. +/// +/// To avoid blocking Krill startup due to HSM connection timeout or failure we start in a `AwaitingNextProbe` status which +/// signifies that we haven't yet verified that we can connect to the HSM or that it supports the capabilities that we +/// require. +/// +/// At some point later once an initial connection has been established the PKCS#11 signer changes status to either +/// `Usable` or `Unusable` based on what was discovered about the PKCS#11 server. +#[derive(Debug)] +pub struct StatefulProbe { + name: String, + + status: RwLock>, + + probe_interval: Duration, +} + +pub enum ProbeStatus { + /// We haven't yet been able to connect to the HSM. If there was already a failed attempt to connect the timestamp + /// of the attempt is remembered so that we can choose to space out connection attempts rather than attempt to + /// connect every time Krill tries to use the signer. + Probing { + config: Arc, + last_probe_time: Option, + phantom: PhantomData, + }, + + /// The HSM was successfully probed but found to be lacking required capabilities and is thus unusable by Krill. + Unusable, + + /// The HSM was successfully probed and confirmed to have the required capabilities. + /// + /// Note that this does not mean that the HSM is currently contactable, only that we were able to contact it at + /// least once since Krill was started. If the domain name/IP address used to connect to Krill now point to a + /// different HSM instance the previously determined conclusion that the HSM is usable may no longer be valid. + /// + /// In this status we keep state concerning our relationship with the HSM. + Usable(S), +} + +impl std::fmt::Debug for ProbeStatus { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::Probing { .. } => write!(f, "Probing"), + Self::Unusable => write!(f, "Unusable"), + Self::Usable(_) => write!(f, "Usable"), + } + } +} + +impl ProbeStatus { + /// Marks now as the last probe attempt timestamp. + /// + /// Calling this function while not in the Probing state will result in a panic. + pub fn mark(&mut self) -> Result<(), ProbeError> { + match self { + #[rustfmt::skip] + ProbeStatus::Probing { last_probe_time, .. } => { + last_probe_time.replace(Instant::now()); + Ok(()) + } + _ => Err(ProbeError::WrongState), + } + } + + pub fn config(&self) -> Result, ProbeError> { + match self { + ProbeStatus::Probing { config, .. } => Ok(config.clone()), + _ => Err(ProbeError::WrongState), + } + } + + #[cfg(test)] + pub fn last_probe_time(&self) -> Result, ProbeError> { + match self { + ProbeStatus::Probing { last_probe_time, .. } => Ok(last_probe_time.clone()), + _ => Err(ProbeError::WrongState), + } + } + + /// Helper function to retrieve the state associated with status Usable. Only callable when in status `Usable`. + /// Calling this function while in another state will result in a panic. + pub fn state(&self) -> Result<&S, ProbeError> { + match self { + ProbeStatus::Usable(state) => Ok(&state), + _ => Err(ProbeError::WrongState), + } + } +} + +impl StatefulProbe { + /// Create a new connector to a server that hasn't been probed yet. + pub fn new(name: String, config: Arc, probe_interval: Duration) -> Self { + let status = RwLock::new(ProbeStatus::Probing { + config, + last_probe_time: None, + phantom: PhantomData, + }); + StatefulProbe { + name, + status, + probe_interval, + } + } + + #[cfg(test)] + pub fn last_probe_time(&self) -> Result, ProbeError> { + self.status.read().unwrap().last_probe_time() + } + + /// Get a read lock on the Usable server status, if the server is usable. + /// + /// Returns `Ok` with the status read lock if the server is usable, otherwise returns an `Err` because the + /// server is unusable or we haven't yet been able to establish if it is usable or not. + /// + /// Will try probing again if we didn't already manage to connect to the server and the delay period between probes + /// has elapsed. + pub fn status(&self, probe: F) -> Result>, ProbeError> + where + F: Fn(String, &ProbeStatus) -> Result>, + { + fn is_time_to_check(time_between_probes: Duration, last_probe_time: Option) -> bool { + match last_probe_time { + None => true, + Some(instant) => Instant::now().saturating_duration_since(instant) > time_between_probes, + } + } + + fn get_if_usable( + name: String, + status: RwLockReadGuard>, + retry_interval: Duration, + ) -> Option>, ProbeError>> { + // Check the status through the unlocked read lock + match &*status { + ProbeStatus::Usable(_) => { + // The server has been confirmed as usable, return the read-lock granting access to the current + // status and via it the current state of our relationship with the server. + Some(Ok(status)) + } + + ProbeStatus::Unusable => { + // The server has been confirmed as unusable, fail. + Some(Err(ProbeError::CompletedUnusable)) + } + + ProbeStatus::Probing { last_probe_time, .. } => { + // We haven't yet established whether the server is usable or not. If we haven't yet checked or we + // haven't tried checking again for a while, then try contacting it again. If we can't establish + // whether or not the server is usable, return an error. + if !is_time_to_check(retry_interval, *last_probe_time) { + if let Some(instant) = last_probe_time { + let until = retry_interval.checked_sub(instant.elapsed()).unwrap(); // TODO + info!( + "Signer '{}' is currently unavailable. {:?}s until next check.", + name, + until.as_secs() + ); + } else { + // This should be unreachable + info!( + "Signer '{}' is currently unavailable. Checking every {:?}s", + name, + retry_interval.as_secs() + ); + } + Some(Err(ProbeError::AwaitingNextProbe)) + } else { + None + } + } + } + } + + /// Verify if the configured server is contactable and supports the required capabilities. + fn send_probe(probe: &StatefulProbe, probe_cb: F) -> Result<(), ProbeError> + where + F: Fn(String, &ProbeStatus) -> Result>, + { + // Hold a write lock for the duration of our attempt to verify the server so that no other attempt occurs + // at the same time. Bail out if another thread is performing a probe and has the lock. This is the same result + // as when attempting to use the server between probe retries. + let mut status = probe.status.try_write().map_err(|_| ProbeError::AwaitingNextProbe)?; + + // Update the timestamp of our last attempt to contact the server. This is used above to know when we have + // waited long enough before attempting to contact the server again. This also guards against attempts to probe + // when probing has already finished as mark() will fail in that case. + status.mark()?; + + match (probe_cb)(probe.name.clone(), &*status) { + Ok(usable_state) => { + *status = ProbeStatus::Usable(usable_state); + Ok(()) + } + Err(err) => { + if matches!(err, ProbeError::CompletedUnusable) { + *status = ProbeStatus::Unusable; + } + Err(err) + } + } + } + + // Return the current status or attempt to set it by probing the server + let status = self.status.read().unwrap(); + get_if_usable(self.name.clone(), status, self.probe_interval).unwrap_or_else(|| { + send_probe(self, probe) + .and_then(|_| Ok(self.status.read().unwrap())) + .map_err(|err| match err { + ProbeError::CompletedUnusable => err, + _ => ProbeError::AwaitingNextProbe, + }) + }) + } +} + +#[cfg(test)] +pub mod tests { + use std::time::Duration; + + use super::*; + + #[derive(Debug, Default)] + struct Config { + hostname: String, + + port: u64, + } + + #[derive(Copy, Clone, Debug, Default)] + struct State { + some_state: u8, + } + + impl State { + fn some_func(&self) -> u8 { + self.some_state + } + } + + #[derive(Debug)] + enum SomeError { + SomeErrorCode, + } + + fn probe_func( + _name: String, + _status: &ProbeStatus, + ) -> Result> { + Err(ProbeError::CompletedUnusable) + } + + #[test] + fn probe_should_be_permanently_unavailable_with_closure() { + let config = Arc::new(Config::default()); + let conn = StatefulProbe::<_, SomeError, State>::new("dummy".to_string(), config, Duration::from_secs(0)); + let res = conn.status(|_, _| Err(ProbeError::CompletedUnusable)); + match res { + Err(ProbeError::CompletedUnusable) => {} + other => panic!("Expected Err(ProbeError::PermanentlyUnusable) but got {:?}", other), + } + } + + #[test] + fn probe_should_be_permanently_unavailable_with_fn() { + let config = Arc::new(Config::default()); + let conn = StatefulProbe::<_, SomeError, State>::new("dummy".to_string(), config, Duration::from_secs(0)); + let res = conn.status(probe_func); + match res { + Err(ProbeError::CompletedUnusable) => {} + other => panic!("Expected Err(ProbeError::PermanentlyUnusable) but got {:?}", other), + } + } + + #[test] + fn probe_should_be_permanently_unavailable() { + let config = Arc::new(Config::default()); + let conn = StatefulProbe::<_, SomeError, State>::new("dummy".to_string(), config, Duration::from_secs(0)); + let res = conn.status(|_, _| Err(ProbeError::CompletedUnusable)); + match res { + Err(ProbeError::CompletedUnusable) => {} + other => panic!("Expected Err(ProbeError::PermanentlyUnusable) but got {:?}", other), + } + } + + #[test] + fn probe_should_be_temporarily_unavailable() { + let config = Arc::new(Config::default()); + let conn = StatefulProbe::<_, SomeError, State>::new("dummy".to_string(), config, Duration::from_secs(0)); + let res = conn.status(|_, _| Err(ProbeError::AwaitingNextProbe)); + match res { + Err(ProbeError::AwaitingNextProbe) => {} + other => panic!("Expected Err(ProbeError::AwaitingNextProbe) but got {:?}", other), + } + } + + #[test] + fn probe_should_be_temporarily_unavailable_on_custom_error() { + let config = Arc::new(Config::default()); + let conn = StatefulProbe::<_, SomeError, State>::new("dummy".to_string(), config, Duration::from_secs(0)); + let res = conn.status(|_, _| Err(ProbeError::CallbackFailed(SomeError::SomeErrorCode))); + match res { + Err(ProbeError::AwaitingNextProbe) => {} + other => panic!("Expected Err(ProbeError::AwaitingNextProbe) but got {:?}", other), + } + } + + #[test] + fn last_probe_time_should_advance() -> Result<(), ProbeError> { + let config = Arc::new(Config::default()); + + // Probing is only done when .get() is called + let conn = StatefulProbe::<_, SomeError, State>::new("dummy".to_string(), config, Duration::from_millis(100)); + assert_eq!(None, conn.last_probe_time()?); + + // The first call to .get() should trigger a probe + let _ = conn.status(|_, _| Err(ProbeError::AwaitingNextProbe)); + let t1 = conn.last_probe_time()?; + assert!(t1.is_some()); + + // A call to .get() before the next probe interval should NOT result in an updated last probe time + std::thread::sleep(Duration::from_millis(10)); + let _ = conn.status(|_, _| Err(ProbeError::AwaitingNextProbe)); + let t2 = conn.last_probe_time()?; + assert!(t2 == t1); + + // A call to .get() after the next probe interval SHOULD result in an updated last probe time + std::thread::sleep(Duration::from_millis(200)); + let _ = conn.status(|_, _| Err(ProbeError::AwaitingNextProbe)); + let t3 = conn.last_probe_time()?; + assert!(t3 > t1); + + Ok(()) + } + + #[test] + fn probe_should_change_state_when_usable() -> Result<(), ProbeError> { + let config = Arc::new(Config::default()); + let new_state = State { some_state: 1 }; + + // Probing only happens when .get() is called + let conn = StatefulProbe::<_, SomeError, State>::new("dummy".to_string(), config, Duration::from_millis(0)); + let new_status = conn.status(|_, _| Ok(new_state))?; + assert_eq!(1, new_status.state()?.some_state); + assert_eq!(1, new_status.state()?.some_func()); + + Ok(()) + } +} diff --git a/src/commons/util/softsigner.rs b/src/commons/crypto/signing/signers/softsigner.rs similarity index 57% rename from src/commons/util/softsigner.rs rename to src/commons/crypto/signing/signers/softsigner.rs index 6f7ce050..77f9d3cc 100644 --- a/src/commons/util/softsigner.rs +++ b/src/commons/crypto/signing/signers/softsigner.rs @@ -1,39 +1,128 @@ //! Support for signing things using software keys (through openssl) and //! storing them unencrypted on disk. use std::{ - fmt, fs, + fs, fs::File, io::Write, path::{Path, PathBuf}, + str::FromStr, sync::Arc, + sync::RwLock, }; use bytes::Bytes; use serde::{de, ser, Deserialize, Deserializer, Serialize, Serializer}; use openssl::{ - error::ErrorStack, hash::MessageDigest, pkey::{PKey, PKeyRef, Private}, rsa::Rsa, }; use rpki::repository::crypto::{ - signer::KeyError, KeyIdentifier, PublicKey, PublicKeyFormat, Signature, SignatureAlgorithm, Signer, SigningError, + signer::KeyError, KeyIdentifier, PublicKey, PublicKeyFormat, Signature, SignatureAlgorithm, SigningError, }; -use crate::commons::error::KrillIoError; +use crate::{ + commons::{ + api::Handle, + crypto::{dispatch::signerinfo::SignerMapper, signers::error::SignerError}, + error::KrillIoError, + }, + constants::KEYS_DIR, +}; //------------ OpenSslSigner ------------------------------------------------- +#[derive(Clone, Debug, Default, Deserialize, Hash, PartialEq, Eq)] +pub struct OpenSslSignerConfig { + #[serde(default)] + pub keys_path: Option, +} + +impl OpenSslSignerConfig { + pub fn new(path: &Path) -> Self { + Self { + keys_path: Some(path.into()), + } + } +} + /// An openssl based signer. -#[derive(Clone, Debug)] +#[derive(Debug)] pub struct OpenSslSigner { keys_dir: Arc, + + name: String, + + handle: RwLock>, + + info: Option, + + mapper: Option>, } impl OpenSslSigner { - pub fn build(work_dir: &Path) -> Result { + /// The OpenSslSigner can be used with or without a SignerMapper. Without a SignerMapper a caller that needs to + /// dispatch requests to the Signer that owns a given KeyIdentifier will be unable to do so as the SignerMapper + /// only knows about keys created by the OpenSslSigner if the OpenSslSigner registers the new keys in the mapper. + pub fn build(work_dir: &Path, name: &str, mapper: Option>) -> Result { + let keys_dir = Self::init_keys_dir(work_dir)?; + + let s = OpenSslSigner { + name: name.to_string(), + info: Some(format!( + "OpenSSL Soft Signer [version: {}, keys dir: {}]", + openssl::version::version(), + keys_dir.as_path().display() + )), + handle: RwLock::new(None), // will be set later + mapper: mapper.clone(), + keys_dir: keys_dir.into(), + }; + + Ok(s) + } + + pub fn get_name(&self) -> &str { + &self.name + } + + pub fn set_handle(&self, handle: Handle) { + let mut writable_handle = self.handle.write().unwrap(); + if writable_handle.is_some() { + panic!("Cannot set signer handle as handle is already set"); + } + *writable_handle = Some(handle); + } + + pub fn get_info(&self) -> Option { + self.info.clone() + } + + pub fn create_registration_key(&self) -> Result<(PublicKey, String), SignerError> { + // For the OpenSslSigner we use the KeyIdentifier as the internal key id so the two are the same. + let key_id = self.build_key()?; + let internal_key_id = key_id.to_string(); + let key_pair = self.load_key(&key_id)?; + let public_key = key_pair.subject_public_key_info()?; + Ok((public_key, internal_key_id)) + } + + pub fn sign_registration_challenge + ?Sized>( + &self, + signer_private_key_id: &str, + challenge: &D, + ) -> Result { + let key_id = KeyIdentifier::from_str(signer_private_key_id).map_err(|_| SignerError::KeyNotFound)?; + let key_pair = self.load_key(&key_id)?; + let signature = Self::sign_with_key(key_pair.pkey.as_ref(), challenge)?; + Ok(signature) + } +} + +impl OpenSslSigner { + fn init_keys_dir(work_dir: &Path) -> Result { let meta_data = fs::metadata(&work_dir).map_err(|e| { KrillIoError::new( format!("Could not get metadata from '{}'", work_dir.to_string_lossy()), @@ -42,7 +131,7 @@ impl OpenSslSigner { })?; if meta_data.is_dir() { let mut keys_dir = work_dir.to_path_buf(); - keys_dir.push("keys"); + keys_dir.push(KEYS_DIR); if !keys_dir.is_dir() { fs::create_dir_all(&keys_dir).map_err(|e| { KrillIoError::new( @@ -54,17 +143,29 @@ impl OpenSslSigner { ) })?; } - - Ok(OpenSslSigner { - keys_dir: keys_dir.into(), - }) + Ok(keys_dir) } else { Err(SignerError::InvalidWorkDir(work_dir.to_path_buf())) } } -} -impl OpenSslSigner { + fn build_key(&self) -> Result { + let kp = OpenSslKeyPair::build()?; + + let pk = &kp.subject_public_key_info()?; + let key_id = pk.key_identifier(); + + let path = self.key_path(&key_id); + let json = serde_json::to_string(&kp)?; + + let mut f = File::create(&path) + .map_err(|e| KrillIoError::new(format!("Could not create key file '{}'", path.to_string_lossy()), e))?; + f.write_all(json.as_ref()) + .map_err(|e| KrillIoError::new(format!("Could write to key file '{}'", path.to_string_lossy()), e))?; + + Ok(key_id) + } + fn sign_with_key + ?Sized>(pkey: &PKeyRef, data: &D) -> Result { let mut signer = ::openssl::sign::Signer::new(MessageDigest::sha256(), pkey)?; signer.update(data.as_ref())?; @@ -91,35 +192,41 @@ impl OpenSslSigner { path.push(&key_id.to_string()); path } + + fn remember_key_id(&self, key_id: &KeyIdentifier) -> Result<(), SignerError> { + // When testing the OpenSSlSigner in isolation there is no need for a mapper as we don't need to determine + // which signer to use for a particular KeyIdentifier as there is only one signer, and the OpenSslSigner + // doesn't need a mapper to map from KeyIdentifier to internal key id as the internal key id IS the + // KeyIdentifier. + if let Some(mapper) = &self.mapper { + let readable_handle = self.handle.read().unwrap(); + let signer_handle = readable_handle.as_ref().ok_or(SignerError::Other( + "OpenSSL: Failed to record signer key: Signer handle not set".to_string(), + ))?; + mapper + .add_key(signer_handle, key_id, &format!("{}", key_id)) + .map_err(|err| SignerError::Other(format!("Failed to record signer key: {}", err))) + } else { + Ok(()) + } + } } -impl Signer for OpenSslSigner { - type KeyId = KeyIdentifier; - type Error = SignerError; - - fn create_key(&self, _algorithm: PublicKeyFormat) -> Result { - let kp = OpenSslKeyPair::build()?; - - let pk = &kp.subject_public_key_info()?; - let key_id = pk.key_identifier(); - - let path = self.key_path(&key_id); - let json = serde_json::to_string(&kp)?; - - let mut f = File::create(&path) - .map_err(|e| KrillIoError::new(format!("Could not create key file '{}'", path.to_string_lossy()), e))?; - f.write_all(json.as_ref()) - .map_err(|e| KrillIoError::new(format!("Could write to key file '{}'", path.to_string_lossy()), e))?; - +// Implement the functions defined by the `Signer` trait because `SignerProvider` expects to invoke them, but as the +// dispatching is not trait based we don't actually have to implement the `Signer` trait. +impl OpenSslSigner { + pub fn create_key(&self, _algorithm: PublicKeyFormat) -> Result { + let key_id = self.build_key()?; + self.remember_key_id(&key_id)?; Ok(key_id) } - fn get_key_info(&self, key_id: &Self::KeyId) -> Result> { + pub fn get_key_info(&self, key_id: &KeyIdentifier) -> Result> { let key_pair = self.load_key(key_id)?; Ok(key_pair.subject_public_key_info()?) } - fn destroy_key(&self, key_id: &Self::KeyId) -> Result<(), KeyError> { + pub fn destroy_key(&self, key_id: &KeyIdentifier) -> Result<(), KeyError> { let path = self.key_path(key_id); if path.exists() { fs::remove_file(&path).map_err(|e| { @@ -132,17 +239,17 @@ impl Signer for OpenSslSigner { Ok(()) } - fn sign + ?Sized>( + pub fn sign + ?Sized>( &self, - key_id: &Self::KeyId, + key_id: &KeyIdentifier, _algorithm: SignatureAlgorithm, data: &D, - ) -> Result> { + ) -> Result> { let key_pair = self.load_key(key_id)?; Self::sign_with_key(key_pair.pkey.as_ref(), data).map_err(SigningError::Signer) } - fn sign_one_off + ?Sized>( + pub fn sign_one_off + ?Sized>( &self, _algorithm: SignatureAlgorithm, data: &D, @@ -155,10 +262,6 @@ impl Signer for OpenSslSigner { Ok((signature, key)) } - - fn rand(&self, target: &mut [u8]) -> Result<(), SignerError> { - openssl::rand::rand_bytes(target).map_err(SignerError::OpenSslError) - } } //------------ OpenSslKeyPair ------------------------------------------------ @@ -214,49 +317,6 @@ impl OpenSslKeyPair { } } -//------------ OpenSslKeyError ----------------------------------------------- - -#[derive(Debug)] -pub enum SignerError { - OpenSslError(ErrorStack), - JsonError(serde_json::Error), - InvalidWorkDir(PathBuf), - IoError(KrillIoError), - KeyNotFound, - DecodeError, -} - -impl fmt::Display for SignerError { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - match self { - SignerError::OpenSslError(e) => write!(f, "OpenSsl Error: {}", e), - SignerError::JsonError(e) => write!(f, "Could not decode public key info: {}", e), - SignerError::InvalidWorkDir(path) => write!(f, "Invalid base path: {}", path.to_string_lossy()), - SignerError::IoError(e) => e.fmt(f), - SignerError::KeyNotFound => write!(f, "Could not find key"), - SignerError::DecodeError => write!(f, "Could not decode key"), - } - } -} - -impl From for SignerError { - fn from(e: ErrorStack) -> Self { - SignerError::OpenSslError(e) - } -} - -impl From for SignerError { - fn from(e: serde_json::Error) -> Self { - SignerError::JsonError(e) - } -} - -impl From for SignerError { - fn from(e: KrillIoError) -> Self { - SignerError::IoError(e) - } -} - //------------ Tests --------------------------------------------------------- #[cfg(test)] @@ -268,7 +328,7 @@ pub mod tests { #[test] fn should_return_subject_public_key_info() { test::test_under_tmp(|d| { - let s = OpenSslSigner::build(&d).unwrap(); + let s = OpenSslSigner::build(&d, "dummy", None).unwrap(); let ki = s.create_key(PublicKeyFormat::Rsa).unwrap(); s.get_key_info(&ki).unwrap(); s.destroy_key(&ki).unwrap(); diff --git a/src/commons/crypto/signing/signers/util.rs b/src/commons/crypto/signing/signers/util.rs new file mode 100644 index 00000000..56534cde --- /dev/null +++ b/src/commons/crypto/signing/signers/util.rs @@ -0,0 +1,17 @@ +use bcder::encode::{PrimitiveContent, Values}; + +use crate::commons::crypto::signers::error::SignerError; + +/// Helper function to create X.509 RSA Public Key bytes from a given RSA modulus and exponent. +pub fn rsa_public_key_bytes_from_parts(modulus: &[u8], public_exponent: &[u8]) -> Result { + let modulus = bcder::Unsigned::from_slice(modulus).map_err(|_| SignerError::DecodeError)?; + let public_exp = bcder::Unsigned::from_slice(public_exponent).map_err(|_| SignerError::DecodeError)?; + let rsa_public_key = bcder::encode::sequence((modulus.encode(), public_exp.encode())); + + let mut bytes: Vec = Vec::new(); + rsa_public_key + .write_encoded(bcder::Mode::Der, &mut bytes) + .map_err(|_| SignerError::DecodeError)?; + + Ok(bytes::Bytes::from(bytes)) +} diff --git a/src/commons/error.rs b/src/commons/error.rs index c378379b..8c36949d 100644 --- a/src/commons/error.rs +++ b/src/commons/error.rs @@ -15,12 +15,13 @@ use crate::{ rrdp::PublicationDeltaError, AspaCustomer, AspaProvidersUpdateConflict, ChildHandle, ErrorResponse, Handle, ParentHandle, PublisherHandle, ResourceClassName, ResourceSetError, RoaDefinition, }, + crypto::SignerError, eventsourcing::{AggregateStoreError, KeyValueError}, remote::{ rfc6492::{self, NotPerformedResponse}, rfc8181::{self, ReportErrorCode}, }, - util::{httpclient, softsigner::SignerError}, + util::httpclient, }, daemon::{ca::RouteAuthorization, http::tls_keys}, upgrades::UpgradeError, diff --git a/src/commons/util/dummysigner.rs b/src/commons/util/dummysigner.rs deleted file mode 100644 index c9fe53d4..00000000 --- a/src/commons/util/dummysigner.rs +++ /dev/null @@ -1,48 +0,0 @@ -use rpki::repository::crypto::{KeyIdentifier, PublicKey, PublicKeyFormat, Signature, SignatureAlgorithm, Signer}; - -use super::softsigner::SignerError; - -/// A dummy signer to prove that compilation with two different Signer implementations works -#[derive(Clone, Debug)] -pub struct DummySigner; - -impl Signer for DummySigner { - type KeyId = KeyIdentifier; - type Error = SignerError; - - fn create_key(&self, _: PublicKeyFormat) -> Result { - unreachable!() - } - - fn get_key_info( - &self, - _: &Self::KeyId, - ) -> Result> { - unreachable!() - } - - fn destroy_key(&self, _: &Self::KeyId) -> Result<(), rpki::repository::crypto::signer::KeyError> { - unreachable!() - } - - fn sign + ?Sized>( - &self, - _: &Self::KeyId, - _: SignatureAlgorithm, - _: &D, - ) -> Result> { - unreachable!() - } - - fn sign_one_off + ?Sized>( - &self, - _: SignatureAlgorithm, - _: &D, - ) -> Result<(Signature, PublicKey), Self::Error> { - unreachable!() - } - - fn rand(&self, _: &mut [u8]) -> Result<(), Self::Error> { - unreachable!() - } -} diff --git a/src/commons/util/ext_serde.rs b/src/commons/util/ext_serde.rs index 7891eb8e..cb00367c 100644 --- a/src/commons/util/ext_serde.rs +++ b/src/commons/util/ext_serde.rs @@ -6,7 +6,10 @@ use log::LevelFilter; use serde::{de, Deserialize, Deserializer, Serialize, Serializer}; use syslog::Facility; -use rpki::repository::resources::{AsBlocks, IpBlocks}; +use rpki::repository::{ + crypto::PublicKey, + resources::{AsBlocks, IpBlocks}, +}; //------------ Bytes --------------------------------------------------------- @@ -163,6 +166,27 @@ where Facility::from_str(&string).map_err(|_| de::Error::custom(format!("Unsupported syslog_facility: \"{}\"", string))) } +//------------ PublicKey ---------------------------------------------------- + +pub fn ser_public_key(public_key: &PublicKey, s: S) -> Result +where + S: Serializer, +{ + base64::encode(public_key.to_info_bytes()).serialize(s) +} + +pub fn de_public_key<'de, D>(d: D) -> Result +where + D: Deserializer<'de>, +{ + let string = String::deserialize(d)?; + let public_key_bytes = base64::decode(string) + .map_err(|err| de::Error::custom(format!("Invalid public key base64 encoding: {}", err)))?; + let public_key = PublicKey::decode(&*public_key_bytes) + .map_err(|err| de::Error::custom(format!("Invalid public key bytes: {}", err)))?; + Ok(public_key) +} + //------------- AtomicU64 ----------------------------------------------------- // Implemented automatically by Serde derive but only for x86_64 architectures, // for other architectures (such as armv7 for the Raspberry Pi 4b) it has to be @@ -180,4 +204,4 @@ where S: Serializer, { s.serialize_u64(v.load(Ordering::SeqCst)) -} \ No newline at end of file +} diff --git a/src/commons/util/mod.rs b/src/commons/util/mod.rs index 65138a30..cacf3d87 100644 --- a/src/commons/util/mod.rs +++ b/src/commons/util/mod.rs @@ -11,12 +11,9 @@ use rpki::{ use crate::constants::KRILL_VERSION; -#[cfg(feature = "hsm")] -pub mod dummysigner; pub mod ext_serde; pub mod file; pub mod httpclient; -pub mod softsigner; pub mod xml; //------------ KrillVersion -------------------------------------------------- diff --git a/src/constants.rs b/src/constants.rs index 13236e56..7920139a 100644 --- a/src/constants.rs +++ b/src/constants.rs @@ -39,6 +39,9 @@ pub fn test_announcements_enabled() -> bool { std::env::var(KRILL_ENV_TEST_ANN).is_ok() } +pub const KEYS_DIR: &str = "keys"; +pub const SIGNERS_DIR: &str = "signers"; + pub const CASERVER_DIR: &str = "cas"; pub const CA_OBJECTS_DIR: &str = "ca_objects"; @@ -103,3 +106,12 @@ pub const PW_HASH_LOG_N: u8 = 13; pub const PW_HASH_R: u32 = 8; #[cfg(feature = "multi-user")] pub const PW_HASH_P: u32 = 1; + +#[cfg(not(any(feature = "hsm-tests-kmip", feature = "hsm-tests-pkcs11")))] +pub const DEFAULT_SIGNER_NAME: &str = "Default OpenSSL signer"; +#[cfg(feature = "hsm-tests-kmip")] +pub const DEFAULT_KMIP_SIGNER_NAME: &str = "(test mode) Default KMIP signer"; +#[cfg(feature = "hsm-tests-pkcs11")] +pub const DEFAULT_PKCS11_SIGNER_NAME: &str = "(test mode) Default PKCS#11 signer"; + +pub const OPENSSL_ONE_OFF_SIGNER_NAME: &str = "OpenSSL one-off signer"; diff --git a/src/daemon/ca/certauth.rs b/src/daemon/ca/certauth.rs index 1b2ce31e..5b168237 100644 --- a/src/daemon/ca/certauth.rs +++ b/src/daemon/ca/certauth.rs @@ -2153,12 +2153,16 @@ impl CertAuth { #[cfg(test)] mod tests { use super::*; - use crate::test; + use crate::{commons::crypto::KrillSignerBuilder, daemon::config::ConfigDefaults, test}; + use std::time::Duration; #[test] fn generate_id_cert() { test::test_under_tmp(|d| { - let signer = KrillSigner::build(&d).unwrap(); + let signers = ConfigDefaults::signers(); + let signer = KrillSignerBuilder::new(&d, Duration::from_secs(1), &signers) + .build() + .unwrap(); let id = Rfc8183Id::generate(&signer).unwrap(); id.cert.validate_ta().unwrap(); }); diff --git a/src/daemon/config.rs b/src/daemon/config.rs index d11babf7..bce1ea3a 100644 --- a/src/daemon/config.rs +++ b/src/daemon/config.rs @@ -20,6 +20,7 @@ use rpki::{repository::x509::Time, uri}; use crate::{ commons::{ api::{PublicationServerUris, PublisherHandle, Token}, + crypto::OpenSslSignerConfig, error::KrillIoError, util::ext_serde, }, @@ -30,6 +31,9 @@ use crate::{ #[cfg(feature = "multi-user")] use crate::daemon::auth::providers::{config_file::config::ConfigAuthUsers, openid_connect::ConfigAuthOpenIDConnect}; +#[cfg(feature = "hsm")] +use crate::commons::crypto::{KmipSignerConfig, Pkcs11SignerConfig}; + //------------ ConfigDefaults ------------------------------------------------ pub struct ConfigDefaults; @@ -202,10 +206,127 @@ impl ConfigDefaults { fn timing_aspa_reissue_weeks_before() -> i64 { 4 } + + pub fn signers() -> Vec { + #[cfg(not(any(feature = "hsm-tests-kmip", feature = "hsm-tests-pkcs11")))] + { + let signer_config = OpenSslSignerConfig { keys_path: None }; + vec![SignerConfig::new( + DEFAULT_SIGNER_NAME.to_string(), + SignerType::OpenSsl(signer_config), + )] + } + + #[cfg(feature = "hsm-tests-kmip")] + { + let signer_config = KmipSignerConfig { + host: "127.0.0.1".to_string(), + port: 5696, + username: None, + password: None, + insecure: true, + force: true, + client_cert_path: Some(PathBuf::from_str("test-resources/pykmip/server.crt").unwrap()), + client_cert_private_key_path: Some(PathBuf::from_str("test-resources/pykmip/server.key").unwrap()), + server_cert_path: Some(PathBuf::from_str("test-resources/pykmip/server.crt").unwrap()), + server_ca_cert_path: Some(PathBuf::from_str("test-resources/pykmip/ca.crt").unwrap()), + retry_seconds: KmipSignerConfig::default_retry_seconds(), + backoff_multiplier: KmipSignerConfig::default_backoff_multiplier(), + max_retry_seconds: KmipSignerConfig::default_max_retry_seconds(), + connect_timeout_seconds: KmipSignerConfig::default_connect_timeout_seconds(), + read_timeout_seconds: KmipSignerConfig::default_read_timeout_seconds(), + write_timeout_seconds: KmipSignerConfig::default_write_timeout_seconds(), + max_lifetime_seconds: KmipSignerConfig::default_max_lifetime_seconds(), + max_idle_seconds: KmipSignerConfig::default_max_idle_seconds(), + max_connections: KmipSignerConfig::default_max_connections(), + max_response_bytes: KmipSignerConfig::default_max_response_bytes(), + }; + vec![SignerConfig::new( + DEFAULT_KMIP_SIGNER_NAME.to_string(), + SignerType::Kmip(signer_config), + )] + } + + #[cfg(feature = "hsm-tests-pkcs11")] + { + use crate::commons::crypto::SlotIdOrLabel; + let signer_config = Pkcs11SignerConfig { + lib_path: "/usr/lib/softhsm/libsofthsm2.so".to_string(), + user_pin: Some("1234".to_string()), + slot: SlotIdOrLabel::Label("My token 1".to_string()), + login: true, + retry_seconds: Pkcs11SignerConfig::default_retry_seconds(), + backoff_multiplier: Pkcs11SignerConfig::default_backoff_multiplier(), + max_retry_seconds: Pkcs11SignerConfig::default_max_retry_seconds(), + }; + vec![SignerConfig::new( + DEFAULT_PKCS11_SIGNER_NAME.to_string(), + SignerType::Pkcs11(signer_config), + )] + } + } + + pub fn signer_probe_retry_seconds() -> u64 { + 30 + } } //------------ Config -------------------------------------------------------- +#[derive(Clone, Debug)] + +pub enum SignerReference { + /// The name of the [[signers]] block being referred to. If supplied it + /// must match the name field of one of the [[signers]] blocks defined in + /// the configuration. + Name(Option), + + /// The index into Config.signers vector that the name was resolved to. + /// Populated based on the value of 'name' and the contents of + /// Config.signers after the config file has been parsed. + Index(usize), +} + +fn deserialize_signer_ref<'de, D>(deserializer: D) -> Result +where + D: Deserializer<'de>, +{ + Ok(SignerReference::new(&String::deserialize(deserializer)?)) +} + +impl Default for SignerReference { + fn default() -> Self { + Self::Name(None) + } +} + +impl SignerReference { + pub fn new(name: &str) -> SignerReference { + SignerReference::Name(Some(name.to_string())) + } + + pub fn name(&self) -> &String { + match self { + SignerReference::Name(Some(name)) => name, + _ => panic!("Signer reference is not named"), + } + } + + pub fn idx(&self) -> usize { + match self { + SignerReference::Index(idx) => *idx, + _ => panic!("Signer reference is not resolved"), + } + } + + pub fn is_named(&self) -> bool { + match self { + SignerReference::Name(Some(_)) => true, + _ => false, + } + } +} + /// Global configuration for the Krill Server. /// /// This will parse a default config file ('./defaults/krill.conf') unless @@ -267,6 +388,18 @@ pub struct Config { #[cfg(feature = "multi-user")] pub auth_openidconnect: Option, + #[serde(default, deserialize_with = "deserialize_signer_ref")] + pub default_signer: SignerReference, + + #[serde(default, deserialize_with = "deserialize_signer_ref")] + pub one_off_signer: SignerReference, + + #[serde(default = "ConfigDefaults::signer_probe_retry_seconds")] + pub signer_probe_retry_seconds: u64, + + #[serde(default = "ConfigDefaults::signers")] + pub signers: Vec, + #[serde(default = "ConfigDefaults::ca_refresh_seconds", alias = "ca_refresh")] pub ca_refresh_seconds: u32, @@ -540,11 +673,31 @@ impl Config { pub fn testbed(&self) -> Option<&TestBed> { self.testbed.as_ref() } + + /// Returns a reference to the default signer configuration. + /// + /// Assumes that the configuration is valid. Will panic otherwise. + pub fn default_signer(&self) -> &SignerConfig { + &self.signers[self.default_signer.idx()] + } + + /// Returns a reference to the one off signer configuration. + /// + /// Assumes that the configuration is valid. Will panic otherwise. + pub fn one_off_signer(&self) -> &SignerConfig { + &self.signers[self.one_off_signer.idx()] + } } /// # Create impl Config { - fn test_config(data_dir: &Path, enable_testbed: bool, enable_ca_refresh: bool, enable_suspend: bool) -> Self { + fn test_config( + data_dir: &Path, + enable_testbed: bool, + enable_ca_refresh: bool, + enable_suspend: bool, + #[allow(unused_variables)] second_signer: bool, + ) -> Self { use crate::test; let ip = ConfigDefaults::ip(); @@ -570,6 +723,23 @@ impl Config { let auth_users = None; #[cfg(feature = "multi-user")] let auth_openidconnect = None; + + let default_signer = SignerReference::default(); + let one_off_signer = SignerReference::default(); + let signer_probe_retry_seconds = ConfigDefaults::signer_probe_retry_seconds(); + + // Multiple signers are only needed and can only be configured when the "hsm" feature is enabled. + #[cfg(not(feature = "hsm"))] + let second_signer = false; + + let signers = match second_signer { + false => ConfigDefaults::signers(), + true => vec![SignerConfig::new( + "Second Test Signer".to_string(), + SignerType::OpenSsl(OpenSslSignerConfig::default()), + )], + }; + let ca_refresh_seconds = if enable_ca_refresh { 1 } else { 86400 }; let ca_refresh_parents_batch_size = 10; let post_limit_api = ConfigDefaults::post_limit_api(); @@ -668,6 +838,10 @@ impl Config { auth_users, #[cfg(feature = "multi-user")] auth_openidconnect, + default_signer, + one_off_signer, + signers, + signer_probe_retry_seconds, ca_refresh_seconds, ca_refresh_parents_batch_size, suspend_child_after_inactive_seconds, @@ -690,12 +864,24 @@ impl Config { } } - pub fn test(data_dir: &Path, enable_testbed: bool, enable_ca_refresh: bool, enable_suspend: bool) -> Self { - Self::test_config(data_dir, enable_testbed, enable_ca_refresh, enable_suspend) + pub fn test( + data_dir: &Path, + enable_testbed: bool, + enable_ca_refresh: bool, + enable_suspend: bool, + second_signer: bool, + ) -> Self { + Self::test_config( + data_dir, + enable_testbed, + enable_ca_refresh, + enable_suspend, + second_signer, + ) } pub fn pubd_test(data_dir: &Path) -> Self { - let mut config = Self::test_config(data_dir, false, false, false); + let mut config = Self::test_config(data_dir, false, false, false, false); config.port = 3001; config } @@ -742,29 +928,82 @@ impl Config { } }?; - if config.ca_refresh_seconds < CA_REFRESH_SECONDS_MIN { + config + .process() + .map_err(|e| ConfigError::Other(format!("Error parsing config file: {}, error: {}", config_file, e)))?; + + Ok(config) + } + + pub fn process(&mut self) -> Result<(), ConfigError> { + self.fix(); + self.verify()?; + self.resolve(); + Ok(()) + } + + fn fix(&mut self) { + if self.ca_refresh_seconds < CA_REFRESH_SECONDS_MIN { warn!( "The value for 'ca_refresh_seconds' was below the minimum value, changing it to {} seconds", CA_REFRESH_SECONDS_MIN ); - config.ca_refresh_seconds = CA_REFRESH_SECONDS_MIN; + self.ca_refresh_seconds = CA_REFRESH_SECONDS_MIN; } - if config.ca_refresh_seconds > CA_REFRESH_SECONDS_MAX { + if self.ca_refresh_seconds > CA_REFRESH_SECONDS_MAX { warn!( "The value for 'ca_refresh_seconds' was above the maximum value, changing it to {} seconds", CA_REFRESH_SECONDS_MAX ); - config.ca_refresh_seconds = CA_REFRESH_SECONDS_MAX; + self.ca_refresh_seconds = CA_REFRESH_SECONDS_MAX; } - - config - .verify() - .map_err(|e| ConfigError::Other(format!("Error parsing config file: {}, error: {}", config_file, e)))?; - Ok(config) } - pub fn verify(&self) -> Result<(), ConfigError> { + fn resolve(&mut self) { + if self.signers.len() == 1 && !self.default_signer.is_named() { + self.default_signer = SignerReference::new(&self.signers[0].name); + } + + let default_signer_idx = self.find_signer_reference(&self.default_signer).unwrap(); + self.default_signer = SignerReference::Index(default_signer_idx); + + let openssl_signer_idx = self.find_openssl_signer(); + let one_off_signer_idx = self.find_signer_reference(&self.one_off_signer); + + // Use the specified one-off signer, if set, else: + // - Use an existing OpenSSL signer config, + // - Or create a new OpenSSL signer config. + let one_off_signer_idx = match (one_off_signer_idx, openssl_signer_idx) { + (Some(one_off_signer_idx), _) => one_off_signer_idx, + (None, Some(openssl_signer_idx)) => openssl_signer_idx, + (None, None) => self.add_openssl_signer(OPENSSL_ONE_OFF_SIGNER_NAME), + }; + + self.one_off_signer = SignerReference::Index(one_off_signer_idx); + } + + fn add_openssl_signer(&mut self, name: &str) -> usize { + let signer_config = SignerConfig::new(name.to_string(), SignerType::OpenSsl(OpenSslSignerConfig::default())); + self.signers.push(signer_config); + let idx = self.signers.len() - 1; + idx + } + + fn find_signer_reference(&self, signer_ref: &SignerReference) -> Option { + match signer_ref.is_named() { + true => self.signers.iter().position(|s| &s.name == signer_ref.name()), + false => None, + } + } + + fn find_openssl_signer(&self) -> Option { + self.signers + .iter() + .position(|s| matches!(s.signer_type, SignerType::OpenSsl(_))) + } + + fn verify(&self) -> Result<(), ConfigError> { if env::var(KRILL_ENV_ADMIN_TOKEN_DEPRECATED).is_ok() { warn!("The environment variable for setting the admin token has been updated from '{}' to '{}', please update as the old value may not be supported in future releases", KRILL_ENV_ADMIN_TOKEN_DEPRECATED, KRILL_ENV_ADMIN_TOKEN) } @@ -853,6 +1092,61 @@ impl Config { } } + if self.signers.is_empty() { + // Since Config.signers defaults via Serde to ConfigDefaults::signers() which creates a vector with a + // single signer, this can only happen if we were invoked on a config object created or modified by test + // code. + return Err(ConfigError::Other("No signers configured".to_string())); + } + + #[cfg(not(feature = "hsm"))] + { + fn mk_err_msg(setting_name: &str) -> String { + format!("This build of Krill lacks support for the '{}' config file setting. Please use a version of Krill that has the 'hsm' feature enabled.", setting_name) + } + + if self.default_signer.is_named() { + return Err(ConfigError::other(&mk_err_msg("default_signer"))); + } + if self.one_off_signer.is_named() { + return Err(ConfigError::other(&mk_err_msg("one_off_signer"))); + } + if self.signers != ConfigDefaults::signers() { + return Err(ConfigError::other(&mk_err_msg("[[signers]]"))); + } + } + + for n in &self.signers { + if self.signers.iter().filter(|m| m.name == n.name).count() > 1 { + return Err(ConfigError::other(&format!("Signer name '{}' is not unique", n.name))); + } + } + + if self.signers.len() > 1 && !self.default_signer.is_named() { + return Err(ConfigError::other( + "'default_signer' must be set when more than one [[signers]] configuration is defined", + )); + } + + if self.default_signer.is_named() { + if self.find_signer_reference(&self.default_signer).is_none() { + return Err(ConfigError::other(&format!( + "'{}' cannot be used as the 'default_signer' as no signer with that name is defined", + self.default_signer.name() + ))); + } + } else { + } + + if self.one_off_signer.is_named() { + if self.find_signer_reference(&self.one_off_signer).is_none() { + return Err(ConfigError::other(&format!( + "'{}' cannot be used as the 'one_off_signer' as no signer with that name is defined", + self.one_off_signer.name() + ))); + } + } + Ok(()) } @@ -1122,16 +1416,98 @@ impl<'de> Deserialize<'de> for AuthType { } } +//------------ Signers ----------------------------------------------------- + +// Supports TOML such as: +// +// default_signer = "" # optional +// one_off_signer = "" # optional +// +// [[signers]] +// name = "My PKCS#11 signer" +// type = "PKCS#11" +// lib_path = "/path/to/pkcs11.so" +// ... +// +// [[signers]] +// name = "My OpenSSL Signer" +// type = "OpenSSL" +// +// [[signers]] +// name = "My KMIP Signer" +// type = "KMIP" +// host = "example.com" +// ... +// +// # Multiple signers of the same type are supported +// [[signers]] +// name = "My Other KMIP Signer" +// type = "KMIP" +// ... + +#[derive(Clone, Debug, Deserialize, PartialEq, Eq)] +pub struct SignerConfig { + /// A friendly name for the signer. Used to identify the signer with the `default_signer` and `one_off_signer` + /// settings. + pub name: String, + + /// Signer specific configuration settings. + #[serde(flatten)] + pub signer_type: SignerType, +} + +#[derive(Clone, Debug, Deserialize, PartialEq, Eq)] +#[serde(tag = "type")] +pub enum SignerType { + #[serde(alias = "OpenSSL")] + OpenSsl(OpenSslSignerConfig), + + #[cfg(feature = "hsm")] + #[serde(alias = "PKCS#11")] + Pkcs11(Pkcs11SignerConfig), + + #[cfg(feature = "hsm")] + #[serde(alias = "KMIP")] + Kmip(KmipSignerConfig), +} + +impl std::fmt::Display for SignerType { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + SignerType::OpenSsl(_) => f.write_str("OpenSSL"), + + #[cfg(feature = "hsm")] + SignerType::Pkcs11(_) => f.write_str("PKCS#11"), + + #[cfg(feature = "hsm")] + SignerType::Kmip(_) => f.write_str("KMIP"), + } + } +} + +impl SignerConfig { + pub fn new(name: String, signer_type: SignerType) -> SignerConfig { + Self { name, signer_type } + } +} + //------------ Tests --------------------------------------------------------- #[cfg(test)] mod tests { - use crate::test; use std::env; use super::*; + fn assert_err_msg(res: Result, expected_err_msg: &str) { + if let Err(ConfigError::Other(msg)) = res { + assert_eq!(msg, expected_err_msg); + } else { + panic!("Expected error '{}' but got: {:?}", expected_err_msg, res); + } + } + #[test] fn should_parse_default_config_file() { // Config for auth token is required! If there is nothing in the conf @@ -1309,11 +1685,167 @@ mod tests { } } + fn parse_and_process_config_str(config_str: &str) -> Result { + let mut c: Config = toml::from_str(config_str).unwrap(); + c.process()?; + Ok(c) + } + #[test] fn config_should_accept_and_warn_about_auth_token() { - let old_config = b"auth_token = \"secret\""; - - let c: Config = toml::from_slice(old_config).unwrap(); + let old_config = r#"auth_token = "secret""#; + let c = parse_and_process_config_str(old_config).unwrap(); assert_eq!(c.admin_token.as_ref(), "secret"); } + + #[cfg(not(feature = "hsm"))] + #[test] + fn should_fail_when_config_defines_signers_but_hsm_support_is_not_enabled() { + fn assert_unexpected_setting_err(res: Result, setting_name: &str) { + let expected_err_msg = format!("This build of Krill lacks support for the '{}' config file setting. Please use a version of Krill that has the 'hsm' feature enabled.", setting_name); + assert_err_msg(res, &expected_err_msg); + } + + let config_str = r#" + auth_token = "secret" + + [[signers]] + type = "OpenSSL" + name = "Signer 1" + "#; + + let res = parse_and_process_config_str(config_str); + assert_unexpected_setting_err(res, "[[signers]]"); + + // --- + + let config_str = r#" + auth_token = "secret" + default_signer = "Signer 1" + + [[signers]] + type = "OpenSSL" + name = "Signer 1" + "#; + + let res = parse_and_process_config_str(config_str); + assert_unexpected_setting_err(res, "default_signer"); + + // --- + + let config_str = r#" + auth_token = "secret" + one_off_signer = "Signer 1" + + [[signers]] + type = "OpenSSL" + name = "Signer 1" + "#; + + let res = parse_and_process_config_str(config_str); + assert_unexpected_setting_err(res, "one_off_signer"); + } + + #[cfg(feature = "hsm")] + #[test] + fn should_fail_with_multiple_signers_and_no_default_signer() { + let config_str = r#" + auth_token = "secret" + + [[signers]] + type = "OpenSSL" + name = "Signer 1" + + [[signers]] + type = "OpenSSL" + name = "Signer 2" + "#; + + let res = parse_and_process_config_str(config_str); + assert_err_msg( + res, + "'default_signer' must be set when more than one [[signers]] configuration is defined", + ); + } + + #[cfg(feature = "hsm")] + #[test] + fn should_fail_if_referenced_signer_is_not_defined() { + let config_str = r#" + auth_token = "secret" + default_signer = "Unknown Signer" + "#; + + let res = parse_and_process_config_str(config_str); + assert_err_msg( + res, + "'Unknown Signer' cannot be used as the 'default_signer' as no signer with that name is defined", + ); + + // --- + + let config_str = r#" + auth_token = "secret" + one_off_signer = "Unknown Signer" + "#; + + let res = parse_and_process_config_str(config_str); + assert_err_msg( + res, + "'Unknown Signer' cannot be used as the 'one_off_signer' as no signer with that name is defined", + ); + } + + #[test] + fn should_use_the_expected_default_signer() { + let config_str = r#" + auth_token = "secret" + "#; + + let c = parse_and_process_config_str(config_str).unwrap(); + + #[cfg(not(any(feature = "hsm-tests-kmip", feature = "hsm-tests-pkcs11")))] + { + assert_eq!(c.signers.len(), 1); + assert_eq!(c.signers[0].name, "Default OpenSSL signer"); + assert!(matches!(c.signers[0].signer_type, SignerType::OpenSsl(_))); + } + + #[cfg(feature = "hsm-tests-kmip")] + { + assert_eq!(c.signers.len(), 2); + assert_eq!(c.signers[0].name, "(test mode) Default KMIP signer"); + assert!(matches!(c.signers[0].signer_type, SignerType::Kmip(_))); + assert_eq!(c.signers[1].name, "OpenSSL one-off signer"); + assert!(matches!(c.signers[1].signer_type, SignerType::OpenSsl(_))); + } + + #[cfg(feature = "hsm-tests-pkcs11")] + { + assert_eq!(c.signers.len(), 2); + assert_eq!(c.signers[0].name, "(test mode) Default PKCS#11 signer"); + assert!(matches!(c.signers[0].signer_type, SignerType::Pkcs11(_))); + assert_eq!(c.signers[1].name, "OpenSSL one-off signer"); + assert!(matches!(c.signers[1].signer_type, SignerType::OpenSsl(_))); + } + } + + #[cfg(feature = "hsm")] + #[test] + fn should_fail_if_signer_name_is_not_unique() { + let config_str = r#" + auth_token = "secret" + + [[signers]] + type = "OpenSSL" + name = "Blah" + + [[signers]] + type = "OpenSSL" + name = "Blah" + "#; + + let res = parse_and_process_config_str(config_str); + assert_err_msg(res, "Signer name 'Blah' is not unique"); + } } diff --git a/src/daemon/http/server.rs b/src/daemon/http/server.rs index efac33af..0809b587 100644 --- a/src/daemon/http/server.rs +++ b/src/daemon/http/server.rs @@ -2075,7 +2075,7 @@ mod tests { #[tokio::test] async fn start_krill_daemon() { - let dir = test::start_krill_with_default_test_config(false, false, false).await; + let dir = test::start_krill_with_default_test_config(false, false, false, false).await; let _ = fs::remove_dir_all(dir); } diff --git a/src/daemon/krillserver.rs b/src/daemon/krillserver.rs index 6aa6202a..2089ebea 100644 --- a/src/daemon/krillserver.rs +++ b/src/daemon/krillserver.rs @@ -19,7 +19,7 @@ use crate::{ Timestamp, UpdateChildRequest, }, bgp::{BgpAnalyser, BgpAnalysisReport, BgpAnalysisSuggestion}, - crypto::KrillSigner, + crypto::KrillSignerBuilder, eventsourcing::CommandKey, remote::rfc8183, KrillEmptyResult, KrillResult, @@ -102,7 +102,15 @@ impl KrillServer { let mut repo_dir = work_dir.clone(); repo_dir.push("repo"); - let signer = Arc::new(KrillSigner::build(work_dir)?); + // Assumes that Config::verify() has already ensured that the signer configuration is valid and that + // Config::resolve() has been used to update signer name references to resolve to the corresponding signer + // configurations. + let probe_interval = std::time::Duration::from_secs(config.signer_probe_retry_seconds); + let signer = KrillSignerBuilder::new(work_dir, probe_interval, &config.signers) + .with_default_signer(config.default_signer()) + .with_one_off_signer(config.one_off_signer()) + .build()?; + let signer = Arc::new(signer); #[cfg(feature = "multi-user")] let login_session_cache = Arc::new(LoginSessionCache::new()); diff --git a/src/pubd/manager.rs b/src/pubd/manager.rs index 1311d755..a5f0ee5a 100644 --- a/src/pubd/manager.rs +++ b/src/pubd/manager.rs @@ -208,7 +208,12 @@ mod tests { use rpki::uri; - use crate::{constants::*, pubd::RrdpServer}; + use crate::{commons::crypto::KrillSignerBuilder, constants::*, pubd::RrdpServer}; + + use crate::{ + commons::crypto::OpenSslSignerConfig, + daemon::config::{SignerConfig, SignerType}, + }; use super::*; @@ -224,11 +229,22 @@ mod tests { }; fn publisher_alice(work_dir: &Path) -> Publisher { - let signer = KrillSigner::build(work_dir).unwrap(); + // When the "hsm" feature is enabled we could be running the tests with PKCS#11 as the default signer type. + // In that case, if the backend signer is SoftHSMv2, attempting to create a second instance of KrillSigner in + // the same process will fail because it will attempt to login to SoftHSMv2 a second time which SoftHSMv2 does + // not support. To work around this issue we therefore explicitly request that the second KrillSigner instance + // that we create here uses OpenSSL as its backend signer. + let signer = { + let signer_type = SignerType::OpenSsl(OpenSslSignerConfig::default()); + let signer_config = SignerConfig::new("Alice".to_string(), signer_type); + let signer_configs = &[signer_config]; + KrillSignerBuilder::new(work_dir, Duration::from_secs(1), signer_configs) + .build() + .unwrap() + }; let key = signer.create_key().unwrap(); let id_cert = IdCertBuilder::new_ta_id_cert(&key, &signer).unwrap(); - let base_uri = uri::Rsync::from_str("rsync://localhost/repo/alice/").unwrap(); Publisher::new(id_cert, base_uri) @@ -241,12 +257,17 @@ mod tests { fn make_server(work_dir: &Path) -> RepositoryManager { enable_test_mode(); - let config = Arc::new(Config::test(work_dir, true, false, false)); - init_config(&config); + let mut config = Config::test(work_dir, true, false, false, false); + init_config(&mut config); + + let signer = KrillSignerBuilder::new(work_dir, Duration::from_secs(1), &config.signers) + .with_default_signer(config.default_signer()) + .with_one_off_signer(config.one_off_signer()) + .build() + .unwrap(); - let signer = KrillSigner::build(work_dir).unwrap(); let signer = Arc::new(signer); - + let config = Arc::new(config); let repository_manager = RepositoryManager::build(config, signer).unwrap(); let rsync_base = rsync("rsync://localhost/repo/"); diff --git a/src/test.rs b/src/test.rs index 49337ec0..8768b00d 100644 --- a/src/test.rs +++ b/src/test.rs @@ -53,7 +53,7 @@ pub const KRILL_PUBD_SERVER_URI: &str = "https://localhost:3001/"; pub fn init_logging() { // Just creates a test config so we can initialize logging, then forgets about it let d = PathBuf::from("."); - let _ = Config::test(&d, false, false, false).init_logging(); + let _ = Config::test(&d, false, false, false, false).init_logging(); } pub fn info(msg: impl std::fmt::Display) { @@ -100,19 +100,25 @@ pub async fn server_ready(uri: &str) -> bool { false } -pub fn test_config(dir: &Path, enable_testbed: bool, enable_ca_refresh: bool, enable_suspend: bool) -> Config { +pub fn test_config( + dir: &Path, + enable_testbed: bool, + enable_ca_refresh: bool, + enable_suspend: bool, + second_signer: bool, +) -> Config { if enable_testbed { crate::constants::enable_test_mode(); crate::constants::enable_test_announcements(); } - Config::test(dir, enable_testbed, enable_ca_refresh, enable_suspend) + Config::test(dir, enable_testbed, enable_ca_refresh, enable_suspend, second_signer) } -pub fn init_config(config: &Config) { +pub fn init_config(config: &mut Config) { if config.init_logging().is_err() { trace!("Logging already initialized"); } - config.verify().unwrap(); + config.process().unwrap(); } /// Starts krill server for testing using the given configuration. Creates a random base directory in the 'work' folder, @@ -130,15 +136,16 @@ pub async fn start_krill_with_default_test_config( enable_testbed: bool, enable_ca_refresh: bool, enable_suspend: bool, + second_signer: bool, ) -> PathBuf { let dir = tmp_dir(); - let config = test_config(&dir, enable_testbed, enable_ca_refresh, enable_suspend); + let config = test_config(&dir, enable_testbed, enable_ca_refresh, enable_suspend, second_signer); start_krill(config).await; dir } -async fn start_krill(config: Config) { - init_config(&config); +async fn start_krill(mut config: Config) { + init_config(&mut config); tokio::spawn(start_krill_with_error_trap(Arc::new(config))); assert!(krill_server_ready().await); } @@ -153,8 +160,8 @@ async fn start_krill_with_error_trap(config: Arc) { /// own temp dir for storage. pub async fn start_krill_pubd() -> PathBuf { let dir = tmp_dir(); - let mut config = test_config(&dir, false, false, false); - init_config(&config); + let mut config = test_config(&dir, false, false, false, true); + init_config(&mut config); config.port = 3001; tokio::spawn(start_krill_with_error_trap(Arc::new(config))); diff --git a/src/upgrades/mod.rs b/src/upgrades/mod.rs index 00e70f45..066ae988 100644 --- a/src/upgrades/mod.rs +++ b/src/upgrades/mod.rs @@ -2,23 +2,30 @@ //! - Updating the format of commands or events //! - Export / Import data -use std::{fmt, path::Path, str::FromStr, sync::Arc}; +use std::{fmt, path::Path, str::FromStr, sync::Arc, time::Duration}; use serde::de::DeserializeOwned; use crate::{ commons::{ api::Handle, - crypto::KrillSigner, + crypto::KrillSignerBuilder, error::KrillIoError, eventsourcing::{AggregateStoreError, CommandKey, KeyStoreKey, KeyValueError, KeyValueStore}, util::{file, KrillVersion}, }, + constants::{CASERVER_DIR, PUBSERVER_DIR}, daemon::{config::Config, krillserver::KrillServer}, pubd::RepositoryManager, upgrades::v0_9_0::{CaObjectsMigration, PubdObjectsMigration}, }; +#[cfg(feature = "hsm")] +use rpki::repository::crypto::KeyIdentifier; + +#[cfg(feature = "hsm")] +use crate::constants::{KEYS_DIR, SIGNERS_DIR}; + pub mod v0_9_0; pub type UpgradeResult = Result; @@ -159,13 +166,101 @@ pub trait UpgradeStore { /// Should be called when Krill starts, before the KrillServer is initiated pub fn pre_start_upgrade(config: Arc) -> Result<(), UpgradeError> { - upgrade_data_to_0_9_0(config) + upgrade_data_to_0_9_0(config.clone())?; + + #[cfg(feature = "hsm")] + record_preexisting_openssl_keys_in_signer_mapper(config.clone())?; + + Ok(()) +} + +/// Prior to Krill having HSM support there was no signer mapper as it wasn't needed, keys were just created by OpenSSL +/// and stored in files on disk in KEYS_DIR named by the string form of their Krill KeyIdentifier. If Krill had created +/// such keys and then the operator upgrades to a version of Krill with HSM support, the keys will become unusable +/// because Krill will not be able to find a mapping from KeyIdentifier to signer as the mappings for the keys were +/// never created. So we detect the case that the signer store SIGNERS_DIR directory has not yet been created, i.e. no +/// signers have been registered and no key mappings have been recorded, and then walk KEYS_DIR adding the keys one by +/// one to the mapping in the signer store, if any. +#[cfg(feature = "hsm")] +fn record_preexisting_openssl_keys_in_signer_mapper(config: Arc) -> Result<(), UpgradeError> { + if !config.data_dir.join(SIGNERS_DIR).exists() { + let mut num_recorded_keys = 0; + let keys_dir = config.data_dir.join(KEYS_DIR); + + info!( + "Scanning for not yet mapped OpenSSL signer keys in {} to record in the signer store", + keys_dir.to_string_lossy() + ); + + let probe_interval = Duration::from_secs(config.signer_probe_retry_seconds); + let krill_signer = KrillSignerBuilder::new(&config.data_dir, probe_interval, &config.signers) + .with_default_signer(config.default_signer()) + .with_one_off_signer(config.one_off_signer()) + .build() + .unwrap(); + + // For every file (key) in the legacy OpenSSL signer keys directory + if let Ok(dir_iter) = keys_dir.read_dir() { + let mut openssl_signer_handle: Option = None; + + for entry in dir_iter { + let entry = entry.map_err(|err| { + UpgradeError::IoError(KrillIoError::new( + format!( + "I/O error while looking for signer keys to register in: {}", + keys_dir.to_string_lossy() + ), + err, + )) + })?; + + if entry.path().is_file() { + // Is it a key identifier? + if let Ok(key_id) = KeyIdentifier::from_str(&entry.file_name().to_string_lossy()) { + // Is the key already recorded in the mapper? It shouldn't be, but asking will cause the initial + // registration of the OpenSSL signer to occur and for it to be assigned a handle. We need the + // handle so that we can register keys with the mapper. + if !krill_signer.get_key_info(&key_id).is_ok() { + // No, record it + + // Find out the handle of the OpenSSL signer used to create this key, if not yet known. + if openssl_signer_handle.is_none() { + // No, find it by asking each of the active signers if they have the key because one of + // them must have it and it should be the one and only OpenSSL signer that Krill was + // using previously. We can't just find and use the only OpenSSL signers as Krill may + // have been configured with more than one each with separate keys directories. + for (a_signer_handle, a_signer) in krill_signer.get_active_signers().iter() { + if a_signer.get_key_info(&key_id).is_ok() { + openssl_signer_handle = Some(a_signer_handle.clone()); + break; + } + } + } + + // Record the key in the signer mapper as being owned by the found signer handle. + if let Some(signer_handle) = &openssl_signer_handle { + let internal_key_id = key_id.to_string(); + if let Some(mapper) = krill_signer.get_mapper() { + mapper.add_key(&signer_handle, &key_id, &internal_key_id)?; + num_recorded_keys += 1; + } + } + } + } + } + } + } + + info!("Recorded {} key identifiers in the signer store", num_recorded_keys); + } + + Ok(()) } /// Should be called when the KrillServer is initiated, before the webserver is started /// and operators can make changes. pub async fn post_start_upgrade(config: &Config, server: &KrillServer) -> Result<(), UpgradeError> { - if needs_upgrade(&config.data_dir, "cas", KrillVersion::candidate(0, 9, 3, 2)) { + if needs_upgrade(&config.data_dir, CASERVER_DIR, KrillVersion::candidate(0, 9, 3, 2)) { info!("Reissue ROAs on upgrade to force short EE certificate subjects in the objects"); server.force_renew_roas().await.map_err(|e| e.into()) } else { @@ -176,12 +271,12 @@ pub async fn post_start_upgrade(config: &Config, server: &KrillServer) -> Result pub async fn update_storage_version(work_dir: &Path) -> Result<(), UpgradeError> { let current = KrillVersion::current(); - if needs_v0_9_0_upgrade(work_dir, "cas") { + if needs_v0_9_0_upgrade(work_dir, CASERVER_DIR) { debug!("Updating version file for cas"); file::save_json(¤t, &work_dir.join("cas/version"))?; } - if needs_v0_9_0_upgrade(work_dir, "pubd") { + if needs_v0_9_0_upgrade(work_dir, PUBSERVER_DIR) { debug!("Updating version file for pubd"); file::save_json(¤t, &work_dir.join("pubd/version"))?; } @@ -191,15 +286,23 @@ pub async fn update_storage_version(work_dir: &Path) -> Result<(), UpgradeError> fn upgrade_data_to_0_9_0(config: Arc) -> Result<(), UpgradeError> { let work_dir = &config.data_dir; - if needs_v0_9_0_upgrade(work_dir, "pubd") { + if needs_v0_9_0_upgrade(work_dir, PUBSERVER_DIR) { PubdObjectsMigration::migrate(config.clone())?; } if needs_v0_9_0_upgrade(work_dir, "cas") { - let signer = Arc::new(KrillSigner::build(work_dir)?); - let repo_manager = RepositoryManager::build(config.clone(), signer)?; + // TODO: should we use the configured signers here or an OpenSSL signer? + // Will any signing actually be done using these signers? + let probe_interval = Duration::from_secs(config.signer_probe_retry_seconds); + let signer = KrillSignerBuilder::new(work_dir, probe_interval, &config.signers) + .with_default_signer(config.default_signer()) + .with_one_off_signer(config.one_off_signer()) + .build() + .unwrap(); + let signer = Arc::new(signer); + let repo_manager = RepositoryManager::build(config.clone(), signer.clone())?; - CaObjectsMigration::migrate(config, repo_manager)?; + CaObjectsMigration::migrate(config, repo_manager, signer)?; } Ok(()) @@ -228,7 +331,7 @@ mod tests { use std::{fs, path::PathBuf}; use crate::commons::util::file; - use crate::test::tmp_dir; + use crate::test::{init_config, tmp_dir}; use super::*; @@ -238,10 +341,10 @@ mod tests { let source = PathBuf::from("test-resources/migrations/v0_8_1/"); file::backup_dir(&source, &work_dir).unwrap(); - let config = Arc::new(Config::test(&work_dir, false, false, false)); - let _ = config.init_logging(); + let mut config = Config::test(&work_dir, false, false, false, false); + init_config(&mut config); - upgrade_data_to_0_9_0(config).unwrap(); + upgrade_data_to_0_9_0(Arc::new(config)).unwrap(); let _ = fs::remove_dir_all(work_dir); } @@ -252,11 +355,71 @@ mod tests { let source = PathBuf::from("test-resources/migrations/v0_6_0/"); file::backup_dir(&source, &work_dir).unwrap(); - let config = Arc::new(Config::test(&work_dir, false, false, false)); - let _ = config.init_logging(); + let mut config = Config::test(&work_dir, false, false, false, false); + init_config(&mut config); - upgrade_data_to_0_9_0(config).unwrap(); + upgrade_data_to_0_9_0(Arc::new(config)).unwrap(); let _ = fs::remove_dir_all(work_dir); } + + #[cfg(all(feature = "hsm", not(any(feature = "hsm-tests-kmip", feature = "hsm-tests-pkcs11"))))] + fn unmapped_keys_test_core(do_upgrade: bool) { + let expected_key_id = KeyIdentifier::from_str("5CBCAB14B810C864F3EEA8FD102B79F4E53FCC70").unwrap(); + + // Place a key previously created by an OpenSSL signer in the KEYS_DIR under the Krill data dir. + // Then run the upgrade. It should find the key and add it to the mapper. + let work_dir = tmp_dir(); + let source = PathBuf::from("test-resources/migrations/unmapped_keys/"); + file::backup_dir(&source, &work_dir).unwrap(); + + let mut config = Config::test(&work_dir, false, false, false, false); + let _ = config.init_logging(); + config.process().unwrap(); + let config = Arc::new(config); + + if do_upgrade { + record_preexisting_openssl_keys_in_signer_mapper(config.clone()).unwrap(); + } + + // Now test that a newly initialized `KrillSigner` with a default OpenSSL signer + // is associated with the newly created mapper store and is thus able to use the + // key that we placed on disk. + let probe_interval = Duration::from_secs(config.signer_probe_retry_seconds); + let krill_signer = KrillSignerBuilder::new(&work_dir, probe_interval, &config.signers) + .with_default_signer(config.default_signer()) + .with_one_off_signer(config.one_off_signer()) + .build() + .unwrap(); + + // Trigger the signer to be bound to the one the migration just registered in the mapper + krill_signer.random_serial().unwrap(); + + // Verify that the mapper has a single registered signer + let mapper = krill_signer.get_mapper().unwrap(); + let signer_handles = mapper.get_signer_handles().unwrap(); + assert_eq!(1, signer_handles.len()); + + if do_upgrade { + // Verify that the mapper has a record of the test key belonging to the signer + mapper.get_signer_for_key(&expected_key_id).unwrap(); + } else { + // Verify that the mapper does NOT have a record of the test key belonging to the signer + assert!(mapper.get_signer_for_key(&expected_key_id).is_err()); + } + + let _ = fs::remove_dir_all(work_dir); + } + + #[cfg(all(feature = "hsm", not(any(feature = "hsm-tests-kmip", feature = "hsm-tests-pkcs11"))))] + #[test] + fn test_key_not_found_error_if_unmapped_keys_are_not_mapped_on_upgrade() { + unmapped_keys_test_core(false); + } + + #[cfg(all(feature = "hsm", not(any(feature = "hsm-tests-kmip", feature = "hsm-tests-pkcs11"))))] + #[test] + fn test_upgrading_with_unmapped_keys() { + unmapped_keys_test_core(true); + } } diff --git a/src/upgrades/v0_9_0/ca_objects_migration.rs b/src/upgrades/v0_9_0/ca_objects_migration.rs index 473df291..db899335 100644 --- a/src/upgrades/v0_9_0/ca_objects_migration.rs +++ b/src/upgrades/v0_9_0/ca_objects_migration.rs @@ -37,13 +37,15 @@ use crate::{ pub struct CaObjectsMigration; impl CaObjectsMigration { - pub fn migrate(config: Arc, repo_manager: RepositoryManager) -> UpgradeResult<()> { + pub fn migrate( + config: Arc, + repo_manager: RepositoryManager, + signer: Arc, + ) -> UpgradeResult<()> { let repo_manager = Arc::new(repo_manager); let store = KeyValueStore::disk(&config.data_dir, CASERVER_DIR)?; let ca_store = AggregateStore::::disk(&config.data_dir, CASERVER_DIR)?; - let signer = Arc::new(KrillSigner::build(&config.data_dir)?); - if store.version_is_before(KrillVersion::release(0, 6, 0))? { Err(UpgradeError::custom("Cannot upgrade Krill installations from before version 0.6.0. Please upgrade to any version ranging from 0.6.0 to 0.8.1 first, and then upgrade to this version.")) } else if store.version_is_before(KrillVersion::candidate(0, 9, 0, 1))? { diff --git a/test-resources/migrations/unmapped_keys/keys/5CBCAB14B810C864F3EEA8FD102B79F4E53FCC70 b/test-resources/migrations/unmapped_keys/keys/5CBCAB14B810C864F3EEA8FD102B79F4E53FCC70 new file mode 100644 index 00000000..af94871a --- /dev/null +++ b/test-resources/migrations/unmapped_keys/keys/5CBCAB14B810C864F3EEA8FD102B79F4E53FCC70 @@ -0,0 +1 @@ +"MIIEowIBAAKCAQEAz1J6um7J2kbecEFWZkATKjc/ObUL1rw+jzCWWZw6tkimlIfkOMRqEWkCyvAZpjtn7Jmfi49O1n6/rZ39IrUTPrBNPxcgh1YpOAyUTdPx+msJkRIw5kbKSKRcmi4GwCOwlgazsfBDeuta8ghOfTwl140UQAwFHC8GKdo4+oEczyeuKY5H9oMtESzOM3WNQybnjAPb6qUJ+E6wv1pGHIxsuTiP7wTE/bRrvLA/WjZ67GO6eVEzZjJeqxYPe5763Gtem+OHwva84aEWonjn4wmVl3Z/2DdvjuLV7qgjbD5SE+pREkOYK5WlS3RUpw4WzBxunJpvHWi2oVC+fJ5GjMBKjQIDAQABAoIBADCdnrnpENxmkfKw35nKnLPhkKnvkn8gVWsucZNB12K3Vpit5Q6KwHSks0rzUpBwjcYloAwR5uLVhg0oZxukaobgwLxyp+YUhkXmbUBy2uNmBjBGm8DkCLtQD4b4FA0qUVZc6/Zl6O0CE6l92Sn02MrbAkyihPA+9qsJySK09BR/RVfz0m4wkWc+00iT2YkmHnwReHqgUYzaUtuplMhqHJRXx5h6kLwjbzMlkMI6bKRN08cG3LYpwpG9xntH3c4Lm0dQ7rR1R0QKvQ5ruUIUNLoISnyA3oSb+H31p3ZPv3JTQOORo+dECYGoPOL4CS+VtpalU6ZcHHq3C43gyK9VZwECgYEA88IAC6mHCtJvG2JICXjDmsa9V5zFMvVkvRJFUrqo1U0jeJYf9WzyF6y1hIM3ZM37bf4XArTNteyyN696drkj8ZprUBom9EWXZ6myKAsoSOlAPJDoW9bsVULYoPskD8GPkiama5OHQez8C4SyDJpCDZZoupvzP0uOq7u4ZzVjFlkCgYEA2bwGiH6CVlq4/fznHcKqEbLApOK3hkzxr5ydzisizwTdDgOzv9yma6x8MoOnSMe+pwUcixhugiwPe7HIreJbIHar9PAKn88LZcpjNOK3eXyN4AziJmrlF+S+wuAf5LxjOJwlgxExsLNUdkHcb+GlgO9XP04n/hWZdOMTEqVa91UCgYA9QiXsyF/tslDF2v8rkjPmM8fluro8Qjtc4LiXP92qLUaayW+iqAx3MqozcR58sN9dFxwFTBbef2uSXwEx0PldgGczrD7Z6JtSK+1f7t3i5OuFzQz3951RGAmE+1hmtUg1fI0aP+sgAESfSGvOiv3KmGAddY+9+mkSc4y2M06BkQKBgBya6TjRdgomTRuBOFPemT1OAKUo6/eG8FGj/89wN1h/SRNoRRs1v99xf7ebOEUFiPSkZUWY5hvP8hDMIJNHo7U2Ig1B5ANuzg6HclUmW/hn9rOyvZtSe9qRwMYVb9s7LMXpLbx0a9TNGpGWwYEpYfb4ervkZrio2cpTctQGSVcpAoGBANwGRqjtSIWhWS349i4z4+xbgOXVorGfKAEvB0T5PXLAuobk1kptvbgv2831azXda+ZjspVHtL74JFdt7LvuAUhINaUtJKmf0leA2dxjUYuWPh/IKlYPMCMOfTs8NAzCl05J1zR+1ZcxkPZ80iWCVO6w3OEisCVChACwBQ6UMN90" \ No newline at end of file diff --git a/test-resources/pykmip/README.md b/test-resources/pykmip/README.md new file mode 100644 index 00000000..f028bb52 --- /dev/null +++ b/test-resources/pykmip/README.md @@ -0,0 +1,48 @@ +The files created in this directory were created and can be used like so: + +Tested on Ubuntu 18:04 + +``` +apt update +apt install -y python3-pip +pip3 install pykmip + +mkdir /etc/pykmip +cd /etc/pykmip +cat <san.cnf +[ext] +subjectAltName = DNS:localhost +EOF + +mkdir demoCA +touch demoCA/index.txt +echo 01 > demoCA/serial +openssl ecparam -out ca.key -name secp256r1 -genkey +openssl req -x509 -new -key ca.key -out ca.crt -outform PEM -days 3650 -subj "/C=NL/ST=Noord Holland/L=Amsterdam/O=NLnet Labs/CN=localhost" + +openssl ecparam -out server.key -name secp256r1 -genkey + +openssl req -new -nodes -key server.key -outform pem -out server.csr -subj "/C=NL/ST=Noord Holland/L=Amsterdam/O=NLnet Labs/CN=localhost" + +openssl ca -keyfile ca.key -cert ca.crt -in server.csr -out server.crt -outdir . -batch -noemailDN -extfile san.cnf -extensions ext + +openssl pkcs8 -topk8 -nocrypt -in server.key -out server.pkcs8.key + +mv server.pkcs8.key server.key + +cat <server.conf +[server] +hostname=localhost +port=5696 +certificate_path=/etc/pykmip/server.crt +key_path=/etc/pykmip/server.key +ca_path=/etc/pykmip/ca.crt +auth_suite=TLS1.2 +enable_tls_client_auth=False +tls_cipher_suites=TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256 +logging_level=DEBUG +database_path=/tmp/pykmip.db +EOF + +pykmip-server +``` diff --git a/test-resources/pykmip/ca.crt b/test-resources/pykmip/ca.crt new file mode 100644 index 00000000..3c23e8f9 --- /dev/null +++ b/test-resources/pykmip/ca.crt @@ -0,0 +1,14 @@ +-----BEGIN CERTIFICATE----- +MIICGTCCAb+gAwIBAgIUf8251O2yXLWfeanKzrtm1TonKpYwCgYIKoZIzj0EAwIw +YjELMAkGA1UEBhMCTkwxFjAUBgNVBAgMDU5vb3JkIEhvbGxhbmQxEjAQBgNVBAcM +CUFtc3RlcmRhbTETMBEGA1UECgwKTkxuZXQgTGFiczESMBAGA1UEAwwJbG9jYWxo +b3N0MB4XDTIxMDkyODE5NTkyNloXDTMxMDkyNjE5NTkyNlowYjELMAkGA1UEBhMC +TkwxFjAUBgNVBAgMDU5vb3JkIEhvbGxhbmQxEjAQBgNVBAcMCUFtc3RlcmRhbTET +MBEGA1UECgwKTkxuZXQgTGFiczESMBAGA1UEAwwJbG9jYWxob3N0MFkwEwYHKoZI +zj0CAQYIKoZIzj0DAQcDQgAEqe0CtnUnyNNcoVGZkNBSsB1eboeMy2469zzh8PJz +vnH6M1o7eEEnX2q9jW2ATzwCy48Zc3V5yMq7+1Ppc5FFEqNTMFEwHQYDVR0OBBYE +FPONAMHT4HJ+QDHWJaGdS37/WcpiMB8GA1UdIwQYMBaAFPONAMHT4HJ+QDHWJaGd +S37/WcpiMA8GA1UdEwEB/wQFMAMBAf8wCgYIKoZIzj0EAwIDSAAwRQIhAKldVHNF +yLWv1AvTy7mTnDuu2oBgpRJfx94Roig36OsEAiBIlHuX4b9YDXQpvn/W8ypZudcZ +GkhXkkl8izmT6ymsOA== +-----END CERTIFICATE----- diff --git a/test-resources/pykmip/run-server.py b/test-resources/pykmip/run-server.py new file mode 100755 index 00000000..295e0778 --- /dev/null +++ b/test-resources/pykmip/run-server.py @@ -0,0 +1,10 @@ +#!/usr/bin/python3 +from kmip.services.server import KmipServer + +server = KmipServer( + config_path='./server.conf', + log_path='./server.log' +) + +with server: + server.serve() diff --git a/test-resources/pykmip/server.conf b/test-resources/pykmip/server.conf new file mode 100644 index 00000000..0bd21639 --- /dev/null +++ b/test-resources/pykmip/server.conf @@ -0,0 +1,11 @@ +[server] +hostname=127.0.0.1 +port=5696 +certificate_path=./server.crt +key_path=./server.key +ca_path=./ca.crt +auth_suite=TLS1.2 +enable_tls_client_auth=False +tls_cipher_suites=TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256 +logging_level=DEBUG +database_path=/tmp/pykmip.db diff --git a/test-resources/pykmip/server.crt b/test-resources/pykmip/server.crt new file mode 100644 index 00000000..e1136f9f --- /dev/null +++ b/test-resources/pykmip/server.crt @@ -0,0 +1,41 @@ +Certificate: + Data: + Version: 3 (0x2) + Serial Number: 1 (0x1) + Signature Algorithm: ecdsa-with-SHA256 + Issuer: C=NL, ST=Noord Holland, L=Amsterdam, O=NLnet Labs, CN=localhost + Validity + Not Before: Sep 28 19:59:26 2021 GMT + Not After : Sep 28 19:59:26 2022 GMT + Subject: C=NL, ST=Noord Holland, O=NLnet Labs, CN=localhost + Subject Public Key Info: + Public Key Algorithm: id-ecPublicKey + Public-Key: (256 bit) + pub: + 04:74:94:a6:97:4b:18:33:71:bb:29:40:75:b0:8a: + 66:ee:75:09:a8:fb:b0:83:71:a7:82:b5:4b:15:76: + d8:bd:2e:16:5c:b3:26:d8:bc:73:9b:fe:10:8b:95: + 4f:41:06:4a:ee:ac:f6:d7:bc:96:a7:ff:ef:39:52: + 0b:ea:25:c7:1d + ASN1 OID: prime256v1 + NIST CURVE: P-256 + X509v3 extensions: + X509v3 Subject Alternative Name: + DNS:localhost + Signature Algorithm: ecdsa-with-SHA256 + 30:45:02:21:00:d8:e6:2e:5b:40:77:dd:3f:1f:9f:5f:79:69: + 63:6c:93:db:c7:84:04:83:21:ed:68:b2:9d:eb:96:00:3e:d9: + db:02:20:50:3d:9a:e1:e3:37:db:94:d5:8d:e6:9f:ba:1f:15: + 18:f3:04:84:96:8e:c4:79:f4:15:90:3e:e5:e5:f0:b6:f9 +-----BEGIN CERTIFICATE----- +MIIBtzCCAV2gAwIBAgIBATAKBggqhkjOPQQDAjBiMQswCQYDVQQGEwJOTDEWMBQG +A1UECAwNTm9vcmQgSG9sbGFuZDESMBAGA1UEBwwJQW1zdGVyZGFtMRMwEQYDVQQK +DApOTG5ldCBMYWJzMRIwEAYDVQQDDAlsb2NhbGhvc3QwHhcNMjEwOTI4MTk1OTI2 +WhcNMjIwOTI4MTk1OTI2WjBOMQswCQYDVQQGEwJOTDEWMBQGA1UECAwNTm9vcmQg +SG9sbGFuZDETMBEGA1UECgwKTkxuZXQgTGFiczESMBAGA1UEAwwJbG9jYWxob3N0 +MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEdJSml0sYM3G7KUB1sIpm7nUJqPuw +g3GngrVLFXbYvS4WXLMm2Lxzm/4Qi5VPQQZK7qz217yWp//vOVIL6iXHHaMYMBYw +FAYDVR0RBA0wC4IJbG9jYWxob3N0MAoGCCqGSM49BAMCA0gAMEUCIQDY5i5bQHfd +Px+fX3lpY2yT28eEBIMh7WiyneuWAD7Z2wIgUD2a4eM325TVjeafuh8VGPMEhJaO +xHn0FZA+5eXwtvk= +-----END CERTIFICATE----- diff --git a/test-resources/pykmip/server.key b/test-resources/pykmip/server.key new file mode 100644 index 00000000..3a9c140c --- /dev/null +++ b/test-resources/pykmip/server.key @@ -0,0 +1,5 @@ +-----BEGIN PRIVATE KEY----- +MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgt8aDv687Trr5Dsfm +bpJIPM5B9VCGCKyv8XiVIUHZGeehRANCAAR0lKaXSxgzcbspQHWwimbudQmo+7CD +caeCtUsVdti9LhZcsybYvHOb/hCLlU9BBkrurPbXvJan/+85UgvqJccd +-----END PRIVATE KEY----- diff --git a/tests/auth_check.rs b/tests/auth_check.rs index 0289f1c9..43696038 100644 --- a/tests/auth_check.rs +++ b/tests/auth_check.rs @@ -15,7 +15,7 @@ async fn auth_check() { // bearer token sent by the test suite support functions not to match and thus be rejected which in turn should // cause a Rust panic. let dir = tmp_dir(); - let mut config = test_config(&dir, false, false, false); + let mut config = test_config(&dir, false, false, false, false); config.admin_token = Token::from("wrong secret"); // Start Krill with the customized config diff --git a/tests/e2e/simple_rp_roa_test.py b/tests/e2e/simple_rp_roa_test.py index 58493a58..40b92976 100644 --- a/tests/e2e/simple_rp_roa_test.py +++ b/tests/e2e/simple_rp_roa_test.py @@ -46,7 +46,7 @@ def krill_with_roas(docker_project, krill_api_config, class_service_manager): return krill_other_api.is_authorized() @retry( - stop_max_attempt_number=3, + stop_max_attempt_number=5, wait_exponential_multiplier=1000, wait_exponential_max=10000, retry_on_result=retry_if_not, @@ -58,7 +58,7 @@ def krill_with_roas(docker_project, krill_api_config, class_service_manager): return [ca for ca in cas if matcher_func(ca)] @retry( - stop_max_attempt_number=3, + stop_max_attempt_number=5, wait_exponential_multiplier=1000, wait_exponential_max=10000, retry_on_result=retry_if_not, @@ -69,7 +69,7 @@ def krill_with_roas(docker_project, krill_api_config, class_service_manager): return f(ca) @retry( - stop_max_attempt_number=3, + stop_max_attempt_number=5, wait_exponential_multiplier=1000, wait_exponential_max=10000, retry_on_result=retry_if_not, @@ -219,6 +219,7 @@ def krill_with_roas(docker_project, krill_api_config, class_service_manager): update_roas() logging.info('Krill configuration complete') + except RetryError as e: if e.last_attempt.has_exception: (ex_type, ex_value, traceback) = e.last_attempt.value @@ -265,7 +266,7 @@ class TestKrillWithRelyingParties: logging.info(f'Connecting RTR client to {docker_host_fqdn}:{service.rtr_port}') received_roas = set(rtr_fetch_one(docker_host_fqdn, service.rtr_port, service.rtr_timeout_seconds)) rtr_elapsed_time = int(time()) - rtr_start_time - + # r is now a list of PFXRecord # see: https://python-rtrlib.readthedocs.io/en/latest/api.html#rtrlib.records.PFXRecord logging.info(f'Received {len(received_roas)} ROAs via RTR from {service.name} in {rtr_elapsed_time} seconds') @@ -273,10 +274,10 @@ class TestKrillWithRelyingParties: if len(received_roas) == 0: # retry, maybe the ROAs are not available yet raise UpdateWasEmpty() - + # are each of the TEST_ROAS items in r? # i.e. is the intersection of the two sets equal to that of the TEST_ROAS set? - + logging.info(f'Comparing {len(received_roas)} received ROAs to {len(TEST_ROAS)} expected ROAs...') expected_roas = set([roa_to_roa_string(r) for r in TEST_ROAS]) assert received_roas == expected_roas @@ -287,7 +288,7 @@ class TestKrillWithRelyingParties: logging.error(f'{service.name} is not ready') except Exception as innerE: logging.error(f'Unable to determine if {service.name} is ready: {innerE}') - + raise e fetch_from_rtr_server() diff --git a/tests/functional.rs b/tests/functional.rs index 5816608e..7d1db70c 100644 --- a/tests/functional.rs +++ b/tests/functional.rs @@ -59,7 +59,7 @@ async fn functional() { info("# #"); info("##################################################################"); info(""); - let krill_dir = start_krill_with_default_test_config(true, false, false).await; + let krill_dir = start_krill_with_default_test_config(true, false, false, false).await; let ta = ta_handle(); let testbed = handle("testbed"); diff --git a/tests/migrate_repository.rs b/tests/migrate_repository.rs index e7d5676a..284baec6 100644 --- a/tests/migrate_repository.rs +++ b/tests/migrate_repository.rs @@ -28,7 +28,7 @@ async fn migrate_repository() { info("# #"); info("##################################################################"); info(""); - let krill_dir = start_krill_with_default_test_config(true, false, false).await; + let krill_dir = start_krill_with_default_test_config(true, false, false, false).await; info("##################################################################"); info("# #"); diff --git a/tests/suspend.rs b/tests/suspend.rs index 102eeb4f..70caff80 100644 --- a/tests/suspend.rs +++ b/tests/suspend.rs @@ -23,7 +23,7 @@ async fn test_suspension() { // testbed enabled // ca_refresh disabled (we will trigger individual CA refreshes manually) // suspend enabled - let krill_dir = start_krill_with_default_test_config(true, false, true).await; + let krill_dir = start_krill_with_default_test_config(true, false, true, false).await; let testbed = handle("testbed"); let ca = handle("CA"); @@ -71,7 +71,7 @@ async fn test_suspension() { // Wait a bit, and then refresh testbed only, it should find that // the child 'CA' has not been updating, and will suspend it. { - sleep_seconds(5).await; + sleep_seconds(15).await; cas_refresh_single(&testbed).await; expect_suspended(&testbed, &ca).await; diff --git a/tests/testbed.rs b/tests/testbed.rs index 2dcf260a..833b193c 100644 --- a/tests/testbed.rs +++ b/tests/testbed.rs @@ -14,7 +14,7 @@ async fn add_and_remove_certificate_authority() { use krill::daemon::ca::testbed_ca_handle; use krill::test::*; - let dir = start_krill_with_default_test_config(true, true, false).await; + let dir = start_krill_with_default_test_config(true, true, false, false).await; // ------------------------------------------------------------------------- // establish/verify starting conditions