diff --git a/certificates/client_server/ca/ca.srl b/certificates/client_server/ca/ca.srl index a7f72e5..ffe1aa0 100644 --- a/certificates/client_server/ca/ca.srl +++ b/certificates/client_server/ca/ca.srl @@ -1 +1 @@ -640BF08B2D2C8FC41FC2300EF05F7A989419005F +640BF08B2D2C8FC41FC2300EF05F7A9894190060 diff --git a/certificates/client_server/co3/co3.client.acme.com.crt b/certificates/client_server/co3/co3.client.acme.com.crt index f29ed7d..ae5a42e 100644 --- a/certificates/client_server/co3/co3.client.acme.com.crt +++ b/certificates/client_server/co3/co3.client.acme.com.crt @@ -1,8 +1,8 @@ -----BEGIN CERTIFICATE----- -MIIDhjCCAm6gAwIBAgIUZAvwiy0sj8QfwjAO8F96mJQZAF8wDQYJKoZIhvcNAQEL +MIIDhjCCAm6gAwIBAgIUZAvwiy0sj8QfwjAO8F96mJQZAGAwDQYJKoZIhvcNAQEL BQAwWjELMAkGA1UEBhMCRlIxDDAKBgNVBAgMA0lkRjEOMAwGA1UEBwwFUGFyaXMx ETAPBgNVBAoMCEFjbWVUZXN0MRowGAYDVQQDDBFBY21lIFRlc3QgUm9vdCBDQTAe -Fw0yNjA4MTMxMTU0NTBaFw0zNjA4MTAxMTU0NTBaMFwxCzAJBgNVBAYTAkZSMQww +Fw0yNjA4MTMxODI5NTNaFw0zNjA4MTAxODI5NTNaMFwxCzAJBgNVBAYTAkZSMQww CgYDVQQIDANJZEYxDjAMBgNVBAcMBVBhcmlzMREwDwYDVQQKDAhBY21lVGVzdDEc MBoGA1UEAwwTY28zLmNsaWVudEBhY21lLmNvbTCCASIwDQYJKoZIhvcNAQEBBQAD ggEPADCCAQoCggEBALl3txZ9MNI7mJ5Y6UkvjPlKOiA92NJZSsuvwD6ZzfHXC0EP @@ -12,10 +12,10 @@ PeXeU25Ft0DCFXawE1g/a0+CefpGJ6ee8YH5Ne0CwkOecmfW4/ou4dEjnVzX5N7O sHPUUsN26TSZp0dcyBax3ws1ZUDGbKtVngC85+JWkLi9t4+NeA+wVhiJfiP3I2ko 4trF9Q7s/20HjG6hwwt0JdUpGZG3n50w6lh6bEsCAwEAAaNCMEAwHQYDVR0OBBYE FPDF8CLmuTR3/P38UVZndL035uWlMB8GA1UdIwQYMBaAFCrsQdVEgrwm8ED+4pCs -lsN3SX3/MA0GCSqGSIb3DQEBCwUAA4IBAQB5ZxqHo5SucStiqn8Z4x0XEWntliTE -s3QEIUl01icjoRdZfFrhLxS7sKp0wbOvxfzsIl3lHAXxxcPW6rQdKcuWwNYY6cOY -B9dfTJbVgGGaAg1vczqCFQ55XOqYkzua7UeZWPUTTpUPI9cD4pEHr25yGgcLs3Jt -yGWkyP3iZEVXHSYCRkDKUMugyC5R29aCjoCyA1sbA2A7PkHz9RB61S7srhYl64Ui -/lC30jOkAH4Rvl8IylISJzfut1WVM9U5qfclCYXCfSDm/BF+68aRePw06ICE0A4+ -RayVj57mkBTePSpcS2c05eFVOyyrRWul1whq0uy9dJN6dWa3m7gh5q8C +lsN3SX3/MA0GCSqGSIb3DQEBCwUAA4IBAQCi2DoSBefV3t6TRiEDqUT/sufFKbb+ +2Ddl2Gz3Ev5lG854VkkmwzDbHuIy0PvdnseRHetKE8U/Avt+o6KWJCEblBw85Q5Q +FmvOoXtD9YdrQediOr5m6ARKTwjHb5C7vwTQluXL16SuC9ATVWBW5/osaoAu+G7w +OYhcLnSwj15Faf+sL7kdD+k1497xTlzdlQK0g5Cw523IKxwqqrp51or9d9XEKRcO +u0w+3pJJvjkE8hoKTQ1o1cXVxl28q+dZAKd9ThdTEKIkqzdcTRJpV7rXwucY1UOu +yh3Sfh/RDTKlCCNshDfJJ2nNQjRSPNKgjnVrB4s6mlSbjmSBfrZud2Td -----END CERTIFICATE----- diff --git a/certificates/client_server/co3/co3.client.acme.com.p12 b/certificates/client_server/co3/co3.client.acme.com.p12 new file mode 100644 index 0000000..7c8b7d6 Binary files /dev/null and b/certificates/client_server/co3/co3.client.acme.com.p12 differ diff --git a/certificates/client_server/generate_certs.sh b/certificates/client_server/generate_certs.sh index fe441b8..0e5bf1d 100755 --- a/certificates/client_server/generate_certs.sh +++ b/certificates/client_server/generate_certs.sh @@ -1,56 +1,81 @@ #!/bin/bash +set -euo pipefail -# on MacOS, you should pass a link to an actually installed openssl binary, and not use the default `libressl` -# which generates PKCS12 files with the deprecated RC2 algorithm +# On macOS, pass the path to a modern OpenSSL binary as the first argument. +# The system `libressl` generates PKCS12 files with the deprecated RC2 algorithm. OPENSSL_BIN=${1:-openssl} -# Generate CA private key -$OPENSSL_BIN genpkey -algorithm RSA -pkeyopt rsa_keygen_bits:2048 -out ca.key - -# Generate self-signed CA certificate -$OPENSSL_BIN req -new -x509 -days 3650 -key ca.key -subj "/C=FR/ST=IdF/L=Paris/O=AcmeTest/CN=Acme Test Root CA" -out ca.crt - - -## Server Cert - -# Generate private key for kmserver.acme.com -$OPENSSL_BIN genpkey -algorithm RSA -pkeyopt rsa_keygen_bits:2048 -out kmserver.acme.com.key - -# Generate certificate signing request for kmserver.acme.com -$OPENSSL_BIN req -new -key kmserver.acme.com.key -subj "/C=FR/ST=IdF/L=Paris/O=AcmeTest/CN=kmserver.acme.com" -out kmserver.acme.com.csr - -# Generate certificate for kmserver.acme.com signed by our own CA -$OPENSSL_BIN x509 -req -days 3650 -in kmserver.acme.com.csr -CA ca.crt -CAkey ca.key -CAcreateserial -out kmserver.acme.com.crt - -# Generate a PKCS12 file -$OPENSSL_BIN pkcs12 -export -out kmserver.acme.com.p12 -inkey kmserver.acme.com.key -in kmserver.acme.com.crt -certfile ca.crt -password pass:password - - -## "owner" client cert - -# Generate private key for owner.client.acme.com -$OPENSSL_BIN genpkey -algorithm RSA -pkeyopt rsa_keygen_bits:2048 -out owner.client.acme.com.key - -# Generate certificate signing request for owner.client.acme.com -$OPENSSL_BIN req -new -key owner.client.acme.com.key -subj "/C=FR/ST=IdF/L=Paris/O=AcmeTest/CN=owner.client@acme.com" -out owner.client.acme.com.csr - -# Generate certificate for owner.client.acme.com signed by our own CA -$OPENSSL_BIN x509 -req -days 3650 -in owner.client.acme.com.csr -CA ca.crt -CAkey ca.key -CAcreateserial -out owner.client.acme.com.crt - -# Generate a PKCS12 file -$OPENSSL_BIN pkcs12 -export -out owner.client.acme.com.p12 -inkey owner.client.acme.com.key -in owner.client.acme.com.crt -certfile ca.crt -password pass:password - - -## "user" client cert - -# Generate private key for user.client.acme.com -$OPENSSL_BIN genpkey -algorithm RSA -pkeyopt rsa_keygen_bits:2048 -out user.client.acme.com.key - -# Generate certificate signing request for user.client.acme.com -$OPENSSL_BIN req -new -key user.client.acme.com.key -subj "/C=FR/ST=IdF/L=Paris/O=AcmeTest/CN=user.client@acme.com" -out user.client.acme.com.csr - -# Generate certificate for user.client.acme.com signed by our own CA -$OPENSSL_BIN x509 -req -days 3650 -in user.client.acme.com.csr -CA ca.crt -CAkey ca.key -CAcreateserial -out user.client.acme.com.crt - -# Generate a PKCS12 file -$OPENSSL_BIN pkcs12 -export -out user.client.acme.com.p12 -inkey user.client.acme.com.key -in user.client.acme.com.crt -certfile ca.crt -password pass:password +# --------------------------------------------------------------------------- +# generate_cert BASENAME CN OUTDIR [CADIR] [PASSOUT] +# +# Issues a CA-signed RSA-2048 certificate and writes four files under OUTDIR: +# BASENAME.key private key +# BASENAME.csr certificate signing request +# BASENAME.crt signed certificate +# BASENAME.p12 PKCS12 bundle (password = PASSOUT, empty string = no password) +# +# Arguments: +# BASENAME filename stem, e.g. "owner.client.acme.com" +# CN X.509 Common Name, e.g. "owner.client@acme.com" +# OUTDIR output directory, e.g. "owner" +# CADIR directory containing ca.crt / ca.key (default: "ca") +# PASSOUT PKCS12 export password (default: empty = no password) +# --------------------------------------------------------------------------- +generate_cert() { + local basename="$1" + local cn="$2" + local outdir="$3" + local cadir="${4:-ca}" + local passout="${5:-password}" + + mkdir -p "$outdir" + + local key="${outdir}/${basename}.key" + local csr="${outdir}/${basename}.csr" + local crt="${outdir}/${basename}.crt" + local p12="${outdir}/${basename}.p12" + + echo "── Generating ${cn} (${outdir}) ──" + + "$OPENSSL_BIN" genpkey -algorithm RSA -pkeyopt rsa_keygen_bits:2048 \ + -out "$key" + + "$OPENSSL_BIN" req -new -key "$key" \ + -subj "/C=FR/ST=IdF/L=Paris/O=AcmeTest/CN=${cn}" \ + -out "$csr" + + "$OPENSSL_BIN" x509 -req -days 3650 \ + -in "$csr" -CA "${cadir}/ca.crt" -CAkey "${cadir}/ca.key" -CAcreateserial \ + -out "$crt" + + "$OPENSSL_BIN" pkcs12 -export \ + -out "$p12" -inkey "$key" -in "$crt" -certfile "${cadir}/ca.crt" \ + -passout "pass:${passout}" +} + + +# --------------------------------------------------------------------------- +# CA +# --------------------------------------------------------------------------- +mkdir -p ca +echo "── Generating CA ──" +"$OPENSSL_BIN" genpkey -algorithm RSA -pkeyopt rsa_keygen_bits:2048 -out ca/ca.key +"$OPENSSL_BIN" req -new -x509 -days 3650 -key ca/ca.key \ + -subj "/C=FR/ST=IdF/L=Paris/O=AcmeTest/CN=Acme Test Root CA" \ + -out ca/ca.crt + + +# --------------------------------------------------------------------------- +# Server cert +# --------------------------------------------------------------------------- +generate_cert "kmserver.acme.com" "kmserver.acme.com" "server" + + +# --------------------------------------------------------------------------- +# Client certs +# --------------------------------------------------------------------------- +generate_cert "owner.client.acme.com" "owner.client@acme.com" "owner" +generate_cert "user.client.acme.com" "user.client@acme.com" "user" +generate_cert "co3.client.acme.com" "co3.client@acme.com" "co3" + +echo "Done." diff --git a/certificates/csr/ca.srl b/certificates/csr/ca.srl index a57be3f..7b5c7bb 100644 --- a/certificates/csr/ca.srl +++ b/certificates/csr/ca.srl @@ -1 +1 @@ -184F7763E7B126D3F188B20D137DD2B2B2756744 +184F7763E7B126D3F188B20D137DD2B2B2756745 diff --git a/certificates/csr/extensions.ext b/certificates/csr/extensions.ext index 12cec5e..bd2b69e 100644 --- a/certificates/csr/extensions.ext +++ b/certificates/csr/extensions.ext @@ -1,4 +1,4 @@ [ v3_ca ] basicConstraints=CA:TRUE,pathlen:0 -keyUsage=keyCertSign,digitalSignature +keyUsage=keyCertSign,cRLSign,digitalSignature extendedKeyUsage=emailProtection diff --git a/certificates/csr/generate.sh b/certificates/csr/generate.sh index 69f0d5c..26cac36 100755 --- a/certificates/csr/generate.sh +++ b/certificates/csr/generate.sh @@ -44,10 +44,12 @@ openssl req -new -key intermediate.key -out intermediate.csr -subj "/C=FR/ST=IdF # ``` # [ v3_ca ] # basicConstraints=CA:TRUE,pathlen:0 -# keyUsage=keyCertSign,digitalSignature +# keyUsage=keyCertSign,cRLSign,digitalSignature # extendedKeyUsage=emailProtection # crlDistributionPoints=URI:http://cse.example.com/crl.pem # ``` +# `cRLSign` is required by RFC 5280 §4.2.1.3 for any CA that issues CRLs — +# without it the KMS server rejects CRL generation requests for this issuer. # The `crlDistributionPoints` option should be replaced with the URL of a publicly accessible HTTP uniformResourceIdentifier that contains the CRL for the intermediate certificate. # This will sign the CSR with the CA's private key and generate an intermediate certificate. The intermediate certificate will be stored in the file `intermediate.crt`. openssl x509 -req -in intermediate.csr -CA ca.crt -CAkey ca.key -out intermediate.crt -days 1825 -extensions v3_ca -extfile extensions.ext diff --git a/certificates/csr/intermediate.crt b/certificates/csr/intermediate.crt index f72a373..bbe5bce 100644 --- a/certificates/csr/intermediate.crt +++ b/certificates/csr/intermediate.crt @@ -1,8 +1,8 @@ -----BEGIN CERTIFICATE----- -MIIExzCCAy+gAwIBAgIUGE93Y+exJtPxiLINE33SsrJ1Z0QwDQYJKoZIhvcNAQEL +MIIExzCCAy+gAwIBAgIUGE93Y+exJtPxiLINE33SsrJ1Z0UwDQYJKoZIhvcNAQEL BQAwXTELMAkGA1UEBhMCRlIxDDAKBgNVBAgMA0lkRjEOMAwGA1UEBwwFUGFyaXMx EDAOBgNVBAoMB0Nvc21pYW4xDDAKBgNVBAsMA1ImRDEQMA4GA1UEAwwHVGVzdCBD -QTAeFw0yNjA2MDcwNzQ4NDFaFw0zMTA2MDYwNzQ4NDFaMGcxCzAJBgNVBAYTAkZS +QTAeFw0yNjA4MjMxODU0NDlaFw0zNjA4MjAxODU0NDlaMGcxCzAJBgNVBAYTAkZS MQwwCgYDVQQIDANJZEYxDjAMBgNVBAcMBVBhcmlzMRAwDgYDVQQKDAdDb3NtaWFu MQwwCgYDVQQLDANSJkQxGjAYBgNVBAMMEVRlc3QgSW50ZXJtZWRpYXRlMIIBojAN BgkqhkiG9w0BAQEFAAOCAY8AMIIBigKCAYEA8fqz6gS1zKOw0ITYtiQZyfBJ0j2z @@ -14,15 +14,15 @@ mT2jjBjl2A9U2Nt9pB/htRwK1T1arK6hsgzSOd6C9J4oV6QPZRuZgLdOWy5Tcnyz nhPaLzKnKZrmb8u4SpkIboIFD9L4Z2uBGM1QyHNFbznV13ph37QbQAb1/gdgFmvT RjU/4K0sWSPyCJ6z3iq7blwH4VpXSrCVe5eNkoEhx4+oFtJA6InR33On7zBFAvln Ru9er+PmddNGMcCDYkFqZvyre3rDeec/hTIvAgMBAAGjdTBzMA8GA1UdEwQIMAYB -Af8CAQAwCwYDVR0PBAQDAgKEMBMGA1UdJQQMMAoGCCsGAQUFBwMEMB0GA1UdDgQW +Af8CAQAwCwYDVR0PBAQDAgGGMBMGA1UdJQQMMAoGCCsGAQUFBwMEMB0GA1UdDgQW BBQEK8XsqNesHDBu2Xz6rEdg09OK9DAfBgNVHSMEGDAWgBQJbsVPDK4X9gjOFhy+ -sVf/0DzKGDANBgkqhkiG9w0BAQsFAAOCAYEAlhNiN9Lktl+wx/BN/sDPSm2a/4xI -ZpjUj/Yrnp1KWMmj9hgmPZx89XSbrW42FxKLId5o9+eCWrVZDQowlnVjrokgM3WM -wcQ67lqg50vW25UOni5HKFF0vDZiMBP+KVUYvGI1P10JM/6aayGl5r+gxqnDI1m8 -72mO8M4Q/nN7DEzZQ3tCnWo6b2wbHsrRRo1LYqTx4QDL4M2EQ/yseC9qCC8N0JPH -fQU7Crq89+VdEnq9WFemnoiEvOU7fpyh884SegmFGKojzW8QU5xMbzcooZseYdTD -mBTzavPwiU2evs6/YDJO5s9mZTtI/GRhBtHqIVnollMzxEYebcmB5Xy0dB4j0tsi -38UyjlO8QIXo2wQ0xaKD4dDksmvKPKcJgJsX21bMdbGHUMrMYm8qcyvPRr+HNm2j -+njI1i6RTL2qbcqVON9zlxYUYBcYto6GCvufOIna/eVlnKd4R761JtwbHHPZOIdh -Ilq5N2JdqLVERca4700lEht+R2+TESLEKFM+ +sVf/0DzKGDANBgkqhkiG9w0BAQsFAAOCAYEAVgVqVapRDa0rY1nNJ7qot25mdCA3 +ve/j9YcifNpYtk75cMu3+lz7M/DVLyVxbF71O2cc8hQP9pnMATXSxXbsVxvpwsTU +i2d5tvgGRvgJT2Wtkw+5nlrmW5X1SI1mqhB1B2DUhE42r7FDWGcieocowXTPyiuo +zEOMD6Blfx+OJwK2ovnGsaqTcANSrMl1K7ABaelktHkXD+QXa6g9bq5cPdg7QZBH +CsaQKLRmLWg4eZ5W7MeNuTyomMhHABB7MWk3FHqXBEyfg6R8MkzJv7zz5DMELco4 +DZtIZN2rEJodY/0GSk4lvbOsJEBpjm+aXLISzffO7cfxzNgQmF0FVzff0pRHCp9e +Kb5lLaZBia7XaK94QYWh4p17NdbNWaZIxhyx2C1vSvJc7lUdkisyeCowY+nZgD8w +JkKV0hcyAP5WQubP9oVkN2DI/XtBI9YlvlXRgJJvCkMZXPFP72Hwe3afbRzOq1kt +4O9nRSNxmvhKMT6rD0nLZ2pBJqumxlA8KXvK -----END CERTIFICATE----- diff --git a/certificates/csr/intermediate.p12 b/certificates/csr/intermediate.p12 index 8b4569c..e58e0a6 100644 Binary files a/certificates/csr/intermediate.p12 and b/certificates/csr/intermediate.p12 differ diff --git a/certificates/csr/intermediate.srl b/certificates/csr/intermediate.srl index ef4f7dd..a42e0cf 100644 --- a/certificates/csr/intermediate.srl +++ b/certificates/csr/intermediate.srl @@ -1 +1 @@ -459C68F5070C2DCB2E6F84F7D0E396B9AD5417F1 +459C68F5070C2DCB2E6F84F7D0E396B9AD5417F2 diff --git a/certificates/csr/leaf.crt b/certificates/csr/leaf.crt index 9db3676..b6ac516 100644 --- a/certificates/csr/leaf.crt +++ b/certificates/csr/leaf.crt @@ -1,8 +1,8 @@ -----BEGIN CERTIFICATE----- -MIIEyTCCAzGgAwIBAgIURZxo9QcMLcsub4T30OOWua1UF/EwDQYJKoZIhvcNAQEL +MIIEyTCCAzGgAwIBAgIURZxo9QcMLcsub4T30OOWua1UF/IwDQYJKoZIhvcNAQEL BQAwZzELMAkGA1UEBhMCRlIxDDAKBgNVBAgMA0lkRjEOMAwGA1UEBwwFUGFyaXMx EDAOBgNVBAoMB0Nvc21pYW4xDDAKBgNVBAsMA1ImRDEaMBgGA1UEAwwRVGVzdCBJ -bnRlcm1lZGlhdGUwHhcNMjYwNjA3MDc0ODUyWhcNMzEwNjA2MDc0ODUyWjBfMQsw +bnRlcm1lZGlhdGUwHhcNMjYwODIzMTg1NDQ5WhcNMzYwODIwMTg1NDQ5WjBfMQsw CQYDVQQGEwJGUjEMMAoGA1UECAwDSWRGMQ4wDAYDVQQHDAVQYXJpczEQMA4GA1UE CgwHQ29zbWlhbjEMMAoGA1UECwwDUiZEMRIwEAYDVQQDDAlUZXN0IExlYWYwggGi MA0GCSqGSIb3DQEBAQUAA4IBjwAwggGKAoIBgQCRHCUJdbJoDRdYOQw4LMsz08Iu @@ -14,15 +14,15 @@ BAuzTranVkPW+ZqsNJ2ct5i/vwQNtvdg4PUPUjLQ0knOnpuJsHHm7V66ZfPluFJn fb+fRkRf60jwKjf8LydqiI8XteaGIo6aPfUzcqGkHAjoKFohZCjRDqqUhNyIbg1x e6TaVPT+FierndR7QdKtte3QRGifhBLKKeKWX6zfNHsPDXP5GxyY42LWTVXSNFgJ XoLh6a7+GaEZOCaRxxKsenO1ot6AHSjnMjzr588CAwEAAaN1MHMwDwYDVR0TBAgw -BgEB/wIBADALBgNVHQ8EBAMCAoQwEwYDVR0lBAwwCgYIKwYBBQUHAwQwHQYDVR0O +BgEB/wIBADALBgNVHQ8EBAMCAYYwEwYDVR0lBAwwCgYIKwYBBQUHAwQwHQYDVR0O BBYEFEXqf3iWtpvlqUYcVC8NbU3QVTe5MB8GA1UdIwQYMBaAFAQrxeyo16wcMG7Z -fPqsR2DT04r0MA0GCSqGSIb3DQEBCwUAA4IBgQAsuH3BBIxT2qR/yeyKqVQsfVIa -2AmpzYd7AE8+017XgGoqvqA71r3gZXaf7/FhBzuIp2+PQTnyu+UarJLztIsnbEMD -BXxRq7t6FHqERf7qQlEZBwhVpE7AcM8/9PyI+qLbHHG5x4ZRsFO7YoxsUjQna7SU -qRhFkj3gwHOQGmbhV46fpQ5ioFqe1X5SIU5SyeezAhO25wn9iBHU7Z4J1v8MAukY -wY+q75xncHa/JK8GQ9v8JtS4buCQNkAEPew0t+AN85rI0t6DHWjHy6kHy8wiACfo -BSno+anWqtiqDyQpzRR8Tk9oXztjOlVhyHxG6r9j4VcUIku2UtJVuaZi3KDrLVwK -L7xURZ0QNEsL9JZ8ojO54F2GLOdF3CW/lG54tQktAWmKzLXZs1jZkIPA3jnzd3G5 -pfp7YlhMJqkHTly2w1qjXgZ8WThh9WbSpWP0RE92lO7H2VFSREwr8o73W6XZpssT -QRsJSRoUulDurvrfKsro5TNskcRX9ZNk9pBWlVo= +fPqsR2DT04r0MA0GCSqGSIb3DQEBCwUAA4IBgQBUPzMNDTt8sb3wsLEIEsLbRBdK +kyDknQv2k3ZhCJmvpctxlSoOPuJhTZi9pF1BCAq7Uy1hJLBWLKr4EYJbHeI3eMgd +O68NTXBmne9AwMKVb3mm51IHt6/bF4dz90KWpsrQswVOMOUMUjGy5zkVGU4S7pHz +86/5P/Hl4hlQUGTEHucsmt7SyLPL5bOfbNERg/m2Z/vtkteDwYqnjyrSbt69XCp2 +Y99B1iWzbseAuOmz95qiw0HNFumAVj1cgulmWK/C2Yeg5TrnuJ68YpRsYV0SzXZg +qaDjgyZ7DbxeQ+r3lzvXfTQHJR04QQtW7L8p3rO+faZawMBqVNIP6cGSsImrChe4 +eVmN4njoBwiavsUjAWuuJ7UNhN0zK1akUBIidWixbxn+iGxriBKIyZ2qF3+2u+hL +bby5x5Y/BgfOejCu/NvJgfzzpjgA8kjrsuySCyRELmXkxtIs4iBbIUcCj1cMasOX +DmNK4GoJAUpfRLdAObRvhtMjFJXCkJduM0Zh3/g= -----END CERTIFICATE----- diff --git a/certificates/csr/leaf.p12 b/certificates/csr/leaf.p12 index b8cbb04..09c26f6 100644 Binary files a/certificates/csr/leaf.p12 and b/certificates/csr/leaf.p12 differ diff --git a/configs/auth_verifier/kms_opa_rbac.toml b/configs/auth_verifier/kms_opa_rbac.toml new file mode 100644 index 0000000..edf9ecc --- /dev/null +++ b/configs/auth_verifier/kms_opa_rbac.toml @@ -0,0 +1,91 @@ +# ============================================================================= +# Authentication Verifier — KMS OPA RBAC configuration +# ============================================================================= +# +# Configures the Authentication Verifier for use with the Cosmian KMS +# OPA RBAC integration. The five role names declared here match exactly +# the roles evaluated by `test_data/opa/kms.rego`. +# +# Start from the `authentication/` directory: +# +# ./target/debug/auth_verifier \ +# ../test_data/configs/auth_verifier/kms_opa_rbac.toml +# +# Default super-admin credentials (realm "_"): +# username : admin +# password : change_me +# +# KMS realm seeded on first start: +# realm : kms +# admin : kms-admin / kms-admin-pass +# +# ============================================================================= + +host_name = "127.0.0.1" +host_port = 8443 + +# --------------------------------------------------------------------------- +# RBAC role names — must match the role names used in test_data/opa/kms.rego. +# Exposed at GET /public/roles so the KMS admin UI can list available roles. +# --------------------------------------------------------------------------- +roles = [ + "SuperAdmin", # Unrestricted, cross-domain (ANSI/INCITS 359 §4.2) + "DomainAdmin", # Full access within own domain + "CryptoOfficer", # Key lifecycle within own domain (FIPS 140-3 §7.4) + "Auditor", # Read-only metadata within own domain (NIST SP 800-53 AU-9) + "User", # Crypto-use only, no lifecycle (FIPS 140-3 §7.4) +] + +# Admin UI — serve the pre-built SPA at /admin-ui. +# Path is relative to the directory where auth_verifier is started (authentication/). +admin_ui_path = "admin-ui/dist" + +# --------------------------------------------------------------------------- +# Console logging +# --------------------------------------------------------------------------- +[log] +level = "info" + +# --------------------------------------------------------------------------- +# TLS — paths are relative to the authentication/ submodule root. +# Replace with production certificates for non-development environments. +# --------------------------------------------------------------------------- +[tls_params] +server_private_key = "server/src/tests/certificates/ec/auth.server.key.pem" +server_certificate = "server/src/tests/certificates/ec/auth.server.cert.pem" +server_ca_chain = "server/src/tests/certificates/ec/auth.ca.pem" + +# --------------------------------------------------------------------------- +# JWT signing — reuse the server TLS key for token signing (dev only). +# In production, use a dedicated signing key. +# --------------------------------------------------------------------------- +[session_jwt_params] +jwt_ec_private_key = "server/src/tests/certificates/ec/auth.server.key.pem" +jwt_ec_public_key = "server/src/tests/certificates/ec/auth.server.cert.pem" + +# --------------------------------------------------------------------------- +# Database — persistent SQLite for the KMS RBAC test environment. +# Switch to `sqlite::memory:` for fully ephemeral test runs. +# --------------------------------------------------------------------------- +[database_params] +auto_init_schema = true +backend = "sqlite" +connection_url = "sqlite:///tmp/kms_opa_rbac_auth.db" + +# --------------------------------------------------------------------------- +# Development seed — creates the `kms` realm and a realm-admin on first start. +# All operations are idempotent; the seed is safe to leave enabled. +# --------------------------------------------------------------------------- +[dev_seed] +realm_id = "kms" +admin_username = "kms-admin" +admin_password = "kms-admin-pass" + +# --------------------------------------------------------------------------- +# CORS — allow the KMS server origin on admin endpoints. +# Adjust to match the actual KMS public URL in your deployment. +# --------------------------------------------------------------------------- +allowed_origins = [ + "http://localhost:9998", + "http://127.0.0.1:9998", +] diff --git a/configs/auth_verifier/provision_opa_users.sh b/configs/auth_verifier/provision_opa_users.sh new file mode 100755 index 0000000..1fa0c8b --- /dev/null +++ b/configs/auth_verifier/provision_opa_users.sh @@ -0,0 +1,231 @@ +#!/usr/bin/env bash +# provision_opa_users.sh +# +# Provision the Cosmian Authentication Verifier with users required for KMS OPA +# RBAC testing. Works for both automated CI runs and the manual `opa.toml` setup. +# +# Creates two realms and 8 users covering every role in kms.rego plus two +# edge-case identities (no roles, unknown role): +# +# Realm ${REALM_A} (primary, default: kms-opa-test): +# kms-opa-super-admin SuperAdmin ${PASSWORD} +# kms-opa-officer CryptoOfficer ${PASSWORD} +# kms-opa-user User ${PASSWORD} +# kms-opa-auditor Auditor ${PASSWORD} +# kms-opa-no-roles (no roles) ${PASSWORD} — OPA deny edge-case +# kms-opa-unknown-role Hacker ${PASSWORD} — OPA deny edge-case +# +# Realm ${REALM_B} (cross-domain, default: kms-opa-other): +# kms-opa-domain-admin-other DomainAdmin ${PASSWORD} +# kms-opa-other-officer CryptoOfficer ${PASSWORD} +# +# Outputs shell export statements to STDOUT (suitable for `eval "$(…)"`): +# +# export KMS_TEST_OPA_SUPER_ADMIN_JWT="…" +# export KMS_TEST_OPA_OFFICER_JWT="…" +# export KMS_TEST_OPA_USER_ROLE_JWT="…" +# export KMS_TEST_OPA_AUDITOR_JWT="…" +# export KMS_TEST_OPA_NO_ROLES_JWT="…" +# export KMS_TEST_OPA_UNKNOWN_ROLE_JWT="…" +# export KMS_TEST_OPA_DOMAIN_ADMIN_OTHER_JWT="…" +# export KMS_TEST_OPA_OTHER_DOMAIN_JWT="…" +# +# Human-readable status messages are written to STDERR so they do not +# interfere with `eval "$(…)"`. +# +# ── Usage ───────────────────────────────────────────────────────────────────── +# +# CI (mise test:opa_rbac) — default realms, evaluate exports: +# eval "$(AUTH_URL=… CA_CERT=… bash provision_opa_users.sh)" +# +# Manual (opa.toml setup) — custom realms, read the summary table: +# REALM_A=acme.com REALM_B=partner.acme.com \ +# bash ../test_data/configs/auth_verifier/provision_opa_users.sh +# # Then login in the KMS Web UI with: +# # realm: acme.com username: kms-opa-officer password: change_me +# +# ── Environment ─────────────────────────────────────────────────────────────── +# +# AUTH_URL — auth verifier HTTPS URL (default: https://127.0.0.1:8443) +# REALM_A — primary realm ID (default: kms-opa-test) +# REALM_B — secondary realm ID (default: kms-opa-other) +# PASSWORD — password for all provisioned users (default: change_me) +# CA_CERT — path to the server CA certificate +# (default: authentication/server/src/tests/certificates/ec/auth.ca.pem +# relative to the repo root; override via env when running from an +# arbitrary directory) +# REPO_ROOT — repository root path (default: auto-detected) + +set -euo pipefail + +# ── Configuration ───────────────────────────────────────────────────────────── +AUTH_URL="${AUTH_URL:-https://127.0.0.1:8443}" +REALM_A="${REALM_A:-kms-opa-test}" +REALM_B="${REALM_B:-kms-opa-other}" +PASSWORD="${PASSWORD:-change_me}" +REPO_ROOT="${REPO_ROOT:-$(cd "$(dirname "$0")/../../../.." && pwd)}" +CA_CERT="${CA_CERT:-${REPO_ROOT}/authentication/server/src/tests/certificates/ec/auth.ca.pem}" + +COOKIE_JAR=$(mktemp /tmp/opa-admin-XXXXXX.txt) +trap 'rm -f "${COOKIE_JAR}"' EXIT + +# ── Helpers ─────────────────────────────────────────────────────────────────── + +# All status messages go to STDERR so STDOUT stays clean for `eval "$(…)"`. +log() { echo "$*" >&2; } +warn() { echo " ⚠ $*" >&2; } +fail() { echo " ✗ $*" >&2; exit 1; } + +# Convert a plain-text password to a JSON byte-array for the auth verifier API. +# The UserPass endpoint expects Vec (Rust serde_json → array of integers). +str_to_json_bytes() { + printf '%s' "$1" \ + | od -v -tu1 -An \ + | tr -s ' \n' ' ' \ + | sed 's/^ //; s/ $//' \ + | tr ' ' ',' +} + +# POST with admin session cookie; non-2xx responses are ignored (idempotent). +admin_post_idempotent() { + local path="$1" body="$2" + curl -sk --cacert "${CA_CERT}" \ + -b "${COOKIE_JAR}" -c "${COOKIE_JAR}" \ + -o /dev/null \ + -H "Content-Type: application/json" \ + -X POST "${AUTH_URL}${path}" \ + -d "${body}" || true +} + +# DELETE — idempotent (404 is fine). +admin_delete_idempotent() { + local path="$1" + curl -sk --cacert "${CA_CERT}" \ + -b "${COOKIE_JAR}" -c "${COOKIE_JAR}" \ + -o /dev/null \ + -X DELETE "${AUTH_URL}${path}" || true +} + +# Login and return the JWT from the `_ea_` cookie via STDOUT. +user_login_jwt() { + local realm="$1" username="$2" ****** + local cookie_output + cookie_output=$(mktemp /tmp/opa-login-XXXXXX.txt) + # shellcheck disable=SC2064 + trap "rm -f '${cookie_output}'" RETURN + + local http_status + http_status=$(curl -sk --cacert "${CA_CERT}" \ + -c "${cookie_output}" \ + -o /dev/null -w "%{http_code}" \ + -u "${username}:${password}" \ + -H "Content-Type: application/json" \ + -X POST "${AUTH_URL}/login?realm=${realm}" \ + -d '{"public_key_pem":null,"totp_code":null}') + + if [[ "${http_status}" != 2* ]]; then + fail "login '${username}' in realm '${realm}' → HTTP ${http_status}" + fi + + local jwt + jwt=$(awk '/\t_ea_\t/{print $NF}' "${cookie_output}" | head -1) + if [[ -z "${jwt}" ]]; then + fail "no '_ea_' cookie in login response for '${username}' in '${realm}'" + fi + printf '%s' "${jwt}" +} + +# ── Step 1: super-admin login ───────────────────────────────────────────────── +log "" +log "Auth Verifier: ${AUTH_URL}" +log "Realms: ${REALM_A} (primary) ${REALM_B} (cross-domain)" +log "" + +http_status=$(curl -sk --cacert "${CA_CERT}" \ + -c "${COOKIE_JAR}" -b "${COOKIE_JAR}" \ + -o /dev/null -w "%{http_code}" \ + -u "admin:change_me" \ + -H "Content-Type: application/json" \ + -X POST "${AUTH_URL}/login?realm=_" \ + -d '{"public_key_pem":null,"totp_code":null}') +if [[ "${http_status}" != 2* ]]; then + fail "super-admin login → HTTP ${http_status}. Is the auth verifier running at ${AUTH_URL}?" +fi +log " ✓ super-admin authenticated" + +# ── Step 2: Create realms (idempotent) ─────────────────────────────────────── +for realm in "${REALM_A}" "${REALM_B}"; do + admin_post_idempotent "/admins/realms" \ + "{\"id\":\"${realm}\",\"auth_params\":{\"username_password_params\":{\"allow_expired_passwords\":false}},\"session_max_age_seconds\":3600,\"session_max_stale_age_seconds\":7200}" + log " ✓ realm '${realm}' ready" +done + +# ── Step 3: Create realm admins (idempotent) ───────────────────────────────── +admin_post_idempotent "/admins" \ + '{"id":"kms-opa-officer","realms":["'"${REALM_A}"'"],"userpass":"kms-opa-officer"}' +admin_post_idempotent "/admins" \ + '{"id":"kms-opa-other-officer","realms":["'"${REALM_B}"'"],"userpass":"kms-opa-other-officer"}' + +# ── Step 4: Create users (delete-then-create for idempotency) ──────────────── +# +# Format: "username:password:realm:roles_json_array" +# Empty roles array ("[]") → OPA deny edge-case. +# Unknown role ("Hacker") → OPA deny edge-case (no matching rule in kms.rego). +USERS=( + "kms-opa-super-admin:${PASSWORD}:${REALM_A}:[\"SuperAdmin\"]" + "kms-opa-officer:${PASSWORD}:${REALM_A}:[\"CryptoOfficer\"]" + "kms-opa-user:${PASSWORD}:${REALM_A}:[\"User\"]" + "kms-opa-auditor:${PASSWORD}:${REALM_A}:[\"Auditor\"]" + "kms-opa-no-roles:${PASSWORD}:${REALM_A}:[]" + "kms-opa-unknown-role:${PASSWORD}:${REALM_A}:[\"Hacker\"]" + "kms-opa-domain-admin-other:${PASSWORD}:${REALM_B}:[\"DomainAdmin\"]" + "kms-opa-other-officer:${PASSWORD}:${REALM_B}:[\"CryptoOfficer\"]" +) + +for entry in "${USERS[@]}"; do + IFS=: read -r username password realm roles <<< "${entry}" + admin_delete_idempotent "/realms/${realm}/userpass/${username}" + password_bytes="[$(str_to_json_bytes "${password}")]" + admin_post_idempotent "/realms/${realm}/userpass" \ + "{\"realm\":\"${realm}\",\"username\":\"${username}\",\"password\":${password_bytes},\"change_password\":false,\"roles\":${roles}}" + log " ✓ ${username} (${roles}) → realm '${realm}'" +done + +# ── Step 5: Login as each user, capture JWTs ───────────────────────────────── +log "" +log "Obtaining JWTs…" + +SUPER_ADMIN_JWT=$(user_login_jwt "${REALM_A}" "kms-opa-super-admin" "${PASSWORD}") +OFFICER_JWT=$(user_login_jwt "${REALM_A}" "kms-opa-officer" "${PASSWORD}") +USER_JWT=$(user_login_jwt "${REALM_A}" "kms-opa-user" "${PASSWORD}") +AUDITOR_JWT=$(user_login_jwt "${REALM_A}" "kms-opa-auditor" "${PASSWORD}") +NO_ROLES_JWT=$(user_login_jwt "${REALM_A}" "kms-opa-no-roles" "${PASSWORD}") +UNKNOWN_JWT=$(user_login_jwt "${REALM_A}" "kms-opa-unknown-role" "${PASSWORD}") +DOM_ADMIN_OTHER_JWT=$(user_login_jwt "${REALM_B}" "kms-opa-domain-admin-other" "${PASSWORD}") +OTHER_OFFICER_JWT=$(user_login_jwt "${REALM_B}" "kms-opa-other-officer" "${PASSWORD}") + +log " ✓ all JWTs obtained" + +# ── Step 6: Summary table (to STDERR) ──────────────────────────────────────── +log "" +log " Realm Username Password Role" +log " ────────────── ──────────────────────────── ──────────── ─────────────────────" +log " ${REALM_A} kms-opa-super-admin ${PASSWORD} SuperAdmin" +log " ${REALM_A} kms-opa-officer ${PASSWORD} CryptoOfficer" +log " ${REALM_A} kms-opa-user ${PASSWORD} User" +log " ${REALM_A} kms-opa-auditor ${PASSWORD} Auditor" +log " ${REALM_A} kms-opa-no-roles ${PASSWORD} (none)" +log " ${REALM_A} kms-opa-unknown-role ${PASSWORD} Hacker (invalid)" +log " ${REALM_B} kms-opa-domain-admin-other ${PASSWORD} DomainAdmin" +log " ${REALM_B} kms-opa-other-officer ${PASSWORD} CryptoOfficer" +log "" + +# ── STDOUT: shell exports (caller does `eval "$(this script)"`) ────────────── +printf "export KMS_TEST_OPA_SUPER_ADMIN_JWT='%s'\n" "${SUPER_ADMIN_JWT}" +printf "export KMS_TEST_OPA_OFFICER_JWT='%s'\n" "${OFFICER_JWT}" +printf "export KMS_TEST_OPA_USER_ROLE_JWT='%s'\n" "${USER_JWT}" +printf "export KMS_TEST_OPA_AUDITOR_JWT='%s'\n" "${AUDITOR_JWT}" +printf "export KMS_TEST_OPA_NO_ROLES_JWT='%s'\n" "${NO_ROLES_JWT}" +printf "export KMS_TEST_OPA_UNKNOWN_ROLE_JWT='%s'\n" "${UNKNOWN_JWT}" +printf "export KMS_TEST_OPA_DOMAIN_ADMIN_OTHER_JWT='%s'\n" "${DOM_ADMIN_OTHER_JWT}" +printf "export KMS_TEST_OPA_OTHER_DOMAIN_JWT='%s'\n" "${OTHER_OFFICER_JWT}" diff --git a/configs/ckms.bad.toml b/configs/ckms.bad.toml deleted file mode 100644 index d6d7d1f..0000000 --- a/configs/ckms.bad.toml +++ /dev/null @@ -1,2 +0,0 @@ -[http_config] -access_token = "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6IjVVU1FrSVlULW9QMWZrcjQtNnRrciJ9.eyJuaWNrbmFtZSI6InRlY2giLCJuYW1lIjoidGVjaEBjb3NtaWFuLmNvbSIsInBpY3R1cmUiOiJodHRwczovL3MuZ3JhdmF0YXIuY29tL2F2YXRhci81MmZiMzFjOGNjYWQzNDU4MTIzZDRmYWQxNDA4NTRjZj9zPTQ4MCZyPXBnJmQ9aHR0cHMlM0ElMkYlMkZjZG4uYXV0aDAuY29tJTJGYXZhdGFycyUyRnRlLnBuZyIsInVwZGF0ZWRfYXQiOiIyMDIzLTA1LTMwVDA5OjMxOjExLjM4NloiLCJlbWFpbCI6InRlY2hAY29zbWlhbi5jb20iLCJlbWFpbF92ZXJpZmllZCI6ZmFsc2UsImlzcyI6Imh0dHBzOi8va21zLWNvc21pYW4uZXUuYXV0aDAuY29tLyIsImF1ZCI6IkszaXhldXhuVDVrM0Roa0tocWhiMXpYbjlFNjJGRXdJIiwiaWF0IjoxNjg1NDM5MDc0LCJleHAiOjE2ODU0NzUwNzQsInN1YiI6ImF1dGgwfDYzZDNkM2VhOTNmZjE2NDJjNzdkZjkyOCIsInNpZCI6ImJnVUNuTTNBRjVxMlpaVHFxMTZwclBCMi11Z0NNaUNPIiwibm9uY2UiOiJVRUZWTlZWeVluWTVUbHBwWjJScGNqSmtVMEZ4TmxkUFEwc3dTVGMwWHpaV2RVVmtkVnBEVGxSMldnPT0ifQ.HmU9fFwZ-JjJVlSy_PTei3ys0upeWQbWWiESmKBtRSClGnAXJNCpwuP4Jw7fgKn-8IBf-PYmP1_54u2Rw3RcJFVl7EblVoGMghYxVq5hViGpd00st3VwZmyCwOUz2CE5RBnBAoES4C8xA3zWg6oau0xjFQbC3jNU20eyFYMDewXA8UXCHQrEiQ56ylqSbyqlBbQIWbmOO4m5w2WDkx0bVyyJ893JfIJr_NANEQMJITYo8Mp_iHCyKp7llsfgCt07xN8ZqnsrMsJ15zC1n50bHGrTQisxURS1dpuFXF1hfrxhzogxYMX8CEISjsFgROjPY84GRMmvpYZfyaJbDDql3A" diff --git a/configs/ckms.toml b/configs/ckms.toml deleted file mode 100644 index 4725088..0000000 --- a/configs/ckms.toml +++ /dev/null @@ -1,4 +0,0 @@ -print_json = false - -[http_config] -server_url = "http://0.0.0.0:9998" diff --git a/configs/ckms_partial.toml b/configs/ckms_partial.toml deleted file mode 100644 index a3f0a21..0000000 --- a/configs/ckms_partial.toml +++ /dev/null @@ -1,3 +0,0 @@ -[http_config] -server_url = "http://0.0.0.0:9990" -access_token = "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6IjVVU1FrSVlULW9QMWZrcjQtNnRrciJ9.eyJuaWNrbmFtZSI6InRlY2giLCJuYW1lIjoidGVjaEBjb3NtaWFuLmNvbSIsInBpY3R1cmUiOiJodHRwczovL3MuZ3JhdmF0YXIuY29tL2F2YXRhci81MmZiMzFjOGNjYWQzNDU4MTIzZDRmYWQxNDA4NTRjZj9zPTQ4MCZyPXBnJmQ9aHR0cHMlM0ElMkYlMkZjZG4uYXV0aDAuY29tJTJGYXZhdGFycyUyRnRlLnBuZyIsInVwZGF0ZWRfYXQiOiIyMDIzLTA1LTMwVDA5OjMxOjExLjM4NloiLCJlbWFpbCI6InRlY2hAY29zbWlhbi5jb20iLCJlbWFpbF92ZXJpZmllZCI6ZmFsc2UsImlzcyI6Imh0dHBzOi8va21zLWNvc21pYW4uZXUuYXV0aDAuY29tLyIsImF1ZCI6IkszaXhldXhuVDVrM0Roa0tocWhiMXpYbjlFNjJGRXdJIiwiaWF0IjoxNjg1NDM5MDc0LCJleHAiOjE2ODU0NzUwNzQsInN1YiI6ImF1dGgwfDYzZDNkM2VhOTNmZjE2NDJjNzdkZjkyOCIsInNpZCI6ImJnVUNuTTNBRjVxMlpaVHFxMTZwclBCMi11Z0NNaUNPIiwibm9uY2UiOiJVRUZWTlZWeVluWTVUbHBwWjJScGNqSmtVMEZ4TmxkUFEwc3dTVGMwWHpaV2RVVmtkVnBEVGxSMldnPT0ifQ.HmU9fFwZ-JjJVlSy_PTei3ys0upeWQbWWiESmKBtRSClGnAXJNCpwuP4Jw7fgKn-8IBf-PYmP1_54u2Rw3RcJFVl7EblVoGMghYxVq5hViGpd00st3VwZmyCwOUz2CE5RBnBAoES4C8xA3zWg6oau0xjFQbC3jNU20eyFYMDewXA8UXCHQrEiQ56ylqSbyqlBbQIWbmOO4m5w2WDkx0bVyyJ893JfIJr_NANEQMJITYo8Mp_iHCyKp7llsfgCt07xN8ZqnsrMsJ15zC1n50bHGrTQisxURS1dpuFXF1hfrxhzogxYMX8CEISjsFgROjPY84GRMmvpYZfyaJbDDql3A" diff --git a/configs/client/auth_plain_owner.toml b/configs/client/auth_plain_owner.toml deleted file mode 100644 index f8372fe..0000000 --- a/configs/client/auth_plain_owner.toml +++ /dev/null @@ -1,6 +0,0 @@ -# Test KMS client — plain HTTP, no authentication. Owner identity. -# Corresponds to server config test/auth_plain.toml (port 12001). - -[http_config] -server_url = "http://localhost:12001" -accept_invalid_certs = true diff --git a/configs/client/auth_plain_user.toml b/configs/client/auth_plain_user.toml deleted file mode 100644 index 234aa83..0000000 --- a/configs/client/auth_plain_user.toml +++ /dev/null @@ -1,6 +0,0 @@ -# Test KMS client — plain HTTP, no authentication. User identity. -# Corresponds to server config test/auth_plain.toml (port 12001). - -[http_config] -server_url = "http://localhost:12001" -accept_invalid_certs = true diff --git a/configs/ckms_for_bash.toml b/configs/client/bash.toml similarity index 100% rename from configs/ckms_for_bash.toml rename to configs/client/bash.toml diff --git a/configs/client/cert_auth_owner.toml b/configs/client/cert_auth_owner.toml deleted file mode 100644 index 9a65057..0000000 --- a/configs/client/cert_auth_owner.toml +++ /dev/null @@ -1,9 +0,0 @@ -# Test KMS client — HTTPS with owner PEM client certificate, no JWT. Owner identity. -# Cert paths are relative to workspace root, resolved by load_client_config(). -# Corresponds to server config test/cert_auth.toml (port 9999). - -[http_config] -server_url = "https://localhost:29998" -accept_invalid_certs = true -tls_client_pem_cert_path = "test_data/certificates/client_server/owner/owner.client.acme.com.crt" -tls_client_pem_key_path = "test_data/certificates/client_server/owner/owner.client.acme.com.key" diff --git a/configs/client/cert_auth_user.toml b/configs/client/cert_auth_user.toml deleted file mode 100644 index 1556a4c..0000000 --- a/configs/client/cert_auth_user.toml +++ /dev/null @@ -1,9 +0,0 @@ -# Test KMS client — HTTPS with user PEM client certificate, no JWT. User identity. -# Cert paths are relative to workspace root, resolved by load_client_config(). -# Corresponds to server config test/cert_auth.toml (port 9999). - -[http_config] -server_url = "https://localhost:29998" -accept_invalid_certs = true -tls_client_pem_cert_path = "test_data/certificates/client_server/user/user.client.acme.com.crt" -tls_client_pem_key_path = "test_data/certificates/client_server/user/user.client.acme.com.key" diff --git a/configs/client/cert_co3.toml b/configs/client/cert_co3.toml index 58cfa73..d8112f3 100644 --- a/configs/client/cert_co3.toml +++ b/configs/client/cert_co3.toml @@ -4,7 +4,7 @@ vendor_id = "cosmian" [http_config] -server_url = "https://localhost:10004" +server_url = "https://localhost:9998" accept_invalid_certs = true tls_client_pem_cert_path = "test_data/certificates/client_server/co3/co3.client.acme.com.crt" tls_client_pem_key_path = "test_data/certificates/client_server/co3/co3.client.acme.com.key" diff --git a/configs/client/cert_owner.toml b/configs/client/cert_owner.toml index 046b58b..cc82b77 100644 --- a/configs/client/cert_owner.toml +++ b/configs/client/cert_owner.toml @@ -5,7 +5,7 @@ vendor_id = "cosmian" [http_config] -server_url = "https://localhost:9999" +server_url = "https://localhost:9998" accept_invalid_certs = true tls_client_pem_cert_path = "test_data/certificates/client_server/owner/owner.client.acme.com.crt" tls_client_pem_key_path = "test_data/certificates/client_server/owner/owner.client.acme.com.key" diff --git a/configs/client/cert_user.toml b/configs/client/cert_user.toml index 7bd2551..02cd75a 100644 --- a/configs/client/cert_user.toml +++ b/configs/client/cert_user.toml @@ -5,7 +5,7 @@ vendor_id = "cosmian" [http_config] -server_url = "https://localhost:9999" +server_url = "https://localhost:9998" accept_invalid_certs = true tls_client_pem_cert_path = "test_data/certificates/client_server/user/user.client.acme.com.crt" tls_client_pem_key_path = "test_data/certificates/client_server/user/user.client.acme.com.key" diff --git a/configs/client/crypto_officer_users_user.toml b/configs/client/crypto_officer_user.toml similarity index 100% rename from configs/client/crypto_officer_users_user.toml rename to configs/client/crypto_officer_user.toml diff --git a/configs/client/crypto_officer_users_owner.toml b/configs/client/crypto_officer_users_owner.toml deleted file mode 100644 index 3c05f51..0000000 --- a/configs/client/crypto_officer_users_owner.toml +++ /dev/null @@ -1,9 +0,0 @@ -# Test KMS client — HTTPS with owner PEM client certificate, no JWT. Owner identity. -# Cert paths are relative to workspace root, resolved by load_client_config(). -# Corresponds to server config crypto_officer_users.toml (port 10003). - -[http_config] -server_url = "https://localhost:10003" -accept_invalid_certs = true -tls_client_pem_cert_path = "test_data/certificates/client_server/owner/owner.client.acme.com.crt" -tls_client_pem_key_path = "test_data/certificates/client_server/owner/owner.client.acme.com.key" diff --git a/configs/ckms_jwt.toml b/configs/client/jwt.toml similarity index 100% rename from configs/ckms_jwt.toml rename to configs/client/jwt.toml diff --git a/configs/client/pem_cert_auth.toml b/configs/client/pem_cert.toml similarity index 100% rename from configs/client/pem_cert_auth.toml rename to configs/client/pem_cert.toml diff --git a/configs/client/pem_cert_and_token_auth.toml b/configs/client/pem_cert_token.toml similarity index 100% rename from configs/client/pem_cert_and_token_auth.toml rename to configs/client/pem_cert_token.toml diff --git a/configs/client/pkcs12_cert_auth.toml b/configs/client/pkcs12_cert.toml similarity index 100% rename from configs/client/pkcs12_cert_auth.toml rename to configs/client/pkcs12_cert.toml diff --git a/configs/client/pqc_tls_owner.toml b/configs/client/pqc_tls_owner.toml index 9b1a622..d3a1803 100644 --- a/configs/client/pqc_tls_owner.toml +++ b/configs/client/pqc_tls_owner.toml @@ -1,3 +1,6 @@ +# Test KMS client — HTTPS with a PQC (ML-DSA-44) TLS server certificate. +# Port is patched at runtime by load_client_config(). +# accept_invalid_certs is required because the server cert is self-signed. vendor_id = "cosmian" [http_config] diff --git a/configs/client/privileged_users_owner.toml b/configs/client/privileged_owner.toml similarity index 100% rename from configs/client/privileged_users_owner.toml rename to configs/client/privileged_owner.toml diff --git a/configs/client/privileged_users_user.toml b/configs/client/privileged_user.toml similarity index 100% rename from configs/client/privileged_users_user.toml rename to configs/client/privileged_user.toml diff --git a/configs/server/audit.toml b/configs/server/audit.toml deleted file mode 100644 index 838e9d0..0000000 --- a/configs/server/audit.toml +++ /dev/null @@ -1,25 +0,0 @@ -default_username = "admin" - -[http] -port = 9998 -hostname = "0.0.0.0" -cors_allowed_origins = ["http://localhost:9998"] - -[db] -database_type = "sqlite" -sqlite_path = "/tmp/kms-data-audit" -clear_database = false -unwrapped_cache_max_age = 15 - -[logging] -rust_log = "info,cosmian_kms=info" -ansi_colors = false - -[ui_config] -ui_index_html_folder = "ui/dist" - -[audit] -enabled = true - -[audit.file] -path = "/tmp/kms-audit.jsonl" diff --git a/configs/server/api_token_auth.toml b/configs/server/auth/api_token.toml similarity index 100% rename from configs/server/api_token_auth.toml rename to configs/server/auth/api_token.toml diff --git a/configs/server/auth/auth_verifier.toml b/configs/server/auth/auth_verifier.toml new file mode 100644 index 0000000..c8490e9 --- /dev/null +++ b/configs/server/auth/auth_verifier.toml @@ -0,0 +1,23 @@ +# Test KMS server — plain HTTP with Auth Verifier (Cosmian authentication server). +# Bound to port 12006. +# Loaded by tests that verify auth_verifier_url round-trip parsing. + +default_username = "tech@cosmian.com" + +[http] +port = 12006 +hostname = "0.0.0.0" + +[db] +database_type = "sqlite" +sqlite_path = "./sqlite-data-auth-verifier-tests" +clear_database = true + +[workspace] +root_data_path = "/tmp/kms_test_workspace_12006" +tmp_path = "/tmp" + +[auth_verifier] +auth_verifier_url = "https://localhost:8443" +auth_verifier_accept_invalid_certs = true +auth_verifier_realm = "_" diff --git a/configs/server/auth/auth_verifier_ui_multi_method.toml b/configs/server/auth/auth_verifier_ui_multi_method.toml new file mode 100644 index 0000000..4f1e004 --- /dev/null +++ b/configs/server/auth/auth_verifier_ui_multi_method.toml @@ -0,0 +1,60 @@ +# Manual test config: Web UI login with all three browser-login methods enabled +# simultaneously (OIDC, auth-verifier username/password, client certificate). +# Corresponds to Scenario C in the multi-method login-page test plan. +# +# Prerequisites: +# 1. Start the auth-verifier (from the authentication/ sibling repo): +# cargo run -p auth_verifier -- server/auth_verifier.dev.toml +# Admin UI at https://127.0.0.1:8443/admin-ui — create a test user in realm "_". +# +# 2. The TLS section below uses the repo's bundled test certificates. +# They work for connections to 127.0.0.1 and localhost in test environments. +# For a custom hostname, replace the paths and update kms_public_url. +# +# 3. Register "https://localhost:9998/ui/callback" as an allowed callback URL +# in the OIDC application (Auth0 or other IdP). +# +# Start the KMS: +# cargo run --bin cosmian_kms -- -c test_data/configs/server/test/auth_ui_multi_method.toml +# +# Expected GET /ui/auth_method response: +# {"auth_method":"JWT","auth_methods":["JWT","AUTH_VERIFIER","CERT"]} + +vendor_identification = "test_vendor" +kms_public_url = "https://localhost:9998" + +[http] +port = 9998 +hostname = "localhost" + +[tls] +tls_cert_file = "test_data/certificates/client_server/server/kmserver.acme.com.crt" +tls_key_file = "test_data/certificates/client_server/server/kmserver.acme.com.key" +clients_ca_cert_file = "test_data/certificates/client_server/ca/ca.crt" + +[idp_auth] +jwt_auth_provider = [ + "https://demo-kms.eu.auth0.com/,https://demo-kms.eu.auth0.com/.well-known/jwks.json,", +] + +[auth_verifier] +auth_verifier_url = "https://127.0.0.1:8443" +auth_verifier_realm = "_" +auth_verifier_accept_invalid_certs = true + +[ui_config.ui_oidc_auth] +ui_oidc_client_id = "mlTpGfub8n8PU2PsBQVdxp2VOyxtkL9m" +ui_oidc_issuer_url = "https://demo-kms.eu.auth0.com/" +ui_oidc_logout_url = "https://demo-kms.eu.auth0.com/logout" + +[ui_config] +ui_index_html_folder = "ui/dist" +# Must be identical across all KMS instances behind the same load balancer. +ui_session_salt = "kms-test-local-salt-do-not-use-in-production" + +[logging] +rust_log = "info,cosmian_kms=debug" +quiet = false +log_to_syslog = false +environment = "development" +ansi_colors = true diff --git a/configs/server/auth/cert.toml b/configs/server/auth/cert.toml index 66122af..9b6268a 100644 --- a/configs/server/auth/cert.toml +++ b/configs/server/auth/cert.toml @@ -3,10 +3,10 @@ # Bound to port 9999. Loaded by start_default_test_kms_server_with_cert_auth(). default_username = "tech@cosmian.com" -kms_public_url = "https://localhost:9999/google_cse" +kms_public_url = "https://localhost:9998/google_cse" [http] -port = 9999 +port = 9998 hostname = "0.0.0.0" [db] diff --git a/configs/server/entra_id.toml b/configs/server/auth/entra_id.toml similarity index 100% rename from configs/server/entra_id.toml rename to configs/server/auth/entra_id.toml diff --git a/configs/server/jwt_auth.toml b/configs/server/auth/jwt.toml similarity index 100% rename from configs/server/jwt_auth.toml rename to configs/server/auth/jwt.toml diff --git a/configs/server/multifactor_jwt_api.toml b/configs/server/auth/multifactor_jwt_api.toml similarity index 100% rename from configs/server/multifactor_jwt_api.toml rename to configs/server/auth/multifactor_jwt_api.toml diff --git a/configs/server/multifactor_tls_jwt.toml b/configs/server/auth/multifactor_tls_jwt.toml similarity index 100% rename from configs/server/multifactor_tls_jwt.toml rename to configs/server/auth/multifactor_tls_jwt.toml diff --git a/configs/server/no_auth.toml b/configs/server/auth/none.toml similarity index 100% rename from configs/server/no_auth.toml rename to configs/server/auth/none.toml diff --git a/configs/server/tls_client_cert.toml b/configs/server/auth/tls_client_cert.toml similarity index 100% rename from configs/server/tls_client_cert.toml rename to configs/server/auth/tls_client_cert.toml diff --git a/configs/server/auth_https.toml b/configs/server/auth_https.toml deleted file mode 100644 index f12cc5f..0000000 --- a/configs/server/auth_https.toml +++ /dev/null @@ -1,32 +0,0 @@ -# Auth test server — HTTPS without client CA (server TLS only, no mutual auth). -# Uses PEM cert + key (works in both FIPS and non-FIPS mode). -# Bound to port 12005. - -default_username = "tech@cosmian.com" -kms_public_url = "https://localhost:12005/google_cse" - -[http] -port = 12005 -hostname = "0.0.0.0" - -[db] -database_type = "sqlite" -sqlite_path = "./sqlite-data-auth-tests" -clear_database = false - -[workspace] -root_data_path = "/tmp/kms_test_workspace_12005" -tmp_path = "/tmp" - -[tls] -tls_cert_file = "test_data/certificates/client_server/server/kmserver.acme.com.crt" -tls_key_file = "test_data/certificates/client_server/server/kmserver.acme.com.key" -# No clients_ca_cert_file — mutual TLS is not enforced. - -[google_cse_config] -google_cse_enable = true -google_cse_disable_tokens_validation = true -google_cse_incoming_url_whitelist = ["https://cse.cosmian.com"] - -[logging] -rust_log = "off" diff --git a/configs/server/auth_https_client_ca.toml b/configs/server/auth_https_client_ca.toml deleted file mode 100644 index 0de1d1b..0000000 --- a/configs/server/auth_https_client_ca.toml +++ /dev/null @@ -1,29 +0,0 @@ -# Auth test server — HTTPS with client-certificate CA, no JWT. -# Uses PEM cert + key (works in both FIPS and non-FIPS mode). -# Bound to port 12004. - -default_username = "tech@cosmian.com" -kms_public_url = "https://localhost:12004/google_cse" - -[http] -port = 12004 -hostname = "0.0.0.0" - -[db] -database_type = "sqlite" -sqlite_path = "./sqlite-data-auth-tests" -clear_database = false - -[workspace] -root_data_path = "/tmp/kms_test_workspace_12004" -tmp_path = "/tmp" - -[tls] -tls_cert_file = "test_data/certificates/client_server/server/kmserver.acme.com.crt" -tls_key_file = "test_data/certificates/client_server/server/kmserver.acme.com.key" -clients_ca_cert_file = "test_data/certificates/client_server/ca/ca.crt" - -[google_cse_config] -google_cse_enable = true -google_cse_disable_tokens_validation = true -google_cse_incoming_url_whitelist = ["https://cse.cosmian.com"] diff --git a/configs/server/auth_https_jwt.toml b/configs/server/auth_https_jwt.toml deleted file mode 100644 index d55549b..0000000 --- a/configs/server/auth_https_jwt.toml +++ /dev/null @@ -1,37 +0,0 @@ -# Auth test server — HTTPS with client-certificate CA + JWT. -# Uses PEM cert + key (works in both FIPS and non-FIPS mode). -# Bound to port 12003. - -default_username = "tech@cosmian.com" -kms_public_url = "https://localhost:12003/google_cse" - -[http] -port = 12003 -hostname = "0.0.0.0" - -[db] -database_type = "sqlite" -sqlite_path = "./sqlite-data-auth-tests" -clear_database = false - -[workspace] -root_data_path = "/tmp/kms_test_workspace_12003" -tmp_path = "/tmp" - -[tls] -tls_cert_file = "test_data/certificates/client_server/server/kmserver.acme.com.crt" -tls_key_file = "test_data/certificates/client_server/server/kmserver.acme.com.key" -clients_ca_cert_file = "test_data/certificates/client_server/ca/ca.crt" - -[idp_auth] -jwt_auth_provider = [ - "https://accounts.google.com,https://www.googleapis.com/oauth2/v3/certs,kacls-migration,996739510374-2tauakcggau7kvk37784k0s2lmveb9c9.apps.googleusercontent.com", - "https://login.microsoftonline.com/612da4de-35c0-42de-ba56-174b69062c96/v2.0,https://login.microsoftonline.com/612da4de-35c0-42de-ba56-174b69062c96/discovery/v2.0/keys", - "https://demo-kms.eu.auth0.com/,https://demo-kms.eu.auth0.com/.well-known/jwks.json", -] - -[google_cse_config] -google_cse_enable = true -# Token validation is active because JWT auth is enabled. -google_cse_disable_tokens_validation = false -google_cse_incoming_url_whitelist = ["https://cse.cosmian.com"] diff --git a/configs/server/auth_plain.toml b/configs/server/auth_plain.toml deleted file mode 100644 index f4189a0..0000000 --- a/configs/server/auth_plain.toml +++ /dev/null @@ -1,23 +0,0 @@ -# Auth test server — plain HTTP, no authentication. -# Bound to port 12001. - -default_username = "tech@cosmian.com" -kms_public_url = "http://localhost:12001/google_cse" - -[http] -port = 12001 -hostname = "0.0.0.0" - -[db] -database_type = "sqlite" -sqlite_path = "./sqlite-data-auth-tests" -clear_database = true - -[workspace] -root_data_path = "/tmp/kms_test_workspace_12001" -tmp_path = "/tmp" - -[google_cse_config] -google_cse_enable = true -google_cse_disable_tokens_validation = true -google_cse_incoming_url_whitelist = ["https://cse.cosmian.com"] diff --git a/configs/server/auth_plain_jwt.toml b/configs/server/auth_plain_jwt.toml deleted file mode 100644 index 9a266d1..0000000 --- a/configs/server/auth_plain_jwt.toml +++ /dev/null @@ -1,31 +0,0 @@ -# Auth test server — plain HTTP with JWT authentication. -# Bound to port 12002. - -default_username = "tech@cosmian.com" -kms_public_url = "http://localhost:12002/google_cse" - -[http] -port = 12002 -hostname = "0.0.0.0" - -[db] -database_type = "sqlite" -sqlite_path = "./sqlite-data-auth-tests" -clear_database = false - -[workspace] -root_data_path = "/tmp/kms_test_workspace_12002" -tmp_path = "/tmp" - -[idp_auth] -jwt_auth_provider = [ - "https://accounts.google.com,https://www.googleapis.com/oauth2/v3/certs,kacls-migration,996739510374-2tauakcggau7kvk37784k0s2lmveb9c9.apps.googleusercontent.com", - "https://login.microsoftonline.com/612da4de-35c0-42de-ba56-174b69062c96/v2.0,https://login.microsoftonline.com/612da4de-35c0-42de-ba56-174b69062c96/discovery/v2.0/keys", - "https://demo-kms.eu.auth0.com/,https://demo-kms.eu.auth0.com/.well-known/jwks.json", -] - -[google_cse_config] -google_cse_enable = true -# Token validation is active because JWT auth is enabled. -google_cse_disable_tokens_validation = false -google_cse_incoming_url_whitelist = ["https://cse.cosmian.com"] diff --git a/configs/server/auth_verifier.toml b/configs/server/auth_verifier.toml deleted file mode 100644 index 3e5fcdf..0000000 --- a/configs/server/auth_verifier.toml +++ /dev/null @@ -1,69 +0,0 @@ -# KMS server configured with Authentication Verifier (auth-verifier) -# as the primary auth method, served over HTTPS. -# -# The KMS server: -# 1. Terminates TLS using a PEM server certificate and private key. -# 2. Validates bearer tokens against the verifier's JWKS endpoint. -# 3. Exposes the Web UI login form (GET /ui/auth_method returns "AUTH_VERIFIER") -# and proxies credentials to the verifier via POST /ui/login_as. -# -# Because kms_public_url uses https://, the session cookie will carry the Secure -# flag — browsers only send it over HTTPS, which is the expected behaviour for -# production. For local dev with a self-signed certificate the KMS client must -# set accept_invalid_certs = true. -# -# Prerequisites: -# - A running Authentication Verifier at auth_verifier_url below. -# - The verifier's JWKS endpoint must be reachable from the KMS server. -# - The TLS cert/key files below must exist (relative to the repo root). -# -# Bound to port 9998 (default KMS port). - -default_username = "admin" -kms_public_url = "https://localhost:9998" - -[http] -port = 9998 -hostname = "0.0.0.0" - -[tls] -# Server certificate and private key (PEM). No client CA → server-TLS only -# (no mutual authentication required). -tls_cert_file = "test_data/certificates/client_server/server/kmserver.acme.com.crt" -tls_key_file = "test_data/certificates/client_server/server/kmserver.acme.com.key" - -[db] -database_type = "sqlite" -sqlite_path = "./sqlite-data-auth-verifier" -clear_database = false - -# ── Authentication Verifier ────────────────────────────────────────────────── -# When both `auth_verifier_url` and `auth_verifier_realm` are set, the -# KMS server: -# 1. Validates bearer tokens against the verifier's JWKS endpoint -# 2. Exposes the Web UI login form (GET /ui/auth_method returns "AUTH_VERIFIER") -# 3. Proxies login credentials to the verifier via POST /ui/login_as -# -# `auth_verifier_accept_invalid_certs` allows the JWKS fetch to target an HTTP -# URL (or a server with a self-signed certificate). In production the verifier -# must be reachable over HTTPS with a valid certificate, and this flag must be -# set to false. -[auth_verifier] -auth_verifier_url = "https://localhost:8443" -auth_verifier_realm = "_" -auth_verifier_accept_invalid_certs = true # dev/test only — never in production - -[ui_config] -ui_index_html_folder = "./ui/dist" -# A stable session salt ties the session cookie encryption key to this server's -# public URL, making it deterministic across restarts and portable across -# load-balanced instances. -# Generate your own with: openssl rand -hex 32 -ui_session_salt = "change-me-use-openssl-rand-hex-32" - -[logging] -rust_log = "info,cosmian_kms=debug" -quiet = false -log_to_syslog = false -environment = "development" -ansi_colors = true diff --git a/configs/server/basic_http.toml b/configs/server/basic_http.toml deleted file mode 100644 index 13f7c52..0000000 --- a/configs/server/basic_http.toml +++ /dev/null @@ -1,4 +0,0 @@ -# Basic HTTP configuration from quick_start.md -[http] -port = 9998 -hostname = "0.0.0.0" diff --git a/configs/server/cert_auth.toml b/configs/server/cert_auth.toml deleted file mode 100644 index 66122af..0000000 --- a/configs/server/cert_auth.toml +++ /dev/null @@ -1,33 +0,0 @@ -# Test KMS server — TLS with mutual client-certificate authentication. -# Uses PEM cert + key (works in both FIPS and non-FIPS mode). -# Bound to port 9999. Loaded by start_default_test_kms_server_with_cert_auth(). - -default_username = "tech@cosmian.com" -kms_public_url = "https://localhost:9999/google_cse" - -[http] -port = 9999 -hostname = "0.0.0.0" - -[db] -database_type = "sqlite" -sqlite_path = "/tmp/kms_test_cert_auth" -clear_database = true - -[workspace] -root_data_path = "/tmp/kms_test_workspace_cert_auth" -tmp_path = "/tmp" - -[tls] -tls_cert_file = "test_data/certificates/client_server/server/kmserver.acme.com.crt" -tls_key_file = "test_data/certificates/client_server/server/kmserver.acme.com.key" -clients_ca_cert_file = "test_data/certificates/client_server/ca/ca.crt" - -[socket_server] -socket_server_start = true -socket_server_port = 10099 - -[google_cse_config] -google_cse_enable = true -google_cse_disable_tokens_validation = true -google_cse_incoming_url_whitelist = ["https://cse.cosmian.com"] diff --git a/configs/server/cert_auth_operator_and_crypto_officer.toml b/configs/server/cert_auth_operator_and_crypto_officer.toml deleted file mode 100644 index e1b637d..0000000 --- a/configs/server/cert_auth_operator_and_crypto_officer.toml +++ /dev/null @@ -1,32 +0,0 @@ -# Test KMS server — TLS with mutual client-certificate authentication + role enforcement. -# `user.client@acme.com` is an unenrolled user (defaults to Operator). -# `owner.client@acme.com` is a CryptoOfficer. -# Bound to port 9992. Loaded by Operator/CryptoOfficer separation-of-duty test vectors. - -default_username = "tech@cosmian.com" -kms_public_url = "https://localhost:9992/google_cse" - -[roles] -crypto_officer_users = ["owner.client@acme.com"] - -[http] -port = 9992 -hostname = "0.0.0.0" - -[db] -database_type = "sqlite" -sqlite_path = "/tmp/kms_test_cert_auth_operator_crypto_officer" -clear_database = true - -[workspace] -root_data_path = "/tmp/kms_test_workspace_cert_auth_operator_crypto_officer" -tmp_path = "/tmp" - -[tls] -tls_cert_file = "test_data/certificates/client_server/server/kmserver.acme.com.crt" -tls_key_file = "test_data/certificates/client_server/server/kmserver.acme.com.key" -clients_ca_cert_file = "test_data/certificates/client_server/ca/ca.crt" - -[socket_server] -socket_server_start = true -socket_server_port = 10092 diff --git a/configs/server/crypto_officer_users_mysql.toml b/configs/server/crypto_officer_users_mysql.toml deleted file mode 100644 index 1bb7eb4..0000000 --- a/configs/server/crypto_officer_users_mysql.toml +++ /dev/null @@ -1,41 +0,0 @@ -# Test KMS server — TLS + JWT auth, for privileged-user tests, MySQL backend. -# Uses PEM cert + key (works in both FIPS and non-FIPS mode). -# Bound to port 10003. crypto_officer_users is injected at runtime. - -default_username = "tech@cosmian.com" -kms_public_url = "https://localhost:10003/google_cse" - -[http] -port = 10003 -hostname = "0.0.0.0" - -[db] -database_type = "mysql" -database_url = "mysql://kms:kms@localhost:3306/kms" -clear_database = false - -[workspace] -root_data_path = "/tmp/kms_test_workspace_crypto_officer_users_mysql" -tmp_path = "/tmp" - -[tls] -tls_cert_file = "test_data/certificates/client_server/server/kmserver.acme.com.crt" -tls_key_file = "test_data/certificates/client_server/server/kmserver.acme.com.key" -clients_ca_cert_file = "test_data/certificates/client_server/ca/ca.crt" - -[socket_server] -socket_server_start = true -socket_server_port = 10103 - -[idp_auth] -jwt_auth_provider = [ - "https://accounts.google.com,https://www.googleapis.com/oauth2/v3/certs,kacls-migration,996739510374-2tauakcggau7kvk37784k0s2lmveb9c9.apps.googleusercontent.com", - "https://login.microsoftonline.com/612da4de-35c0-42de-ba56-174b69062c96/v2.0,https://login.microsoftonline.com/612da4de-35c0-42de-ba56-174b69062c96/discovery/v2.0/keys", - "https://demo-kms.eu.auth0.com/,https://demo-kms.eu.auth0.com/.well-known/jwks.json", -] - -[google_cse_config] -google_cse_enable = true -# Token validation is active because JWT auth is enabled. -google_cse_disable_tokens_validation = false -google_cse_incoming_url_whitelist = ["https://cse.cosmian.com"] diff --git a/configs/server/crypto_officer_users_postgres.toml b/configs/server/crypto_officer_users_postgres.toml deleted file mode 100644 index ab90088..0000000 --- a/configs/server/crypto_officer_users_postgres.toml +++ /dev/null @@ -1,41 +0,0 @@ -# Test KMS server — TLS + JWT auth, for privileged-user tests, PostgreSQL backend. -# Uses PEM cert + key (works in both FIPS and non-FIPS mode). -# Bound to port 10003. crypto_officer_users is injected at runtime. - -default_username = "tech@cosmian.com" -kms_public_url = "https://localhost:10003/google_cse" - -[http] -port = 10003 -hostname = "0.0.0.0" - -[db] -database_type = "postgresql" -database_url = "postgresql://kms:kms@127.0.0.1:5432/kms" -clear_database = false - -[workspace] -root_data_path = "/tmp/kms_test_workspace_crypto_officer_users_postgres" -tmp_path = "/tmp" - -[tls] -tls_cert_file = "test_data/certificates/client_server/server/kmserver.acme.com.crt" -tls_key_file = "test_data/certificates/client_server/server/kmserver.acme.com.key" -clients_ca_cert_file = "test_data/certificates/client_server/ca/ca.crt" - -[socket_server] -socket_server_start = true -socket_server_port = 10103 - -[idp_auth] -jwt_auth_provider = [ - "https://accounts.google.com,https://www.googleapis.com/oauth2/v3/certs,kacls-migration,996739510374-2tauakcggau7kvk37784k0s2lmveb9c9.apps.googleusercontent.com", - "https://login.microsoftonline.com/612da4de-35c0-42de-ba56-174b69062c96/v2.0,https://login.microsoftonline.com/612da4de-35c0-42de-ba56-174b69062c96/discovery/v2.0/keys", - "https://demo-kms.eu.auth0.com/,https://demo-kms.eu.auth0.com/.well-known/jwks.json", -] - -[google_cse_config] -google_cse_enable = true -# Token validation is active because JWT auth is enabled. -google_cse_disable_tokens_validation = false -google_cse_incoming_url_whitelist = ["https://cse.cosmian.com"] diff --git a/configs/server/crypto_officer_users_redis_findex.toml b/configs/server/crypto_officer_users_redis_findex.toml deleted file mode 100644 index d6330e5..0000000 --- a/configs/server/crypto_officer_users_redis_findex.toml +++ /dev/null @@ -1,43 +0,0 @@ -# Test KMS server — TLS + JWT auth, for privileged-user tests, Redis-Findex backend. -# Only valid in non-FIPS mode (redis-findex is gated behind the non-fips feature). -# Uses PEM cert + key (works in both FIPS and non-FIPS mode). -# Bound to port 10003. crypto_officer_users is injected at runtime. - -default_username = "tech@cosmian.com" -kms_public_url = "https://localhost:10003/google_cse" - -[http] -port = 10003 -hostname = "0.0.0.0" - -[db] -database_type = "redis-findex" -database_url = "redis://localhost:6379" -clear_database = true -redis_master_password = "password" - -[workspace] -root_data_path = "/tmp/kms_test_workspace_crypto_officer_users_redis" -tmp_path = "/tmp" - -[tls] -tls_cert_file = "test_data/certificates/client_server/server/kmserver.acme.com.crt" -tls_key_file = "test_data/certificates/client_server/server/kmserver.acme.com.key" -clients_ca_cert_file = "test_data/certificates/client_server/ca/ca.crt" - -[socket_server] -socket_server_start = true -socket_server_port = 10103 - -[idp_auth] -jwt_auth_provider = [ - "https://accounts.google.com,https://www.googleapis.com/oauth2/v3/certs,kacls-migration,996739510374-2tauakcggau7kvk37784k0s2lmveb9c9.apps.googleusercontent.com", - "https://login.microsoftonline.com/612da4de-35c0-42de-ba56-174b69062c96/v2.0,https://login.microsoftonline.com/612da4de-35c0-42de-ba56-174b69062c96/discovery/v2.0/keys", - "https://demo-kms.eu.auth0.com/,https://demo-kms.eu.auth0.com/.well-known/jwks.json", -] - -[google_cse_config] -google_cse_enable = true -# Token validation is active because JWT auth is enabled. -google_cse_disable_tokens_validation = false -google_cse_incoming_url_whitelist = ["https://cse.cosmian.com"] diff --git a/configs/server/mysql.toml b/configs/server/db/mysql.toml similarity index 100% rename from configs/server/mysql.toml rename to configs/server/db/mysql.toml diff --git a/configs/server/mysql_database.toml b/configs/server/db/mysql_snippet.toml similarity index 100% rename from configs/server/mysql_database.toml rename to configs/server/db/mysql_snippet.toml diff --git a/configs/server/postgres.toml b/configs/server/db/postgres.toml similarity index 100% rename from configs/server/postgres.toml rename to configs/server/db/postgres.toml diff --git a/configs/server/lb_kms1_postgres.toml b/configs/server/db/postgres_lb_primary.toml similarity index 100% rename from configs/server/lb_kms1_postgres.toml rename to configs/server/db/postgres_lb_primary.toml diff --git a/configs/server/lb_kms2_postgres.toml b/configs/server/db/postgres_lb_secondary.toml similarity index 100% rename from configs/server/lb_kms2_postgres.toml rename to configs/server/db/postgres_lb_secondary.toml diff --git a/configs/server/redis_findex.toml b/configs/server/db/redis_findex.toml similarity index 100% rename from configs/server/redis_findex.toml rename to configs/server/db/redis_findex.toml diff --git a/configs/server/google_cse.toml b/configs/server/google_cse.toml deleted file mode 100644 index 7e09873..0000000 --- a/configs/server/google_cse.toml +++ /dev/null @@ -1,9 +0,0 @@ -# Google CSE configuration from google_cse/index.md -kms_public_url = "https://cse.example.com" - -# Use Google Identity for JWT validation -[idp_auth] -jwt_auth_provider = ["https://accounts.google.com,https://www.googleapis.com/oauth2/v3/certs,"] - -[google_cse_config] -google_cse_enable = true diff --git a/configs/server/google_cse_new_format.toml b/configs/server/google_cse_new_format.toml deleted file mode 100644 index 36c9be9..0000000 --- a/configs/server/google_cse_new_format.toml +++ /dev/null @@ -1,9 +0,0 @@ -# Google CSE configuration from google_cse/index.md - using new idp_auth format -kms_public_url = "https://cse.example.com" - -[idp_auth] -# Use Google Identity for JWT validation -jwt_auth_provider = ["https://accounts.google.com,https://www.googleapis.com/oauth2/v3/certs,"] - -[google_cse_config] -google_cse_enable = true diff --git a/configs/server/hsm.toml b/configs/server/hsm/hsm_test.toml similarity index 100% rename from configs/server/hsm.toml rename to configs/server/hsm/hsm_test.toml diff --git a/configs/server/installation_basic.toml b/configs/server/installation_basic.toml deleted file mode 100644 index fdf924c..0000000 --- a/configs/server/installation_basic.toml +++ /dev/null @@ -1,4 +0,0 @@ -# Installation getting started configuration from installation/installation_getting_started.md -[http] -port = 9998 -hostname = "0.0.0.0" diff --git a/configs/server/jwt_idp_auth.toml b/configs/server/jwt_idp_auth.toml deleted file mode 100644 index 1aee3ec..0000000 --- a/configs/server/jwt_idp_auth.toml +++ /dev/null @@ -1,3 +0,0 @@ -# JWT authentication configuration using new idp_auth format -[idp_auth] -jwt_auth_provider = ["https://accounts.google.com,https://www.googleapis.com/oauth2/v3/certs,cosmian_kms"] diff --git a/configs/server/lb_kms3_postgres.toml b/configs/server/lb_kms3_postgres.toml deleted file mode 100644 index f0bfd06..0000000 --- a/configs/server/lb_kms3_postgres.toml +++ /dev/null @@ -1,18 +0,0 @@ -default_username = "admin" - -[http] -port = 9998 -hostname = "0.0.0.0" - -[db] -database_type = "postgresql" -database_url = "postgresql://kms:kms@postgres:5432/kms" -clear_database = false -unwrapped_cache_max_age = 15 - -[logging] -rust_log = "info,cosmian_kms=info" -ansi_colors = false - -[ui_config] -ui_index_html_folder = "/usr/local/cosmian/ui/dist" diff --git a/configs/server/openapi_validation.toml b/configs/server/openapi_validation.toml deleted file mode 100644 index b58549a..0000000 --- a/configs/server/openapi_validation.toml +++ /dev/null @@ -1,37 +0,0 @@ -# OpenAPI path validation test — maximally-configured server. -# Enables all enterprise scopes so the route validation test can verify -# that every path in openapi.yaml is registered. -# No real credentials are needed — routes just need to be *registered*. - -default_username = "tech@cosmian.com" -kms_public_url = "http://localhost:12099/google_cse" -ms_dke_service_url = "https://dke.example.com" - -[http] -port = 12099 -hostname = "0.0.0.0" - -[db] -database_type = "sqlite" -sqlite_path = "./sqlite-data-openapi-validation" -clear_database = true - -[workspace] -root_data_path = "/tmp/kms_test_openapi_validation" -tmp_path = "/tmp" - -[google_cse_config] -google_cse_enable = true -google_cse_disable_tokens_validation = true -google_cse_incoming_url_whitelist = ["https://cse.cosmian.com"] - -[aws_xks_config] -aws_xks_enable = true -aws_xks_region = "us-east-1" -aws_xks_service = "kms-xks-proxy" -aws_xks_sigv4_access_key_id = "AKIAIOSFODNN7EXAMPLE" -aws_xks_sigv4_secret_access_key = "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY" - -[azure_ekm_config] -azure_ekm_enable = true -azure_ekm_disable_client_auth = true diff --git a/configs/server/otlp_logging.toml b/configs/server/otlp_logging.toml deleted file mode 100644 index 65e282e..0000000 --- a/configs/server/otlp_logging.toml +++ /dev/null @@ -1,4 +0,0 @@ -# OTLP logging configuration from logging.md -[logging] -otlp = "http://localhost:4317" -quiet = true diff --git a/configs/server/privileged_users.toml b/configs/server/privileged_users.toml deleted file mode 100644 index 0ed5015..0000000 --- a/configs/server/privileged_users.toml +++ /dev/null @@ -1,41 +0,0 @@ -# Test KMS server — TLS + JWT auth, for privileged-user tests. -# Uses PEM cert + key (works in both FIPS and non-FIPS mode). -# Bound to port 10003. privileged_users is injected at runtime. - -default_username = "tech@cosmian.com" -kms_public_url = "https://localhost:10003/google_cse" - -[http] -port = 10003 -hostname = "0.0.0.0" - -[db] -database_type = "sqlite" -sqlite_path = "/tmp/kms_test_privileged_users" -clear_database = true - -[workspace] -root_data_path = "/tmp/kms_test_workspace_privileged_users" -tmp_path = "/tmp" - -[tls] -tls_cert_file = "test_data/certificates/client_server/server/kmserver.acme.com.crt" -tls_key_file = "test_data/certificates/client_server/server/kmserver.acme.com.key" -clients_ca_cert_file = "test_data/certificates/client_server/ca/ca.crt" - -[socket_server] -socket_server_start = true -socket_server_port = 10103 - -[idp_auth] -jwt_auth_provider = [ - "https://accounts.google.com,https://www.googleapis.com/oauth2/v3/certs,kacls-migration,996739510374-2tauakcggau7kvk37784k0s2lmveb9c9.apps.googleusercontent.com", - "https://login.microsoftonline.com/612da4de-35c0-42de-ba56-174b69062c96/v2.0,https://login.microsoftonline.com/612da4de-35c0-42de-ba56-174b69062c96/discovery/v2.0/keys", - "https://demo-kms.eu.auth0.com/,https://demo-kms.eu.auth0.com/.well-known/jwks.json", -] - -[google_cse_config] -google_cse_enable = true -# Token validation is active because JWT auth is enabled. -google_cse_disable_tokens_validation = false -google_cse_incoming_url_whitelist = ["https://cse.cosmian.com"] diff --git a/configs/server/quick_start_minimal.toml b/configs/server/quick_start_minimal.toml deleted file mode 100644 index b13062b..0000000 --- a/configs/server/quick_start_minimal.toml +++ /dev/null @@ -1,4 +0,0 @@ -# Minimal default configuration from quick_start.md -[http] -port = 9998 -hostname = "0.0.0.0" diff --git a/configs/server/quick_start_minimal_alt_port.toml b/configs/server/quick_start_minimal_alt_port.toml deleted file mode 100644 index c362b06..0000000 --- a/configs/server/quick_start_minimal_alt_port.toml +++ /dev/null @@ -1,4 +0,0 @@ -# Minimal default configuration from quick_start.md -[http] -port = 19998 -hostname = "0.0.0.0" diff --git a/configs/server/crypto_officer_users.toml b/configs/server/rbac/crypto_officer_users.toml similarity index 96% rename from configs/server/crypto_officer_users.toml rename to configs/server/rbac/crypto_officer_users.toml index 69802ba..08fa892 100644 --- a/configs/server/crypto_officer_users.toml +++ b/configs/server/rbac/crypto_officer_users.toml @@ -33,3 +33,6 @@ jwt_auth_provider = [ "https://login.microsoftonline.com/612da4de-35c0-42de-ba56-174b69062c96/v2.0,https://login.microsoftonline.com/612da4de-35c0-42de-ba56-174b69062c96/discovery/v2.0/keys", "https://demo-kms.eu.auth0.com/,https://demo-kms.eu.auth0.com/.well-known/jwks.json", ] + +[ui_config] +ui_index_html_folder = "ui/dist" diff --git a/configs/server/rbac/crypto_officers.toml b/configs/server/rbac/crypto_officers.toml index 029762d..b0d2915 100644 --- a/configs/server/rbac/crypto_officers.toml +++ b/configs/server/rbac/crypto_officers.toml @@ -1,37 +1,60 @@ -# Test KMS server — ceremony mode (require_ceremony = true). -# Loaded by start_ceremony_test_kms_server(). -# Bound to port 10004. +# ── Full RBAC — production reference configuration ────────────────────────── # -# CO users: owner.client@acme.com, user.client@acme.com, co3.client@acme.com -# The ceremony_secret is a 32-byte test secret (hex-encoded). -# NOTE: this secret is for testing only — never use in production. +# Role model: CryptoOfficer + Operator +# FIPS 140-3 §7.4 / NIST SP 800-57 Part 2 Rev 1 §4.3–§4.4 +# +# Role hierarchy and permissions: +# +# ┌──────────────────┬────────────────────────────────────────────────────┐ +# │ Role │ Permitted operations │ +# ├──────────────────┼────────────────────────────────────────────────────┤ +# │ Operator │ Encrypt, Decrypt, Sign, Verify, MAC, Hash, │ +# │ (default) │ GetAttributes, Locate, Validate │ +# ├──────────────────┼────────────────────────────────────────────────────┤ +# │ CryptoOfficer │ Create, Import, Destroy, Revoke, Activate, │ +# │ │ Get, Export, CreateKeyPair, CreateSplitKey, │ +# │ │ JoinSplitKey, Certify, GrantAccess, RevokeAccess; │ +# │ │ bypass per-object ownership check │ +# └──────────────────┴────────────────────────────────────────────────────┘ +# +# Unenrolled users default to Operator (fail-secure minimum privilege). +# There is no `operator_users` config key — it is the implicit default. +# ───────────────────────────────────────────────────────────────────────────── -default_username = "tech@cosmian.com" -kms_public_url = "https://localhost:10004" +default_username = "admin" + +[roles] +# ── CryptoOfficer role ──────────────────────────────────────────────────────── +# Key-management engineers: create, rotate, revoke keys. Ownership bypass on all objects. +# Certs: test_data/certificates/client_server/crypto-officer-1/crypto-officer-1.example.com.{key,crt} +# test_data/certificates/client_server/crypto-officer-2/crypto-officer-2.example.com.{key,crt} +crypto_officer_users = [ + "user.client@acme.com", + "owner.client@acme.com", + "co3.client@acme.com",] + +# Require a split-key ceremony (XOR n-of-n) to activate the Crypto Officer role. +# All ceremony activation records are AES-256-GCM encrypted with keys derived +# from `ceremony_secret`. Generate one with: openssl rand -hex 32 +crypto_officer_require_ceremony = true +ceremony_secret = "deadbeefcafebabe0102030405060708090a0b0c0d0e0f10deadbeefcafebabe" [http] -port = 10004 +port = 9998 hostname = "0.0.0.0" [db] database_type = "sqlite" -sqlite_path = "/tmp/kms_test_ceremony" -clear_database = true - -[workspace] -root_data_path = "/tmp/kms_test_workspace_ceremony" -tmp_path = "/tmp" +sqlite_path = "cosmian-kms/kms-rbac-full" +clear_database = false [tls] tls_cert_file = "test_data/certificates/client_server/server/kmserver.acme.com.crt" tls_key_file = "test_data/certificates/client_server/server/kmserver.acme.com.key" clients_ca_cert_file = "test_data/certificates/client_server/ca/ca.crt" -[roles] -crypto_officer_users = [ - "user.client@acme.com", - "owner.client@acme.com", - "co3.client@acme.com", -] -crypto_officer_require_ceremony = true -ceremony_secret = "0102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f20" +[logging] +rust_log = "info,cosmian_kms=info" + +[ui_config] +ui_index_html_folder = "ui/dist" diff --git a/configs/server/rbac/opa.toml b/configs/server/rbac/opa.toml new file mode 100644 index 0000000..63ded65 --- /dev/null +++ b/configs/server/rbac/opa.toml @@ -0,0 +1,140 @@ +# ============================================================================= +# KMS server — OPA RBAC + Cosmian Authentication Verifier (Mode 3) +# ============================================================================= +# +# Authorization model: Mode 3 — Enforcing (OPA + Native KMS) +# +# Every KMIP request must pass TWO authorization gates: +# Gate 1 — OPA evaluates kms.rego: role hierarchy, domain scoping, owner override +# Gate 2 — Native KMS: object ownership, per-object grants, HSM admin +# +# Role hierarchy (defined in test_data/opa/kms.rego): +# SuperAdmin > DomainAdmin > CryptoOfficer > User +# > Auditor +# +# Prerequisites: +# 1. Authentication Verifier running (from authentication/ directory): +# cargo run -p auth_verifier -- \ +# ../test_data/configs/auth_verifier/kms_opa_rbac.toml +# +# 2. OPA running with kms.rego: +# docker compose up -d opa +# +# 3. Provision test users (from the repository root): +# REALM_A=acme.com REALM_B=partner.acme.com \ +# bash test_data/configs/auth_verifier/provision_opa_users.sh +# Usernames: kms-opa-officer, kms-opa-super-admin, kms-opa-auditor, … +# Password: change_me (default) +# +# 4. Start the KMS server from the repository root: +# cargo run -p cosmian_kms_server --features non-fips -- \ +# -c test_data/configs/server/rbac/opa.toml +# +# 5. Open the KMS Web UI at: https://localhost:9998 +# The login page offers two authentication methods: +# a) Auth Verifier (primary) — username/password form with realm selector: +# realm: acme.com username: kms-opa-officer password: change_me +# b) Client Certificate (secondary) — click "Client certificate" button: +# present owner.client@acme.com.p12 (or equivalent) from the browser. +# +# ============================================================================= + +# Used by the Web UI for redirect URLs and CORS. +kms_public_url = "https://localhost:9998" + +# --------------------------------------------------------------------------- +# HTTP server — plain HTTP for local development. +# JWT authentication does not require TLS between client and KMS; +# TLS is required only for the auth_verifier endpoint (handled separately). +# --------------------------------------------------------------------------- +[http] +hostname = "0.0.0.0" +port = 9998 + +# --------------------------------------------------------------------------- +# Authentication — Cosmian Authentication Verifier +# +# The KMS validates bearer tokens issued by the Authentication Verifier: +# - sub → user identity forwarded to OPA as `input.user` +# - roles → JWT `roles` claim (RFC 9068) forwarded as `input.roles` +# - as_rid → realm ID forwarded as `input.user_domain` +# +# JWKS endpoint is derived automatically as: +# {auth_verifier_url}/.well-known/jwks.json +# +# auth_verifier_realm enables the Web UI login form (POST /ui/login_as). +# --------------------------------------------------------------------------- +[auth_verifier] +auth_verifier_url = "https://localhost:8443" +# Multiple realms: the UI shows a realm selector dropdown when more than one is listed. +# Set to a single string or an array. +auth_verifier_realm = ["acme.com", "kms-opa-test"] +# Accept the self-signed test certificate — remove in production. +auth_verifier_accept_invalid_certs = true + +# --------------------------------------------------------------------------- +# Native KMS Crypto Officers (no split-key ceremony required) +# +# These users are trusted at the KMS level and can bypass OPA Gate 1 in +# enforcing mode. In exclusive mode, OPA remains the sole authority. +# +# Use case: cert-authenticated service accounts (e.g. mTLS operator certs) +# that need Create/Import access without an OIDC JWT. +# --------------------------------------------------------------------------- +[roles] +crypto_officer_users = [ + "owner.client@acme.com", + "user.client@acme.com", + "co3.client@acme.com", +] +crypto_officer_require_ceremony = false + +# --------------------------------------------------------------------------- +# Authorization — Open Policy Agent (Mode 3: enforcing) +# +# OPA evaluates test_data/opa/kms.rego on every KMIP operation. +# In "enforcing" mode OPA is Gate 1; the native KMS grant check is Gate 2. +# In "exclusive" mode the native KMS check is bypassed entirely. +# +# Reference: test_data/opa/kms.rego +# --------------------------------------------------------------------------- +[opa] +opa_url = "http://localhost:8181" +opa_mode = "enforcing" + +# --------------------------------------------------------------------------- +# Database +# --------------------------------------------------------------------------- +[db] +database_type = "sqlite" +sqlite_path = "/tmp/kms-opa-rbac" +clear_database = false + +# --------------------------------------------------------------------------- +# Workspace +# --------------------------------------------------------------------------- +[workspace] +root_data_path = "/tmp/kms-opa-rbac-workspace" +tmp_path = "/tmp" + +# --------------------------------------------------------------------------- +# Web UI — serves the pre-built SPA so http://localhost:9998 shows a login page. +# The login form authenticates against auth_verifier_realm = "kms" above. +# Build the UI once with: pnpm -C ui build +# --------------------------------------------------------------------------- +[ui_config] +ui_index_html_folder = "ui/dist" + +# --------------------------------------------------------------------------- +# Logging +# --------------------------------------------------------------------------- +[logging] +rust_log = "info,cosmian_kms_server=info" + +[tls] +tls_cert_file = "test_data/certificates/client_server/server/kmserver.acme.com.crt" +tls_key_file = "test_data/certificates/client_server/server/kmserver.acme.com.key" +# Client CA: enables mTLS authentication and adds "CERT" as a login method in the Web UI. +# Native KMS CO certs signed by this CA (owner.client@acme.com, etc.) can authenticate +# directly without a JWT when listed in [roles] crypto_officer_users. +clients_ca_cert_file = "test_data/certificates/client_server/ca/ca.crt" diff --git a/configs/server/rbac/operator_only.toml b/configs/server/rbac/operator_only.toml new file mode 100644 index 0000000..e14cf01 --- /dev/null +++ b/configs/server/rbac/operator_only.toml @@ -0,0 +1,45 @@ +# ── Minimal RBAC — all users default ───────────────────────────────────────── +# +# When any role list is configured, users not listed in any role receive the +# default permissions (crypto use only, no lifecycle operations). +# +# Default (Operator) permissions: +# • Use key material: Encrypt, Decrypt, Sign, SignatureVerify, MAC, Hash +# • Read metadata: Get, GetAttributes, Locate, Validate, Export (own objects) +# • BLOCKED: Create, Import, Register, Destroy, Revoke, Activate, +# CreateKeyPair, CreateSplitKey, JoinSplitKey, ReKey, etc. +# +# This config sets an empty crypto_officer_users list to activate role enforcement +# while leaving all users at the default level (no one can do lifecycle ops unless +# explicitly granted Create access). +# ───────────────────────────────────────────────────────────────────────────── + +default_username = "admin" + +[roles] +# Empty list: triggers role enforcement. All users get default permissions. +crypto_officer_users = [] + +[http] +port = 9998 +hostname = "0.0.0.0" + +[db] +database_type = "sqlite" +sqlite_path = "/tmp/kms-rbac-operator" +clear_database = false + +[workspace] +root_data_path = "/tmp/kms-rbac-operator-workspace" +tmp_path = "/tmp" + +[tls] +tls_cert_file = "test_data/certificates/client_server/server/kmserver.acme.com.crt" +tls_key_file = "test_data/certificates/client_server/server/kmserver.acme.com.key" +clients_ca_cert_file = "test_data/certificates/client_server/ca/ca.crt" + +[logging] +rust_log = "info,cosmian_kms=info" + +[ui_config] +ui_index_html_folder = "ui/dist" diff --git a/configs/server/non_revocable.toml b/configs/server/test/non_revocable.toml similarity index 100% rename from configs/server/non_revocable.toml rename to configs/server/test/non_revocable.toml diff --git a/configs/server/tls_auth_fips.toml b/configs/server/tls/fips.toml similarity index 100% rename from configs/server/tls_auth_fips.toml rename to configs/server/tls/fips.toml diff --git a/configs/server/tls_auth_non_fips.toml b/configs/server/tls/non_fips.toml similarity index 100% rename from configs/server/tls_auth_non_fips.toml rename to configs/server/tls/non_fips.toml diff --git a/configs/server/pqc_tls.toml b/configs/server/tls/pqc_tls.toml similarity index 100% rename from configs/server/pqc_tls.toml rename to configs/server/tls/pqc_tls.toml diff --git a/configs/server/tls13_auth_fips.toml b/configs/server/tls/tls13_fips.toml similarity index 100% rename from configs/server/tls13_auth_fips.toml rename to configs/server/tls/tls13_fips.toml diff --git a/configs/server/tls13_auth_non_fips.toml b/configs/server/tls/tls13_non_fips.toml similarity index 100% rename from configs/server/tls13_auth_non_fips.toml rename to configs/server/tls/tls13_non_fips.toml diff --git a/vectors/access_control/crypto_officer_role_allowed_ops/manifest.toml b/vectors/access_control/crypto_officer_role_allowed_ops/manifest.toml new file mode 100644 index 0000000..e7b331e --- /dev/null +++ b/vectors/access_control/crypto_officer_role_allowed_ops/manifest.toml @@ -0,0 +1,34 @@ +name = "Access Control: CryptoOfficer allowed lifecycle operations" +description = "CryptoOfficer can perform lifecycle operations: Create, Locate, GetAttributes, Destroy." +server_config = "test_data/configs/server/auth/cert_roles.toml" + +[identities.owner] +client_cert = "test_data/certificates/client_server/owner/owner.client.acme.com.crt" +client_key = "test_data/certificates/client_server/owner/owner.client.acme.com.key" + +[identities.user] +client_cert = "test_data/certificates/client_server/user/user.client.acme.com.crt" +client_key = "test_data/certificates/client_server/user/user.client.acme.com.key" + +[[steps]] +operation = "Create" +request = "step1_create_key.json" +assert_success = true + +[steps.capture] +key_id = "UniqueIdentifier" + +[[steps]] +operation = "Locate" +request = "step2_locate.json" +assert_success = true + +[[steps]] +operation = "GetAttributes" +request = "step3_get_attributes.json" +assert_success = true + +[[steps]] +operation = "Destroy" +request = "step4_destroy.json" +assert_success = true diff --git a/vectors/access_control/crypto_officer_role_allowed_ops/step1_create_key.json b/vectors/access_control/crypto_officer_role_allowed_ops/step1_create_key.json new file mode 100644 index 0000000..692c4b5 --- /dev/null +++ b/vectors/access_control/crypto_officer_role_allowed_ops/step1_create_key.json @@ -0,0 +1,30 @@ +{ + "tag": "Create", + "value": [ + { + "tag": "ObjectType", + "type": "Enumeration", + "value": "SymmetricKey" + }, + { + "tag": "Attributes", + "value": [ + { + "tag": "CryptographicAlgorithm", + "type": "Enumeration", + "value": "AES" + }, + { + "tag": "CryptographicLength", + "type": "Integer", + "value": 256 + }, + { + "tag": "CryptographicUsageMask", + "type": "Integer", + "value": 12 + } + ] + } + ] +} diff --git a/vectors/access_control/crypto_officer_role_allowed_ops/step2_locate.json b/vectors/access_control/crypto_officer_role_allowed_ops/step2_locate.json new file mode 100644 index 0000000..f3ceab5 --- /dev/null +++ b/vectors/access_control/crypto_officer_role_allowed_ops/step2_locate.json @@ -0,0 +1,9 @@ +{ + "tag": "Locate", + "value": [ + { + "tag": "Attributes", + "value": [] + } + ] +} diff --git a/vectors/access_control/crypto_officer_role_allowed_ops/step3_get_attributes.json b/vectors/access_control/crypto_officer_role_allowed_ops/step3_get_attributes.json new file mode 100644 index 0000000..e96d057 --- /dev/null +++ b/vectors/access_control/crypto_officer_role_allowed_ops/step3_get_attributes.json @@ -0,0 +1,10 @@ +{ + "tag": "GetAttributes", + "value": [ + { + "tag": "UniqueIdentifier", + "type": "TextString", + "value": "{{key_id}}" + } + ] +} diff --git a/vectors/access_control/crypto_officer_role_allowed_ops/step4_destroy.json b/vectors/access_control/crypto_officer_role_allowed_ops/step4_destroy.json new file mode 100644 index 0000000..ce2dc0e --- /dev/null +++ b/vectors/access_control/crypto_officer_role_allowed_ops/step4_destroy.json @@ -0,0 +1,10 @@ +{ + "tag": "Destroy", + "value": [ + { + "tag": "UniqueIdentifier", + "type": "TextString", + "value": "{{key_id}}" + } + ] +} diff --git a/vectors/access_control/grant_access_aes/manifest.toml b/vectors/access_control/grant_access_aes/manifest.toml index 6204b63..f722bf7 100644 --- a/vectors/access_control/grant_access_aes/manifest.toml +++ b/vectors/access_control/grant_access_aes/manifest.toml @@ -1,6 +1,6 @@ name = "Access Control: grant Get/Encrypt/Decrypt to user" description = "Owner creates AES key, grants user access, user can Get/Encrypt/Decrypt, owner destroys key" -server_config = "test_data/configs/server/cert_auth.toml" +server_config = "test_data/configs/server/auth/cert.toml" [identities.owner] client_cert = "test_data/certificates/client_server/owner/owner.client.acme.com.crt" diff --git a/vectors/access_control/grant_partial_permissions/manifest.toml b/vectors/access_control/grant_partial_permissions/manifest.toml index 47c3446..360861a 100644 --- a/vectors/access_control/grant_partial_permissions/manifest.toml +++ b/vectors/access_control/grant_partial_permissions/manifest.toml @@ -1,6 +1,6 @@ name = "Access Control: grant partial permissions" description = "Owner grants only Get; user Get succeeds and Encrypt is denied" -server_config = "test_data/configs/server/cert_auth.toml" +server_config = "test_data/configs/server/auth/cert.toml" [identities.owner] client_cert = "test_data/certificates/client_server/owner/owner.client.acme.com.crt" diff --git a/vectors/access_control/operator_role_blocked_lifecycle/manifest.toml b/vectors/access_control/operator_role_blocked_lifecycle/manifest.toml new file mode 100644 index 0000000..8fb9016 --- /dev/null +++ b/vectors/access_control/operator_role_blocked_lifecycle/manifest.toml @@ -0,0 +1,25 @@ +name = "Access Control: Operator blocked from lifecycle operations" +description = "Operator role cannot perform lifecycle operations (Create, CreateKeyPair) without explicit Create grant." +server_config = "test_data/configs/server/auth/cert_roles.toml" + +[identities.owner] +client_cert = "test_data/certificates/client_server/owner/owner.client.acme.com.crt" +client_key = "test_data/certificates/client_server/owner/owner.client.acme.com.key" + +[identities.user] +client_cert = "test_data/certificates/client_server/user/user.client.acme.com.crt" +client_key = "test_data/certificates/client_server/user/user.client.acme.com.key" + +[[steps]] +operation = "Create" +request = "step1_operator_create_blocked.json" +identity = "user" +assert_success = false +assert_error_reason = "Permission_Denied" + +[[steps]] +operation = "CreateKeyPair" +request = "step2_operator_create_keypair_blocked.json" +identity = "user" +assert_success = false +assert_error_reason = "Permission_Denied" diff --git a/vectors/access_control/operator_role_blocked_lifecycle/step1_operator_create_blocked.json b/vectors/access_control/operator_role_blocked_lifecycle/step1_operator_create_blocked.json new file mode 100644 index 0000000..692c4b5 --- /dev/null +++ b/vectors/access_control/operator_role_blocked_lifecycle/step1_operator_create_blocked.json @@ -0,0 +1,30 @@ +{ + "tag": "Create", + "value": [ + { + "tag": "ObjectType", + "type": "Enumeration", + "value": "SymmetricKey" + }, + { + "tag": "Attributes", + "value": [ + { + "tag": "CryptographicAlgorithm", + "type": "Enumeration", + "value": "AES" + }, + { + "tag": "CryptographicLength", + "type": "Integer", + "value": 256 + }, + { + "tag": "CryptographicUsageMask", + "type": "Integer", + "value": 12 + } + ] + } + ] +} diff --git a/vectors/access_control/operator_role_blocked_lifecycle/step2_operator_create_keypair_blocked.json b/vectors/access_control/operator_role_blocked_lifecycle/step2_operator_create_keypair_blocked.json new file mode 100644 index 0000000..5a8ba68 --- /dev/null +++ b/vectors/access_control/operator_role_blocked_lifecycle/step2_operator_create_keypair_blocked.json @@ -0,0 +1,20 @@ +{ + "tag": "CreateKeyPair", + "value": [ + { + "tag": "CommonAttributes", + "value": [ + { + "tag": "CryptographicAlgorithm", + "type": "Enumeration", + "value": "RSA" + }, + { + "tag": "CryptographicLength", + "type": "Integer", + "value": 2048 + } + ] + } + ] +} diff --git a/vectors/access_control/owner_full_permissions/manifest.toml b/vectors/access_control/owner_full_permissions/manifest.toml index fa13ef6..6704b3d 100644 --- a/vectors/access_control/owner_full_permissions/manifest.toml +++ b/vectors/access_control/owner_full_permissions/manifest.toml @@ -1,6 +1,6 @@ name = "Access Control: owner full permissions" description = "Owner performs Get/Encrypt/Decrypt/Revoke/Destroy without grants" -server_config = "test_data/configs/server/cert_auth.toml" +server_config = "test_data/configs/server/auth/cert.toml" [identities.owner] client_cert = "test_data/certificates/client_server/owner/owner.client.acme.com.crt" diff --git a/vectors/access_control/privilege_escalation_activate_without_permission/manifest.toml b/vectors/access_control/privilege_escalation_activate_without_permission/manifest.toml index 3539e17..aedd8da 100644 --- a/vectors/access_control/privilege_escalation_activate_without_permission/manifest.toml +++ b/vectors/access_control/privilege_escalation_activate_without_permission/manifest.toml @@ -1,6 +1,6 @@ name = "Access Control: non-owner cannot Activate without explicit grant" description = "Owner creates a PreActive AES key, grants user only Encrypt. User's Activate attempt is denied because Encrypt grant does not imply Activate permission." -server_config = "test_data/configs/server/cert_auth.toml" +server_config = "test_data/configs/server/auth/cert.toml" [identities.owner] client_cert = "test_data/certificates/client_server/owner/owner.client.acme.com.crt" diff --git a/vectors/access_control/privilege_escalation_destroy_without_permission/manifest.toml b/vectors/access_control/privilege_escalation_destroy_without_permission/manifest.toml index 165cde3..013a2b9 100644 --- a/vectors/access_control/privilege_escalation_destroy_without_permission/manifest.toml +++ b/vectors/access_control/privilege_escalation_destroy_without_permission/manifest.toml @@ -1,6 +1,6 @@ name = "Access Control: user cannot destroy without permission" description = "Owner creates AES key, grants user only Get. Get acts as wildcard for crypto ops but NOT for Destroy — user's Destroy attempt is denied." -server_config = "test_data/configs/server/cert_auth.toml" +server_config = "test_data/configs/server/auth/cert.toml" [identities.owner] client_cert = "test_data/certificates/client_server/owner/owner.client.acme.com.crt" diff --git a/vectors/access_control/privilege_escalation_non_owner_grant/manifest.toml b/vectors/access_control/privilege_escalation_non_owner_grant/manifest.toml index df0d026..abd29a1 100644 --- a/vectors/access_control/privilege_escalation_non_owner_grant/manifest.toml +++ b/vectors/access_control/privilege_escalation_non_owner_grant/manifest.toml @@ -1,6 +1,6 @@ name = "Access Control: non-owner cannot grant access" description = "Owner creates AES key, user (non-owner) attempts to grant themselves access — must be denied because user does not own the key" -server_config = "test_data/configs/server/cert_auth.toml" +server_config = "test_data/configs/server/auth/cert.toml" [identities.owner] client_cert = "test_data/certificates/client_server/owner/owner.client.acme.com.crt" diff --git a/vectors/access_control/privilege_escalation_rekey_without_permission/manifest.toml b/vectors/access_control/privilege_escalation_rekey_without_permission/manifest.toml index 0562c8c..6f98cf0 100644 --- a/vectors/access_control/privilege_escalation_rekey_without_permission/manifest.toml +++ b/vectors/access_control/privilege_escalation_rekey_without_permission/manifest.toml @@ -1,6 +1,6 @@ name = "Access Control: non-owner cannot ReKey without explicit grant" description = "Owner creates AES key, grants user only Get. User's ReKey attempt is denied because Get wildcard does NOT apply to lifecycle-mutating operations like ReKey." -server_config = "test_data/configs/server/cert_auth.toml" +server_config = "test_data/configs/server/auth/cert.toml" [identities.owner] client_cert = "test_data/certificates/client_server/owner/owner.client.acme.com.crt" diff --git a/vectors/access_control/privilege_escalation_self_grant/manifest.toml b/vectors/access_control/privilege_escalation_self_grant/manifest.toml index b3d5195..b694e73 100644 --- a/vectors/access_control/privilege_escalation_self_grant/manifest.toml +++ b/vectors/access_control/privilege_escalation_self_grant/manifest.toml @@ -1,6 +1,6 @@ name = "Access Control: owner cannot self-grant" description = "Owner creates AES key, then attempts to grant themselves additional permissions — which must be denied" -server_config = "test_data/configs/server/cert_auth.toml" +server_config = "test_data/configs/server/auth/cert.toml" [identities.owner] client_cert = "test_data/certificates/client_server/owner/owner.client.acme.com.crt" diff --git a/vectors/access_control/revoke_access/manifest.toml b/vectors/access_control/revoke_access/manifest.toml index d3624a5..168cc07 100644 --- a/vectors/access_control/revoke_access/manifest.toml +++ b/vectors/access_control/revoke_access/manifest.toml @@ -1,6 +1,6 @@ name = "Access Control: revoke granted access" description = "Owner grants user Get, revokes it, user can no longer Get" -server_config = "test_data/configs/server/cert_auth.toml" +server_config = "test_data/configs/server/auth/cert.toml" [identities.owner] client_cert = "test_data/certificates/client_server/owner/owner.client.acme.com.crt" diff --git a/vectors/access_control/unauthorized_access/manifest.toml b/vectors/access_control/unauthorized_access/manifest.toml index 55df061..f18a174 100644 --- a/vectors/access_control/unauthorized_access/manifest.toml +++ b/vectors/access_control/unauthorized_access/manifest.toml @@ -1,6 +1,6 @@ name = "Access Control: unauthorized user denied" description = "Owner creates AES key and ungranted user cannot Get it" -server_config = "test_data/configs/server/cert_auth.toml" +server_config = "test_data/configs/server/auth/cert.toml" [identities.owner] client_cert = "test_data/certificates/client_server/owner/owner.client.acme.com.crt" diff --git a/vectors/fips/kmip_operations/create_split_key_sss/cleanup_destroy_key_id.json b/vectors/fips/kmip_operations/create_split_key_sss/cleanup_destroy_key_id.json new file mode 100644 index 0000000..ce2dc0e --- /dev/null +++ b/vectors/fips/kmip_operations/create_split_key_sss/cleanup_destroy_key_id.json @@ -0,0 +1,10 @@ +{ + "tag": "Destroy", + "value": [ + { + "tag": "UniqueIdentifier", + "type": "TextString", + "value": "{{key_id}}" + } + ] +} diff --git a/vectors/fips/kmip_operations/create_split_key_sss/cleanup_destroy_reconstructed_id.json b/vectors/fips/kmip_operations/create_split_key_sss/cleanup_destroy_reconstructed_id.json new file mode 100644 index 0000000..13817a6 --- /dev/null +++ b/vectors/fips/kmip_operations/create_split_key_sss/cleanup_destroy_reconstructed_id.json @@ -0,0 +1,10 @@ +{ + "tag": "Destroy", + "value": [ + { + "tag": "UniqueIdentifier", + "type": "TextString", + "value": "{{reconstructed_id}}" + } + ] +} diff --git a/vectors/fips/kmip_operations/create_split_key_sss/cleanup_destroy_share1_id.json b/vectors/fips/kmip_operations/create_split_key_sss/cleanup_destroy_share1_id.json new file mode 100644 index 0000000..87a3361 --- /dev/null +++ b/vectors/fips/kmip_operations/create_split_key_sss/cleanup_destroy_share1_id.json @@ -0,0 +1,10 @@ +{ + "tag": "Destroy", + "value": [ + { + "tag": "UniqueIdentifier", + "type": "TextString", + "value": "{{share1_id}}" + } + ] +} diff --git a/vectors/fips/kmip_operations/create_split_key_sss/cleanup_destroy_share2_id.json b/vectors/fips/kmip_operations/create_split_key_sss/cleanup_destroy_share2_id.json new file mode 100644 index 0000000..b2700d0 --- /dev/null +++ b/vectors/fips/kmip_operations/create_split_key_sss/cleanup_destroy_share2_id.json @@ -0,0 +1,10 @@ +{ + "tag": "Destroy", + "value": [ + { + "tag": "UniqueIdentifier", + "type": "TextString", + "value": "{{share2_id}}" + } + ] +} diff --git a/vectors/fips/kmip_operations/create_split_key_sss/cleanup_destroy_share3_id.json b/vectors/fips/kmip_operations/create_split_key_sss/cleanup_destroy_share3_id.json new file mode 100644 index 0000000..51ff86b --- /dev/null +++ b/vectors/fips/kmip_operations/create_split_key_sss/cleanup_destroy_share3_id.json @@ -0,0 +1,10 @@ +{ + "tag": "Destroy", + "value": [ + { + "tag": "UniqueIdentifier", + "type": "TextString", + "value": "{{share3_id}}" + } + ] +} diff --git a/vectors/fips/kmip_operations/create_split_key_sss/cleanup_revoke_key_id.json b/vectors/fips/kmip_operations/create_split_key_sss/cleanup_revoke_key_id.json new file mode 100644 index 0000000..2cffb71 --- /dev/null +++ b/vectors/fips/kmip_operations/create_split_key_sss/cleanup_revoke_key_id.json @@ -0,0 +1,20 @@ +{ + "tag": "Revoke", + "value": [ + { + "tag": "UniqueIdentifier", + "type": "TextString", + "value": "{{key_id}}" + }, + { + "tag": "RevocationReason", + "value": [ + { + "tag": "RevocationReasonCode", + "type": "Enumeration", + "value": "CessationOfOperation" + } + ] + } + ] +} diff --git a/vectors/fips/kmip_operations/create_split_key_sss/cleanup_revoke_reconstructed_id.json b/vectors/fips/kmip_operations/create_split_key_sss/cleanup_revoke_reconstructed_id.json new file mode 100644 index 0000000..e10ed77 --- /dev/null +++ b/vectors/fips/kmip_operations/create_split_key_sss/cleanup_revoke_reconstructed_id.json @@ -0,0 +1,20 @@ +{ + "tag": "Revoke", + "value": [ + { + "tag": "UniqueIdentifier", + "type": "TextString", + "value": "{{reconstructed_id}}" + }, + { + "tag": "RevocationReason", + "value": [ + { + "tag": "RevocationReasonCode", + "type": "Enumeration", + "value": "CessationOfOperation" + } + ] + } + ] +} diff --git a/vectors/fips/kmip_operations/create_split_key_sss/cleanup_revoke_share1_id.json b/vectors/fips/kmip_operations/create_split_key_sss/cleanup_revoke_share1_id.json new file mode 100644 index 0000000..083f706 --- /dev/null +++ b/vectors/fips/kmip_operations/create_split_key_sss/cleanup_revoke_share1_id.json @@ -0,0 +1,20 @@ +{ + "tag": "Revoke", + "value": [ + { + "tag": "UniqueIdentifier", + "type": "TextString", + "value": "{{share1_id}}" + }, + { + "tag": "RevocationReason", + "value": [ + { + "tag": "RevocationReasonCode", + "type": "Enumeration", + "value": "CessationOfOperation" + } + ] + } + ] +} diff --git a/vectors/fips/kmip_operations/create_split_key_sss/cleanup_revoke_share2_id.json b/vectors/fips/kmip_operations/create_split_key_sss/cleanup_revoke_share2_id.json new file mode 100644 index 0000000..4560b60 --- /dev/null +++ b/vectors/fips/kmip_operations/create_split_key_sss/cleanup_revoke_share2_id.json @@ -0,0 +1,20 @@ +{ + "tag": "Revoke", + "value": [ + { + "tag": "UniqueIdentifier", + "type": "TextString", + "value": "{{share2_id}}" + }, + { + "tag": "RevocationReason", + "value": [ + { + "tag": "RevocationReasonCode", + "type": "Enumeration", + "value": "CessationOfOperation" + } + ] + } + ] +} diff --git a/vectors/fips/kmip_operations/create_split_key_sss/cleanup_revoke_share3_id.json b/vectors/fips/kmip_operations/create_split_key_sss/cleanup_revoke_share3_id.json new file mode 100644 index 0000000..385bb4b --- /dev/null +++ b/vectors/fips/kmip_operations/create_split_key_sss/cleanup_revoke_share3_id.json @@ -0,0 +1,20 @@ +{ + "tag": "Revoke", + "value": [ + { + "tag": "UniqueIdentifier", + "type": "TextString", + "value": "{{share3_id}}" + }, + { + "tag": "RevocationReason", + "value": [ + { + "tag": "RevocationReasonCode", + "type": "Enumeration", + "value": "CessationOfOperation" + } + ] + } + ] +} diff --git a/vectors/fips/kmip_operations/create_split_key_sss/manifest.toml b/vectors/fips/kmip_operations/create_split_key_sss/manifest.toml new file mode 100644 index 0000000..1cace22 --- /dev/null +++ b/vectors/fips/kmip_operations/create_split_key_sss/manifest.toml @@ -0,0 +1,106 @@ +name = "CreateSplitKey + JoinSplitKey: XOR n-of-n round-trip (via PolynomialSharingGf28 method)" +description = """ +Creates an AES-256 symmetric key, splits it into 2 shares using XOR-based split knowledge +(requested via the PolynomialSharingGf28 KMIP method, which the server maps to XOR). +Threshold must equal total_parts. Joins all shares to reconstruct the key and verifies +the reconstructed object is a SymmetricKey. +""" + +# Uses the default SQLite backend (single user, no cert auth required). + +[[steps]] +operation = "Create" +request = "step1_create_key.json" +assert_success = true + +[steps.capture] +key_id = "UniqueIdentifier" + +# ── Split the key into 3 shares (threshold = 2) ────────────────────────────── + +[[steps]] +operation = "CreateSplitKey" +request = "step2_create_split_key.json" +assert_success = true + +[steps.assert_count] +PrivateKeyUniqueIdentifier = 2 + +[steps.capture] +share1_id = "PrivateKeyUniqueIdentifier" + +[steps.capture_nth.share2_id] +tag = "PrivateKeyUniqueIdentifier" +index = 1 + +# ── Join 2 of the 3 shares to reconstruct the key ─────────────────────────── + +[[steps]] +operation = "JoinSplitKey" +request = "step3_join_split_key.json" +assert_success = true + +[steps.capture] +reconstructed_id = "UniqueIdentifier" + +# ── Verify the reconstructed key is retrievable ────────────────────────────── + +[[steps]] +operation = "Get" +request = "step4_get_reconstructed.json" +assert_success = true + +[steps.assert_fields] +ObjectType = "SymmetricKey" + +# ── Cleanup: revoke + destroy all objects (best-effort) ────────────────────── + +[[steps]] +operation = "Revoke" +request = "cleanup_revoke_key_id.json" +allow_failure = true + +[[steps]] +operation = "Destroy" +request = "cleanup_destroy_key_id.json" +allow_failure = true + +[[steps]] +operation = "Revoke" +request = "cleanup_revoke_share1_id.json" +allow_failure = true + +[[steps]] +operation = "Destroy" +request = "cleanup_destroy_share1_id.json" +allow_failure = true + +[[steps]] +operation = "Revoke" +request = "cleanup_revoke_share2_id.json" +allow_failure = true + +[[steps]] +operation = "Destroy" +request = "cleanup_destroy_share2_id.json" +allow_failure = true + +[[steps]] +operation = "Revoke" +request = "cleanup_revoke_share3_id.json" +allow_failure = true + +[[steps]] +operation = "Destroy" +request = "cleanup_destroy_share3_id.json" +allow_failure = true + +[[steps]] +operation = "Revoke" +request = "cleanup_revoke_reconstructed_id.json" +allow_failure = true + +[[steps]] +operation = "Destroy" +request = "cleanup_destroy_reconstructed_id.json" +allow_failure = true diff --git a/vectors/fips/kmip_operations/create_split_key_sss/step1_create_key.json b/vectors/fips/kmip_operations/create_split_key_sss/step1_create_key.json new file mode 100644 index 0000000..1b5500d --- /dev/null +++ b/vectors/fips/kmip_operations/create_split_key_sss/step1_create_key.json @@ -0,0 +1,35 @@ +{ + "tag": "Create", + "value": [ + { + "tag": "ObjectType", + "type": "Enumeration", + "value": "SymmetricKey" + }, + { + "tag": "Attributes", + "value": [ + { + "tag": "CryptographicAlgorithm", + "type": "Enumeration", + "value": "AES" + }, + { + "tag": "CryptographicLength", + "type": "Integer", + "value": 256 + }, + { + "tag": "CryptographicUsageMask", + "type": "Integer", + "value": 12 + }, + { + "tag": "ObjectType", + "type": "Enumeration", + "value": "SymmetricKey" + } + ] + } + ] +} diff --git a/vectors/fips/kmip_operations/create_split_key_sss/step2_create_split_key.json b/vectors/fips/kmip_operations/create_split_key_sss/step2_create_split_key.json new file mode 100644 index 0000000..897b2fe --- /dev/null +++ b/vectors/fips/kmip_operations/create_split_key_sss/step2_create_split_key.json @@ -0,0 +1,25 @@ +{ + "tag": "CreateSplitKey", + "value": [ + { + "tag": "UniqueIdentifier", + "type": "TextString", + "value": "{{key_id}}" + }, + { + "tag": "SplitKeyParts", + "type": "Integer", + "value": 2 + }, + { + "tag": "SplitKeyThreshold", + "type": "Integer", + "value": 2 + }, + { + "tag": "SplitKeyMethod", + "type": "Enumeration", + "value": "PolynomialSharingGf28" + } + ] +} diff --git a/vectors/fips/kmip_operations/create_split_key_sss/step3_join_split_key.json b/vectors/fips/kmip_operations/create_split_key_sss/step3_join_split_key.json new file mode 100644 index 0000000..9d85f48 --- /dev/null +++ b/vectors/fips/kmip_operations/create_split_key_sss/step3_join_split_key.json @@ -0,0 +1,25 @@ +{ + "tag": "JoinSplitKey", + "value": [ + { + "tag": "ObjectType", + "type": "Enumeration", + "value": "SymmetricKey" + }, + { + "tag": "PrivateKeyUniqueIdentifier", + "type": "TextString", + "value": "{{share1_id}}" + }, + { + "tag": "PrivateKeyUniqueIdentifier", + "type": "TextString", + "value": "{{share2_id}}" + }, + { + "tag": "SplitKeyMethod", + "type": "Enumeration", + "value": "PolynomialSharingGf28" + } + ] +} diff --git a/vectors/fips/kmip_operations/create_split_key_sss/step4_get_reconstructed.json b/vectors/fips/kmip_operations/create_split_key_sss/step4_get_reconstructed.json new file mode 100644 index 0000000..0467262 --- /dev/null +++ b/vectors/fips/kmip_operations/create_split_key_sss/step4_get_reconstructed.json @@ -0,0 +1,10 @@ +{ + "tag": "Get", + "value": [ + { + "tag": "UniqueIdentifier", + "type": "TextString", + "value": "{{reconstructed_id}}" + } + ] +} diff --git a/vectors/fips/kmip_operations/create_split_key_xor/cleanup_destroy_key_id.json b/vectors/fips/kmip_operations/create_split_key_xor/cleanup_destroy_key_id.json new file mode 100644 index 0000000..ce2dc0e --- /dev/null +++ b/vectors/fips/kmip_operations/create_split_key_xor/cleanup_destroy_key_id.json @@ -0,0 +1,10 @@ +{ + "tag": "Destroy", + "value": [ + { + "tag": "UniqueIdentifier", + "type": "TextString", + "value": "{{key_id}}" + } + ] +} diff --git a/vectors/fips/kmip_operations/create_split_key_xor/cleanup_destroy_reconstructed_id.json b/vectors/fips/kmip_operations/create_split_key_xor/cleanup_destroy_reconstructed_id.json new file mode 100644 index 0000000..13817a6 --- /dev/null +++ b/vectors/fips/kmip_operations/create_split_key_xor/cleanup_destroy_reconstructed_id.json @@ -0,0 +1,10 @@ +{ + "tag": "Destroy", + "value": [ + { + "tag": "UniqueIdentifier", + "type": "TextString", + "value": "{{reconstructed_id}}" + } + ] +} diff --git a/vectors/fips/kmip_operations/create_split_key_xor/cleanup_destroy_share1_id.json b/vectors/fips/kmip_operations/create_split_key_xor/cleanup_destroy_share1_id.json new file mode 100644 index 0000000..87a3361 --- /dev/null +++ b/vectors/fips/kmip_operations/create_split_key_xor/cleanup_destroy_share1_id.json @@ -0,0 +1,10 @@ +{ + "tag": "Destroy", + "value": [ + { + "tag": "UniqueIdentifier", + "type": "TextString", + "value": "{{share1_id}}" + } + ] +} diff --git a/vectors/fips/kmip_operations/create_split_key_xor/cleanup_destroy_share2_id.json b/vectors/fips/kmip_operations/create_split_key_xor/cleanup_destroy_share2_id.json new file mode 100644 index 0000000..b2700d0 --- /dev/null +++ b/vectors/fips/kmip_operations/create_split_key_xor/cleanup_destroy_share2_id.json @@ -0,0 +1,10 @@ +{ + "tag": "Destroy", + "value": [ + { + "tag": "UniqueIdentifier", + "type": "TextString", + "value": "{{share2_id}}" + } + ] +} diff --git a/vectors/fips/kmip_operations/create_split_key_xor/cleanup_revoke_key_id.json b/vectors/fips/kmip_operations/create_split_key_xor/cleanup_revoke_key_id.json new file mode 100644 index 0000000..2cffb71 --- /dev/null +++ b/vectors/fips/kmip_operations/create_split_key_xor/cleanup_revoke_key_id.json @@ -0,0 +1,20 @@ +{ + "tag": "Revoke", + "value": [ + { + "tag": "UniqueIdentifier", + "type": "TextString", + "value": "{{key_id}}" + }, + { + "tag": "RevocationReason", + "value": [ + { + "tag": "RevocationReasonCode", + "type": "Enumeration", + "value": "CessationOfOperation" + } + ] + } + ] +} diff --git a/vectors/fips/kmip_operations/create_split_key_xor/cleanup_revoke_reconstructed_id.json b/vectors/fips/kmip_operations/create_split_key_xor/cleanup_revoke_reconstructed_id.json new file mode 100644 index 0000000..e10ed77 --- /dev/null +++ b/vectors/fips/kmip_operations/create_split_key_xor/cleanup_revoke_reconstructed_id.json @@ -0,0 +1,20 @@ +{ + "tag": "Revoke", + "value": [ + { + "tag": "UniqueIdentifier", + "type": "TextString", + "value": "{{reconstructed_id}}" + }, + { + "tag": "RevocationReason", + "value": [ + { + "tag": "RevocationReasonCode", + "type": "Enumeration", + "value": "CessationOfOperation" + } + ] + } + ] +} diff --git a/vectors/fips/kmip_operations/create_split_key_xor/cleanup_revoke_share1_id.json b/vectors/fips/kmip_operations/create_split_key_xor/cleanup_revoke_share1_id.json new file mode 100644 index 0000000..083f706 --- /dev/null +++ b/vectors/fips/kmip_operations/create_split_key_xor/cleanup_revoke_share1_id.json @@ -0,0 +1,20 @@ +{ + "tag": "Revoke", + "value": [ + { + "tag": "UniqueIdentifier", + "type": "TextString", + "value": "{{share1_id}}" + }, + { + "tag": "RevocationReason", + "value": [ + { + "tag": "RevocationReasonCode", + "type": "Enumeration", + "value": "CessationOfOperation" + } + ] + } + ] +} diff --git a/vectors/fips/kmip_operations/create_split_key_xor/cleanup_revoke_share2_id.json b/vectors/fips/kmip_operations/create_split_key_xor/cleanup_revoke_share2_id.json new file mode 100644 index 0000000..4560b60 --- /dev/null +++ b/vectors/fips/kmip_operations/create_split_key_xor/cleanup_revoke_share2_id.json @@ -0,0 +1,20 @@ +{ + "tag": "Revoke", + "value": [ + { + "tag": "UniqueIdentifier", + "type": "TextString", + "value": "{{share2_id}}" + }, + { + "tag": "RevocationReason", + "value": [ + { + "tag": "RevocationReasonCode", + "type": "Enumeration", + "value": "CessationOfOperation" + } + ] + } + ] +} diff --git a/vectors/fips/kmip_operations/create_split_key_xor/manifest.toml b/vectors/fips/kmip_operations/create_split_key_xor/manifest.toml new file mode 100644 index 0000000..53b0c0b --- /dev/null +++ b/vectors/fips/kmip_operations/create_split_key_xor/manifest.toml @@ -0,0 +1,95 @@ +name = "CreateSplitKey + JoinSplitKey: XOR 2-of-2 round-trip" +description = """ +Creates an AES-256 symmetric key, splits it into 2 shares using XOR splitting (both shares +required). Then joins the 2 shares to reconstruct the key and verifies the reconstructed +object is a SymmetricKey. +""" + +# Uses the default SQLite backend (single user, no cert auth required). + +[[steps]] +operation = "Create" +request = "step1_create_key.json" +assert_success = true + +[steps.capture] +key_id = "UniqueIdentifier" + +# ── Split the key into 2 shares using XOR ──────────────────────────────────── + +[[steps]] +operation = "CreateSplitKey" +request = "step2_create_split_key.json" +assert_success = true + +[steps.assert_count] +PrivateKeyUniqueIdentifier = 2 + +[steps.capture] +share1_id = "PrivateKeyUniqueIdentifier" + +[steps.capture_nth.share2_id] +tag = "PrivateKeyUniqueIdentifier" +index = 1 + +# ── Join both shares to reconstruct the key ────────────────────────────────── + +[[steps]] +operation = "JoinSplitKey" +request = "step3_join_split_key.json" +assert_success = true + +[steps.capture] +reconstructed_id = "UniqueIdentifier" + +# ── Verify the reconstructed key is retrievable ────────────────────────────── + +[[steps]] +operation = "Get" +request = "step4_get_reconstructed.json" +assert_success = true + +[steps.assert_fields] +ObjectType = "SymmetricKey" + +# ── Cleanup ────────────────────────────────────────────────────────────────── + +[[steps]] +operation = "Revoke" +request = "cleanup_revoke_key_id.json" +allow_failure = true + +[[steps]] +operation = "Destroy" +request = "cleanup_destroy_key_id.json" +allow_failure = true + +[[steps]] +operation = "Revoke" +request = "cleanup_revoke_share1_id.json" +allow_failure = true + +[[steps]] +operation = "Destroy" +request = "cleanup_destroy_share1_id.json" +allow_failure = true + +[[steps]] +operation = "Revoke" +request = "cleanup_revoke_share2_id.json" +allow_failure = true + +[[steps]] +operation = "Destroy" +request = "cleanup_destroy_share2_id.json" +allow_failure = true + +[[steps]] +operation = "Revoke" +request = "cleanup_revoke_reconstructed_id.json" +allow_failure = true + +[[steps]] +operation = "Destroy" +request = "cleanup_destroy_reconstructed_id.json" +allow_failure = true diff --git a/vectors/fips/kmip_operations/create_split_key_xor/step1_create_key.json b/vectors/fips/kmip_operations/create_split_key_xor/step1_create_key.json new file mode 100644 index 0000000..1b5500d --- /dev/null +++ b/vectors/fips/kmip_operations/create_split_key_xor/step1_create_key.json @@ -0,0 +1,35 @@ +{ + "tag": "Create", + "value": [ + { + "tag": "ObjectType", + "type": "Enumeration", + "value": "SymmetricKey" + }, + { + "tag": "Attributes", + "value": [ + { + "tag": "CryptographicAlgorithm", + "type": "Enumeration", + "value": "AES" + }, + { + "tag": "CryptographicLength", + "type": "Integer", + "value": 256 + }, + { + "tag": "CryptographicUsageMask", + "type": "Integer", + "value": 12 + }, + { + "tag": "ObjectType", + "type": "Enumeration", + "value": "SymmetricKey" + } + ] + } + ] +} diff --git a/vectors/fips/kmip_operations/create_split_key_xor/step2_create_split_key.json b/vectors/fips/kmip_operations/create_split_key_xor/step2_create_split_key.json new file mode 100644 index 0000000..6215a14 --- /dev/null +++ b/vectors/fips/kmip_operations/create_split_key_xor/step2_create_split_key.json @@ -0,0 +1,25 @@ +{ + "tag": "CreateSplitKey", + "value": [ + { + "tag": "UniqueIdentifier", + "type": "TextString", + "value": "{{key_id}}" + }, + { + "tag": "SplitKeyParts", + "type": "Integer", + "value": 2 + }, + { + "tag": "SplitKeyThreshold", + "type": "Integer", + "value": 2 + }, + { + "tag": "SplitKeyMethod", + "type": "Enumeration", + "value": "XOR" + } + ] +} diff --git a/vectors/fips/kmip_operations/create_split_key_xor/step3_join_split_key.json b/vectors/fips/kmip_operations/create_split_key_xor/step3_join_split_key.json new file mode 100644 index 0000000..4688286 --- /dev/null +++ b/vectors/fips/kmip_operations/create_split_key_xor/step3_join_split_key.json @@ -0,0 +1,25 @@ +{ + "tag": "JoinSplitKey", + "value": [ + { + "tag": "ObjectType", + "type": "Enumeration", + "value": "SymmetricKey" + }, + { + "tag": "PrivateKeyUniqueIdentifier", + "type": "TextString", + "value": "{{share1_id}}" + }, + { + "tag": "PrivateKeyUniqueIdentifier", + "type": "TextString", + "value": "{{share2_id}}" + }, + { + "tag": "SplitKeyMethod", + "type": "Enumeration", + "value": "XOR" + } + ] +} diff --git a/vectors/fips/kmip_operations/create_split_key_xor/step4_get_reconstructed.json b/vectors/fips/kmip_operations/create_split_key_xor/step4_get_reconstructed.json new file mode 100644 index 0000000..0467262 --- /dev/null +++ b/vectors/fips/kmip_operations/create_split_key_xor/step4_get_reconstructed.json @@ -0,0 +1,10 @@ +{ + "tag": "Get", + "value": [ + { + "tag": "UniqueIdentifier", + "type": "TextString", + "value": "{{reconstructed_id}}" + } + ] +} diff --git a/vectors/negative/create_split_key_parts_less_than_threshold/cleanup_destroy_key.json b/vectors/negative/create_split_key_parts_less_than_threshold/cleanup_destroy_key.json new file mode 100644 index 0000000..ce2dc0e --- /dev/null +++ b/vectors/negative/create_split_key_parts_less_than_threshold/cleanup_destroy_key.json @@ -0,0 +1,10 @@ +{ + "tag": "Destroy", + "value": [ + { + "tag": "UniqueIdentifier", + "type": "TextString", + "value": "{{key_id}}" + } + ] +} diff --git a/vectors/negative/create_split_key_parts_less_than_threshold/manifest.toml b/vectors/negative/create_split_key_parts_less_than_threshold/manifest.toml new file mode 100644 index 0000000..cb25121 --- /dev/null +++ b/vectors/negative/create_split_key_parts_less_than_threshold/manifest.toml @@ -0,0 +1,28 @@ +name = "CreateSplitKey: parts < threshold is rejected" +description = """ +A CreateSplitKey request where split_key_parts < split_key_threshold must be rejected. +You cannot require more shares than exist. +""" + +[[steps]] +operation = "Create" +request = "step1_create_key.json" +assert_success = true + +[steps.capture] +key_id = "UniqueIdentifier" + +# ── Attempt CreateSplitKey with parts(3) < threshold(5) ────────────────────── + +[[steps]] +operation = "CreateSplitKey" +request = "step2_create_split_key_bad_parts.json" +assert_success = false +assert_error_contains = "split_key_parts must be >= split_key_threshold" + +# ── Cleanup ────────────────────────────────────────────────────────────────── + +[[steps]] +operation = "Destroy" +request = "cleanup_destroy_key.json" +allow_failure = true diff --git a/vectors/negative/create_split_key_parts_less_than_threshold/step1_create_key.json b/vectors/negative/create_split_key_parts_less_than_threshold/step1_create_key.json new file mode 100644 index 0000000..1b5500d --- /dev/null +++ b/vectors/negative/create_split_key_parts_less_than_threshold/step1_create_key.json @@ -0,0 +1,35 @@ +{ + "tag": "Create", + "value": [ + { + "tag": "ObjectType", + "type": "Enumeration", + "value": "SymmetricKey" + }, + { + "tag": "Attributes", + "value": [ + { + "tag": "CryptographicAlgorithm", + "type": "Enumeration", + "value": "AES" + }, + { + "tag": "CryptographicLength", + "type": "Integer", + "value": 256 + }, + { + "tag": "CryptographicUsageMask", + "type": "Integer", + "value": 12 + }, + { + "tag": "ObjectType", + "type": "Enumeration", + "value": "SymmetricKey" + } + ] + } + ] +} diff --git a/vectors/negative/create_split_key_parts_less_than_threshold/step2_create_split_key_bad_parts.json b/vectors/negative/create_split_key_parts_less_than_threshold/step2_create_split_key_bad_parts.json new file mode 100644 index 0000000..f2cd74c --- /dev/null +++ b/vectors/negative/create_split_key_parts_less_than_threshold/step2_create_split_key_bad_parts.json @@ -0,0 +1,25 @@ +{ + "tag": "CreateSplitKey", + "value": [ + { + "tag": "UniqueIdentifier", + "type": "TextString", + "value": "{{key_id}}" + }, + { + "tag": "SplitKeyParts", + "type": "Integer", + "value": 3 + }, + { + "tag": "SplitKeyThreshold", + "type": "Integer", + "value": 5 + }, + { + "tag": "SplitKeyMethod", + "type": "Enumeration", + "value": "PolynomialSharingGf28" + } + ] +} diff --git a/vectors/negative/create_split_key_threshold_too_low/cleanup_destroy_key.json b/vectors/negative/create_split_key_threshold_too_low/cleanup_destroy_key.json new file mode 100644 index 0000000..ce2dc0e --- /dev/null +++ b/vectors/negative/create_split_key_threshold_too_low/cleanup_destroy_key.json @@ -0,0 +1,10 @@ +{ + "tag": "Destroy", + "value": [ + { + "tag": "UniqueIdentifier", + "type": "TextString", + "value": "{{key_id}}" + } + ] +} diff --git a/vectors/negative/create_split_key_threshold_too_low/manifest.toml b/vectors/negative/create_split_key_threshold_too_low/manifest.toml new file mode 100644 index 0000000..cd1e576 --- /dev/null +++ b/vectors/negative/create_split_key_threshold_too_low/manifest.toml @@ -0,0 +1,28 @@ +name = "CreateSplitKey: threshold < 2 is rejected" +description = """ +A CreateSplitKey request with split_key_threshold = 1 must be rejected by the server. +A threshold of at least 2 is required for meaningful secret sharing. +""" + +[[steps]] +operation = "Create" +request = "step1_create_key.json" +assert_success = true + +[steps.capture] +key_id = "UniqueIdentifier" + +# ── Attempt CreateSplitKey with an invalid threshold ───────────────────────── + +[[steps]] +operation = "CreateSplitKey" +request = "step2_create_split_key_bad_threshold.json" +assert_success = false +assert_error_contains = "split_key_threshold must be at least 2" + +# ── Cleanup ────────────────────────────────────────────────────────────────── + +[[steps]] +operation = "Destroy" +request = "cleanup_destroy_key.json" +allow_failure = true diff --git a/vectors/negative/create_split_key_threshold_too_low/step1_create_key.json b/vectors/negative/create_split_key_threshold_too_low/step1_create_key.json new file mode 100644 index 0000000..1b5500d --- /dev/null +++ b/vectors/negative/create_split_key_threshold_too_low/step1_create_key.json @@ -0,0 +1,35 @@ +{ + "tag": "Create", + "value": [ + { + "tag": "ObjectType", + "type": "Enumeration", + "value": "SymmetricKey" + }, + { + "tag": "Attributes", + "value": [ + { + "tag": "CryptographicAlgorithm", + "type": "Enumeration", + "value": "AES" + }, + { + "tag": "CryptographicLength", + "type": "Integer", + "value": 256 + }, + { + "tag": "CryptographicUsageMask", + "type": "Integer", + "value": 12 + }, + { + "tag": "ObjectType", + "type": "Enumeration", + "value": "SymmetricKey" + } + ] + } + ] +} diff --git a/vectors/negative/create_split_key_threshold_too_low/step2_create_split_key_bad_threshold.json b/vectors/negative/create_split_key_threshold_too_low/step2_create_split_key_bad_threshold.json new file mode 100644 index 0000000..7d1e5e1 --- /dev/null +++ b/vectors/negative/create_split_key_threshold_too_low/step2_create_split_key_bad_threshold.json @@ -0,0 +1,25 @@ +{ + "tag": "CreateSplitKey", + "value": [ + { + "tag": "UniqueIdentifier", + "type": "TextString", + "value": "{{key_id}}" + }, + { + "tag": "SplitKeyParts", + "type": "Integer", + "value": 3 + }, + { + "tag": "SplitKeyThreshold", + "type": "Integer", + "value": 1 + }, + { + "tag": "SplitKeyMethod", + "type": "Enumeration", + "value": "PolynomialSharingGf28" + } + ] +} diff --git a/vectors/opa/mode_enforcing_auditor_create_denied/manifest.toml b/vectors/opa/mode_enforcing_auditor_create_denied/manifest.toml new file mode 100644 index 0000000..e99fb41 --- /dev/null +++ b/vectors/opa/mode_enforcing_auditor_create_denied/manifest.toml @@ -0,0 +1,26 @@ +name = "OPA Mode 3 (Enforcing): Auditor denied Create (not in auditor_ops)" +description = """ +OPA enforcing mode. A user holding the `Auditor` role attempts to create a +new symmetric key. `Create` is intentionally absent from `auditor_ops` in +kms.rego — auditors are read-only observers and must not generate key material. +OPA returns allow=false → KMS returns 401 Unauthorized. + +This enforces separation-of-duties: the Auditor role cannot generate or modify +cryptographic objects, even though it can read metadata (GetAttributes, Locate). + +Ref: kms.rego `auditor_ops` set (NIST SP 800-53 AU-9 separation-of-duties; +PCI-DSS v4.0 Req 10 — auditor must not create evidence-generating objects). +""" +requires_env = ["KMS_OPA_URL", "KMS_AUTH_SERVER_URL", "KMS_TEST_OPA_AUDITOR_JWT"] +backends = ["sqlite"] + +# Auditor JWT — valid role, same domain as the server's OPA test realm. +[identities.auditor] +access_token_env = "KMS_TEST_OPA_AUDITOR_JWT" + +[[steps]] +operation = "Create" +request = "step1_create.json" +identity = "auditor" +assert_success = false +assert_error_contains = "access-right" diff --git a/vectors/opa/mode_enforcing_auditor_create_denied/step1_create.json b/vectors/opa/mode_enforcing_auditor_create_denied/step1_create.json new file mode 100644 index 0000000..c994e09 --- /dev/null +++ b/vectors/opa/mode_enforcing_auditor_create_denied/step1_create.json @@ -0,0 +1,40 @@ +{ + "tag": "Create", + "value": [ + { + "tag": "ObjectType", + "type": "Enumeration", + "value": "SymmetricKey" + }, + { + "tag": "Attributes", + "value": [ + { + "tag": "CryptographicAlgorithm", + "type": "Enumeration", + "value": "AES" + }, + { + "tag": "CryptographicLength", + "type": "Integer", + "value": 256 + }, + { + "tag": "CryptographicUsageMask", + "type": "Integer", + "value": 2108 + }, + { + "tag": "KeyFormatType", + "type": "Enumeration", + "value": "TransparentSymmetricKey" + }, + { + "tag": "ObjectType", + "type": "Enumeration", + "value": "SymmetricKey" + } + ] + } + ] +} diff --git a/vectors/opa/mode_enforcing_co_get_attributes_allowed/manifest.toml b/vectors/opa/mode_enforcing_co_get_attributes_allowed/manifest.toml new file mode 100644 index 0000000..17ef9c9 --- /dev/null +++ b/vectors/opa/mode_enforcing_co_get_attributes_allowed/manifest.toml @@ -0,0 +1,58 @@ +name = "OPA Mode 3 (Enforcing): CryptoOfficer GetAttributes on non-owned key — allowed" +description = """ +OPA enforcing mode. A `CryptoOfficer` in realm `kms-opa-test` (the default owner / JWT +client) creates an AES key. A second user — also a `CryptoOfficer` in the same realm +(kms-opa-auditor_jwt in `kms-opa-test` holds the Auditor role, so we use the officer JWT +directly as the non-owning reader) — reads GetAttributes on the key using a JWT identity +that is NOT the owner. + +Wait: to test non-owner access we need a different identity that is also a CryptoOfficer. +The `Auditor` role is in `auditor_ops` for `get_attributes` too, so use `Auditor` identity +for the GetAttributes step. This proves that any role that has `get_attributes` in its +`*_ops` set in kms.rego can read attributes of a key they do not own, once OPA is the +authoritative gate in enforcing mode. + +OPA evaluation for the auditor GetAttributes step: + input.roles = ["Auditor"] + input.user_domain = "kms-opa-test" + input.object_domain= "kms-opa-test" (same domain) + input.is_owner = false + input.operation = "get_attributes" + → auditor_ops contains "get_attributes" ∧ same_domain → allow = true + +Previously (before fix) OPA Gate 1 allowed but KMS Gate 2 (legacy ownership check) +denied → N/A displayed for all fields except UID. +After fix: OPA approval is authoritative in enforcing mode for non-HSM objects. + +Ref: kms.rego `auditor_ops`; NIST SP 800-57 Part 2 §4.3 — Auditor read-only metadata. +""" +requires_env = ["KMS_OPA_URL", "KMS_AUTH_SERVER_URL", "KMS_TEST_OPA_AUDITOR_JWT"] +backends = ["sqlite"] + +# Auditor JWT — same domain as the key owner (kms-opa-test), non-owner. +[identities.auditor] +access_token_env = "KMS_TEST_OPA_AUDITOR_JWT" + +# Step 1: Officer (default identity) creates the key. +[[steps]] +operation = "Create" +request = "step1_create.json" +assert_success = true + +[steps.capture] +key_id = "UniqueIdentifier" + +# Step 3: Auditor (non-owner, same domain) reads GetAttributes → must succeed. +# data-testid scenario: this is the operation that showed N/A in the Web UI +# Locate page before the enforcing-mode Gate 2 fix. +[[steps]] +operation = "GetAttributes" +request = "step3_get_attributes.json" +identity = "auditor" +assert_success = true + +# Step 4: Officer (owner) destroys the key. +[[steps]] +operation = "Destroy" +request = "step4_destroy.json" +allow_failure = true diff --git a/vectors/opa/mode_enforcing_co_get_attributes_allowed/step1_create.json b/vectors/opa/mode_enforcing_co_get_attributes_allowed/step1_create.json new file mode 100644 index 0000000..c994e09 --- /dev/null +++ b/vectors/opa/mode_enforcing_co_get_attributes_allowed/step1_create.json @@ -0,0 +1,40 @@ +{ + "tag": "Create", + "value": [ + { + "tag": "ObjectType", + "type": "Enumeration", + "value": "SymmetricKey" + }, + { + "tag": "Attributes", + "value": [ + { + "tag": "CryptographicAlgorithm", + "type": "Enumeration", + "value": "AES" + }, + { + "tag": "CryptographicLength", + "type": "Integer", + "value": 256 + }, + { + "tag": "CryptographicUsageMask", + "type": "Integer", + "value": 2108 + }, + { + "tag": "KeyFormatType", + "type": "Enumeration", + "value": "TransparentSymmetricKey" + }, + { + "tag": "ObjectType", + "type": "Enumeration", + "value": "SymmetricKey" + } + ] + } + ] +} diff --git a/vectors/opa/mode_enforcing_co_get_attributes_allowed/step3_get_attributes.json b/vectors/opa/mode_enforcing_co_get_attributes_allowed/step3_get_attributes.json new file mode 100644 index 0000000..e96d057 --- /dev/null +++ b/vectors/opa/mode_enforcing_co_get_attributes_allowed/step3_get_attributes.json @@ -0,0 +1,10 @@ +{ + "tag": "GetAttributes", + "value": [ + { + "tag": "UniqueIdentifier", + "type": "TextString", + "value": "{{key_id}}" + } + ] +} diff --git a/vectors/opa/mode_enforcing_co_get_attributes_allowed/step4_destroy.json b/vectors/opa/mode_enforcing_co_get_attributes_allowed/step4_destroy.json new file mode 100644 index 0000000..ce2dc0e --- /dev/null +++ b/vectors/opa/mode_enforcing_co_get_attributes_allowed/step4_destroy.json @@ -0,0 +1,10 @@ +{ + "tag": "Destroy", + "value": [ + { + "tag": "UniqueIdentifier", + "type": "TextString", + "value": "{{key_id}}" + } + ] +} diff --git a/vectors/opa/mode_enforcing_empty_roles_denied/manifest.toml b/vectors/opa/mode_enforcing_empty_roles_denied/manifest.toml new file mode 100644 index 0000000..7d91d5c --- /dev/null +++ b/vectors/opa/mode_enforcing_empty_roles_denied/manifest.toml @@ -0,0 +1,24 @@ +name = "OPA Mode 3 (Enforcing): empty JWT roles denied Create" +description = """ +OPA enforcing mode. A bearer token with an empty `roles` claim (and no domain) +is presented to the KMS. OPA evaluates `input.roles = []` against kms.rego: +no allow rule matches → allow=false → KMS returns 401 Unauthorized. + +This proves that a JWT with no role claims (e.g. a misconfigured or legacy token) +cannot bypass Gate 1 even in enforcing mode. + +Ref: kms.rego default deny (NIST SP 800-53 AC-6 least-privilege; fail-closed). +""" +requires_env = ["KMS_OPA_URL", "KMS_AUTH_SERVER_URL", "KMS_TEST_OPA_NO_ROLES_JWT"] +backends = ["sqlite"] + +# Token issued by the auth server for a user with no roles assigned. +[identities.no_roles] +access_token_env = "KMS_TEST_OPA_NO_ROLES_JWT" + +[[steps]] +operation = "Create" +request = "step1_create.json" +identity = "no_roles" +assert_success = false +assert_error_contains = "access-right" diff --git a/vectors/opa/mode_enforcing_empty_roles_denied/step1_create.json b/vectors/opa/mode_enforcing_empty_roles_denied/step1_create.json new file mode 100644 index 0000000..c994e09 --- /dev/null +++ b/vectors/opa/mode_enforcing_empty_roles_denied/step1_create.json @@ -0,0 +1,40 @@ +{ + "tag": "Create", + "value": [ + { + "tag": "ObjectType", + "type": "Enumeration", + "value": "SymmetricKey" + }, + { + "tag": "Attributes", + "value": [ + { + "tag": "CryptographicAlgorithm", + "type": "Enumeration", + "value": "AES" + }, + { + "tag": "CryptographicLength", + "type": "Integer", + "value": 256 + }, + { + "tag": "CryptographicUsageMask", + "type": "Integer", + "value": 2108 + }, + { + "tag": "KeyFormatType", + "type": "Enumeration", + "value": "TransparentSymmetricKey" + }, + { + "tag": "ObjectType", + "type": "Enumeration", + "value": "SymmetricKey" + } + ] + } + ] +} diff --git a/vectors/opa/mode_enforcing_native_co_cert_allowed/manifest.toml b/vectors/opa/mode_enforcing_native_co_cert_allowed/manifest.toml new file mode 100644 index 0000000..18ec553 --- /dev/null +++ b/vectors/opa/mode_enforcing_native_co_cert_allowed/manifest.toml @@ -0,0 +1,38 @@ +name = "OPA Mode 3 (Enforcing): native KMS CO cert user allowed Create (privileged)" +description = """ +OPA enforcing mode. A client authenticated via mTLS (cert CN = owner.client@acme.com) +IS listed as a native KMS Crypto Officer in `[roles] crypto_officer_users`. +Privileged users bypass OPA Gate 1 in enforcing mode — the KMS-native role is +sufficient — so the Create succeeds even though the client presents no JWT. + +Gate 2 (native KMS ownership/grant check) also passes because the user is an +explicitly trusted CO. + +This proves the two-gate enforcing model: privileged native COs can create in +enforcing mode, but NOT in exclusive mode (where OPA is the sole authority). + +Ref: kms.rego enforcing bypass for co_users (FIPS 140-3 §7.4 Crypto Officer role; +NIST SP 800-57 Part 2 §4.3 Key Management Officer). +""" +requires_env = ["KMS_OPA_URL"] +backends = ["sqlite"] + +# mTLS cert client — in crypto_officer_users → KMS bypasses OPA Gate 1. +[identities.co_cert] +client_cert = "test_data/certificates/client_server/owner/owner.client.acme.com.crt" +client_key = "test_data/certificates/client_server/owner/owner.client.acme.com.key" + +[[steps]] +operation = "Create" +request = "step1_create.json" +identity = "co_cert" +assert_success = true + +[steps.capture] +key_id = "UniqueIdentifier" + +[[steps]] +operation = "Destroy" +request = "step3_destroy.json" +identity = "co_cert" +allow_failure = true diff --git a/vectors/opa/mode_enforcing_native_co_cert_allowed/step1_create.json b/vectors/opa/mode_enforcing_native_co_cert_allowed/step1_create.json new file mode 100644 index 0000000..c994e09 --- /dev/null +++ b/vectors/opa/mode_enforcing_native_co_cert_allowed/step1_create.json @@ -0,0 +1,40 @@ +{ + "tag": "Create", + "value": [ + { + "tag": "ObjectType", + "type": "Enumeration", + "value": "SymmetricKey" + }, + { + "tag": "Attributes", + "value": [ + { + "tag": "CryptographicAlgorithm", + "type": "Enumeration", + "value": "AES" + }, + { + "tag": "CryptographicLength", + "type": "Integer", + "value": 256 + }, + { + "tag": "CryptographicUsageMask", + "type": "Integer", + "value": 2108 + }, + { + "tag": "KeyFormatType", + "type": "Enumeration", + "value": "TransparentSymmetricKey" + }, + { + "tag": "ObjectType", + "type": "Enumeration", + "value": "SymmetricKey" + } + ] + } + ] +} diff --git a/vectors/opa/mode_enforcing_native_co_cert_allowed/step3_destroy.json b/vectors/opa/mode_enforcing_native_co_cert_allowed/step3_destroy.json new file mode 100644 index 0000000..ce2dc0e --- /dev/null +++ b/vectors/opa/mode_enforcing_native_co_cert_allowed/step3_destroy.json @@ -0,0 +1,10 @@ +{ + "tag": "Destroy", + "value": [ + { + "tag": "UniqueIdentifier", + "type": "TextString", + "value": "{{key_id}}" + } + ] +} diff --git a/vectors/opa/mode_enforcing_unknown_role_denied/manifest.toml b/vectors/opa/mode_enforcing_unknown_role_denied/manifest.toml new file mode 100644 index 0000000..57f4f2c --- /dev/null +++ b/vectors/opa/mode_enforcing_unknown_role_denied/manifest.toml @@ -0,0 +1,25 @@ +name = "OPA Mode 3 (Enforcing): unknown JWT role denied Create" +description = """ +OPA enforcing mode. A bearer token carrying an unrecognised role `Hacker` +(not in kms.rego's role hierarchy) is presented. OPA finds no allow rule for +this role → allow=false → KMS returns 401 Unauthorized. + +This proves that an arbitrary or fabricated role claim cannot grant access: +the policy is allow-list based, not deny-list based. + +Ref: kms.rego default deny (ANSI/INCITS 359 §4.2 RBAC named-role principle; +NIST SP 800-53 AC-6 least-privilege). +""" +requires_env = ["KMS_OPA_URL", "KMS_AUTH_SERVER_URL", "KMS_TEST_OPA_UNKNOWN_ROLE_JWT"] +backends = ["sqlite"] + +# Token with a role name that does not appear in kms.rego's allow rules. +[identities.unknown_role] +access_token_env = "KMS_TEST_OPA_UNKNOWN_ROLE_JWT" + +[[steps]] +operation = "Create" +request = "step1_create.json" +identity = "unknown_role" +assert_success = false +assert_error_contains = "access-right" diff --git a/vectors/opa/mode_enforcing_unknown_role_denied/step1_create.json b/vectors/opa/mode_enforcing_unknown_role_denied/step1_create.json new file mode 100644 index 0000000..c994e09 --- /dev/null +++ b/vectors/opa/mode_enforcing_unknown_role_denied/step1_create.json @@ -0,0 +1,40 @@ +{ + "tag": "Create", + "value": [ + { + "tag": "ObjectType", + "type": "Enumeration", + "value": "SymmetricKey" + }, + { + "tag": "Attributes", + "value": [ + { + "tag": "CryptographicAlgorithm", + "type": "Enumeration", + "value": "AES" + }, + { + "tag": "CryptographicLength", + "type": "Integer", + "value": 256 + }, + { + "tag": "CryptographicUsageMask", + "type": "Integer", + "value": 2108 + }, + { + "tag": "KeyFormatType", + "type": "Enumeration", + "value": "TransparentSymmetricKey" + }, + { + "tag": "ObjectType", + "type": "Enumeration", + "value": "SymmetricKey" + } + ] + } + ] +} diff --git a/vectors/opa/mode_enforcing_wrong_domain/manifest.toml b/vectors/opa/mode_enforcing_wrong_domain/manifest.toml new file mode 100644 index 0000000..ceeca15 --- /dev/null +++ b/vectors/opa/mode_enforcing_wrong_domain/manifest.toml @@ -0,0 +1,49 @@ +name = "OPA Mode 3 (Enforcing): CryptoOfficer wrong domain denied (cross-domain isolation in dual-gate mode)" +description = """ +OPA enforcing (dual-gate) mode — multi-tenant isolation. + +The default CryptoOfficer (kms-opa-test, domain=kms-opa-test) creates an AES key. +A CryptoOfficer from realm `kms-opa-other` (domain=kms-opa-other, non-owner) attempts +`Get` on that key. In enforcing mode, BOTH the OPA gate AND the native KMS permission +check must allow. The OPA gate fails first: `same_domain` check returns false because +`user_domain (kms-opa-other) != object_domain (kms-opa-test)` → OPA returns +`allow=false` → KMS returns Object_Not_Found. + +The existing `mode_exclusive_wrong_domain` covers Mode 2 (exclusive). This vector +proves that domain isolation is not an exclusive-mode artefact — it is enforced in +Mode 3 as well, which is the recommended production mode. + +Ref: kms.rego same_domain helper (ANSI/INCITS 359-2004 §4.2 Constrained RBAC; +NIST SP 800-53 Rev 5 AC-3 access enforcement, AC-4 information flow enforcement; +NIST SP 800-53 Rev 5 SC-28 protection of information at rest). +""" +requires_env = ["KMS_OPA_URL", "KMS_AUTH_SERVER_URL"] +backends = ["sqlite"] + +# JWT-based identity: other-domain CryptoOfficer (kms-opa-other). +[identities.other_domain] +access_token_env = "KMS_TEST_OPA_OTHER_DOMAIN_JWT" + +# Step 1: default client (test-domain CO) creates the key. +[[steps]] +operation = "Create" +request = "step1_create.json" +assert_success = true + +[steps.capture] +key_id = "UniqueIdentifier" + +# Step 2: other-domain CO (non-owner, wrong domain) attempts Get — OPA gate denies. +[[steps]] +operation = "Get" +request = "step2_get.json" +identity = "other_domain" +assert_success = false +assert_error_reason = "Object_Not_Found" +assert_error_contains = "not found" + +# Cleanup: test-domain CO (owner) destroys the key. +[[steps]] +operation = "Destroy" +request = "step3_destroy.json" +allow_failure = true diff --git a/vectors/opa/mode_enforcing_wrong_domain/step1_create.json b/vectors/opa/mode_enforcing_wrong_domain/step1_create.json new file mode 100644 index 0000000..c994e09 --- /dev/null +++ b/vectors/opa/mode_enforcing_wrong_domain/step1_create.json @@ -0,0 +1,40 @@ +{ + "tag": "Create", + "value": [ + { + "tag": "ObjectType", + "type": "Enumeration", + "value": "SymmetricKey" + }, + { + "tag": "Attributes", + "value": [ + { + "tag": "CryptographicAlgorithm", + "type": "Enumeration", + "value": "AES" + }, + { + "tag": "CryptographicLength", + "type": "Integer", + "value": 256 + }, + { + "tag": "CryptographicUsageMask", + "type": "Integer", + "value": 2108 + }, + { + "tag": "KeyFormatType", + "type": "Enumeration", + "value": "TransparentSymmetricKey" + }, + { + "tag": "ObjectType", + "type": "Enumeration", + "value": "SymmetricKey" + } + ] + } + ] +} diff --git a/vectors/opa/mode_enforcing_wrong_domain/step2_get.json b/vectors/opa/mode_enforcing_wrong_domain/step2_get.json new file mode 100644 index 0000000..63a255e --- /dev/null +++ b/vectors/opa/mode_enforcing_wrong_domain/step2_get.json @@ -0,0 +1,10 @@ +{ + "tag": "Get", + "value": [ + { + "tag": "UniqueIdentifier", + "type": "TextString", + "value": "{{key_id}}" + } + ] +} diff --git a/vectors/opa/mode_enforcing_wrong_domain/step3_destroy.json b/vectors/opa/mode_enforcing_wrong_domain/step3_destroy.json new file mode 100644 index 0000000..ce2dc0e --- /dev/null +++ b/vectors/opa/mode_enforcing_wrong_domain/step3_destroy.json @@ -0,0 +1,10 @@ +{ + "tag": "Destroy", + "value": [ + { + "tag": "UniqueIdentifier", + "type": "TextString", + "value": "{{key_id}}" + } + ] +} diff --git a/vectors/opa/mode_exclusive_auditor_wrong_domain/manifest.toml b/vectors/opa/mode_exclusive_auditor_wrong_domain/manifest.toml new file mode 100644 index 0000000..586ecc2 --- /dev/null +++ b/vectors/opa/mode_exclusive_auditor_wrong_domain/manifest.toml @@ -0,0 +1,55 @@ +name = "OPA Mode 2 (Exclusive): Auditor wrong domain denied (cross-domain isolation)" +description = """ +OPA exclusive mode — multi-tenant isolation. + +A CryptoOfficer from realm `kms-opa-other` (domain=kms-opa-other) creates an AES key. +An Auditor from realm `kms-opa-test` (domain=kms-opa-test) then calls `GetAttributes` +on that key. Even though `GetAttributes` is in `auditor_ops`, the `same_domain` +helper in kms.rego fails because `user_domain (kms-opa-test) != object_domain +(kms-opa-other)` → OPA returns `allow=false` → KMS returns Object_Not_Found. + +This test completes the multi-tenant isolation matrix for the Auditor role: the +existing `mode_exclusive_auditor_get_attributes_allowed` vector proves an Auditor CAN +read key metadata within their own domain; this vector proves they CANNOT read across +domain boundaries — even for read-only metadata operations. + +Ref: kms.rego same_domain helper (ANSI/INCITS 359-2004 §4.2 Constrained RBAC; +NIST SP 800-53 Rev 5 AC-3 access enforcement, AC-4 information flow enforcement; +NIST SP 800-57 Part 2 §4.3 Audit and Compliance Officer role). +""" +requires_env = ["KMS_OPA_URL", "KMS_AUTH_SERVER_URL"] +backends = ["sqlite"] + +# other-domain CryptoOfficer creates the key (kms-opa-other domain). +[identities.other_domain] +access_token_env = "KMS_TEST_OPA_OTHER_DOMAIN_JWT" + +# kms-opa-test Auditor — domain mismatch triggers isolation. +[identities.auditor] +access_token_env = "KMS_TEST_OPA_AUDITOR_JWT" + +# Step 1: other-domain CO creates the key (object_domain = kms-opa-other). +[[steps]] +operation = "Create" +request = "step1_create.json" +identity = "other_domain" +assert_success = true + +[steps.capture] +key_id = "UniqueIdentifier" + +# Step 2: test-domain Auditor tries GetAttributes — same_domain fails → denied. +[[steps]] +operation = "GetAttributes" +request = "step3_get_attributes.json" +identity = "auditor" +assert_success = false +assert_error_reason = "Object_Not_Found" +assert_error_contains = "not found" + +# Cleanup: other-domain CO (owner) destroys the key. +[[steps]] +operation = "Destroy" +request = "step3_destroy.json" +identity = "other_domain" +allow_failure = true diff --git a/vectors/opa/mode_exclusive_auditor_wrong_domain/step1_create.json b/vectors/opa/mode_exclusive_auditor_wrong_domain/step1_create.json new file mode 100644 index 0000000..c994e09 --- /dev/null +++ b/vectors/opa/mode_exclusive_auditor_wrong_domain/step1_create.json @@ -0,0 +1,40 @@ +{ + "tag": "Create", + "value": [ + { + "tag": "ObjectType", + "type": "Enumeration", + "value": "SymmetricKey" + }, + { + "tag": "Attributes", + "value": [ + { + "tag": "CryptographicAlgorithm", + "type": "Enumeration", + "value": "AES" + }, + { + "tag": "CryptographicLength", + "type": "Integer", + "value": 256 + }, + { + "tag": "CryptographicUsageMask", + "type": "Integer", + "value": 2108 + }, + { + "tag": "KeyFormatType", + "type": "Enumeration", + "value": "TransparentSymmetricKey" + }, + { + "tag": "ObjectType", + "type": "Enumeration", + "value": "SymmetricKey" + } + ] + } + ] +} diff --git a/vectors/opa/mode_exclusive_auditor_wrong_domain/step3_destroy.json b/vectors/opa/mode_exclusive_auditor_wrong_domain/step3_destroy.json new file mode 100644 index 0000000..ce2dc0e --- /dev/null +++ b/vectors/opa/mode_exclusive_auditor_wrong_domain/step3_destroy.json @@ -0,0 +1,10 @@ +{ + "tag": "Destroy", + "value": [ + { + "tag": "UniqueIdentifier", + "type": "TextString", + "value": "{{key_id}}" + } + ] +} diff --git a/vectors/opa/mode_exclusive_auditor_wrong_domain/step3_get_attributes.json b/vectors/opa/mode_exclusive_auditor_wrong_domain/step3_get_attributes.json new file mode 100644 index 0000000..e96d057 --- /dev/null +++ b/vectors/opa/mode_exclusive_auditor_wrong_domain/step3_get_attributes.json @@ -0,0 +1,10 @@ +{ + "tag": "GetAttributes", + "value": [ + { + "tag": "UniqueIdentifier", + "type": "TextString", + "value": "{{key_id}}" + } + ] +} diff --git a/vectors/opa/mode_exclusive_native_co_cert_denied/manifest.toml b/vectors/opa/mode_exclusive_native_co_cert_denied/manifest.toml new file mode 100644 index 0000000..73c98cc --- /dev/null +++ b/vectors/opa/mode_exclusive_native_co_cert_denied/manifest.toml @@ -0,0 +1,28 @@ +name = "OPA Mode 2 (Exclusive): native KMS CO cert user denied Create (no JWT)" +description = """ +OPA exclusive mode. A client authenticated via mTLS (cert CN = owner.client@acme.com) +is NOT listed as a privileged/crypto-officer user in the server configuration. +Because no JWT is presented, OPA receives `input.roles = []` and no allow rule +fires → allow=false → KMS returns 401 Unauthorized. + +This proves that in exclusive OPA mode, even mTLS-authenticated clients are +subject to OPA policy when they are not explicitly whitelisted as native COs: +the KMS legacy privilege bypass does not apply. + +Ref: kms.rego default deny; OPA Mode 2 "OPA is sole authority" design contract. +""" +requires_env = ["KMS_OPA_URL"] +backends = ["sqlite"] + +# mTLS cert client — authenticated but no JWT → empty roles → OPA denies. +# The server is configured WITHOUT this user in crypto_officer_users. +[identities.co_cert] +client_cert = "test_data/certificates/client_server/owner/owner.client.acme.com.crt" +client_key = "test_data/certificates/client_server/owner/owner.client.acme.com.key" + +[[steps]] +operation = "Create" +request = "step1_create.json" +identity = "co_cert" +assert_success = false +assert_error_contains = "access-right" diff --git a/vectors/opa/mode_exclusive_native_co_cert_denied/step1_create.json b/vectors/opa/mode_exclusive_native_co_cert_denied/step1_create.json new file mode 100644 index 0000000..c994e09 --- /dev/null +++ b/vectors/opa/mode_exclusive_native_co_cert_denied/step1_create.json @@ -0,0 +1,40 @@ +{ + "tag": "Create", + "value": [ + { + "tag": "ObjectType", + "type": "Enumeration", + "value": "SymmetricKey" + }, + { + "tag": "Attributes", + "value": [ + { + "tag": "CryptographicAlgorithm", + "type": "Enumeration", + "value": "AES" + }, + { + "tag": "CryptographicLength", + "type": "Integer", + "value": 256 + }, + { + "tag": "CryptographicUsageMask", + "type": "Integer", + "value": 2108 + }, + { + "tag": "KeyFormatType", + "type": "Enumeration", + "value": "TransparentSymmetricKey" + }, + { + "tag": "ObjectType", + "type": "Enumeration", + "value": "SymmetricKey" + } + ] + } + ] +} diff --git a/vectors/opa/mode_exclusive_other_domain_allowed/manifest.toml b/vectors/opa/mode_exclusive_other_domain_allowed/manifest.toml new file mode 100644 index 0000000..13fb7d8 --- /dev/null +++ b/vectors/opa/mode_exclusive_other_domain_allowed/manifest.toml @@ -0,0 +1,42 @@ +name = "OPA Mode 2 (Exclusive): other-domain owner key lifecycle allowed (multi-tenancy positive)" +description = """ +OPA exclusive mode. A CryptoOfficer from realm `kms-opa-other` (domain=kms-opa-other) +creates an AES key **in their own domain**, then retrieves and destroys it. + +This is the multi-tenancy positive counterpart to `mode_exclusive_wrong_domain`: it +verifies that domain isolation does NOT block legitimate intra-domain operations. +The `same_domain` check succeeds because `user_domain == object_domain == kms-opa-other`. + +Ref: kms.rego same_domain helper (ANSI/INCITS 359-2004 §4.2 Constrained RBAC; +NIST SP 800-53 Rev 5 AC-6 least privilege; domain isolation requirement). +""" +requires_env = ["KMS_OPA_URL", "KMS_AUTH_SERVER_URL", "KMS_TEST_OPA_OTHER_DOMAIN_JWT"] +backends = ["sqlite"] + +# JWT-based identity: other-domain CryptoOfficer (kms-opa-other realm). +[identities.other_domain] +access_token_env = "KMS_TEST_OPA_OTHER_DOMAIN_JWT" + +# Step 1: other-domain officer creates their own key. +[[steps]] +operation = "Create" +request = "step1_create.json" +identity = "other_domain" +assert_success = true + +[steps.capture] +key_id = "UniqueIdentifier" + +# Step 2: same officer retrieves their own key (is_owner=true, same domain → allowed). +[[steps]] +operation = "Get" +request = "step2_get.json" +identity = "other_domain" +assert_success = true + +# Step 3: same officer destroys their own key. +[[steps]] +operation = "Destroy" +request = "step3_destroy.json" +identity = "other_domain" +assert_success = true diff --git a/vectors/opa/mode_exclusive_other_domain_allowed/step1_create.json b/vectors/opa/mode_exclusive_other_domain_allowed/step1_create.json new file mode 100644 index 0000000..c994e09 --- /dev/null +++ b/vectors/opa/mode_exclusive_other_domain_allowed/step1_create.json @@ -0,0 +1,40 @@ +{ + "tag": "Create", + "value": [ + { + "tag": "ObjectType", + "type": "Enumeration", + "value": "SymmetricKey" + }, + { + "tag": "Attributes", + "value": [ + { + "tag": "CryptographicAlgorithm", + "type": "Enumeration", + "value": "AES" + }, + { + "tag": "CryptographicLength", + "type": "Integer", + "value": 256 + }, + { + "tag": "CryptographicUsageMask", + "type": "Integer", + "value": 2108 + }, + { + "tag": "KeyFormatType", + "type": "Enumeration", + "value": "TransparentSymmetricKey" + }, + { + "tag": "ObjectType", + "type": "Enumeration", + "value": "SymmetricKey" + } + ] + } + ] +} diff --git a/vectors/opa/mode_exclusive_other_domain_allowed/step2_get.json b/vectors/opa/mode_exclusive_other_domain_allowed/step2_get.json new file mode 100644 index 0000000..63a255e --- /dev/null +++ b/vectors/opa/mode_exclusive_other_domain_allowed/step2_get.json @@ -0,0 +1,10 @@ +{ + "tag": "Get", + "value": [ + { + "tag": "UniqueIdentifier", + "type": "TextString", + "value": "{{key_id}}" + } + ] +} diff --git a/vectors/opa/mode_exclusive_other_domain_allowed/step3_destroy.json b/vectors/opa/mode_exclusive_other_domain_allowed/step3_destroy.json new file mode 100644 index 0000000..ce2dc0e --- /dev/null +++ b/vectors/opa/mode_exclusive_other_domain_allowed/step3_destroy.json @@ -0,0 +1,10 @@ +{ + "tag": "Destroy", + "value": [ + { + "tag": "UniqueIdentifier", + "type": "TextString", + "value": "{{key_id}}" + } + ] +} diff --git a/vectors/opa/mode_exclusive_super_admin_cross_domain/manifest.toml b/vectors/opa/mode_exclusive_super_admin_cross_domain/manifest.toml new file mode 100644 index 0000000..210b243 --- /dev/null +++ b/vectors/opa/mode_exclusive_super_admin_cross_domain/manifest.toml @@ -0,0 +1,55 @@ +name = "OPA Mode 2 (Exclusive): SuperAdmin cross-domain access allowed (isolation bypass for top role)" +description = """ +OPA exclusive mode — SuperAdmin cross-domain positive test. + +A CryptoOfficer from realm `kms-opa-other` (domain=kms-opa-other) creates an AES key. +A SuperAdmin (kms-opa-test realm, no domain restriction) then calls `Get` and `Destroy` +on that key and both succeed. + +The `SuperAdmin` rule in kms.rego is unconditional: `allow if { input.roles[_] == +"SuperAdmin" }`. The `same_domain` helper is NOT invoked for this role, so +`user_domain (kms-opa-test) != object_domain (kms-opa-other)` does not block access. + +This is the positive counterpart to `mode_exclusive_wrong_domain` (CO) and +`mode_exclusive_domain_admin_wrong_domain` (DomainAdmin): it proves that domain +isolation is correctly relaxed only for the one role that is explicitly allowed +cross-domain access, and that no other role accidentally inherits this privilege. + +Ref: kms.rego SuperAdmin rule (ANSI/INCITS 359-2004 §4.2 top of the role hierarchy; +NIST SP 800-53 Rev 5 AC-6(1) least privilege / authorize access to security functions; +NIST SP 800-57 Part 2 §4.3 Key Management Authority role). +""" +requires_env = ["KMS_OPA_URL", "KMS_AUTH_SERVER_URL"] +backends = ["sqlite"] + +# other-domain CryptoOfficer creates the key (object_domain = kms-opa-other). +[identities.other_domain] +access_token_env = "KMS_TEST_OPA_OTHER_DOMAIN_JWT" + +# SuperAdmin is cross-domain; its domain claim is irrelevant to OPA's decision. +[identities.super_admin] +access_token_env = "KMS_TEST_OPA_SUPER_ADMIN_JWT" + +# Step 1: other-domain CO creates the key. +[[steps]] +operation = "Create" +request = "step1_create.json" +identity = "other_domain" +assert_success = true + +[steps.capture] +key_id = "UniqueIdentifier" + +# Step 2: SuperAdmin retrieves the key across domain boundaries → allowed. +[[steps]] +operation = "Get" +request = "step2_get.json" +identity = "super_admin" +assert_success = true + +# Step 3: SuperAdmin destroys the key across domain boundaries → allowed. +[[steps]] +operation = "Destroy" +request = "step3_destroy.json" +identity = "super_admin" +assert_success = true diff --git a/vectors/opa/mode_exclusive_super_admin_cross_domain/step1_create.json b/vectors/opa/mode_exclusive_super_admin_cross_domain/step1_create.json new file mode 100644 index 0000000..c994e09 --- /dev/null +++ b/vectors/opa/mode_exclusive_super_admin_cross_domain/step1_create.json @@ -0,0 +1,40 @@ +{ + "tag": "Create", + "value": [ + { + "tag": "ObjectType", + "type": "Enumeration", + "value": "SymmetricKey" + }, + { + "tag": "Attributes", + "value": [ + { + "tag": "CryptographicAlgorithm", + "type": "Enumeration", + "value": "AES" + }, + { + "tag": "CryptographicLength", + "type": "Integer", + "value": 256 + }, + { + "tag": "CryptographicUsageMask", + "type": "Integer", + "value": 2108 + }, + { + "tag": "KeyFormatType", + "type": "Enumeration", + "value": "TransparentSymmetricKey" + }, + { + "tag": "ObjectType", + "type": "Enumeration", + "value": "SymmetricKey" + } + ] + } + ] +} diff --git a/vectors/opa/mode_exclusive_super_admin_cross_domain/step2_get.json b/vectors/opa/mode_exclusive_super_admin_cross_domain/step2_get.json new file mode 100644 index 0000000..63a255e --- /dev/null +++ b/vectors/opa/mode_exclusive_super_admin_cross_domain/step2_get.json @@ -0,0 +1,10 @@ +{ + "tag": "Get", + "value": [ + { + "tag": "UniqueIdentifier", + "type": "TextString", + "value": "{{key_id}}" + } + ] +} diff --git a/vectors/opa/mode_exclusive_super_admin_cross_domain/step3_destroy.json b/vectors/opa/mode_exclusive_super_admin_cross_domain/step3_destroy.json new file mode 100644 index 0000000..ce2dc0e --- /dev/null +++ b/vectors/opa/mode_exclusive_super_admin_cross_domain/step3_destroy.json @@ -0,0 +1,10 @@ +{ + "tag": "Destroy", + "value": [ + { + "tag": "UniqueIdentifier", + "type": "TextString", + "value": "{{key_id}}" + } + ] +} diff --git a/vectors/opa/mode_exclusive_user_wrong_domain/manifest.toml b/vectors/opa/mode_exclusive_user_wrong_domain/manifest.toml new file mode 100644 index 0000000..a1f93d8 --- /dev/null +++ b/vectors/opa/mode_exclusive_user_wrong_domain/manifest.toml @@ -0,0 +1,55 @@ +name = "OPA Mode 2 (Exclusive): User role wrong domain denied (cross-domain isolation)" +description = """ +OPA exclusive mode — multi-tenant isolation. + +A CryptoOfficer from realm `kms-opa-other` (domain=kms-opa-other) creates an AES key. +A User from realm `kms-opa-test` (domain=kms-opa-test) then calls `GetAttributes` +on that key. Even though `GetAttributes` is in `user_ops`, the `same_domain` +helper in kms.rego fails because `user_domain (kms-opa-test) != object_domain +(kms-opa-other)` → OPA returns `allow=false` → KMS returns Object_Not_Found. + +This test completes the multi-tenant isolation matrix for the User role. The +`same_domain` constraint applies to every non-SuperAdmin role, including the +least-privileged User role. A compromised tenant in one domain must not be able to +discover or use key material belonging to another tenant. + +Ref: kms.rego same_domain helper (ANSI/INCITS 359-2004 §4.2 Constrained RBAC; +NIST SP 800-53 Rev 5 AC-3 access enforcement, AC-4 information flow enforcement; +FIPS 140-3 §7.4 User role restrictions). +""" +requires_env = ["KMS_OPA_URL", "KMS_AUTH_SERVER_URL"] +backends = ["sqlite"] + +# other-domain CryptoOfficer creates the key (kms-opa-other domain). +[identities.other_domain] +access_token_env = "KMS_TEST_OPA_OTHER_DOMAIN_JWT" + +# kms-opa-test User — domain mismatch triggers isolation. +[identities.user_role] +access_token_env = "KMS_TEST_OPA_USER_ROLE_JWT" + +# Step 1: other-domain CO creates the key (object_domain = kms-opa-other). +[[steps]] +operation = "Create" +request = "step1_create.json" +identity = "other_domain" +assert_success = true + +[steps.capture] +key_id = "UniqueIdentifier" + +# Step 2: test-domain User tries GetAttributes — same_domain fails → denied. +[[steps]] +operation = "GetAttributes" +request = "step3_get_attributes.json" +identity = "user_role" +assert_success = false +assert_error_reason = "Object_Not_Found" +assert_error_contains = "not found" + +# Cleanup: other-domain CO (owner) destroys the key. +[[steps]] +operation = "Destroy" +request = "step3_destroy.json" +identity = "other_domain" +allow_failure = true diff --git a/vectors/opa/mode_exclusive_user_wrong_domain/step1_create.json b/vectors/opa/mode_exclusive_user_wrong_domain/step1_create.json new file mode 100644 index 0000000..c994e09 --- /dev/null +++ b/vectors/opa/mode_exclusive_user_wrong_domain/step1_create.json @@ -0,0 +1,40 @@ +{ + "tag": "Create", + "value": [ + { + "tag": "ObjectType", + "type": "Enumeration", + "value": "SymmetricKey" + }, + { + "tag": "Attributes", + "value": [ + { + "tag": "CryptographicAlgorithm", + "type": "Enumeration", + "value": "AES" + }, + { + "tag": "CryptographicLength", + "type": "Integer", + "value": 256 + }, + { + "tag": "CryptographicUsageMask", + "type": "Integer", + "value": 2108 + }, + { + "tag": "KeyFormatType", + "type": "Enumeration", + "value": "TransparentSymmetricKey" + }, + { + "tag": "ObjectType", + "type": "Enumeration", + "value": "SymmetricKey" + } + ] + } + ] +} diff --git a/vectors/opa/mode_exclusive_user_wrong_domain/step3_destroy.json b/vectors/opa/mode_exclusive_user_wrong_domain/step3_destroy.json new file mode 100644 index 0000000..ce2dc0e --- /dev/null +++ b/vectors/opa/mode_exclusive_user_wrong_domain/step3_destroy.json @@ -0,0 +1,10 @@ +{ + "tag": "Destroy", + "value": [ + { + "tag": "UniqueIdentifier", + "type": "TextString", + "value": "{{key_id}}" + } + ] +} diff --git a/vectors/opa/mode_exclusive_user_wrong_domain/step3_get_attributes.json b/vectors/opa/mode_exclusive_user_wrong_domain/step3_get_attributes.json new file mode 100644 index 0000000..e96d057 --- /dev/null +++ b/vectors/opa/mode_exclusive_user_wrong_domain/step3_get_attributes.json @@ -0,0 +1,10 @@ +{ + "tag": "GetAttributes", + "value": [ + { + "tag": "UniqueIdentifier", + "type": "TextString", + "value": "{{key_id}}" + } + ] +} diff --git a/vectors/tls/mtls/manifest.toml b/vectors/tls/mtls/manifest.toml index 0f5c2da..e1f6c72 100644 --- a/vectors/tls/mtls/manifest.toml +++ b/vectors/tls/mtls/manifest.toml @@ -1,6 +1,6 @@ name = "Transport: HTTPS mTLS (mutual TLS with client certificate)" description = "Verifies the KMS can be reached over HTTPS with mutual TLS (client certificate required)" -server_config = "test_data/configs/server/cert_auth.toml" +server_config = "test_data/configs/server/auth/cert.toml" [[steps]] operation = "Create" diff --git a/vectors/tls/server_tls/manifest.toml b/vectors/tls/server_tls/manifest.toml index 54a62d5..794ee6f 100644 --- a/vectors/tls/server_tls/manifest.toml +++ b/vectors/tls/server_tls/manifest.toml @@ -1,6 +1,6 @@ name = "Transport: HTTPS (server-TLS, no client cert)" description = "Verifies the KMS can be reached over HTTPS with server-TLS only (self-signed cert, no mTLS)" -server_config = "test_data/configs/server/auth_https.toml" +server_config = "test_data/configs/server/auth/tls.toml" [[steps]] operation = "Create"