diff --git a/Cargo.lock b/Cargo.lock index 0d3ab3dd1..a89e924c2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -15,16 +15,27 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1973cfbc1a2daf9cf550e74e1f088c28e7f7d8c1e1418fb6c9dc5184b7e84c99" dependencies = [ "crypto-common 0.2.2", - "inout", + "inout 0.2.2", ] [[package]] name = "aes" -version = "0.9.1" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" +dependencies = [ + "cfg-if", + "cipher 0.4.4", + "cpufeatures 0.2.17", +] + +[[package]] +name = "aes" +version = "0.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1fc76eaeac4c9164506c466d4ffdd8ec9d0c5bf57ee97177c4d8eceb3a0e138" +checksum = "f8eb277bec05f56a0e0591f155a484cbd0f4f07ff2905051a48c72f004f7ed58" dependencies = [ - "cipher", + "cipher 0.5.2", "cpubits", "cpufeatures 0.3.0", ] @@ -36,14 +47,24 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fdf011db2e21ce0d575593d749db5554b47fed37aff429e4dc50bc91ac93a028" dependencies = [ "aead", - "aes", - "cipher", + "aes 0.9.2", + "cipher 0.5.2", "ctr", "ghash", "subtle", "zeroize", ] +[[package]] +name = "aes-kw" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41ac571010bd60765c56085a4f1d412012a9be2663b1a2f2b19b49318653fd0d" +dependencies = [ + "aes 0.9.2", + "const-oid 0.10.2", +] + [[package]] name = "ahash" version = "0.8.12" @@ -240,9 +261,9 @@ dependencies = [ [[package]] name = "async-compression" -version = "0.4.42" +version = "0.4.43" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e79b3f8a79cccc2898f31920fc69f304859b3bd567490f75ebf51ae1c792a9ac" +checksum = "3976abdc8fe7d1133d43d304afd42abdf5bc3e1319d263d223bde07b5efc4be8" dependencies = [ "compression-codecs", "compression-core", @@ -295,11 +316,28 @@ checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" name = "auth-service" version = "0.1.0" dependencies = [ + "askama", "axum", "axum-extra", - "rand", + "base64", + "bergshamra-dsig", + "bergshamra-enc", + "bergshamra-keys", + "chrono", + "cookie", + "parking_lot", + "reqwest", + "roxmltree", + "rustls-pki-types", + "rustls-webpki", "secrecy", + "sha1 0.11.0", + "sha2 0.11.0", "thiserror", + "tokio", + "tracing", + "uuid", + "x509-cert", ] [[package]] @@ -457,12 +495,30 @@ dependencies = [ "zstd", ] +[[package]] +name = "base16ct" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf" + +[[package]] +name = "base16ct" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd307490d624467aa6f74b0eabb77633d1f758a7b25f12bceb0b22e08d9726f6" + [[package]] name = "base64" version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" +[[package]] +name = "base64ct" +version = "1.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06" + [[package]] name = "basic-toml" version = "0.1.10" @@ -472,6 +528,191 @@ dependencies = [ "serde", ] +[[package]] +name = "bergshamra-c14n" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5df7691c69dc661238db6f693117db2af68870a526fcc7bdc449ade69e1c3c0e" +dependencies = [ + "bergshamra-core", + "bergshamra-xml", + "memchr", + "uppsala", +] + +[[package]] +name = "bergshamra-core" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "317746fca574a8b8446264f3d23f43eb800891bba5b8250ee7d0cea944b67979" +dependencies = [ + "thiserror", +] + +[[package]] +name = "bergshamra-crypto" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ec56fc4095a935e8e0f950afcc8aa98e7885ea1cde368db6e0a6fd001929dc3" +dependencies = [ + "bergshamra-core", + "digest 0.10.7", + "dsa", + "ecdsa", + "ed25519-dalek", + "getrandom 0.4.3", + "hkdf 0.12.4", + "hmac 0.12.1", + "kryptering", + "ml-dsa", + "num-bigint-dig", + "num-traits", + "p256", + "p384", + "p521", + "pbkdf2", + "pkcs1", + "pkcs8 0.10.2", + "pkcs8 0.11.0", + "rand 0.8.7", + "rsa", + "sha1 0.10.7", + "sha2 0.10.9", + "sha3 0.10.9", + "signature 2.2.0", + "slh-dsa", + "x25519-dalek", +] + +[[package]] +name = "bergshamra-dsig" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69ca432b180c711e9f9298ecdd838b2f5c871ab84b1da91c450c60030786a972" +dependencies = [ + "base64", + "bergshamra-c14n", + "bergshamra-core", + "bergshamra-crypto", + "bergshamra-keys", + "bergshamra-transforms", + "bergshamra-xml", + "der 0.7.10", + "dsa", + "ed25519-dalek", + "kryptering", + "p256", + "p384", + "p521", + "rsa", + "uppsala", + "x509-cert", +] + +[[package]] +name = "bergshamra-enc" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "561ee8301ecf4b6f5fbcf88a50fec3199de455d68f0997513f1b630faec10dfc" +dependencies = [ + "base64", + "bergshamra-c14n", + "bergshamra-core", + "bergshamra-crypto", + "bergshamra-keys", + "bergshamra-transforms", + "bergshamra-xml", + "kryptering", + "p256", + "p384", + "p521", + "rand 0.8.7", + "uppsala", +] + +[[package]] +name = "bergshamra-keys" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc28316317ae5ca8082d5a1c5461a18504408c59541f585cf03ff5c70d95536e" +dependencies = [ + "base64", + "bergshamra-core", + "bergshamra-crypto", + "bergshamra-pkcs12", + "const-oid 0.10.2", + "der 0.7.10", + "digest 0.10.7", + "dsa", + "ecdsa", + "ed25519-dalek", + "md-5 0.10.6", + "ml-dsa", + "num-bigint-dig", + "num-traits", + "p256", + "p384", + "p521", + "pem-rfc7468", + "pkcs1", + "pkcs5", + "pkcs8 0.10.2", + "pkcs8 0.11.0", + "rsa", + "sha1 0.10.7", + "sha2 0.10.9", + "signature 2.2.0", + "slh-dsa", + "spki 0.7.3", + "tsp-ltv", + "uppsala", + "x25519-dalek", + "x509-cert", +] + +[[package]] +name = "bergshamra-pkcs12" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32bce5ea0f2b97066f29c1777ea6b31dcf086ddbe6be4be32000d4fea7103a96" +dependencies = [ + "aes 0.9.2", + "bergshamra-core", + "cbc 0.2.1", + "cipher 0.5.2", + "des 0.9.0", + "hmac 0.12.1", + "pbkdf2", + "sha1 0.10.7", + "sha2 0.10.9", + "yasna 0.5.2", +] + +[[package]] +name = "bergshamra-transforms" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c23fa6cc3240ec0aeb6b45565feae28dc922ff78870a96333b1cb045368a1d3" +dependencies = [ + "base64", + "bergshamra-c14n", + "bergshamra-core", + "bergshamra-crypto", + "bergshamra-keys", + "bergshamra-xml", + "uppsala", +] + +[[package]] +name = "bergshamra-xml" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edf4a5ceeee09287a2456e73a91df212b4fcdb7eb24d890f9668e8e0fca6c20f" +dependencies = [ + "bergshamra-core", + "uppsala", +] + [[package]] name = "bit-vec" version = "0.9.1" @@ -508,6 +749,24 @@ dependencies = [ "hybrid-array", ] +[[package]] +name = "block-padding" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8894febbff9f758034a5b8e12d87918f56dfc64a8e1fe757d65e29041538d93" +dependencies = [ + "generic-array", +] + +[[package]] +name = "block-padding" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "710f1dd022ef4e93f8a438b4ba958de7f64308434fa6a87104481645cc30068b" +dependencies = [ + "hybrid-array", +] + [[package]] name = "brotli" version = "8.0.4" @@ -567,6 +826,24 @@ version = "1.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04" +[[package]] +name = "cbc" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26b52a9543ae338f279b96b0b9fed9c8093744685043739079ce85cd58f289a6" +dependencies = [ + "cipher 0.4.4", +] + +[[package]] +name = "cbc" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce2dc9ee5f88d11e0beb842c88b33c8a5cf0d1329c4b19494af42b07dbfe8896" +dependencies = [ + "cipher 0.5.2", +] + [[package]] name = "cc" version = "1.4.0" @@ -599,7 +876,7 @@ checksum = "d524456ba66e72eb8b115ff89e01e497f8e6d11d78b70b1aa13c0fbd97540a81" dependencies = [ "cfg-if", "cpufeatures 0.3.0", - "rand_core", + "rand_core 0.10.1", ] [[package]] @@ -624,6 +901,16 @@ dependencies = [ "phf 0.12.1", ] +[[package]] +name = "cipher" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" +dependencies = [ + "crypto-common 0.1.7", + "inout 0.1.4", +] + [[package]] name = "cipher" version = "0.5.2" @@ -632,7 +919,7 @@ checksum = "e8cf2a2c93cd704877c0858356ed03480ff301ee950b43f1cbe4573b088bfa6c" dependencies = [ "block-buffer 0.12.1", "crypto-common 0.2.2", - "inout", + "inout 0.2.2", ] [[package]] @@ -650,6 +937,18 @@ version = "0.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0c9ea0ac24bc397ab3c98583a3c9ba74fa56b09a4449bbe172b9b1ddb016027a" +[[package]] +name = "cms" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b77c319abfd5219629c45c34c89ba945ed3c5e49fcde9d16b6c3885f118a730" +dependencies = [ + "const-oid 0.9.6", + "der 0.7.10", + "spki 0.7.3", + "x509-cert", +] + [[package]] name = "cobs" version = "0.3.0" @@ -686,13 +985,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cc14f565cf027a105f7a44ccf9e5b424348421a1d8952a8fc9d499d313107789" [[package]] -name = "concurrent-queue" -version = "2.5.0" +name = "const-oid" +version = "0.9.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" -dependencies = [ - "crossbeam-utils", -] +checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" + +[[package]] +name = "const-oid" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6ef517f0926dd24a1582492c791b6a4818a4d94e789a334894aa15b0d12f55c" [[package]] name = "cookie" @@ -784,6 +1086,32 @@ version = "0.8.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "61803da095bee82a81bb1a452ecc25d3b2f1416d1897eb86430c6159ef717c17" +[[package]] +name = "crypto-bigint" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0dc92fb57ca44df6db8059111ab3af99a63d5d0f8375d9972e319a379c6bab76" +dependencies = [ + "generic-array", + "rand_core 0.6.4", + "subtle", + "zeroize", +] + +[[package]] +name = "crypto-bigint" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a52aa3fcda4e6302a9f48734f234d35d4721b96f8fe07d073f07ce9df4f0271" +dependencies = [ + "cpubits", + "ctutils", + "num-traits", + "rand_core 0.10.1", + "serdect", + "zeroize", +] + [[package]] name = "crypto-common" version = "0.1.7" @@ -802,7 +1130,29 @@ checksum = "ce6e4c961d6cd6c9a86db418387425e8bdeaf05b3c8bc1411e6dca4c252f1453" dependencies = [ "getrandom 0.4.3", "hybrid-array", - "rand_core", + "rand_core 0.10.1", +] + +[[package]] +name = "cryptoki" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff765b99fc49f3116c9a908484486a2b92fd73c48da45c3a69716471c6cc56c6" +dependencies = [ + "bitflags", + "cryptoki-sys", + "libloading", + "log", + "secrecy", +] + +[[package]] +name = "cryptoki-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1fd850498411e4057f1cba79e6e2bc7cbe960544c1046ab46d4685c403a1121" +dependencies = [ + "libloading", ] [[package]] @@ -832,7 +1182,7 @@ version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baaca1c4b237092596f64d571e9db6ce4109c4ef9742e27590f1709594461f21" dependencies = [ - "cipher", + "cipher 0.5.2", ] [[package]] @@ -844,12 +1194,62 @@ dependencies = [ "cmov", ] +[[package]] +name = "curve25519-dalek" +version = "4.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be" +dependencies = [ + "cfg-if", + "cpufeatures 0.2.17", + "curve25519-dalek-derive", + "digest 0.10.7", + "fiat-crypto", + "rustc_version", + "subtle", + "zeroize", +] + +[[package]] +name = "curve25519-dalek-derive" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + [[package]] name = "data-encoding" version = "2.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a4ae5f15dda3c708c0ade84bfee31ccab44a3da4f88015ed22f63732abe300c8" +[[package]] +name = "der" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb" +dependencies = [ + "const-oid 0.9.6", + "der_derive", + "flagset", + "pem-rfc7468", + "zeroize", +] + +[[package]] +name = "der" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a69dedd701da44b0536442edf09c81a64b0ab97a7a4a5e3d1971f00027cbc63d" +dependencies = [ + "const-oid 0.10.2", + "zeroize", +] + [[package]] name = "der-parser" version = "10.0.0" @@ -864,12 +1264,41 @@ dependencies = [ "rusticata-macros", ] +[[package]] +name = "der_derive" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8034092389675178f570469e6c3b0465d3d30b4505c294a6550db47f3c17ad18" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + [[package]] name = "deranged" version = "0.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" +[[package]] +name = "des" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffdd80ce8ce993de27e9f063a444a4d53ce8e8db4c1f00cc03af5ad5a9867a1e" +dependencies = [ + "cipher 0.4.4", +] + +[[package]] +name = "des" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "916a94e407b54f9034d71dd748234cd1e516ced6284009906ae246f177eafe5a" +dependencies = [ + "cipher 0.5.2", +] + [[package]] name = "digest" version = "0.10.7" @@ -877,7 +1306,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ "block-buffer 0.10.4", + "const-oid 0.9.6", "crypto-common 0.1.7", + "subtle", ] [[package]] @@ -887,19 +1318,20 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f1dd6dbb5841937940781866fa1281a1ff7bd3bf827091440879f9994983d5c2" dependencies = [ "block-buffer 0.12.1", + "const-oid 0.10.2", "crypto-common 0.2.2", "ctutils", ] [[package]] name = "displaydoc" -version = "0.2.6" +version = "0.2.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ac70aa55017e108007fbaf5aa0f54b021c98f92ff8af59d42eda9da96e3dd4f" +checksum = "c6232dd377dcc64799954cbd3a9bb882e9cdc1308ccd87b1c098f1fb2eaf82a8" dependencies = [ "proc-macro2", "quote", - "syn 2.0.119", + "syn 3.0.3", ] [[package]] @@ -908,17 +1340,72 @@ version = "0.15.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b" +[[package]] +name = "dsa" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48bc224a9084ad760195584ce5abb3c2c34a225fa312a128ad245a6b412b7689" +dependencies = [ + "digest 0.10.7", + "num-bigint-dig", + "num-traits", + "pkcs8 0.10.2", + "rfc6979", + "sha2 0.10.9", + "signature 2.2.0", + "zeroize", +] + [[package]] name = "dunce" version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" +[[package]] +name = "ecdsa" +version = "0.16.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee27f32b5c5292967d2d4a9d7f1e0b0aed2c15daded5a60300e4abb9d8020bca" +dependencies = [ + "der 0.7.10", + "digest 0.10.7", + "elliptic-curve", + "rfc6979", + "signature 2.2.0", + "spki 0.7.3", +] + +[[package]] +name = "ed25519" +version = "2.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "115531babc129696a58c64a4fef0a8bf9e9698629fb97e9e40767d235cfbcd53" +dependencies = [ + "pkcs8 0.10.2", + "signature 2.2.0", +] + +[[package]] +name = "ed25519-dalek" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70e796c081cee67dc755e1a36a0a172b897fab85fc3f6bc48307991f64e4eca9" +dependencies = [ + "curve25519-dalek", + "ed25519", + "rand_core 0.6.4", + "serde", + "sha2 0.10.9", + "subtle", + "zeroize", +] + [[package]] name = "either" -version = "1.16.0" +version = "1.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91622ff5e7162018101f2fea40d6ebf4a78bbe5a49736a2020649edf9693679e" +checksum = "9e5e8f6c15a24b9a3ee5efec809ccd006d3b30e8b3bb63c39af737c7f87daa1d" dependencies = [ "serde", ] @@ -941,7 +1428,7 @@ dependencies = [ "csv", "eks-locales", "eml-nl", - "hkdf", + "hkdf 0.13.0", "http-body-util", "hyper-util", "instant-acme", @@ -949,7 +1436,7 @@ dependencies = [ "parking_lot", "phf 0.14.0", "postcard", - "rand", + "rand 0.10.2", "rcgen", "regex", "reqwest", @@ -1003,6 +1490,27 @@ dependencies = [ "uuid", ] +[[package]] +name = "elliptic-curve" +version = "0.13.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5e6043086bf7973472e0c7dff2142ea0b680d30e18d9cc40f267efbf222bd47" +dependencies = [ + "base16ct 0.2.0", + "crypto-bigint 0.5.5", + "digest 0.10.7", + "ff", + "generic-array", + "group", + "hkdf 0.12.4", + "pem-rfc7468", + "pkcs8 0.10.2", + "rand_core 0.6.4", + "sec1", + "subtle", + "zeroize", +] + [[package]] name = "embedded-io" version = "0.4.0" @@ -1083,11 +1591,10 @@ dependencies = [ [[package]] name = "event-listener" -version = "5.4.1" +version = "5.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e13b66accf52311f30a0db42147dadea9850cb48cd070028831ae5f5d4b856ab" +checksum = "5a23add41df1562121a9393cb065eab5146a1242410f23a644851e90cfd669d2" dependencies = [ - "concurrent-queue", "parking", "pin-project-lite", ] @@ -1098,12 +1605,34 @@ version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "da7c62ceae207dd37ea5b845da6a0696c799f85e97da1ab5b7910be3c1c80223" +[[package]] +name = "ff" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0b50bfb653653f9ca9095b427bed08ab8d75a137839d9ad64eb11810d5b6393" +dependencies = [ + "rand_core 0.6.4", + "subtle", +] + +[[package]] +name = "fiat-crypto" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" + [[package]] name = "find-msvc-tools" version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" +[[package]] +name = "flagset" +version = "0.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7ac824320a75a52197e8f2d787f6a38b6718bb6897a35142d749af3c0e8f4fe" + [[package]] name = "flate2" version = "1.1.9" @@ -1293,6 +1822,7 @@ checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" dependencies = [ "typenum", "version_check", + "zeroize", ] [[package]] @@ -1332,7 +1862,7 @@ dependencies = [ "js-sys", "libc", "r-efi 6.0.0", - "rand_core", + "rand_core 0.10.1", "wasm-bindgen", ] @@ -1361,6 +1891,17 @@ dependencies = [ "smallvec", ] +[[package]] +name = "group" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0f9ef7462f7c099f518d754361858f86d8a07af53ba9af0fe635bbccb151a63" +dependencies = [ + "ff", + "rand_core 0.6.4", + "subtle", +] + [[package]] name = "h2" version = "0.4.15" @@ -1466,13 +2007,31 @@ version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" +[[package]] +name = "hkdf" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7" +dependencies = [ + "hmac 0.12.1", +] + [[package]] name = "hkdf" version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4aaa26c720c68b866f2c96ef5c1264b3e6f473fe5d4ce61cd44bbe913e553018" dependencies = [ - "hmac", + "hmac 0.13.0", +] + +[[package]] +name = "hmac" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +dependencies = [ + "digest 0.10.7", ] [[package]] @@ -1486,9 +2045,9 @@ dependencies = [ [[package]] name = "http" -version = "1.4.2" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6970f50e31d6fc17d3fa27329444bfa74e196cf62e95052a3f6fee181dba6425" +checksum = "918d3568bebf352712bc2ef3d46a8bcf1a75b373be6539de198e9105cbbf9ce0" dependencies = [ "bytes", "itoa", @@ -1531,10 +2090,11 @@ checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" [[package]] name = "hybrid-array" -version = "0.4.13" +version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "818356c5132c1fede50f837ca96afbe78ff42413047f4abb886217845e1b6c8c" +checksum = "707114b52a152fa7bdb290cd7cd5912d9467273b6d74e21b8d81aca1f8533f6b" dependencies = [ + "ctutils", "typenum", ] @@ -1737,12 +2297,23 @@ dependencies = [ "hashbrown 0.17.1", ] +[[package]] +name = "inout" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01" +dependencies = [ + "block-padding 0.3.3", + "generic-array", +] + [[package]] name = "inout" version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4250ce6452e92010fdf7268ccc5d14faa80bb12fc741938534c58f16804e03c7" dependencies = [ + "block-padding 0.4.2", "hybrid-array", ] @@ -1855,6 +2426,25 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "keccak" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb26cec98cce3a3d96cbb7bced3c4b16e3d13f27ec56dbd62cbc8f39cfb9d653" +dependencies = [ + "cpufeatures 0.2.17", +] + +[[package]] +name = "keccak" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e24a010dd405bd7ed803e5253182815b41bf2e6a80cc3bfc066658e03a198aa" +dependencies = [ + "cfg-if", + "cpufeatures 0.3.0", +] + [[package]] name = "krilla" version = "0.8.2" @@ -1877,6 +2467,46 @@ dependencies = [ "yoke", ] +[[package]] +name = "kryptering" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "464b8c00fef271a07a2521b625fc70cfce3c4e8ff807f596900d89bce5381570" +dependencies = [ + "aes 0.9.2", + "aes-gcm", + "aes-kw", + "cbc 0.2.1", + "crypto-bigint 0.7.5", + "cryptoki", + "des 0.9.0", + "digest 0.10.7", + "dsa", + "ecdsa", + "ed25519-dalek", + "getrandom 0.4.3", + "hkdf 0.12.4", + "hmac 0.12.1", + "md-5 0.10.6", + "ml-dsa", + "p256", + "p384", + "p521", + "pbkdf2", + "pkcs8 0.11.0", + "rand 0.8.7", + "ripemd", + "rsa", + "sha1 0.10.7", + "sha2 0.10.9", + "sha3 0.10.9", + "signature 2.2.0", + "slh-dsa", + "thiserror", + "x25519-dalek", + "zeroize", +] + [[package]] name = "kurbo" version = "0.13.1" @@ -1894,6 +2524,9 @@ name = "lazy_static" version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" +dependencies = [ + "spin", +] [[package]] name = "libc" @@ -1901,6 +2534,22 @@ version = "0.2.189" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2" +[[package]] +name = "libloading" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7c4b02199fee7c5d21a5ae7d8cfa79a6ef5bb2fc834d6e9058e89c825efdc55" +dependencies = [ + "cfg-if", + "windows-link", +] + +[[package]] +name = "libm" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" + [[package]] name = "libsqlite3-sys" version = "0.37.0" @@ -1953,6 +2602,16 @@ version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3" +[[package]] +name = "md-5" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d89e7ee0cfbedfc4da3340218492196241d89eefb6dab27de5df917a6d2e78cf" +dependencies = [ + "cfg-if", + "digest 0.10.7", +] + [[package]] name = "md-5" version = "0.11.0" @@ -2029,6 +2688,33 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "ml-dsa" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "add6b9d92e496f16f4526d68ff29da1483aba4b119baeab8bed3b9e3544a6f3d" +dependencies = [ + "const-oid 0.10.2", + "crypto-common 0.2.2", + "ctutils", + "hybrid-array", + "module-lattice", + "pkcs8 0.11.0", + "shake", + "signature 3.0.0", +] + +[[package]] +name = "module-lattice" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c61b87c9683ab7cb1c6871d261ad5479b6b10ceb52c4352aaca3b5d35a8febe" +dependencies = [ + "ctutils", + "hybrid-array", + "num-traits", +] + [[package]] name = "multer" version = "3.1.0" @@ -2075,6 +2761,23 @@ dependencies = [ "num-traits", ] +[[package]] +name = "num-bigint-dig" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e661dda6640fad38e827a6d4a310ff4763082116fe217f279885c97f511bb0b7" +dependencies = [ + "lazy_static", + "libm", + "num-integer", + "num-iter", + "num-traits", + "rand 0.8.7", + "serde", + "smallvec", + "zeroize", +] + [[package]] name = "num-conv" version = "0.2.2" @@ -2090,6 +2793,16 @@ dependencies = [ "num-traits", ] +[[package]] +name = "num-iter" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c92800bd69a1eac91786bcfe9da64a897eb72911b8dc3095decbd07429e8048b" +dependencies = [ + "num-integer", + "num-traits", +] + [[package]] name = "num-traits" version = "0.2.19" @@ -2097,6 +2810,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" dependencies = [ "autocfg", + "libm", ] [[package]] @@ -2129,6 +2843,44 @@ dependencies = [ "num-traits", ] +[[package]] +name = "p256" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9863ad85fa8f4460f9c48cb909d38a0d689dba1f6f6988a5e3e0d31071bcd4b" +dependencies = [ + "ecdsa", + "elliptic-curve", + "primeorder", + "sha2 0.10.9", +] + +[[package]] +name = "p384" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe42f1670a52a47d448f14b6a5c61dd78fce51856e68edaa38f7ae3a46b8d6b6" +dependencies = [ + "ecdsa", + "elliptic-curve", + "primeorder", + "sha2 0.10.9", +] + +[[package]] +name = "p521" +version = "0.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fc9e2161f1f215afdfce23677034ae137bbd45016a880c2eb3ba8eb95f085b2" +dependencies = [ + "base16ct 0.2.0", + "ecdsa", + "elliptic-curve", + "primeorder", + "rand_core 0.6.4", + "sha2 0.10.9", +] + [[package]] name = "parking" version = "2.2.1" @@ -2158,6 +2910,16 @@ dependencies = [ "windows-link", ] +[[package]] +name = "pbkdf2" +version = "0.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ed6a7761f76e3b9f92dfb0a60a6a6477c61024b775147ff0973a02653abaf2" +dependencies = [ + "digest 0.10.7", + "hmac 0.12.1", +] + [[package]] name = "pdf-writer" version = "0.15.0" @@ -2180,6 +2942,15 @@ dependencies = [ "serde_core", ] +[[package]] +name = "pem-rfc7468" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88b39c9bfcfc231068454382784bb460aae594343fb030d46e9f50a645418412" +dependencies = [ + "base64ct", +] + [[package]] name = "percent-encoding" version = "2.3.2" @@ -2268,6 +3039,56 @@ version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" +[[package]] +name = "pkcs1" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8ffb9f10fa047879315e6625af03c164b16962a5368d724ed16323b68ace47f" +dependencies = [ + "der 0.7.10", + "pkcs8 0.10.2", + "spki 0.7.3", +] + +[[package]] +name = "pkcs5" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e847e2c91a18bfa887dd028ec33f2fe6f25db77db3619024764914affe8b69a6" +dependencies = [ + "aes 0.8.4", + "cbc 0.1.2", + "der 0.7.10", + "des 0.8.1", + "pbkdf2", + "scrypt", + "sha1 0.10.7", + "sha2 0.10.9", + "spki 0.7.3", +] + +[[package]] +name = "pkcs8" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" +dependencies = [ + "der 0.7.10", + "pkcs5", + "rand_core 0.6.4", + "spki 0.7.3", +] + +[[package]] +name = "pkcs8" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "451913da69c775a56034ea8d9003d27ee8948e12443eae7c038ba100a4f21cb7" +dependencies = [ + "der 0.8.1", + "spki 0.8.0", +] + [[package]] name = "pkg-config" version = "0.3.33" @@ -2321,6 +3142,24 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "primeorder" +version = "0.13.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "353e1ca18966c16d9deb1c69278edbc5f194139612772bd9537af60ac231e1e6" +dependencies = [ + "elliptic-curve", +] + [[package]] name = "proc-macro2" version = "1.0.107" @@ -2369,7 +3208,7 @@ dependencies = [ "bytes", "getrandom 0.4.3", "lru-slab", - "rand", + "rand 0.10.2", "rand_pcg", "ring", "rustc-hash", @@ -2417,6 +3256,17 @@ version = "6.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" +[[package]] +name = "rand" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22f6172bdec972074665ed81ed53b71da00bfc44b65a753cfde883ec4c702a1a" +dependencies = [ + "libc", + "rand_chacha", + "rand_core 0.6.4", +] + [[package]] name = "rand" version = "0.10.2" @@ -2425,7 +3275,26 @@ checksum = "c7f5fa3a058cd35567ef9bfa5e75732bee0f9e4c55fa90477bef2dfcdbc4be80" dependencies = [ "chacha20", "getrandom 0.4.3", - "rand_core", + "rand_core 0.10.1", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core 0.6.4", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom 0.2.17", ] [[package]] @@ -2440,7 +3309,7 @@ version = "0.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "caa0f4137e1c0a72f4c651489402276c8e8e1cf081f3b0ba156d2cbeef09e86a" dependencies = [ - "rand_core", + "rand_core 0.10.1", ] [[package]] @@ -2454,7 +3323,7 @@ dependencies = [ "rustls-pki-types", "time", "x509-parser", - "yasna", + "yasna 0.6.0", ] [[package]] @@ -2542,6 +3411,7 @@ dependencies = [ "rustls-platform-verifier", "serde", "serde_json", + "serde_urlencoded", "sync_wrapper", "tokio", "tokio-rustls", @@ -2556,6 +3426,16 @@ dependencies = [ "web-sys", ] +[[package]] +name = "rfc6979" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dd2a808d456c4a54e300a23e9f5a67e122c3024119acbfd73e3bf664491cb2" +dependencies = [ + "hmac 0.12.1", + "subtle", +] + [[package]] name = "ring" version = "0.17.14" @@ -2570,6 +3450,45 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "ripemd" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd124222d17ad93a644ed9d011a40f4fb64aa54275c08cc216524a9ea82fb09f" +dependencies = [ + "digest 0.10.7", +] + +[[package]] +name = "roxmltree" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1964b10c76125c36f8afe190065a4bf9a87bf324842c05701330bba9f1cacbb" +dependencies = [ + "memchr", +] + +[[package]] +name = "rsa" +version = "0.9.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8573f03f5883dcaebdfcf4725caa1ecb9c15b2ef50c43a07b816e06799bb12d" +dependencies = [ + "const-oid 0.9.6", + "digest 0.10.7", + "num-bigint-dig", + "num-integer", + "num-traits", + "pkcs1", + "pkcs8 0.10.2", + "rand_core 0.6.4", + "sha2 0.10.9", + "signature 2.2.0", + "spki 0.7.3", + "subtle", + "zeroize", +] + [[package]] name = "rustc-hash" version = "2.1.3" @@ -2596,9 +3515,9 @@ dependencies = [ [[package]] name = "rustls" -version = "0.23.42" +version = "0.23.43" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c54fcab019b409d04215d3a17cb438fd7fbf192ee61461f20f4fe18704bc138" +checksum = "0283386ce02abc0151e1761d08802dfe86c173b0b494af5cbc086574e453da06" dependencies = [ "aws-lc-rs", "log", @@ -2682,6 +3601,15 @@ version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" +[[package]] +name = "salsa20" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97a22f5af31f73a954c10289c93e8a50cc23d971e80ee446f1f6f7137a088213" +dependencies = [ + "cipher 0.4.4", +] + [[package]] name = "same-file" version = "1.0.6" @@ -2728,6 +3656,31 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" +[[package]] +name = "scrypt" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0516a385866c09368f0b5bcd1caff3366aace790fcd46e2bb032697bb172fd1f" +dependencies = [ + "pbkdf2", + "salsa20", + "sha2 0.10.9", +] + +[[package]] +name = "sec1" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3e97a565f76233a6003f9f5c54be1d9c5bdfa3eccfb189469f11ec4901c47dc" +dependencies = [ + "base16ct 0.2.0", + "der 0.7.10", + "generic-array", + "pkcs8 0.10.2", + "subtle", + "zeroize", +] + [[package]] name = "secrecy" version = "0.10.3" @@ -2861,6 +3814,16 @@ dependencies = [ "serde", ] +[[package]] +name = "serdect" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "66cf8fedced2fcf12406bcb34223dffb92eaf34908ede12fed414c82b7f00b3e" +dependencies = [ + "base16ct 1.0.0", + "serde", +] + [[package]] name = "sha1" version = "0.10.7" @@ -2924,6 +3887,37 @@ dependencies = [ "tokio", ] +[[package]] +name = "sha3" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77fd7028345d415a4034cf8777cd4f8ab1851274233b45f84e3d955502d93874" +dependencies = [ + "digest 0.10.7", + "keccak 0.1.6", +] + +[[package]] +name = "sha3" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be176f1a57ce4e3d31c1a166222d9768de5954f811601fb7ca06fc8203905ce1" +dependencies = [ + "digest 0.11.3", + "keccak 0.2.0", +] + +[[package]] +name = "shake" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09057cb2149ad4cbd2da1e26b351f9a4c354219421229c69c3063e6f61947c4a" +dependencies = [ + "digest 0.11.3", + "keccak 0.2.0", + "sponge-cursor", +] + [[package]] name = "sharded-slab" version = "0.1.7" @@ -2949,6 +3943,26 @@ dependencies = [ "libc", ] +[[package]] +name = "signature" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" +dependencies = [ + "digest 0.10.7", + "rand_core 0.6.4", +] + +[[package]] +name = "signature" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28d567dcbaf0049cb8ac2608a76cd95ff9e4412e1899d389ee400918ca7537f5" +dependencies = [ + "digest 0.11.3", + "rand_core 0.10.1", +] + [[package]] name = "simd-adler32" version = "0.3.10" @@ -2993,6 +4007,25 @@ version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" +[[package]] +name = "slh-dsa" +version = "0.2.0-rc.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "371c02fe34044d8866ddf7cb0e8204a87ef31a39f0408bed41c4253ea9dd61ed" +dependencies = [ + "const-oid 0.10.2", + "digest 0.11.3", + "hmac 0.13.0", + "hybrid-array", + "pkcs8 0.11.0", + "rand_core 0.10.1", + "sha2 0.11.0", + "sha3 0.11.0", + "signature 3.0.0", + "typenum", + "zerocopy", +] + [[package]] name = "smallvec" version = "1.15.2" @@ -3021,6 +4054,32 @@ dependencies = [ "lock_api", ] +[[package]] +name = "spki" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" +dependencies = [ + "base64ct", + "der 0.7.10", +] + +[[package]] +name = "spki" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d9efca8738c78ee9484207732f728b1ef517bbb1833d6fc0879ca898a522f6f" +dependencies = [ + "base64ct", + "der 0.8.1", +] + +[[package]] +name = "sponge-cursor" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a0219bd7d979d58245a4f41f695e1ac9f8befdffadd7f61f1bae9e39abc6620" + [[package]] name = "sqlx" version = "0.9.0" @@ -3157,13 +4216,13 @@ dependencies = [ "futures-core", "futures-util", "hex", - "hkdf", - "hmac", + "hkdf 0.13.0", + "hmac 0.13.0", "itoa", "log", - "md-5", + "md-5 0.11.0", "memchr", - "rand", + "rand 0.10.2", "serde", "serde_json", "sha2 0.11.0", @@ -3327,9 +4386,9 @@ dependencies = [ [[package]] name = "time" -version = "0.3.54" +version = "0.3.55" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e1d5e639ff6bab73cb6885cc7e7b1de96c3f32c68ec55f3952614bec1092244" +checksum = "cdb87b95ec50ddfa440816d227a17b2ccbdda963a316a727fda0fc4334f7d134" dependencies = [ "deranged", "num-conv", @@ -3391,6 +4450,27 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" +[[package]] +name = "tls_codec" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0de2e01245e2bb89d6f05801c564fa27624dbd7b1846859876c7dad82e90bf6b" +dependencies = [ + "tls_codec_derive", + "zeroize", +] + +[[package]] +name = "tls_codec_derive" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d2e76690929402faae40aebdda620a2c0e25dd6d3b9afe48867dfd95991f4bd" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + [[package]] name = "tokio" version = "1.53.1" @@ -3410,13 +4490,13 @@ dependencies = [ [[package]] name = "tokio-macros" -version = "2.7.1" +version = "2.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6328af13490e73a9b4694030fafd93f8c8c6a9dede33e821c3fc63eddf8042ba" +checksum = "78773a2a397f451582ce068015985c33193cf6dea8b74d2a639fe457b2f07b0e" dependencies = [ "proc-macro2", "quote", - "syn 2.0.119", + "syn 3.0.3", ] [[package]] @@ -3620,6 +4700,39 @@ version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" +[[package]] +name = "tsp-ltv" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aac44d2540b70e2dfed4db52494941a96d6d37deec7af280d37446171741d3ba" +dependencies = [ + "base64", + "chrono", + "cms", + "const-oid 0.9.6", + "der 0.7.10", + "digest 0.10.7", + "dsa", + "ecdsa", + "ed25519-dalek", + "getrandom 0.2.17", + "hex", + "log", + "md-5 0.10.6", + "p256", + "p384", + "p521", + "pem-rfc7468", + "rsa", + "sha1 0.10.7", + "sha2 0.10.9", + "sha3 0.10.9", + "signature 2.2.0", + "spki 0.7.3", + "thiserror", + "x509-cert", +] + [[package]] name = "typenum" version = "1.20.1" @@ -3687,6 +4800,12 @@ version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" +[[package]] +name = "uppsala" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b83b4950a552cd9812b6afd150ea10729617d28208bba122912eb196c2261d4d" + [[package]] name = "url" version = "2.5.8" @@ -4081,6 +5200,30 @@ version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4" +[[package]] +name = "x25519-dalek" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7e468321c81fb07fa7f4c636c3972b9100f0346e5b6a9f2bd0603a52f7ed277" +dependencies = [ + "curve25519-dalek", + "rand_core 0.6.4", + "serde", + "zeroize", +] + +[[package]] +name = "x509-cert" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1301e935010a701ae5f8655edc0ad17c44bad3ac5ce8c39185f75453b720ae94" +dependencies = [ + "const-oid 0.9.6", + "der 0.7.10", + "spki 0.7.3", + "tls_codec", +] + [[package]] name = "x509-parser" version = "0.18.1" @@ -4105,6 +5248,12 @@ version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9440ea3e5aeabb0ac63af70daf835274065238cdd0cec83418f417eae38bacee" +[[package]] +name = "yasna" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e17bb3549cc1321ae1296b9cdc2698e2b6cb1992adfa19a8c72e5b7a738f44cd" + [[package]] name = "yasna" version = "0.6.0" @@ -4184,6 +5333,20 @@ name = "zeroize" version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e13c156562582aa81c60cb29407084cdb54c4164760106ab78e6c5b0858cf64e" +dependencies = [ + "zeroize_derive", +] + +[[package]] +name = "zeroize_derive" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c50655cbb0fe3fc43170059e702f1ce5e19b84cec58dc87b037a09935c2f328" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] [[package]] name = "zerotrie" diff --git a/auth-service/.gitignore b/auth-service/.gitignore new file mode 100644 index 000000000..ab167bf84 --- /dev/null +++ b/auth-service/.gitignore @@ -0,0 +1 @@ +rd-metadata.xml diff --git a/auth-service/Cargo.toml b/auth-service/Cargo.toml index f0d7f6834..15d7d2c69 100644 --- a/auth-service/Cargo.toml +++ b/auth-service/Cargo.toml @@ -5,15 +5,33 @@ edition.workspace = true license.workspace = true publish.workspace = true -[lib] -path = "src/lib.rs" - [features] +# Talk to the online TVS mock service: default `TVS_ENV` to `test` and `BASE_URL` +# to `http://localhost:3000`, and embed the committed DV cert/key bundle +# (`auth-service/fixtures`) tvs-mock = [] [dependencies] -axum = { workspace = true } +bergshamra-dsig = "0.7" +bergshamra-enc = "0.7" +bergshamra-keys = "0.7" +cookie = "0.18" +base64 = "0.22" +sha1 = "0.11" +roxmltree = "0.21" +rustls-webpki = { version = "0.103", default-features = false, features = ["std", "aws-lc-rs"] } +rustls-pki-types = "1" +x509-cert = "0.2" + +axum = { workspace = true, features = ["macros"] } axum-extra = { workspace = true, features = ["typed-routing"] } -rand = { workspace = true } +tokio = { workspace = true } +askama = { workspace = true } +uuid = { workspace = true } +chrono = { workspace = true } +parking_lot = { workspace = true } secrecy = { workspace = true } +sha2 = { workspace = true } +reqwest = { workspace = true, features = ["form", "rustls"] } +tracing = { workspace = true } thiserror = { workspace = true } diff --git a/auth-service/README.md b/auth-service/README.md new file mode 100644 index 000000000..050950833 --- /dev/null +++ b/auth-service/README.md @@ -0,0 +1,263 @@ +# auth-service + +SAML 2.0 **Service Provider** for the Dutch TVS *Routeringsdienst* (eID login via +DigiD / eHerkenning / eIDAS). + +This crate implements the **DV (Dienstverlener / Service Provider)** side of the +*Koppelvlakspecificatie eID SAML v4.4* interface, talking to the TVS RD +(*Routeringsdienst*, the IdP). It is consumed as a library: an embedding +application mounts the [`router`](src/lib.rs) and implements the +[`AuthState`](src/state.rs) trait; this crate owns all SAML message building, +cryptography, and validation. + +A non-authoritative extract of the requirements this code targets lives in +[eid-saml-4.4-requirements.md](eid-saml-4.4-requirements.md); `§` references in the +source and below point at it (and, where noted, at the OASIS `saml-*-2.0-os` specs +and the TVS *Checklist Testen*). + +> The crate never touches the application's session. On success it hands the +> embedding app a verified subject identifier via `AuthState::on_authenticated`; +> the app creates and owns the session. + +## Abbreviations + +| Abbreviation | Meaning | +|---|---| +| ACS | Assertion Consumer Service: the SP endpoint that receives the authentication result (here: the artifact) | +| AD | *Authenticatiedienst*: the actual authentication service behind the RD (DigiD, eHerkenning, eIDAS) | +| ARS | Artifact Resolution Service: the RD's back-channel SOAP endpoint that exchanges an artifact for the `ArtifactResponse` | +| c14n | XML canonicalization (exclusive c14n is used in all signatures) | +| CSP | Content Security Policy (HTTP response header) | +| CSRF | Cross-Site Request Forgery | +| DV | *Dienstverlener*: Service Provider; the role this crate implements | +| eID | The Dutch electronic-identity system; the *Koppelvlakspecificatie eID SAML* defines this DV↔RD interface | +| eIDAS | EU *electronic Identification, Authentication and trust Services*: cross-border European login | +| IdP | Identity Provider: the SAML role the RD plays towards this SP | +| LoA | Level of Assurance (*betrouwbaarheidsniveau*) of the authentication | +| mTLS | Mutual TLS: both client and server authenticate with certificates | +| OIN | *Organisatie-identificatienummer*: Dutch government organisation number, carried in PKIoverheid certificates | +| PII | Personally Identifiable Information | +| PKIoverheid | The Dutch government PKI; its root CAs anchor all participant certificates | +| RD | *Routeringsdienst*: routing service between DV and the ADs; the IdP this SP talks to (TVS) | +| SAML | Security Assertion Markup Language (v2.0) | +| SLO | Single Logout | +| SLS | Single Logout Service: the SP endpoint that receives the `LogoutResponse` | +| SP | Service Provider (SAML term for the DV role) | +| SSO | Single Sign-On | +| TVS | *Toegangsverleningsservice*: the government RD implementation this crate connects to | +| UA | User-Agent (the end user's browser) | +| XML-DSig | XML Digital Signature | +| XML-Enc | XML Encryption | +| XSW | XML Signature Wrapping: signature-relocation attack class the validation defends against | + +## Endpoints + +The router mounts the protocol endpoints (their paths are fixed; the ACS and SLS +paths are advertised in the SP metadata): + +| Method + path | Role | Channel | +|---|---|---| +| `GET /saml/sp/metadata` | Serve the signed DV SP metadata (§8.3) | front, browser/RD | +| `GET /saml/sp/acs` | Assertion Consumer Service (HTTP-Artifact, §7.4) | front, browser | +| `GET /login/error` | Query-clean landing page for a failed authentication | front, browser | +| `POST /saml/sp/logout` | Receive the RD `LogoutResponse` (§7.7.2) | front, browser | +| `GET /saml/sp/autosubmit.js` | Script the HTTP-POST binding page submits | front, browser | + +The two browser-facing *entry points* are **not** mounted by `router` and are +not in the SP metadata, so the app mounts them at URLs (and methods) of its +choosing: + +| Path | Role | Handler | +|---|---|---| +| `/login` | Start SSO (§7.1 step 2 / §3.1.1) | [`handle_login`](src/handlers/login.rs) | +| `/logout` | Start SP-initiated logout (§7.7.1 / §3.1.1.1) | [`handle_logout`](src/handlers/logout.rs) | + +## Channels + +Two channels are used, and they have very different trust properties: + +- **Front-channel**: the user's browser, over public TLS (https). Carries the + AuthnRequest (out), the artifact (in), and both logout messages. Everything + here is attacker-reachable, so nothing on it is trusted without a signature. +- **Back-channel**: direct DV→RD HTTPS with **mutual TLS** (§9.4): PKIoverheid + client certificate, TLS ≥ 1.2, and the RD server pinned to the back-channel + root CA ([`pki`](src/saml/pki.rs)). Carries the SOAP `ArtifactResolve` / + `ArtifactResponse` exchange that actually delivers the assertion. + +## The authentication happy flow + +The three participants are the **Browser**, the **DV** (this crate), and the +**RD / IdP (TVS)**. + +1. **Browser → DV:** `/login` (the app's login route). +2. **DV:** builds + signs the AuthnRequest, registers the request-id (replay + store), and sets the flow cookie (bound to the request-id + UA). +3. **DV → Browser:** `200` with an HTML auto-POST form. +4. **Browser → RD:** `POST {sso_url}` with `SAMLRequest=base64(AuthnRequest)` + (HTTP-POST binding). +5. **RD:** the user authenticates at DigiD / eHerkenning / eIDAS. +6. **RD → Browser → DV:** `GET /saml/sp/acs?SAMLart=` via a + `302` (HTTP-Artifact binding). +7. **DV → RD:** `POST {ars_url}` with `SOAP(ArtifactResolve)` over the mTLS + back-channel. +8. **RD → DV:** `SOAP(ArtifactResponse)`. +9. **DV:** validates the chain, decrypts the SubjectID, verifies the flow + cookie, and consumes the request-id. +10. **DV → Browser:** `302` to the app's post-login page; meanwhile + `on_authenticated(subject_id)` fires and the app creates a session. + +### Messages, formats, and channels + +| # | Message | Built/parsed by | Wire format | Channel | +|---|---|---|---|---| +| 1 | **AuthnRequest** | [`create_authn_request`](src/saml/messages.rs) | `samlp:AuthnRequest` XML, enveloped XML-DSig | base64 in a `SAMLRequest` form field, auto-POSTed (HTTP-POST binding) | +| 2 | **Artifact** | RD | opaque `SAMLart` value | query parameter on the ACS redirect (HTTP-Artifact binding) | +| 3 | **ArtifactResolve** | [`create_artifact_resolve`](src/saml/messages.rs) | `samlp:ArtifactResolve` XML, enveloped XML-DSig, wrapped in a SOAP 1.1 envelope | `POST text/xml` over **mTLS** to the ARS | +| 4 | **ArtifactResponse** | RD → [validation](src/saml/validation/) | SOAP → `samlp:ArtifactResponse` (RD-signed) → `samlp:Response` → `saml:Assertion` (SubjectIDs as `EncryptedID`) | SOAP body of the mTLS response | + +All outgoing signatures use **exclusive c14n + enveloped transform, RSA-SHA256, +DigestMethod SHA-256** (the algorithms are fixed in the +[`templates/saml/*.xml`](templates/saml/), which are the source of truth for the +outgoing wire format. + +The AuthnRequest additionally carries `ForceAuthn="true"`, a +`RequestedAuthnContext Comparison="minimum"` for the DV's minimum LoA, the +`IntendedAudience` + `ServiceUUID` extension attributes, and (optionally) a +`Scoping/IDPList` that pre-selects a single AD (DigiD / eHerkenning / eIDAS). + +## What we verify / validate + +Verification runs at two times: **RD metadata trust** is established once at +startup (and on each background refresh); the **per-login chain** is validated on +every ACS callback. + +### RD metadata trust anchor ([`idp_metadata.rs`](src/saml/idp_metadata.rs)) + +This is the linchpin: every RD signing key used later comes from here, so the +metadata document is trusted by an *external* anchor, never by its own signature. + +- `entityID` **equals** the pinned RD EntityID (a config constant per + environment, not a value read from the document). +- The signing certificate carries the **expected RD OIN** in its + `Subject.serialNumber` (§9.1). +- The signing certificate **chains to a pinned PKIoverheid root** via the + embedded intermediates (§9.2). The signing keys themselves are *not* pinned + (they rotate); trust derives from chain + OIN. +- The document's enveloped XML signature is then verified **against those + pinned certs**. +- `validUntil`, if present, has to be in the future (§8.2/§8.5); endpoints are clean + absolute **https** URLs with no characters that could break out of an HTML + attribute / CSP header / request target. + +### Front-channel binding, login-CSRF / forced login ([`flow.rs`](src/handlers/flow.rs)) + +- A one-shot `__Host-`-prefixed cookie set by `/login` binds the flow to the + browser and its `User-Agent`; the ACS callback must present a cookie matching + the assertion's `InResponseTo`. Cleared regardless of outcome. + +### ArtifactResponse, §7.6.1 ([`artifact_response.rs`](src/saml/validation/artifact_response.rs)) + +- **Exactly one** top-level `ds:Signature` (the enveloped message signature), + valid against an RD signing key selected from verified metadata (KeyInfo only + *selects* the cert, §9.2). +- Signature / digest algorithm **allow-list** (RSA-SHA256+ / SHA-256+); an + rsa-sha1 / sha1 downgrade is rejected (§9.1). +- **XSW defense**: every `ds:Reference` URI must target the consumed root element + (empty or `#`), so a signature whose digest matches a sibling/nested + element cannot authenticate a forged wrapper. +- `@InResponseTo` equals our `ArtifactResolve` id; status is `Success`. + +### Inner Response, §7.6.2 ([`response.rs`](src/saml/validation/response.rs)) + +- Status `Success` (else mapped to user-cancelled → `Cancelled`, or → `Error`). +- `@Destination` equals our ACS URL; `Issuer` equals the RD EntityID. +- No `EncryptedAssertion` present; an `Assertion` is present on success. + +### Assertion, §7.6.3 / processing rules §7.6.3.5 ([`assertion.rs`](src/saml/validation/assertion.rs)) + +The Assertion is **not** verified by its own signature: its authenticity comes +from the enveloping RD signature on the ArtifactResponse (verified above), plus +binding its `Issuer` to the RD EntityID. Signatures inside an `Assertion`/`Advice` +are evidence-only (§9.1), and claims are read only from the outer assertion +(the `Advice` subtree is pruned). + +- `Issuer` = RD EntityID (rule 1). +- `SubjectConfirmation` Method = `bearer`; `Recipient` = ACS URL (rule 2); + `NotOnOrAfter` not passed (rule 3, ±30 s clock skew). +- `Conditions` `NotBefore`/`NotOnOrAfter` window valid (rule 6). +- `AudienceRestriction` contains the DV EntityID (rule 5). +- `AuthnContextClassRef` LoA ≥ the DV minimum (`Low`); equal-or-higher accepted, + lower rejected (§7.6.3.2 / TVS T6); see [`loa.rs`](src/saml/validation/loa.rs). +- `InResponseTo` is extracted, then **matched-and-consumed atomically** against + the outstanding-request store (rule 4 / replay §9.7): an absent, unknown, + expired, or already-consumed value is rejected. The store is owned by the + embedding app so the id survives `/login` and ACS landing on different instances + ([`PendingRequests`](src/pending.rs) is the default in-memory implementation; + TTL 15 min). + +### SubjectID decryption, §7.6.3.4 / §9.3 ([`decryption.rs`](src/saml/decryption.rs)) + +- Encryption algorithm **allow-list** before decrypting: data cipher + **AES-256-CBC**, key transport **RSA-OAEP** (RSA-1.5 and weaker ciphers + rejected, blocking a Bleichenbacher-style downgrade). +- Decrypted with the DV's private encryption keys (each is tried, so a blob + wrapped to a rotated key still decrypts). +- The decrypted `NameID` must use the `persistent` format, carry a + `NameQualifier`, and must not carry `SPNameQualifier`/`SPProvidedID`. +- An assertion with no **acting** SubjectID is treated as an authentication + failure (no usable identity). + +> **PII:** decrypted SubjectIDs and the SAML `NameID` are wrapped in +> `SecretString` (zeroized on drop) and are never logged; only non-PII metadata +> (presence flags, lengths, LoA, entity URNs) appears in traces. + +## Logout (SP-initiated only, §3.1.1.1) + +1. `/logout` (the app's logout route): the app tears down its session and + returns the recorded `NameID`; the DV builds a **signed `LogoutRequest`** + (`saml:NameID`), registers its id, and auto-POSTs it to the RD SLO endpoint + (HTTP-POST binding). +2. `POST /saml/sp/logout`: the RD's `LogoutResponse` must be a + `samlp:LogoutResponse`, carry a **valid RD signature**, have an + `@InResponseTo` matching a `LogoutRequest` this DV issued (consumed once, so a + replay is rejected), and should report `Success`. The local session is already + gone, so a failed/forged/replayed response is logged and dropped; the browser + is always redirected to the post-logout page. + +## Configuration + +[`AuthConfig::from_env`](src/config.rs) reads four inputs; everything +environment-specific (RD endpoints, the Kiesraad DV EntityID / ServiceUUID, the +back-channel trust anchor, cert/key paths) derives from them: + +| Variable | Meaning | +|---|---| +| `TVS_ENV` | `test` \| `preproduction` \| `production` | +| `CERTS_DIR` | Directory holding the DV certificate/key bundle | +| `BASE_URL` | Public origin (used to derive the SP ACS/SLO URLs) | +| `PRESELECTED_AD` | `Select` (default, RD shows its own picker) \| `DigiD` \| `eHerkenning` \| `eIDAS` | + +The `tvs-mock` cargo feature targets the online shared TVS mock: it defaults +`TVS_ENV`/`BASE_URL` and embeds the committed test DV bundle from +[`fixtures/`](fixtures/). **Mock builds only; never enable it for a real +deployment** (it bakes in test private keys). + +## Cryptography + +XML-DSig signing/verification and XML-Enc decryption are delegated to the +pure-Rust `bergshamra-*` crates through the thin [`crypto`](src/saml/crypto.rs) +adapter; this crate owns the SAML-level policy around them (algorithm allow-lists, +key selection from verified metadata, the XSW root-coverage check). + +## Tests + +``` +cargo test -p auth-service +``` + +Beyond the per-module unit tests, [`tests/`](tests/) covers message round-trips, +metadata validation, the full validation chain, and two XML-signature-wrapping +attack PoCs ([`xsw_sibling_poc.rs`](tests/xsw_sibling_poc.rs), +[`xsw_exploit_check.rs`](tests/xsw_exploit_check.rs)) that must stay rejected. +[`tvs_metadata.rs`](tests/tvs_metadata.rs) validates the real TVS mock metadata; +its tests are `#[ignore]`d because they need network access. diff --git a/auth-service/askama.toml b/auth-service/askama.toml new file mode 100644 index 000000000..c2ffc92ce --- /dev/null +++ b/auth-service/askama.toml @@ -0,0 +1,5 @@ +[general] +# SAML/XML and HTML templates live here. Both `.xml` and `.html` are escaped by +# askama's default HTML escaper; raw embedded XML (e.g. the SOAP body) is +# emitted with `|safe`. +dirs = ["templates"] diff --git a/auth-service/eid-saml-4.4-requirements.md b/auth-service/eid-saml-4.4-requirements.md new file mode 100644 index 000000000..34212f39c --- /dev/null +++ b/auth-service/eid-saml-4.4-requirements.md @@ -0,0 +1,740 @@ +Source: https://tvs.dictu.nl/sites/default/files/documents/Koppelvlakspecificatie-eID-SAML-v4.4.pdf + +# eID SAML 4.4 Specification — Requirements + +Source: *Koppelvlakspecificatie eID SAML v4.4 (16 september 2020, Definitief)* +Published by: Logius, Ministerie van Binnenlandse Zaken en Koninkrijksrelaties + +--- + +## 1 Disclaimer + +- This document is not a normative source for conducting audits on eID participants. +- The 4.4 version describes representation information exchange between DV/LC and RD, but this functionality will not be available initially. + +## 2 History + +| Version | Changes | +|-----------|---------| +| 4.4 RC1 | Initial version | +| 4.4 RC2 | Corrections related to test findings | +| 4.4 RC3 | Added optional @ProviderName to AuthnRequest for eIDAS; support for multi-use certs in metadata; use of sender/receiver instead of specific roles | +| 4.4 final | Added requestorID in AuthnRequest for representation; fixed naming-scheme (base URN `urn:nl-eid-gdi:1.0`); fixed validUntil/cacheDuration; clarified certificate-use in signatures | + +## 3 Frameworks + +Based on OASIS SAML 2.0: +- saml-core-2.0-os +- saml-profiles-2.0-os +- saml-metadata-2.0-os +- saml-bindings-2.0-os +- saml errata + +Also references: +- NORA (Nederlandse Overheid Referentie Architectuur) +- NCSC ICT-beveiligingsrichtlijnen voor TLS + +### 3.1 SAML Profiles + +Two profiles are used: +1. **Web Browser SSO profile** with HTTP-POST binding +2. **Single Logout profile** with HTTP-POST binding, issued by Session Participant to Identity Provider + +### 3.1.1 SAML Message Flows and Bindings + +#### Front-channel (re)authentication + +| Step | Route | Message | Endpoint | Binding | Metadata | +|------|-------|---------|----------|---------|----------| +| 2 | DV/LC → Browser → RD | AuthnRequest | SingleSignOnService | HTTP-POST | RD IdP | +| 4 | RD → Browser → DV/LC | Artifact | AssertionConsumerService | HTTP-Artifact | DV/LC SP | + +#### Back-channel (Assertion) + +| Step | Route | Message | Endpoint | Binding | Metadata | +|------|-------|---------|----------|---------|----------| +| 5 | DV/LC → RD | ArtifactResolve | ArtifactResolutionService | SOAP | RD IdP | +| 6 | RD → DV/LC | ArtifactResponse | (direct response) | SOAP | — | + +#### 3.1.1.1 SingleLogout Messages and Bindings + +- Only SP-initiated logout is supported by the RD within an SSO federation context. +- IdP-initiated Logout is NOT supported. +- A DV participating in an SSO federation MUST send a Logout request to the RD (step 2a/b). +- The RD validates the LogoutRequest; if an active session exists for that user's browser, the RD terminates it. +- The RD replies with a LogoutResponse with success status if the LogoutRequest was valid. + +| Step | Route | Message | Endpoint | Binding | Metadata | +|------|-------|---------|----------|---------|----------| +| 2 | DV/LC → Browser → RD | LogoutRequest | SingleLogoutService | HTTP-POST | RD IdP | +| 6 | RD → Browser → DV/LC | LogoutResponse | SingleLogoutService | HTTP-POST | DV/LC SP | + +## 4 Glossary + +| Term | English | Description | +|------|---------|-------------| +| Artifact | Artifact | Pointer to a SAML message sent through the front-channel to avoid exposing sensitive data to the end-user's UA | +| Assertion | Assertion | SAML Assertion | +| Back channel | Back channel | Communication channel between DV/LC, RD, AD, BVD, eTD (not interacting with end user) | +| LoA | Level of Assurance | Betrouwbaarheidsniveau | +| BVD | BVD | Bevoegdheidsverklaringsdienst | +| DV | SP | Dienstverlener (Service Provider) | +| SSO | Single Sign On | Eenmalig Inloggen | +| Front channel | Front channel | Communication between DV/LC, RD, AD or BVD and UA of End-user | +| Identity Provider (IDP) | Identity Provider (IDP) | De AuthentcatieDienst (AD) | +| LC | Cluster Connection Provider | Leverancier Clusteraansluiting | +| Metadata | Metadata | Before a SAML connection can be established, all parties must exchange connection properties through Metadata | +| Participant | Participant | Any party in authentication/representation processes (DV, RD, LC, AD, BVD) | +| RD | Routeringsdienst | See Roles | +| RV | Routeringsvoorziening | Facility which unburdens Service Providers when accepting multiple Identity Providers | +| SAML | SAML | SAML v 2.0 standard | +| SLO | SLO | Single Log Off | +| UA | UA | User Agent (e.g., browser) | + +### 4.1 Roles + +| Abbreviation | Role | Description | Example | +|-------------|------|-------------|---------| +| AD | Authenticatiedienst | Identity Provider (IDP) | DigiD, eTD AD, eIDAS out | +| DV | Dienstverlener | Service Provider (SP) | Gemeente, huisarts, overheidsinstelling | +| RD | Routeringsdienst | Technical realization of the Routeringsvoorziening | TVS, IdentityBridge | +| RV | Routeringsvoorziening | Facility which unburdens SPs when accepting multiple IDPs | Beheerorganisatie Routeringsvoorziening | +| LC | Leverancier Clusteraansluiting | Cluster connection provider; assists the DV in connecting to the RD | SaaS providers within health-care field | +| BVD | Bevoegdheidsverklaringsdienst | Service providing assertions for representation relationships | De BVD van programma Machtigen | +| MR | Mandate Register | Public or Private entity registering formalized representation relationships | eHerkenning MR's | + +## 5 Introduction + +### 5.1 Introduction + +eID SAML 4.4 specifies the communication between Dienstverlener (DV) and Routeringsdienst (RD), and between Leverancier Clusteraansluiting (LC) and RD. + +### 5.2 Interface Versioning + +| Version | Description | Status | +|---------|-------------|--------| +| 4.4 | First version of eID SAML specs for connecting to an RD. Scope: DV-RD and LC-RD. | Final | +| 4.0 | DigiD CA 4.0 Specification. Introducing Encrypted BSN and support for LC. | Pilot | + +## 6 Supported Use Cases (DV/LC - RD) + +### 6.1 Authentication + +- An End User authenticates on his/her own behalf at a DV which requires authentication. +- The DV redirects to the RD requesting authentication. +- At the RD, the End User selects an AD. +- After successful authentication at the AD, the End User is redirected back to the RD. +- The RD provides an interface response to the DV including at minimum: an identifier of the End User, the Level of Assurance, and the Service authenticated for. +- The DV can then take an access control decision. + +#### 6.1.1 Actors +End User, DV, RD, AD + +### 6.2 Authentication with Representation + +- An Acting End User authenticates with the intent to consume a Service on behalf of another person. +- The representation relationship must be registered in a Machtigingenregister (MR). +- After successful authentication at the AD, the RD redirects to a BVD where the Acting End User selects the representation relationship. +- The RD includes both the attestation of identity and the attestation of representation in the response. + +#### 6.2.1 Actors +Acting End User, Represented party, DV, RD, AD, BVD + +### 6.3 Cluster Connection Connectivity + +- In SaaS/multi-tenant solutions, the software vendor acts as an LC. +- The LC is registered with the RD and registers all DV's it provides access for. +- DV initiates authentication via the LC. The LC sends the AuthnRequest to the RD for the DV. +- Data in the response from the RD is encrypted to the DV, not the LC. +- The LC facilitates the authentication process but cannot access the sensitive information. + +**Note:** The Cluster Connection is applicable to SaaS and PaaS (if eID-connectivity is direct), but NOT to IaaS. + +#### 6.3.1 Actors +DV, LC, RD + +### 6.4 Authentication with AD/BVD Preselection + +- The User makes the AD selection at the DV rather than at the RD, improving UX. +- The DV includes the preselection in the AuthnRequest via the Scoping/IDPList element. +- The RD applies the preselection as a filter on available options. + +**Requirement:** Service Providers SHOULD offer the choice for each AD/BVD in a non-discriminatory way for all applicable AD/BVDs. + +#### 6.4.1 Actors +End-User, DV, RD, AD, BVD + +## 7 SAML Message Specification + +### 7.1 SAML Authentication Steps + +1. End user (UA) wants to access a part of the web service requiring authentication. +2. The DV or LC sends the end-user to the RD for authentication and/or proof of representation. +3. (Out of scope) The RD offers the user a choice of AD's and BVD's meeting the authentication requirements. +4. The RD sends the end user back to the DV or LC via a redirect. A meaningless **artifact** is sent (not the actual response). Even if authentication was unsuccessful, an artifact is sent. +5. The DV or LC retrieves the response message from RD via the **back channel** based on the artifact. Artifacts are stored by RD for a maximum of **15 minutes** and can only be retrieved **once**. +6. RD replies with the ArtifactResponse containing the Response and Assertion. If successful, the requested identities and attributes are encrypted so that only the intended DV(s) can obtain plain text. +7. & 8. Successful authentication provides the DV the information needed for access control. + +**Important:** The distinction between front channel and back channel ensures that user-defined attributes (e.g., BSN) are never sent via the front channel, preventing interception by the browser. + +### 7.2 SAML Message Specification + +Rules: +1. **Messages contain at least the elements specified as mandatory by the standard.** +2. **Messages also contain optional elements. It is indicated whether these are mandatory, conditional, or optional.** +3. **Optional SAML elements that are not in this specification SHOULD NOT be included. When present they will be ignored when possible.** + +### 7.3 SAML AuthnRequest + +Sender: DV or LC +Recipient: RD + +| Element/@Attribute | Card. | Description | +|-------------------|-------|-------------| +| @ID | 1 | Unique message identifier. MUST identify the message uniquely within the scope of sender and receiver for at least 12 months. | +| @Version | 1 | MUST be '2.0'. | +| @IssueInstant | 1 | Time of issuing the request. | +| @Destination | 1 | URL of the recipient. MUST match the metadata. | +| @ForceAuthn | 0..1 | 'true' indicates existing SSO session MUST NOT be used. Default is 'false'. | +| @AssertionConsumerServiceIndex | 1 | This index MUST refer to an endpoint of an AssertionConsumerService in the issuer's metadata. Note: @AssertionConsumerServiceURL MUST NOT be included. | +| @ProviderName | 0..1 | Conditional. Reserved for eIDAS UIT. SHOULD NOT be used in other use cases. | +| Issuer | 1 | MUST contain the EntityID of the issuer as registered in the metadata. | +| Signature | 1 | MUST contain the XML signature of the sender for the enveloped message. MUST contain a `` element with a `` or ``. | +| AttributeConsumingServiceIndex | 0..1 | Conditional. Only one of `` or `` MUST be present. MAY only be used if the issuer is a DV. MUST NOT be used in other cases. If present, MUST refer to an AttributeConsumingService in the DV's metadata. | +| Extensions | 0..1 | Conditional. Only one of `` or `` MUST be present. MUST be included if the issuer is not a DV. MAY be used when the issuer is a DV. | +| -Attribute (IntendedAudience) | 1 | An `` with @Name="urn:nl-eid-gdi:1.0:IntendedAudience" MUST be present and contain an AttributeValue with the EntityID of the DV for which authentication is requested. | +| -Attribute (ServiceUUID) | 1 | An `` with @Name="urn:nl-eid-gdi:1.0:ServiceUUID" MUST be present and contain an AttributeValue with a ServiceUUID known at the service catalogus of the receiver. | +| Scoping | 0..1 | OPTIONAL element. | +| -IDPList | 0..1 | OPTIONAL. MAY be used to limit the AD/BVD selection at the RD. | +| --IDPEntry | 1..n | At least one IDPEntry MUST be present if IDPList is present. If no valid IDPEntry is present, the AuthnRequest will fail with `urn:oasis:names:tc:saml:2.0:status:Requester` and the applicable second-level code. MUST contain at least the EntityID of one AD. If it also contains a BVD EntityID, representation using the BVD is optional. | +| ---@ProviderID | 1 | MUST contain the EntityID of a pre-selected AD or BVD. | +| -RequesterID | 0..n | Optional. MAY be used to make representation with a BVD mandatory. If used, MUST contain one or more EntityID's of BVD's. | + +#### 7.3.1.1 Use of `` or `` for Service Definitions + +- A service definition has a unique UUID (ServiceUUID) in the service catalog. +- Only a DV MAY use the AttributeConsumingServiceIndex. All other participants MUST use the `` element. +- A DV MAY use either the `` or the `` element. +- When using the AttributeConsumingService, the RD will retrieve the DV's EntityID from the `` element. + +#### 7.3.1.2 Processing Rules + +The RD MUST: +1. Validate that the DV is registered for the requested ServiceDefinition (serviceUUID) referenced by the ServiceIndex in the DV metadata or the ``, and that the registration is valid. +2. (When an LC is involved) validate that the DV is registered to use the requested ServiceUUID with the LC that sends the AuthnRequest on behalf of the DV, and that the registration is valid. +3. If any of these validations fails, the authentication MUST fail. + +### 7.4 SAML AuthnRequest Response Message + +- The receiver of the AuthnRequest message sends a SAML-artifact via the front channel by a redirect to the AssertionConsumerService referenced in the AuthnRequest. +- An artifact is a reference to the SAML Response message. +- Even if no authentication has taken place, an artifact will be sent. +- The artifact is sent to the recipient via an HTTP Redirect. + +### 7.5 SAML ArtifactResolve + +Sent via **SOAP binding** over the **back channel** protected with **two-sided TLS authentication**. + +Sender: DV or LC +Recipient: RD + +| Element/@Attribute | Card. | Description | +|-------------------|-------|-------------| +| @ID | 1 | Unique message identifier. MUST identify the message uniquely for at least 12 months. | +| @Version | 1 | MUST be '2.0'. | +| @IssueInstant | 1 | Time at which the message was created. | +| @Destination | 0..1 | MAY be included. If included, MUST contain URL of the receiver matching one of the `` elements in the receiver's metadata. | +| Issuer | 1 | MUST contain the EntityID of the sender. | +| Signature | 1 | MUST contain the Digital signature of the sender. MUST contain a `` with `` or ``. | +| Artifact | 1 | Contains the Artifact that was received as query parameter. | + +### 7.6 SAML ArtifactResponse + +The `` is the response to the `` request in a SOAP message. It in turn contains the `` to the Original AuthnRequest. + +Sender: RD +Recipient: DV or LC + +#### 7.6.1 `` + +| Element/@Attribute | Card. | Description | +|-------------------|-------|-------------| +| @ID | 1 | Unique message identifier (at least 12 months). | +| @InResponseTo | 1 | Unique @ID attribute of the ArtifactResolve request. | +| @Version | 1 | MUST be '2.0'. | +| @IssueInstant | 1 | Time of issuing the Response. | +| Issuer | 1 | MUST contain the entityID of the sender. | +| Signature | 1 | MUST contain the Digital signature of the sender. MUST contain a `` element with a `` element. | +| Status | 1 | MUST contain a `` element with the status of the artifact resolve. | +| -StatusCode @Value | 1 | Top-level list per SAML core section 3.2.2.2, following SAML-bindings section 3.6.6. | +| --StatusCode | 0..1 | Conditional. Should only be present if top-level StatusCode is not 'Success'. | +| -StatusMessage | 0..1 | Only present if top-level StatusCode is not 'Success'. MAY contain a message detailing the error. | +| Response | 0..1 | Conditional. If the artifact resolves to a response, this MUST contain the `` to the AuthnRequest. | + +**Special `` rules per SAML-bindings section 3.6.6:** Even if the ArtifactResponse's Status indicates "Success", it may still not contain a Response if the artifact requester is not authorized or the artifact is no longer valid. + +#### 7.6.2 `` + +| Element/@Attribute | Card. | Description | +|-------------------|-------|-------------| +| @ID | 1 | Unique message characteristic (at least 12 months). | +| @InResponseTo | 1 | Unique @ID attribute of the AuthnRequest. | +| @Version | 1 | MUST be '2.0'. | +| @IssueInstant | 1 | Time of issuing the Response. | +| @Destination | 1 | URL of the endpoint. MUST match a recipient's metadata AssertionConsumerService. | +| Issuer | 1 | MUST contain the EntityID of the sender. | +| Signature | 0..1 | SHOULD NOT be used as the `` is part of the `` which is already signed by the RD. If included, MUST contain a `` element with a `` or ``. | +| Status | 1 | MUST contain a `` element with the status of the authentication. | +| -StatusCode @Value | 1 | If not 'Success', additional info SHOULD be in the embedded StatusCode element. | +| --StatusCode | 0..1 | Conditional. Should only be present if top-level is not 'Success'. | +| --@Value | 1 | In the event of a cancellation or error, MUST be populated with "AuthnFailed". | +| -StatusMessage | 0..1 | Only present if top-level StatusCode is not 'Success'. MUST contain exact phrase 'Authentication cancelled' when authentication is cancelled. | +| Assertion | 0..1 | Conditional. MUST be present if status is "Success". MUST NOT be included otherwise. | +| EncryptedAssertion | 0 | **MUST NOT be included.** | + +#### 7.6.3 SAML Assertion + +Issuer = RD +Recipient = DV or LC + +| Element/@Attribute | Card. | Description | +|-------------------|-------|-------------| +| @ID | 1 | Unique message identifier (at least 12 months). | +| @Version | 1 | MUST be '2.0'. | +| @IssueInstant | 1 | Time of issuance of the assertion. | +| Issuer | 1 | MUST contain the EntityID of the issuer. | +| Signature | 1 | MUST contain the Digital signature of the sender. MUST contain a `` element with a `` element. | +| Subject | 1 | MUST be included. | +| -NameID | 1 | NameID MUST contain a TransientID. | +| -SubjectConfirmation | 1 | Contains the `` conform the WebSSO profile. | +| Conditions | 1 | NotBefore and NotOnOrAfter limit the window during which the assertion can be delivered. | +| -@NotBefore | 1 | MUST be included. | +| -@NotOnOrAfter | 1 | MUST be included. | +| -AudienceRestriction | 1 | MUST be included. | +| --Audience | 1..n | MUST contain the EntityID(s) of all parties intended to receive and process the assertion. MUST always contain the DV's EntityID. If an LC is involved, MUST also contain the LC's EntityID. | +| AuthnStatement | 1 | MUST be included. | +| -@AuthnInstant | 1 | MUST contain the time of creation of the enclosing Assertion. | +| -AuthnContext | 1 | MUST be included. | +| --AuthnContextClassRef | 1 | MUST contain the level of assurance at which authentication took place. | +| --AuthenticatingAuthority | 0..n | MUST contain the EntityID(s) of all authorities involved in the authentication and representation assertion issuance except for the assertion issuer. | +| AttributeStatement | 0..1 | Conditional. MUST be included if StatusCode is 'Success'. MUST NOT be included otherwise. | +| Advice | 1 | MUST be included. Contains the original assertions received from AD and BVD. | +| -Assertion | 1..n | Contains the original `` elements. MUST contain the original AD ``. MAY contain the original BVD `` in case of representation. | + +##### 7.6.3.1 Audience Restriction + +- An Assertion may only be processed if the `` contains the `` of the recipient. +- The values for DVs included in `` are reflected in the @Recipient attribute of the `` elements in the ``. +- The LC is not a `` and cannot decrypt EncryptedID's or attributes. + +##### 7.6.3.2 Level of Assurance Validation + +- The `` always states the authentication level at which the citizen authenticated. +- DV's MUST be prepared for a higher LoA than requested. +- DV's MUST accept authentications with a level equal to or higher than the minimum level registered for the Service. +- DV must configure minimum LoA for the Service when providing information in the onboarding process. +- The AD is responsible for providing the correct LoA for a given authentication-request. + +##### 7.6.3.3 SubjectConfirmation + +| Element/@Attribute | Card. | Description | +|-------------------|-------|-------------| +| SubjectConfirmation | 1 | Association of client with assertion to conform to SAML Web SSO profile. | +| -@Method | 1 | MUST contain "urn:oasis:names:tc:SAML:2.0:cm:bearer". | +| -SubjectConfirmationData | 1 | MUST be included. | +| --@NotOnOrAfter | 1 | Initially set to +2 minutes relative to creation time. The @NotBefore MUST NOT be used. | +| --@Recipient | 1 | The assertion consumer service URL of the immediate requester. | +| --@InResponseTo | 1 | The @ID of the `` this Assertion is in response to. A receiving DV or LC MUST verify this value corresponds to the initiating AuthnRequest @ID. | + +##### 7.6.3.4 AttributeStatement + +- When present, MUST contain an ``. +- `` contains one or more `` elements. +- MUST contain at least one `` with @Name="urn:nl-eid-gdi:1.0:ActingSubjectID". +- MAY contain an `` with @Name="urn:nl-eid-gdi:1.0:LegalSubjectID" indicating representation. + +**Attribute structure:** + +| Element/@Attribute | Card. | Description | +|-------------------|-------|-------------| +| AttributeStatement | 1 | MUST be included. | +| -Attribute | 1..n | See tables below. | +| --@Name | 1 | MUST contain the type of the attribute. | +| --AttributeValue | 1..n | MUST contain one or more AttributeValues — one for each recipient. | +| ---EncryptedID | 1 | MUST contain one encrypted `` element. | +| ----EncryptedData | 1 | MUST contain the encrypted data containing the XML encrypted NameID (BSN). | +| ----EncryptedKey | 1..n | MUST contain the wrapped decryption keys. This element MUST include the intended Recipient. | +| -----@Recipient | 1 | The recipient (DV, LC or RD) for which this EncryptedID is intended. MUST contain an EntityID. | + +**Unencrypted attributes (always present):** + +| Attribute | Card. | @Name | Description | +|-----------|-------|-------|-------------| +| ServiceUUID | 1 | urn:nl-eid-gdi:1.0:ServiceUUID | The ServiceUUID for which this Assertion is intended. | + +**Encrypted attributes — Authentication:** + +| Attribute | Card. | @Name | Description | +|-----------|-------|-------|-------------| +| ActingSubjectID | 1 | urn:nl-eid-gdi:1.0:ActingSubjectID | Contains the identity of the authenticated subject. | + +**Encrypted attributes — Representation:** + +| Attribute | Card. | @Name | Description | +|-----------|-------|-------|-------------| +| ActingSubjectID | 1 | urn:nl-eid-gdi:1.0:ActingSubjectID | The encrypted ActingSubjectID as received from the AD. | +| LegalSubjectID | 1..n | urn:nl-eid-gdi:1.0:LegalSubjectID | SAML eID 4.4 will only support 1 LegalSubjectID. The encrypted LegalSubjectID as received from BVD. | + +**Multiple recipients:** + +- Each EncryptedKey MUST have a CarriedKeyName equal to the KeyName used in the KeyInfo of the EncryptedData. +- Each EncryptedKey SHOULD have a ReferenceList referring back to the data encrypted with the symmetric key. +- Elements without an EncryptedKey intended for the decrypting recipient MAY be ignored. +- EncryptedKeys for other recipients of encrypted elements SHOULD be ignored. + +##### 7.6.3.4.4 EncryptedID + +- Identifiers (NameID) are contained in SAML `` elements in all cases. +- The specific type of identifier is communicated through a @NameQualifier attribute within the ``. +- All identifiers are XML encrypted so that only the intended recipient(s) can decrypt. +- The intended recipient is communicated through the @Recipient attribute within the EncryptedKey element. + +An `` MUST contain a SAML `` after decryption, with the following properties: +- The Format attribute MUST be set to 'urn:oasis:names:tc:SAML:2.0:nameid-format:persistent'. +- The @NameQualifier attribute MUST be populated with the full name of the type of identifying attribute. +- The attributes SPNameQualifier and SPProvidedID MUST NOT be used. +- If more than one certificate is listed for encryption for the recipient in the metadata, the content-encryption-key MUST be encrypted for each certificate. This results in multiple `` each with the same @Recipient. + +##### 7.6.3.5 Response Message Processing Rules for DV + +The service provider MUST do the following: +1. Verify any signatures present on the assertion(s) or the response. +2. Verify that the Recipient attribute in any bearer `` matches the assertion consumer service URL to which the `` or artifact was delivered. +3. Verify that the @NotOnOrAfter attribute in any bearer `` has not passed, subject to allowable clock skew. +4. Verify that the @InResponseTo attribute in the bearer `` equals the ID of its original `` message. +5. Verify that it's EntityID is included as `` in the ``. +6. Verify that any assertions relied upon are valid in other respects. +7. Any assertion which is not valid, or whose subject confirmation requirements cannot be met SHOULD be discarded and SHOULD NOT be used to establish a security context. + +### 7.7 Federated Login and Logout + +**SSO is defined at the AD level.** There is no SSO over AD's unless the AD's mutually agree. + +A DV who wants to grant access through SSO can do so via the SSO service from the AD if the AD offers SSO. Details on the SSO service are to be provided by the AD. + +Cases where the user is still asked to re-authenticate: +1. The LoA required by the service provider is higher than the level in the existing SSO session. +2. The existing SSO session applies to a different SSO federation. +3. The service provider includes `` element with value True. +4. The existing SSO session has expired. + +SP initiated logout is limited: sessions with other active DV's within the same federation will continue to be active until the local DV session times out or the user logs out. + +#### 7.7.1 SP Initiated `` + +Sender: DV or LC +Recipient: RD + +| Element/@Attribute | Card. | Description | +|-------------------|-------|-------------| +| @ID | 1 | Unique message attribute. | +| @Version | 1 | MUST be '2.0'. | +| @IssueInstant | 1 | Time at which the message was created. | +| @Destination | 1 | URL of the recipient. | +| Signature | 1 | MUST contain the Digital signature of the DV or LC. MUST contain a `` with `` or ``. When the sender is an RD, it MUST contain a `` with a ``. | +| NameID | 1 | MUST contain the TransientID `` element from the `` of the original Assertion. | +| Issuer | 1 | MUST contain the EntityID of the sender. | + +#### 7.7.2 IdP `` + +Sender: RD +Recipient: DV or LC + +| Element/@Attribute | Card. | Description | +|-------------------|-------|-------------| +| @ID | 1 | Unique message attribute. | +| @Version | 1 | MUST be '2.0'. | +| @IssueInstant | 1 | Time at which the message was created. | +| @Destination | 1 | URL of the recipient. | +| @InResponseTo | 1 | @ID of the LogoutRequest. | +| Signature | 1 | MUST contain the Digital signature. MUST contain a `` with `` or ``. When the sender is an RD it MUST contain a `` with a ``. | +| Issuer | 1 | MUST contain the EntityID of the sender. | +| Status | 1 | MUST contain a StatusCode element with the status of the logout. | + +### 7.8 Error Codes + +#### 7.8.1 Top-level Code + +Standard SAML 2.0 error codes: + +| Status Code | Description | +|------------|-------------| +| urn:oasis:names:tc:SAML:2.0:status:Requester | Errors caused by the initiator of the SAML request (e.g., unsupported assurance level, expired request). | +| urn:oasis:names:tc:SAML:2.0:status:Responder | Errors caused by the recipient (e.g., technical failure, unsupported functionality). | + +#### 7.8.2 Second-level Status Codes + +| Status Code | Description | +|------------|-------------| +| urn:oasis:names:tc:SAML:2.0:status:AuthnFailed | User cannot be authenticated (e.g., invalid credentials, cancel button used). | +| urn:oasis:names:tc:SAML:2.0:status:NoAuthnContext | User cannot be authenticated at the minimum level specified in the dienstencatalogus (DC). | +| urn:oasis:names:tc:SAML:2.0:status:RequestUnsupported | Message is correctly formatted and understood, but the requested functionality is not supported. | +| urn:oasis:names:tc:SAML:2.0:status:RequestDenied | SAML responder refuses to perform a message exchange (e.g., mandatory signature could not be verified). | +| urn:oasis:names:tc:SAML:2.0:status:NoSupportedIDP | None of the identity providers in an `` are supported by the intermediary. | + +#### 7.8.3 Cancelling + +- If a user cancels, the participant MUST direct the user to the latest sender of a SAML request, with valid SAML status codes: `urn:oasis:names:tc:SAML:2.0:status:Responder` with `urn:oasis:names:tc:SAML:2.0:status:AuthnFailed`. +- A `` element MUST be included, containing the exact phrase **"Authentication cancelled"**. +- If the RD receives a cancellation from an AD or BVD, it MUST ask the user to re-select or cancel (only if selection took place at the RD). Otherwise, the cancellation must be forwarded to the DV or LC. +- If the DV or LC receives a cancellation, it MUST indicate to the user that he is not logged in, and MAY offer re-authentication. + +#### 7.8.4 Attributes Not Supported + +- A participant receiving such a message MUST show the user a message indicating something went wrong (without revealing security sensitive details). +- MAY offer the user the option to cancel. + +#### 7.8.5 Incorrect Message (Recoverable) + +- The recipient MUST direct the user to the initiator of the SAML request, with status codes: `urn:oasis:names:tc:SAML:2.0:status:Responder` with `urn:oasis:names:tc:SAML:2.0:status:RequestUnsupported`. +- A `` element MUST be included, containing a description of the problem (e.g., "Level of assurance not supported"). +- If the participant is an RD and no interaction with the user took place, the message must be forwarded to the DV or LC. + +#### 7.8.6 Incorrect Message (Non-recoverable) + +Examples: not a valid SAML message, XML does not match XSD, unknown issuer, invalid signature, invalid ServiceID/attributes/EntityConcernedTypes, response not matching the request. + +- A participant MUST investigate the nature of the error. +- MUST show the user a message indicating a non-recoverable error. +- MUST return a SAML response with status codes: `urn:oasis:names:tc:SAML:2.0:status:Requester` and `urn:oasis:names:tc:SAML:2.0:status:RequestUnsupported`, or an HTTP error in case a synchronous response is expected (like SOAP). + +## 8 SAML Metadata + +### 8.1 TLS Certificates in Metadata + +- TLS certificates MUST be included in the LC metadata as a signing certificate (@use=signing). +- Difference with normal signing certificates can be made via extended key usage (per SAML Version 2.0 Errata 05, E62). + +### 8.2 General Processing Requirements + +- The metadata MUST be validated by the consuming parties. +- The consuming parties MUST NOT use the metadata if the validation is not successful. + +### 8.3 DV Metadata + +Published by: DV connecting directly to RD +Consumed by: RD + +| Element/@Attribute | Card. | Description | +|-------------------|-------|-------------| +| EntityDescriptor | 1 | MUST contain one `` with one ``. | +| -@ID | 1 | Document-unique identifier, used as reference point when signing. | +| -@entityID | 1 | The unique identifier of the SAML entity. Contains the EntityID of the DV. | +| -@validUntil | 0..1 | MAY contain a datetime at which the metadata expires. Either validUntil or cacheDuration MUST be present. | +| -@cacheDuration | 0..1 | MAY contain cacheDuration. Either validUntil or cacheDuration MUST be present. | +| -Signature | 1 | Digital signature of the DV. MUST contain a `` element with a `` or ``. | +| -SPSSODescriptor | 1 | | +| --@AuthnRequestsSigned | 1 | MUST be set to "true". | +| --@protocolSupportEnumeration | 1 | MUST be set to "urn:oasis:names:tc:SAML:2.0:protocol". | +| --@WantAssertionsSigned | 1 | MUST be set to "true". | +| --KeyDescriptor | 2..n | MUST contain KeyDescriptor element(s) for signing and TLS. Can be achieved with 2 KeyDescriptor elements with @use="signing" or a single certificate supporting both functions. A second `` MAY be present for both to support certificate rollover. MUST contain at least 1 KeyDescriptor with @use="encryption". A second @use="encryption" MAY be present for rollover. All certificates must be PKIoverheid certificates. | +| ---KeyInfo | 1 | | +| ----KeyName | 1 | Contains the name which identifies the key. | +| ----X509Data | 1 | Contains the encoded PKIoverheid X509 certificate with the public key. | +| --SingleLogoutService | 0..n | Conditional: MUST be present if the DV supports SSO. At least one MUST contain HTTP-POST binding. | +| ---@Binding | 1 | MUST contain the appropriate binding for the endpoint. | +| ---@Location | 1 | MUST contain the URL of the SingleLogoutService endpoint. | +| --AssertionConsumerService | 1..n | Must contain at least one URL to redirect to after authentication. If more than one, one must have @isDefault="true". | +| ---@Binding | 1 | The binding. | +| ---@Location | 1 | The URL. | +| ---@Index | 1 | MUST be present. | +| ---@isDefault | 0..1 | MUST be present if more than one ACS. | +| --AttributeConsumingService | 0..n | Conditional: MUST be used if the DV does not support Extensions in the AuthnRequest. | +| ---@Index | 1 | MUST be present. | +| ---@isDefault | 0..1 | MUST be present if more than one index. | +| ---ServiceName | 1..n | One or more language-qualified names for the service. Only one per language. | +| ---RequestedAttribute | 1..n | At least one with @name="urn:nl-eid-gdi:1.0:ServiceUUID". | +| ----AttributeValue | 1 | MUST contain the ServiceUUID to be used. Must be pre-registered with the RV service catalogue (DC). | + +### 8.4 LC SAML SP Metadata + +Published by: LC +Consumed by: RD + +#### 8.4.1 LC SP Metadata + +Uses `` (plural) wrapping the LC's own EntityDescriptor plus one EntityDescriptor per DV. + +| Element/@Attribute | Card. | Description | +|-------------------|-------|-------------| +| EntitiesDescriptor | 1 | Required element containing multiple EntityDescriptors. | +| -@ID | 1 | Document-unique identifier for signing. | +| -@validUntil | 0..1 | Either validUntil or cacheDuration MUST be present. | +| -@cacheDuration | 0..1 | Either validUntil or cacheDuration MUST be present. | +| -Signature | 1 | Digital signature of the LC. MUST contain a `` with `` or ``. | +| -EntityDescriptor | 1..n | MUST contain the LC's EntityDescriptor and EntityDescriptors of all DV's the LC supports. | + +#### 8.4.2 LC EntityDescriptor within LC Metadata + +Similar to DV metadata but with LC-specific differences: +- SPSSODescriptor with AuthnRequestsSigned="true", WantAssertionsSigned="true". +- 1..4 KeyDescriptor elements for signing and TLS, with optional rollover keys. +- TLS certificates for client authentication MUST be included as a signing certificate in the LC SAML metadata. +- All certificates must be PKIoverheid certificates containing the OIN of the EntityDescriptor's entityID. +- AssertionConsumerService with HTTP-Artifact binding. + +#### 8.4.3 DV EntityDescriptor within LC Metadata + +| Element/@Attribute | Card. | Description | +|-------------------|-------|-------------| +| EntityDescriptor | 1 | Per DV supported by the LC. | +| -@entityID | 1 | MUST contain the EntityID of the DV. | +| -SPSSODescriptor | 1 | protocolSupportEnumeration = SAML 2.0 protocol. | +| --KeyDescriptor | 1..2 | MUST contain at least 1 with @use="encryption". A second MAY be present for rollover. All certs must be PKIoverheid certs containing the OIN referred to in the entityID. | +| --AssertionConsumerService | 1..n | MUST contain only one entry. MUST contain a copy of the LC's ACS. Will be ignored as the LC's ACS definitions are used. | + +### 8.5 RD SAML IdP Metadata + +Published by: RD +Consumed by: DV and LC + +| Element/@Attribute | Card. | Description | +|-------------------|-------|-------------| +| EntityDescriptor | 1 | | +| -@ID | 1 | Document-unique identifier for signing. | +| -@entityID | 1 | Contains the EntityID of the RD. | +| -@validUntil | 0..1 | Either validUntil or cacheDuration MUST be present. | +| -@cacheDuration | 0..1 | Either validUntil or cacheDuration MUST be present. | +| -Signature | 1 | Digital signature of RD. MUST contain a `` with a `` element. | +| -IDPSSODescriptor | 1 | | +| --@protocolSupportEnumeration | 1 | Set to "urn:oasis:names:tc:SAML:2.0:protocol". | +| --@WantAuthnRequestsSigned | 1 | Set to "true". | +| --KeyDescriptor | 1..n | At least 1 with @use="signing". | +| ---KeyInfo | 1 | | +| ----KeyName | 1 | Contains the name identifying the key. | +| ----X509Data | 1 | Contains the encoded X509 certificate. | +| --ArtifactResolutionService | 1..n | MUST be implemented at least once per service. | +| ---@Binding | 1 | SAML-SOAP binding only. | +| ---@Location | 1 | URL of the SAML artifact resolution endpoint. | +| ---@Index | 1 | MUST be unique for all ArtifactResolutionService elements. | +| --SingleSignOnService | 1..n | Endpoints supporting the Authentication Request protocol. | +| ---@Binding | 1 | HTTP-POST binding only. | +| ---@Location | 1 | URL of the SingleSignOnService endpoint. | +| --SingleLogoutService | 1..n | Endpoint for logout. | +| ---@Binding | 1 | MUST be set to "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST". Other bindings are NOT supported. | +| ---@Location | 1 | The URL of the SAML endpoint. | + +## 9 Technical Requirements and Recommendations + +### 9.1 Signing, Encryption Algorithms and Hash Functions + +eID SAML 4.x **no longer supports SHA1** except for the padding function (xmldsig # rsa-sha1). Only RSA is supported. + +**Signing algorithms:** + +| Algorithm | Namespace | +|-----------|-----------| +| RSAwithSHA256 | http://www.w3.org/2001/04/xmldsig-more#rsa-sha256 | +| RSAwithSHA384 | http://www.w3.org/2001/04/xmldsig-more#rsa-sha384 | +| RSAwithSHA512 | http://www.w3.org/2001/04/xmldsig-more#rsa-sha512 | + +**Digest algorithms (minimum SHA256):** + +| Algorithm | Namespace | +|-----------|-----------| +| SHA256 | http://www.w3.org/2001/04/xmlenc#sha256 | +| SHA384 | http://www.w3.org/2001/04/xmldsig-more#sha384 | +| SHA512 | http://www.w3.org/2001/04/xmldsig-more#sha512 | + +**Signature requirements:** +- The digital signature is embedded in the message content with **Enveloped Signature Transform**. +- Canonicalization MUST be carried out according to the **exclusive c14n** method without comments (`http://www.w3.org/2001/10/xml-exc-c14n#`). +- Digests MUST be calculated with at minimum the SHA256 algorithm. +- The SignatureValue MUST be calculated with at minimum the RSAwithSHA256 algorithm. +- Participants MUST sign messages and metadata with a **PKIoverheid certificate** with a key length of at least **2048 bits**, containing the OIN (organisatie-identificatienummer) of the participant. The extended key usage MUST allow use for signing. +- The Reference MUST refer to the signed element via an ID attribute in the local document. + +### 9.2 Signature + +- Each `` in SAML messages generated by a DV or LC and in the DV or LC SAML SP metadata MUST contain either a `` element with a `` element OR a `` element. The use of `` is preferred as this limits data amount. +- Each `` in SAML messages generated by a RD and in the RD SAML IdP metadata **MUST** contain a `` element with a `` element containing a keyname that corresponds to a `` in a `` in the RD's metadata. +- Certificates used to verify a `` MUST be retrieved from the party's **verified metadata**. The `` or `` in the `` of the Signature MUST only be used to retrieve the corresponding certificate from the verified metadata. + +### 9.3 Encryption + +- Encryption is achieved via XML-encryption. +- Block encryption algorithm: **AES-256** (`http://www.w3.org/2001/04/xmlenc#aes256-cbc`). +- Asymmetric encryption for key wrapping: RSA algorithm with **OAEP padding** and a **SHA digest** (`http://www.w3.org/TR/xmlenc-core1/#sec-RSA-OAEP`). +- The SHA1 version SHOULD NOT be used (`http://www.w3.org/2009/xmlenc11#mgf1sha1`). + +### 9.4 TLS Transport + +- The RD requires that a service provider always protects http traffic with **TLS v1.2 or higher** in accordance with the NCSC directive with 'good' assessment. +- The certificate must be issued under PKIoverheid with a key length of at least **2048 bits**. +- When connecting directly between the RD and the LC or DV (back channel), both parties must use a PKIoverheid certificate and **mutual authentication** is mandatory (**mutual TLS**). + +### 9.5 NotBefore and NotOnOrAfter + +- LCs and DVs must respect the NotBefore and NotOnOrAfter parameters and reject messages that do not comply. +- With a re-authentication, the entire protocol handling must take place. +- It is advisable to use NTP servers (e.g., from nl.pool.ntp.org) to avoid clock skew vulnerabilities. + +### 9.6 Levels of Assurance + +See section 10.3. + +### 9.7 Local Session + +- The DV is responsible for keeping track of the local End User session. +- This session MUST be terminated after at most **30 minutes** inactivity. +- The DV must recognize and ward off **replay attacks**. +- If the DV uses cookies to manage sessions, the **"Secure"** and **"HttpOnly"** parameters must be used. + +### 9.8 RelayState + +- DVs may provide a RelayState for their own session monitoring. +- The RD returns the RelayState without any verification. +- The monitoring of the content and integrity of the RelayState must be done by the service provider. +- The SAML standard uses a maximum of **80 characters** for the RelayState. + +### 9.9 User Interaction + +When a web service forwards an end user to an RD, AD, or BVD: +1. The end user must be redirected to the AD in the same screen where the user clicked "Log in to ". +2. The end user must see a browser window with the full address bar (allowing URL/certificate inspection). +3. It is not allowed to invoke an RD, AD, or BVD website in a **frame or iframe**, or to embed it in any other way. + +If the status in the Assertion is not successful or the user does not have the required LoA: +1. The DV or LC MUST immediately end the current session. +2. Should show an appropriate error message. + +## 10 Type Definitions + +### 10.1 Attribute Identifier Types + +| Attribute | Identification-code | Remarks | +|-----------|-------------------|---------| +| BSN | urn:nl-eid-gdi:1.0:id:legacy-BSN | BSN. Encoded in 9-digits, padded with leading 0 if needed. Example: 123456789 or 012345678. | +| BSN | urn:nl-eid-gdi:1.0:id:BSN | Encrypted Identity | +| Pseudonym | urn:nl-eid-gdi:1.0:id:Pseudonym | Encrypted Pseudonym | + +### 10.2 EntityID + +Format: **`urn:nl-eid-gdi:1.0:::entities:`** + +| Attribute | Value | Remarks | +|-----------|-------|---------| +| `` | The OIN of the organisation. | | +| `` | Indication of the role: AD, DV, BVD, LC, RD | | +| `` | A number with 4 positions between 0000 and 8999 that can be selected by the participant or service provider to define different endpoints (in the metadata). Numbers between 9000 and 9999 are reserved for test systems. | | + +### 10.3 Levels of Assurance + +| DigiD 3.3 | DigiD | eTD | eIDAS | eID | +|-----------|-------|-----|-------|-----| +| - | - | 1 | - | - | +| urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport | Basis | 2 | Low | http://eID.logius.nl/LoA/basic | +| urn:oasis:names:tc:SAML:2.0:ac:classes:MobileTwoFactorContract | Midden | 2+ | Low | http://eidas.europa.eu/LoA/low | +| urn:oasis:names:tc:SAML:2.0:ac:classes:Smartcard | Substantieel | 3 | Substantial | http://eidas.europa.eu/LoA/substantial | +| urn:oasis:names:tc:SAML:2.0:ac:classes:SmartcardPKI | Hoog | 4 | High | http://eidas.europa.eu/LoA/high | diff --git a/auth-service/fixtures/ca.pem b/auth-service/fixtures/ca.pem new file mode 100644 index 000000000..7d9bca62d --- /dev/null +++ b/auth-service/fixtures/ca.pem @@ -0,0 +1,19 @@ +-----BEGIN CERTIFICATE----- +MIIDGDCCAgCgAwIBAgIUJY1SeS9WHVLTbHoquNOU2Lkz5xYwDQYJKoZIhvcNAQEL +BQAwKzEXMBUGA1UEAwwORGV2ZWxvcG1lbnQgQ0ExEDAOBgNVBAoMB2Vrcy10dnMw +IBcNMjUwMTAxMDAwMDAwWhgPMjEyNTAxMDEwMDAwMDBaMCsxFzAVBgNVBAMMDkRl +dmVsb3BtZW50IENBMRAwDgYDVQQKDAdla3MtdHZzMIIBIjANBgkqhkiG9w0BAQEF +AAOCAQ8AMIIBCgKCAQEAoxv8ZUiucRw/PO+nij/OXo/8gYr0QyAtc599Mp6LCUJ9 +nGvc9LSjkX3aP+J7yd0w9faM31LwkJis2fJAFPOSnYF12sIxJkfpT3eyOx0SwETV +BObO7Atm0BRCjcRQnlnDFobG+N/EEj+P8QEfdBtAwJA+W7CskxPQu7d13wnG/+Gn +yhXLRDSbv2qa7muPjg0qbTDgEHwfjrilFoFnVJU0SpxHZ1v/3sy7YMokYCStjHeX +iIxVfcKtdmkmTPR0z88yIdUSFeMGa1sRLci9CSZXKAaRCvd7wxXE+gntvVcvq5SM +gjsSdjJQ2E21HaRvkDEPPylcyI1Q8WlU+W5Xmmr8tQIDAQABozIwMDAdBgNVHQ4E +FgQUImmk0Jeaa6VCK9RpsAQuETbnVzEwDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG +9w0BAQsFAAOCAQEAMcRIXw0qYAXBPQIIHdlI43oROagmFslKhEwyDgca8fcc2DOS +9+p5dvVX5mlvC2dGIcUEQAM8X73itrYhubcwnBcllz66dvfqO0J6ge1poKGKqHGe +xC+4Rzzd3or6saoCjnHsQQPvAgMc8onweLZ5Lw8zpJmNsGlDnjJVn1tUQYSXmPid +SlG64bbA7VnBH9GJBwagptFaVEGC6aDABJQ6EvfQBExTi8DDQJBdxAa2kJvN3iEB +1VxD8ayeJKLLupP1tqWrS0Gb9KQKpcbnMGB+O5ochVIJDgxx+g2Db9XhpnBAm5TA +DvuCBnFtzt3UmQk1qD/4PXzoRS3sk6Kv/7uMsA== +-----END CERTIFICATE----- diff --git a/auth-service/fixtures/dv-encryption-1-key.pem b/auth-service/fixtures/dv-encryption-1-key.pem new file mode 100644 index 000000000..8d14bc413 --- /dev/null +++ b/auth-service/fixtures/dv-encryption-1-key.pem @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQC1dnIDTE/kSaz7 +svCdMqivPHm7K9Zsp33LIJJ4ZitC9RktvccfxQJm592ZoAOLd49bir8FE058NKqD +EHrAUsX6ixPfBU30CxsIasdyPzoqkBrf8srYWoySxogBVvxkUsLUn/WbesayQbqe +bNkw5kano+xJIDTS4RPYu3cGrFHehTyOghvSHMdVZ7AFEeRWYHyLE8MH/2QsgQht +cIN7zzSroyP/iG6UyoS9o0PU0SwZdREMWEJst74eP3caMfijw5q4ZCWBBYDZq4H8 +q8iP+4c1F4tqZ9tO+QilGj+fE1mCZDYDfX4sgECqNYz6sutA8Wf2hb3V7eVZuyNz +ewsOZKUvAgMBAAECggEAGaxARScuxmWGC7YtRJA+PUyZDFoPZB/uNCbPkikiRb7X +aRlIar48sMJtH6b72Wd46MKgc99fY0MwPIrAUyVizSeQECV3+eb4VCCHRyqa2Bq7 +MqPu/t0QtlNQOqBL5QWwetSGsxCTu/SAqplLwRdTN7SBYXclAhDF9gvPsyn+CzUh +d1+FRNoS/pZT908NKXDMMxqHyzR0jEOUppthhzSU6lAGEJsUM1e2RyP2TFI1KHZN +2TMUw1ux4RPbgPwKebYmHJ2swTZ6rUvgbqOge01UvkZlZ2DsZ2PjHgEU5QqsJsFi +zUO04FbGM7UnI/m8rdvsJG9HgmXLwvSI1I4y2SXTQQKBgQDWs+Qir1N5wBgRey1b +mGvSHb3zxl5cz11qs2feEd5khkaalBD264AiMAQc64ZSOCRvpc+uYi3SNiwBUNWB +kSsHugMfTuF1BMK78o3257ohKE4bfmaPgDZOYGFUxdOaGxBSfermyZKrr1N0W2Wl +55N2ypN1WoKVNvGmlGs8LY3AHQKBgQDYXcrnkM2BB7C8bDYi5xiDccmxDu4fa34m +1TN+PyRinrWKgb51RnL7bxQRemWJZ/TKf3egdBtWPS1lv+A8KOM8jaAodoY6/uqa +dOkNi5n4ZNTSRmQlXal6YcM3/NKZl/1CwiKLyfpu64zCAZG5w5f6cFMj/ZeKNOQc +GIRzAp2QuwKBgQCI0IVV3OeSs+gia53RumUqta0vxH6pIbLjpYNQrY5L/IzNQAE3 +d4ZlQre8PhGQHxqI0t5uvz13sPGFvR7wu7Lx74TB1KAy01/MKJpydePT8T0ko/1z +AD6a3yvmu1FF6G0URA0GF/kQ0l6ZzQwPFmyPzDL/GzgPIb4HapiS9mxXuQKBgElv +E+WiFM19MbPRulIAQyahoQWx/om1bvx5EeAASDYcjZrzzBN7lJzXp2UDOKD5InDz +8EMKBRkw7E6DX/SrH+y2WKxDBe3SBQflNA//2IMgnca8U1MHRfUeZyaCbWvp1PAq +eZUeORwaYAycgbwdsWAzbVRixmMG7oDGpXVvZtDvAoGBAKS3lH/puyL1V1gg2q5A +lhBnCMtyKi0Fs4+qWZ060iFS9Pgds2OIdlJP6uJozaGgKEVxRv8XIUpGhOe8MI4l +bM5AFy02Xy+S2OZVE4WvPxI32cQjF/nTA7Vjuc0ZEo39dlUXcsGh+D0EZFEQGPOC +CTRqILQehYUpnimpQQov+gJS +-----END PRIVATE KEY----- diff --git a/auth-service/fixtures/dv-encryption-1.pem b/auth-service/fixtures/dv-encryption-1.pem new file mode 100644 index 000000000..ec0ad1c2f --- /dev/null +++ b/auth-service/fixtures/dv-encryption-1.pem @@ -0,0 +1,19 @@ +-----BEGIN CERTIFICATE----- +MIIDBDCCAeygAwIBAgIUWMeH192Z1hvp7o8kMBejhafr4G0wDQYJKoZIhvcNAQEL +BQAwKzEXMBUGA1UEAwwORGV2ZWxvcG1lbnQgQ0ExEDAOBgNVBAoMB2Vrcy10dnMw +IBcNMjUwMTAxMDAwMDAwWhgPMjEyNTAxMDEwMDAwMDBaMEsxGDAWBgNVBAMMD2R2 +LWVuY3J5cHRpb24tMTEQMA4GA1UECgwHZWtzLXR2czEdMBsGA1UEBRMUMDAwMDAw +MDAwMDAwMDAwMDAwMDEwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC1 +dnIDTE/kSaz7svCdMqivPHm7K9Zsp33LIJJ4ZitC9RktvccfxQJm592ZoAOLd49b +ir8FE058NKqDEHrAUsX6ixPfBU30CxsIasdyPzoqkBrf8srYWoySxogBVvxkUsLU +n/WbesayQbqebNkw5kano+xJIDTS4RPYu3cGrFHehTyOghvSHMdVZ7AFEeRWYHyL +E8MH/2QsgQhtcIN7zzSroyP/iG6UyoS9o0PU0SwZdREMWEJst74eP3caMfijw5q4 +ZCWBBYDZq4H8q8iP+4c1F4tqZ9tO+QilGj+fE1mCZDYDfX4sgECqNYz6sutA8Wf2 +hb3V7eVZuyNzewsOZKUvAgMBAAEwDQYJKoZIhvcNAQELBQADggEBAHYE7N7nZ+n8 +3ZKWm1N4Ltx/t2gC83pxq6/YJPu2Fa03TSEn9uPWFkY0NvTy9o3rppaicYPinZee +sU3Th65mhjm0kfg4gJD5cZ3E6UcHqLkN7LuiPJoeaw6sgt3lRem1guJsY+NfF7+J +3FB1bi2vw52uDBP9Js8RNN/hvxdmRdZzAD1wCAbb7ThbyiUp+C0OQEEMh8U7MlDA +Mff4A54WoYMh8YdPPXgU8tFG9kDqDWRQegW2eVSA6Z89pnoOb9GMv1JKKLkX0fl6 +X6//XXCYXNFSSXfThdr8GIdz6kVRicOmrbcvKCZQpfeIFAN3wmNiAHUrB668feMR +6aTBTrd5sGw= +-----END CERTIFICATE----- diff --git a/auth-service/fixtures/dv-encryption-2-key.pem b/auth-service/fixtures/dv-encryption-2-key.pem new file mode 100644 index 000000000..b354c2778 --- /dev/null +++ b/auth-service/fixtures/dv-encryption-2-key.pem @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQC6SMe1IccZbeyS +bncsDf5D1Pxzs5saIemp9XjlF7KtqkCt0D1g99B3/DBloavEJ3N1UzuHkgrqAz1z +arfWK6Qn3GF4Ecpu8RWwx4kOndi36G2yyxiNgTXosD8AEDP3bWRzZv6mBGkPlGsq +ziplEhgcUiEh/5ajg3gSZ7IJUZBL2/oyboWozGrRJ9p+23Mwe1ms/OuIF79d3UPt +E+i3JVel7JIpAW9woDOC1wqGTS5hvJzBA4hQBco6n1UcwBDn6cR91ivH9d9e0JAf +HSJXEN4qYPm/qjAoPvHqC8EAYcMVHHNVZMlgHvk1kEKrbCEXwL8GkBn21SaslK6C +Qca5vqH3AgMBAAECggEBAImGd6F6mWFlrNSCJwroHcH4OJQ2kZgeV0I5ThJbh3b8 +jRmE9ftjmI0sKpKtY4DB21RPqVAx+kq/L5vIuoWr3FntMiwRflCfPXMwLDgVQkyr +lGj91VeKf8EUdL5YN/U/1nv4GqF3+hQdb2y/nb7WN+SYoYtXBOAcjAWSdna4t324 +Wnzlg8BUJEdqR6tbR0EE4xfeMdmSp8An3Uq4VIyAfERy8Plg+yufmvjINGNfYpNR +5GolpPXKiS+jIlZrwVMwNklTmpjq6a0tDYTSZvnum38EsQjY5FDC1w33LlV+60gs +PG7ddo3CS5NSDxCOaSc2i/INNPZkhK85hrEaHEmEmfECgYEAyEhc626vtG1Dt0Yr +Jemo4IqmPRlVQ5Rhx/E7HcGm6Cpt/5dpfWikbQFpamEARhqlU7iAaasKG+AJ3gJr +nqCuptwBwq6/DodXTdtzATfFsxhx0QmOVSd5oZmREF1T9PYzLvKBf+9R2Yls6+2/ +CwOPqIwBwqYRnXqIQA3OMBnrvDkCgYEA7ht83aRU9Cvt65/UIqZc8kPRuk1GXOSq +M9YaHutUWIu3Q7wWMvib1BdTFvhy5zHQGvabU3PpJCC6pr25awebk1c5wGzy183o +pMpzaj+8U7lCahiacc02m6ku/Ql7t/oZIfTat+WSWSbeDLWtwSSBfvxUk/F1LJIT +bUAfmbder68CgYA1K/j3TCh7lzKOGqPj6de63TRrLimTynX3NhUCvmHIa1NOsuz/ +rWNRyTkqU9xVwEc6JdZ5tz30zpLwDOZzc6dKh0g0Xl48xjOoaOVsYlkrRgcZfxb5 +hoRlIbcAiDZ850wJX7p/s5KgVTrEnBbHHsfp8FmbSqmMjHZVwxSFcqxCCQKBgCNv +McjmQMJJm1Gaan5xlZl0IE0CXPtMCExIi4CYcufueup4njEdsEsC4Zmkha0ES70H +gBwOPt7ihAR2bZaQ/kUSEQUi8YzOFtYXTs+8BnHLOygA21MeQAcwSU7cz22UqZLV +FouRfZMI5rirfN0maqMUOB4AK/jjEEMM7Ob6U/v/AoGAKqILFJTYsNCG/E37hj5W +xmenrZpauOiuhixYscndWLCQxzMO1BmUojeYR/wVjfXCP5lhPAwio6LrbguJshZd +/XRW9ZUavZ1PqE/fjLiKeWobM/jP7ef4ZFSXmhPev/9Uho7gC4nKqWV7GvFn771k +25Zt2opgDelcgZVDaFmSqwA= +-----END PRIVATE KEY----- diff --git a/auth-service/fixtures/dv-encryption-2.pem b/auth-service/fixtures/dv-encryption-2.pem new file mode 100644 index 000000000..4fb96c01d --- /dev/null +++ b/auth-service/fixtures/dv-encryption-2.pem @@ -0,0 +1,19 @@ +-----BEGIN CERTIFICATE----- +MIIDBDCCAeygAwIBAgIUWAevXgatrZhqBVLNr6jS56iKp9kwDQYJKoZIhvcNAQEL +BQAwKzEXMBUGA1UEAwwORGV2ZWxvcG1lbnQgQ0ExEDAOBgNVBAoMB2Vrcy10dnMw +IBcNMjUwMTAxMDAwMDAwWhgPMjEyNTAxMDEwMDAwMDBaMEsxGDAWBgNVBAMMD2R2 +LWVuY3J5cHRpb24tMjEQMA4GA1UECgwHZWtzLXR2czEdMBsGA1UEBRMUMDAwMDAw +MDAwMDAwMDAwMDAwMDEwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC6 +SMe1IccZbeySbncsDf5D1Pxzs5saIemp9XjlF7KtqkCt0D1g99B3/DBloavEJ3N1 +UzuHkgrqAz1zarfWK6Qn3GF4Ecpu8RWwx4kOndi36G2yyxiNgTXosD8AEDP3bWRz +Zv6mBGkPlGsqziplEhgcUiEh/5ajg3gSZ7IJUZBL2/oyboWozGrRJ9p+23Mwe1ms +/OuIF79d3UPtE+i3JVel7JIpAW9woDOC1wqGTS5hvJzBA4hQBco6n1UcwBDn6cR9 +1ivH9d9e0JAfHSJXEN4qYPm/qjAoPvHqC8EAYcMVHHNVZMlgHvk1kEKrbCEXwL8G +kBn21SaslK6CQca5vqH3AgMBAAEwDQYJKoZIhvcNAQELBQADggEBAKGkssHxKFpZ +9k2pd8p6uE/mjbey+HCdVK5t9Gy7am7odfqciJIUBocI/lvjAJ+CstMZydlP24We +ZkU39qKQF4PiXQ/f9zw11L8ui8w+akETsTWqT4HOSnJ9HvKFjVwScfjZYIEf7RH+ +bIKHMpwv1xud9yeMJDNyN0gEzTNeI7tup43a2GaA2+BKTspTCn8FsJyg0Q43vNSP +an8pG7zqwNayQ/WztGN7XfNGic0gRsqwFi02TUHmhePMPRA1h0DwuY5+1dcqgZME +H+XUlZvaBZHFVTsSUJr7DrjMfMBLSKKcNzjRIO7baImda1xx0Tq2RfrY8XYtGgFU +YDiavPbmfDM= +-----END CERTIFICATE----- diff --git a/auth-service/fixtures/dv-signing-1-key.pem b/auth-service/fixtures/dv-signing-1-key.pem new file mode 100644 index 000000000..c858815f0 --- /dev/null +++ b/auth-service/fixtures/dv-signing-1-key.pem @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQC96v+wKrm1qJUW +Cg++CxZ7JJvvZweP1lTiSFwr4hhKAQ8srKUusxg3Kues9EDNjTk23jL7PZQBCjqH +l4EZti8Y09lZ8o6hO8lRMjpGvNjTBPlLOoksvL5hyq67zneTYerx6gNZyq2nvUYX +kP85HJ8mtE1EDD0kU5BHKxAkEnRagrycW/hkAzkCFKR/WzaoNXw/SXF7cEY1Hg+A +c0dOIbh/vq8yxZH1gUmbzidY1zF5eJZCatJcFnDEfEHnZaHlYGY1wwoUQx/fVYCg +bltIkVD1da7gG1wHZX0AdKWskOZBeEWTOxn2jXWTaIZBCEm0r9FoCAQPV5LPFGA+ +jq0QXnSjAgMBAAECggEAGumdOBKl5GcyUUA1wOGDWwnyCvfXq3tc5gAJB7LvbDVA +OD1y3u5EJTUwRWMLd9VmYS8VnGK6gXc4mhRPGByoCeWjbEqiCZkWSypJ9dI3C5gt +14pA/BT3CZaWMkjsyvbaO1PFC8lb2GRK9XX3pNk4eFyegmEVnPs2e89PZz0yPcz/ +5ZicLWeVrxq3n1Wmm+ho9tJ+Z7L6u1y/esaGlh/um/UZGzPM0StC6TBMuaEPzqRV +Rmy4aJg5YExGsdXVkob4kMEBCnr6Fm7YtLSHT5tFc0WEGIiL6PSxcb85gcIvOUk6 +RwOXRwtVtvmYfcO7oPE3BbKYfRaSSlKGKU9EhuK8GQKBgQD9A3x6+LU5oIJ164Zx +JYZ+INWXPamtEIq773DtaGRlA08gJbzjwZHSNsSZn7L35ARkQ5iZdQyuuoJJnWWS ++9NckLQBPUe5Tr8TRSKrAqrfoQaG5Z3Xezd+0FCfwS96qubHd8g9xeZmAjIsqFSp +yAdcAzsKhemn3nxZyT6ryJuTDQKBgQDAKNxajGrUHKyKwTlN3HauFxR4S8vExP4z +r7eHVbk56I4upXklluuwDof/oB40Ik8tXv89vs2AMshoT9RbF9iJawSQuVI3Tm6z +mwxJpK57vA2zyfUco8JPtuO7y+jLZH9Bnz4XhVgev0sJ7Cnelu8VBwifBU6Cop3M +9h0mvMv6bwKBgQDmg4+dBg0ulKNnxg619WdkcIvJAGAfkiuj8BHl6q9VLL+hXWCL +7ObrLATQpiRLH040/zT/0xmb88cSRLmvQYofEqM9YdR/61PAvcNJJOQDXNTygC7f +jJ5SBn7+HechDqW149bnGO/BzoxiDXcdPGYgGb1n5/uqetS2hNh5cAr13QKBgEps +CNuw4ncAG/spVq5HIlfObIkioeq1eNY/yc7KbpB09wwt3jumteVb22QveUMrvoqN +7kDVsvVhtahgspgNJjoijoQWojb1U36QIDQtab5WWOA26Y2MBuekp5LGKIUNiRO7 +PusRI83UvJU9OAITZsk4Dii4f22s2XzWHPgxyrZVAoGAYw/FHQIx9amTWu+rzt23 +dIFMZKzHWUKjmmBVZXqmFPCr8QS7dAD3CJ52hF1ExlT0wY1yjIF60O7DWfMWwf4y +I/8Qd6Qziu67QNsCkL/rJCNYfvdIIvcgF4bPd3PCxmpC5oCWVEUIajvQGQ7VUHae +TY4VdDB+CVB2AHfGeWYWXpg= +-----END PRIVATE KEY----- diff --git a/auth-service/fixtures/dv-signing-1.pem b/auth-service/fixtures/dv-signing-1.pem new file mode 100644 index 000000000..2522d778e --- /dev/null +++ b/auth-service/fixtures/dv-signing-1.pem @@ -0,0 +1,19 @@ +-----BEGIN CERTIFICATE----- +MIIDATCCAemgAwIBAgIUZETG6Bx62Kt/eFMGpzOi4JOEUg8wDQYJKoZIhvcNAQEL +BQAwKzEXMBUGA1UEAwwORGV2ZWxvcG1lbnQgQ0ExEDAOBgNVBAoMB2Vrcy10dnMw +IBcNMjUwMTAxMDAwMDAwWhgPMjEyNTAxMDEwMDAwMDBaMEgxFTATBgNVBAMMDGR2 +LXNpZ25pbmctMTEQMA4GA1UECgwHZWtzLXR2czEdMBsGA1UEBRMUMDAwMDAwMDAw +MDAwMDAwMDAwMDEwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC96v+w +Krm1qJUWCg++CxZ7JJvvZweP1lTiSFwr4hhKAQ8srKUusxg3Kues9EDNjTk23jL7 +PZQBCjqHl4EZti8Y09lZ8o6hO8lRMjpGvNjTBPlLOoksvL5hyq67zneTYerx6gNZ +yq2nvUYXkP85HJ8mtE1EDD0kU5BHKxAkEnRagrycW/hkAzkCFKR/WzaoNXw/SXF7 +cEY1Hg+Ac0dOIbh/vq8yxZH1gUmbzidY1zF5eJZCatJcFnDEfEHnZaHlYGY1wwoU +Qx/fVYCgbltIkVD1da7gG1wHZX0AdKWskOZBeEWTOxn2jXWTaIZBCEm0r9FoCAQP +V5LPFGA+jq0QXnSjAgMBAAEwDQYJKoZIhvcNAQELBQADggEBAI3IFf4PO6qEn1r+ +y92YwLyWrbQG8HWCZos8dXHCcOlhVsoL+onFDtBiDVuh/YrDI1Dg1TCfPXRPSLNU +taJHKy3LuAnrXRzaAvmMxkAjBWSB9vuz2vdd7mPKqWoDexYnWYnbEJ0B5+WH+s+m +LzgskrPGulUWbRHXns1lDQ/zoJ/cKSw+RSBXK0EGJMPVM605bgKlIk7o3CHvPh6a +VN3EXmPViMWGZ6p7Bld37/ISKr8ZNor+hE3ttJj/zZioIAwVI7WTeTNGR0WE+D3j +VH1WNLDZlspWKgnCLuNrU6HHt5sdZrkoM9vm+j41+q5QNSrhwtkUihJ+jy6cp7kx +lrWee+8= +-----END CERTIFICATE----- diff --git a/auth-service/fixtures/dv-signing-2-key.pem b/auth-service/fixtures/dv-signing-2-key.pem new file mode 100644 index 000000000..af6294c8b --- /dev/null +++ b/auth-service/fixtures/dv-signing-2-key.pem @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCvw9SpD+D17o5f +Z5ETVTyeZCAlkz/QRjR2Pfby7+IZFbpV3Mf6QF9jM58fLbQK8fIxERABusqC7wYF +83fm9Te5L9tmykkS9Dw8TF+64vwpLUXSObXPfXTDQb8CY5HVRnSoibM84+o8qlnK +NKnkRMVMElO9M8WHSkEjt+KPVsfk1nErUZgyIxGerjKnI03QcsnasuJ78wgBPl1i +wISUAIzpwUw7RZtLpI3sPASN6V87eIa6z6/k93iFB7ztXy26Cv6wKi4g+J34sITe +UNthgdAcq2tT09Ro3q2fzOnHjFyZWAclqRjy7WJOghfEwvZl7NORh7dqwkNo2M/m +Gp9V2b2PAgMBAAECggEASHUZkQxrt9a9OJVQWED2OXGbUwIt92VJsUkAKcw8UA0G +AOmAQykQaavZOBg2NaDiL9oR2DSx0WJKAGALGNoUO7J/5xMTJs4qmsz9VeTHAQ6Z +zv1ddwkH/op2/LwhXWOrKeOCUWK/qHytgU9OjjzPfPm9La1PC+PPkLL4ohCrXSlf +EorY56GDEptVeirIH9A9kRzvz7BDvvwe3hkm6l65bcYq2yVWynbBLE0O5xMkQukj +1Y0Zzn/ldE7xURV+vjmjQLhBRrUKRe+eDpxrpeSgyDx5c806Gg2ZYsjNHoN5tkYe +vpK7mJRXM6hKD712+ZxB/0BIOU2gs0zr87HJIPdz+QKBgQDPIF8rG2TerlYDuNs3 +Z9leHJWZg/kXojzePh9gQEl2Eaj+dAw56MDk0wNO9uXWf2usj8UTy/IXZ3o+VKct +wKcqyYGLOeHli2Lss9gwTe2ANjVYCWOuM+vGKZEuJyvy12B6G465adMn5Z1gLzqd +Y0R6iD6tVmbOdZ01vdKfazXYqwKBgQDZPQt3NrDBi1J4ArydWm4RGLR8jpHpobSM +Icby9s+pQUvCZl+AQmbtpNTiAoXIB9AIA3tWKamva04Wgf0gG7nBtGLlhoB3pKG1 +Z7nd6N7eWNGcnebGLebikaAR75rw9Oseb0cuaVnBDB+cMKuUkqBaq7PVWiovIp0t +u4/JsMb2rQKBgGY0sCFiVCT52+HcTvgJG56v9jYZhEngOa7k8rCZKGbFkiF1fC77 +JmwDoAdQF9v/Zk9xZis9oQlhe1XUieYuwY6AfpcICesuG7N3jz9OxNpWZX46q+Bj +KH3RNx2SIMKk+qtme8gMazCtwjmbNI5DVKAuKNd54iMQsHG7aHpX+esfAoGAF3mb +RZSpYSDXFpTlIPPmtP5Acr1NUzvgMMxtnU/CfP3LXASICWojiUaooou3PThq6JLH +vzXdWlF0LMZ7Pf8BAcemcDsqosMtnlbJJQvHGavrass9ZukK7CzkaUvFKIiu56kK +vh8wTcYrnEs+H14A6gtn8YQ+zr0k0w5TqW3q8jkCgYEAmZOa/Dp/35NmAtkAJkrk +VhKB8LD2hnYQl9zHdZieuHvMLniCNPnjDql2ojP9GB2I+QhNU3JThbGySPoyG/bq +7oFtqh0djMMC5spXD3czT53ox3bJ1MZxch/epF7vrqIgTL3LpGzZZuaUWRhHfMKg +acjCIG4LSD+enSMjthEIs2U= +-----END PRIVATE KEY----- diff --git a/auth-service/fixtures/dv-signing-2.pem b/auth-service/fixtures/dv-signing-2.pem new file mode 100644 index 000000000..a94330b81 --- /dev/null +++ b/auth-service/fixtures/dv-signing-2.pem @@ -0,0 +1,19 @@ +-----BEGIN CERTIFICATE----- +MIIDATCCAemgAwIBAgIUTQ5dMCzqHvc4xw99wM9o5WI8AlQwDQYJKoZIhvcNAQEL +BQAwKzEXMBUGA1UEAwwORGV2ZWxvcG1lbnQgQ0ExEDAOBgNVBAoMB2Vrcy10dnMw +IBcNMjUwMTAxMDAwMDAwWhgPMjEyNTAxMDEwMDAwMDBaMEgxFTATBgNVBAMMDGR2 +LXNpZ25pbmctMjEQMA4GA1UECgwHZWtzLXR2czEdMBsGA1UEBRMUMDAwMDAwMDAw +MDAwMDAwMDAwMDEwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCvw9Sp +D+D17o5fZ5ETVTyeZCAlkz/QRjR2Pfby7+IZFbpV3Mf6QF9jM58fLbQK8fIxERAB +usqC7wYF83fm9Te5L9tmykkS9Dw8TF+64vwpLUXSObXPfXTDQb8CY5HVRnSoibM8 +4+o8qlnKNKnkRMVMElO9M8WHSkEjt+KPVsfk1nErUZgyIxGerjKnI03QcsnasuJ7 +8wgBPl1iwISUAIzpwUw7RZtLpI3sPASN6V87eIa6z6/k93iFB7ztXy26Cv6wKi4g ++J34sITeUNthgdAcq2tT09Ro3q2fzOnHjFyZWAclqRjy7WJOghfEwvZl7NORh7dq +wkNo2M/mGp9V2b2PAgMBAAEwDQYJKoZIhvcNAQELBQADggEBAGIxqMvp1247S4n6 +J9mKQnIPLvbRlXMkwLZcfGk6Y/DX8XNXCheP23tyPi1bTRYTsiyDYXPLkXnE2H/0 +iSxSZnx7DFeZLZDzQZLB4Es3ANQTbNQb5+7dggOC5w7+43zhG/AYIJv+z5p91eVL +vNXMXFdf2DlcSO8PON8+Bf3OPRMh/yL79TAeD8Vg+iVcX2qLQ3SHWfN3cyIFloHA +VCWViX25zCDmfNPKuycCfQgvkdx27E5WpgLmlHvOhYNq6l54Ljxe8tXwY2Rxd87L +OIA1qsaXa8jbpSnBGDDfxiENGeJb0j7VIZdX3IpRPfM8rrZwXVjp5Sd1PggEbTLH +VP8miZ4= +-----END CERTIFICATE----- diff --git a/auth-service/fixtures/dv-tls-key.pem b/auth-service/fixtures/dv-tls-key.pem new file mode 100644 index 000000000..4acae7f49 --- /dev/null +++ b/auth-service/fixtures/dv-tls-key.pem @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQC8zpFI7mfzLmLy +v9Tg5tFM1y5SOtVbgZS0pn9xGRUievxbRElVe2qlKSzVapdz4j2e3WJwO8J8jPsV +IMCdJzitq20P7gTR5bNxxkeM8xGzmHyNR2oZJBtuQtcpxKsOwvH6A0G3mHTmL7I9 +ILPOIFod7UUBYJbhJsVoTU7NFSmFDOB+3w48vjFBJpBYxn/05eF4T8a8lpFIZqh2 +fjcwZjc4bnt94ErbLYkRO/iIp5/apeDyJZFcHwv6OvEAWbzNBhTahKjTx2EmB2fj +b0PLjDeqBcOVuqrKZ02H8U+W+3+0z0q9eCXx7xuken05GYZM0Nvs0f873A0pdIVZ +CfUNurSBAgMBAAECggEBAKKXLKvmimPznUg4hGUMwE7wuzYa7iM4Cswq2XKWzAl+ +aZVaz+AqQONr+uzqNvSXbsNBGIVYIRrK+WnvY4+x9/AuwOPH8CSlL4uZ2YH6zFKh +zRiI2EhAPNMOAmY4s6opnOHVLiGhy4Fyr4ZOhCjM2W4OJq+R8zx++97fPC4FvFST +EIJdWbot1/5jwkqqabLu0uCor+PaG+Uu99t5CJUp/NNh9b9tffVl98V/mlsa53Ei +65v8ZCA6m7SMlGlVIy4NCWFvAx0QV/C9ebxQa0xDICpcz0tQ1WmyXGCsr+k/TWIu +l/hluXuIxiyfQiAMU1wVJxfYyZfkM0/cMGDWPPHFQsECgYEA15M7dl7F574p8t77 +zni0DRH7sY/jiEa41agbYka6ZwDfu/kPT5+g+fikj+LXajcL74CqJcDKQxa+Og1h +y92HAk/UbE0D9FYwccIdaJ7ameLuEtpmuHmwDaqozQ3+G72VOslxLGR/fDUMrPgX +nwLdVGsQJTd8IBdTqr8rff5bvakCgYEA4DZQ/LWv9G7MS1OIHJ2U/iNZ9FJ9MZIq +c2OG185NfJNRZc///bW0aEB9Gbexc+etLCrMEOD4NGH9wqgwhpwvx9hEMdt56gKv +zLsShp2Gkm0kpZYOBTNvz6k0a/R+JuZERbEPBIrIAjlES90q1tyvXsAo8vd6zj+K +cnVL9hnDFxkCgYBGr2cy8/Zue2Mj2Mu2MsvO8VOXMDgKKi5XGE0KV2itd5Lk/tBv +akcNT+MdruK1WCFYwdaIlIN9pgpv3I1xnEy1jFl5vXMLfetakay07kl9ZfRsSCKg +YH5CrO+2dN0aWXqCESVDyhwnLPngCWUtZXIPSR+9JElV+HAdr5tiL7IDKQKBgQDZ +1qwU+4l7GXNP9FaYRtqNmFc7tDGCyhqIhbhrUDukv5UkMFTtcW9Yot9o7zpv/DYa +ZXJjK9C7REHCsJqFDpa1uf932V+35uhbJ562P7KR7YzffM0ebH1D64fNI3y12p+W +fpKyvQvabT1X4AhRPZT9oAGpHp2oab/fuOsWrq0HcQKBgEOppJvYMCR1cgqWvCXa ++bmq50KwstsaUTk/3pmG3j1h9+Cn6T11ftIbPN9nTuTVDzgNgF0DU4L+ZyQucN36 +uWvoYypoKvXOnEpkChWBzhCHaM7vpbK4YN96AH/UF68aG6UqmmFfXgkpcMyfQNh/ +8JgYPDT+qs/QEYHT05Ovx2Fw +-----END PRIVATE KEY----- diff --git a/auth-service/fixtures/dv-tls.pem b/auth-service/fixtures/dv-tls.pem new file mode 100644 index 000000000..c53d4f799 --- /dev/null +++ b/auth-service/fixtures/dv-tls.pem @@ -0,0 +1,20 @@ +-----BEGIN CERTIFICATE----- +MIIDOjCCAiKgAwIBAgIUE8EFnBlE2hiDsjmSay4arNzA2kYwDQYJKoZIhvcNAQEL +BQAwKzEXMBUGA1UEAwwORGV2ZWxvcG1lbnQgQ0ExEDAOBgNVBAoMB2Vrcy10dnMw +IBcNMjUwMTAxMDAwMDAwWhgPMjEyNTAxMDEwMDAwMDBaMEIxDzANBgNVBAMMBmR2 +LXRsczEQMA4GA1UECgwHZWtzLXR2czEdMBsGA1UEBRMUMDAwMDAwMDAwMDAwMDAw +MDAwMDEwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC8zpFI7mfzLmLy +v9Tg5tFM1y5SOtVbgZS0pn9xGRUievxbRElVe2qlKSzVapdz4j2e3WJwO8J8jPsV +IMCdJzitq20P7gTR5bNxxkeM8xGzmHyNR2oZJBtuQtcpxKsOwvH6A0G3mHTmL7I9 +ILPOIFod7UUBYJbhJsVoTU7NFSmFDOB+3w48vjFBJpBYxn/05eF4T8a8lpFIZqh2 +fjcwZjc4bnt94ErbLYkRO/iIp5/apeDyJZFcHwv6OvEAWbzNBhTahKjTx2EmB2fj +b0PLjDeqBcOVuqrKZ02H8U+W+3+0z0q9eCXx7xuken05GYZM0Nvs0f873A0pdIVZ +CfUNurSBAgMBAAGjPTA7MBoGA1UdEQQTMBGCCWxvY2FsaG9zdIcEfwAAATAdBgNV +HSUEFjAUBggrBgEFBQcDAQYIKwYBBQUHAwIwDQYJKoZIhvcNAQELBQADggEBAG3x +zR3XbnCv0qsKGC4wn1K9xZ978GqB0F4M4kWWXd9Th1iq5GAmqUt7ISxgU7bV4dOo +352664+P450XLpRQPJxHODC1GaF0b77woKpOVVtZPXjZ0miX4nesQv2pPZAS51vK +u18+g46ortm+4fxs48WZV6WAQfRaWIoJJJxmvtH/FrcDaXGpNiSdmoGLZXhp4Cpw +YcTijBEzI+63xCWo4CC6iBY79/OhYSW5LA+Q4l0I9AuQt9AYERTY8gwNSFsaP4sO +dCmzE/ho4+02AnVXJ7eEHn4p956bcQyTRYXEzTS2MXa5iVAgnS0msQJe29UpQ5oC +EBtZvpszj/gsgNMRvus= +-----END CERTIFICATE----- diff --git a/auth-service/fixtures/rd-signing-1-key.pem b/auth-service/fixtures/rd-signing-1-key.pem new file mode 100644 index 000000000..9122ca1fe --- /dev/null +++ b/auth-service/fixtures/rd-signing-1-key.pem @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDIHhEEaeILYEAt +g01K6C4FYeWG3+cr3CPnaH0rtL4M7hXK/WWVhDmbFO/gZ9vlBQJaFQK7rs0YzzOL +hShLwpvxZtUoqUWbLDhBUrMj0gMAUUyZWrfTEdoa4tbOcNLLukQeDQlHZa7LIbN2 +PGTKT5rNtNYfNrqun7kTtOs6ZN0GLO+mWsEwGo03cdY96gUmotf29Rvr+U/09QtO +k65HOVhtbZE4YaBy8hFvLGNUy/l92PcXVE1d07+eJ+chlkWlUrQclUwR6lswSDM6 +GHT0TSZXPpsQTnqIGwLXE5oMnFDrLaUFFeRkUR+Xl96z4SoXRQqvgOmePweqi1Wx +O1ZwnpVvAgMBAAECggEAJaJp5t+W0Dbguu53ib/Sb9ayWMQg8IGtCub+q0qJd1sL +JCq1gQHl54Fvf09o7lAb2h8bbXp5PdIxympXYrANrb9+FPCIk58kUTyhTfo/8D5Z +3nlOGta/otDIPvi+2+uCxfgDp7a6wZ7r5i+DcPy5izgYELS1NQP6xXb/yfKQEonp +W1ilehjqQICaP03omI+bpfH98pZK86msjsnNsJnDwNG6N5IFQCyosqC5Lh32SWO1 +rXnsjBaOoDdxjlXXYs8W4jTXu9gKfqeSvwAfffP1lcGV/z3CywejWpDFuj5doCTX +EWdVD7OLF/qcJsg/ehzO5WIfCpwyrsc8mmuGvXBqYQKBgQD5YFm+sWPEANc28/o0 +ZZzeeYSi03vbvWIK82GvTgZMFiT1kPj6WCqfbtNhgeYDoMg+mgz0DNXboH9ijFqb +i346DCdHWASng3JNIsiiO3JCEOZy/OcGmY/AVNUePJYw/Pl70GDiUF70rCkaZ4yC +MT/UL4D6oGbrEft7HXNHMo8ekQKBgQDNbsblkQvlvmK7tId8X5ADTZdGbHYcVjgK +oM+9ftmaEZZvd+FaBGPaHo943VKAC1e8Dt6UneIQTLXg1KNWVu2DEpMoarlEHx1y +IveJe5Jj9XBGzKSfQ5dnGkidG5tdlp/aZX1FOqvCSU1StyA0R+LrjhT8f4gzTIWQ +eWZfBhRz/wKBgQCVQ3mULWNUv1gSGWiqkiOZ+kxMlZ8uQKX9ff37Sw1z31FR9utA +FSO1+ETnuTOu+j7BUb3tGf2rjt6QYwgc0BrGrd2Y03M9vOzehLJogIJ8TtdQ/lVd +2u5zJZPGnGcdfV3rISfMbmTXuHCF0HMGvmnBHInr+bSAgWvwHgmRFQ8bMQKBgQCL +zn0L1KeNJgFZjyW5jeubY920rrG+3SpsiT53zvFWOevwVlyF/4DY+31WmwC/ZXjC +4je2WrBzuVD5/pxecPBZCHYfxmnpI2fNumBOL0vUPT+43yKVxNEv+KI8UYJtscl+ +Px1+1Mv5gBtgVIJVMisLdflwUXamusgXJ5d0ydV3jwKBgC6XJRRCggUOhbnK/Oul +SQEMRYq7O7zoFphWHFezuybmehIXbF1WQWp2qH1ohDz/vnGm7ZIwYC2nBAkbvrpe +knTjb6YDRAEXAsB4yCIyyRLw0wHkDXqKVSmXUql37HyHjM1mmVwCZco/bT9H3svi +WVFzCSEiCYEJ7vC5XjDJ7w2f +-----END PRIVATE KEY----- diff --git a/auth-service/fixtures/rd-signing-1.pem b/auth-service/fixtures/rd-signing-1.pem new file mode 100644 index 000000000..c8df65256 --- /dev/null +++ b/auth-service/fixtures/rd-signing-1.pem @@ -0,0 +1,19 @@ +-----BEGIN CERTIFICATE----- +MIIDATCCAemgAwIBAgIUQkJSiDehARN0JY1JPuY9CjFdZe0wDQYJKoZIhvcNAQEL +BQAwKzEXMBUGA1UEAwwORGV2ZWxvcG1lbnQgQ0ExEDAOBgNVBAoMB2Vrcy10dnMw +IBcNMjUwMTAxMDAwMDAwWhgPMjEyNTAxMDEwMDAwMDBaMEgxFTATBgNVBAMMDHJk +LXNpZ25pbmctMTEQMA4GA1UECgwHZWtzLXR2czEdMBsGA1UEBRMUMDAwMDAwMDQw +MDAwMDAxNDkwMDAwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDIHhEE +aeILYEAtg01K6C4FYeWG3+cr3CPnaH0rtL4M7hXK/WWVhDmbFO/gZ9vlBQJaFQK7 +rs0YzzOLhShLwpvxZtUoqUWbLDhBUrMj0gMAUUyZWrfTEdoa4tbOcNLLukQeDQlH +Za7LIbN2PGTKT5rNtNYfNrqun7kTtOs6ZN0GLO+mWsEwGo03cdY96gUmotf29Rvr ++U/09QtOk65HOVhtbZE4YaBy8hFvLGNUy/l92PcXVE1d07+eJ+chlkWlUrQclUwR +6lswSDM6GHT0TSZXPpsQTnqIGwLXE5oMnFDrLaUFFeRkUR+Xl96z4SoXRQqvgOme +Pweqi1WxO1ZwnpVvAgMBAAEwDQYJKoZIhvcNAQELBQADggEBAHi9W46hfa8Pmpnn +Z4MaWkzsZBSVsicFDgDW5kqn/Ox7rSf4vEnFFg67bF4vKXDLZov98kD+Zj6lzZ6K +uFNvxz4w02NSxNF0uF26hkaf28jk/c16uQEhd1bsM3hlbcleDz60bmlJycFXIKAX +zRmjaL/D9IgbJxy8rVeWcwubxA1b1+CvzKpFQZ+QHbEoc876kxJ5nnp7s2r1AGBS +tj/rpZJe28Hz6CLnVg1Nn/l6x7oAkQZ2/AfhjkQ2fJ+z2tAcjE8ILacJTeIh3UXY +L84F11Ma/O77Id9dX7bR7VlOxq7XFj1laEGTzkSVGS8ylrnD6IIrXASNiGhG6HDX +l4AuwQs= +-----END CERTIFICATE----- diff --git a/auth-service/fixtures/rd-tls-key.pem b/auth-service/fixtures/rd-tls-key.pem new file mode 100644 index 000000000..35c034409 --- /dev/null +++ b/auth-service/fixtures/rd-tls-key.pem @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQCs0VDnzkTgWg/O +nmdlMpwuOnEe/q0n5ODL+oWxby9t2PNO3gEWc50dw4zAYqkcv6Qm3Nop47WGm5ba +/VflcftQGa/YMmXi4acA3KM8mBu6zcnIqplE0s2R2MWf6Lwl28reaBwLCgcip7lo +Xg5/tH5Q5vbu21D04LbG4s5yX84DsHUQ2k6LMIP5Ht4VD142LCahb48mfmNwLdLT +Y10AOI46TSB3pLxRR/RGImKp5/vOIC1F7Gt3FjRM5JXyJniyyQVDs0KauVDxPbb+ +DoGoZ0uOQBlGoUXCbk1ijuh3WVzsV5KnpposAQQH/qFpqa2T2seUa8lKqK5YeSQR +eJvlXPQ1AgMBAAECggEBAIwaiffTrt0fxo7gtrxBIoML2CejmqesE17q9DYcCJyK +dP5sVv9nrWmy3kod8jIfX6ouBCRPuhutycgx+W9HT9HpcTjZ6uXbwLOfLCt0uY5h +RYSMYgN8zzG9RsJUQSyEGtziFFISyCQdS9/D2QdJQdq+1FRisyhDgYqN14a8Ct3L +e7K83lAMtsUyYFYRgAdhY1JMQ+LGy16bdtd1+GXxkNMsy3tlO3VE8bAFIIH7N8iz +GaRxJ54f9BnPIXzVKbJkIo1LbJW/VB74g009zN0nLk5eAErr2t0+PCHOqt9mdTHn +NHuZNmBE9yUQHid7kKW46FPHRgp5zfYyjaL5eWyKpqECgYEA4nbCOZV4wsvuspkj +17D/rIxS0CKingZebPYNOoadJkL57LPJgdLHsJqH4P4u5Hsvhyxu/d5yZlYvZ7Ga +4nysmnGi4ub0oui0CNIJ7ZggwVfW6XXego7srIIbZLnh7IILH+nw40eQgxj3ISuQ +TpCmNB6rGVR+OZ2ncJv58XiXL+0CgYEAw1tmlL7t04F0T+1pGpOt+DZK/vVQSnfZ +9lTYW+ZScueksgbzyHyGCqpaB9SXzYDm8+q/sZD9kWihSNllsD3q3I3RZkYF0ta3 +9CxPseEauGIrRMoxWQ3U7gqS1vw1ZtgpwTZmSQOUD4Bra99wx2drCC8uZ78VgkEx +k3BfVxVB/GkCgYEAgRH+kKNIOH3ShagSbZRLpCQ17rpOxJIMrz1tPLYMSwpRNz8g +0gU4Gsd4OhukzkQHSptghwCJYxor2oW3KO610TCDDZ9kPpUyRvSvA+N4h7lwm466 +St76tTdSNY5b3Vggzweea2sWSiGoeBdEIAMBNyNjkQQ/DMd17xB229FHGeECgYEA +gm5H8w/24bwQ2Dx+bAwlnw9AyzEmRlH26IyFV/ZTHfyi7WnO4hChvgU2zvuVztM9 +X6VgD0Hqyma9KFJdnmn4oQDeyhX+VWKetxvOqcjNekWw9eC5Sef+PmA44ejKDrxM +RH/5ks4c0KzBC+jG95uIzpIRvZrK1YiJfRMN1KqQv+ECgYA/Cp8S/eOYtAUINtVg +6QjzetwefE4WfONrbFv1alApvjD5UttF/yINGIR4ZCEma1l0DdbbWQca8ZC7S0hz +BZCamM1HWD47BwMbw1GYm2BW8QOvKmZBSI4+e46ey4BSuMH9HJGkVlRvaas+tVqg +e+ygkpLN68DJO13NgZ91jXWtRQ== +-----END PRIVATE KEY----- diff --git a/auth-service/fixtures/rd-tls.pem b/auth-service/fixtures/rd-tls.pem new file mode 100644 index 000000000..80a686a38 --- /dev/null +++ b/auth-service/fixtures/rd-tls.pem @@ -0,0 +1,20 @@ +-----BEGIN CERTIFICATE----- +MIIDVDCCAjygAwIBAgIUH61OWBF5qc1+TB90NAUtOThxslcwDQYJKoZIhvcNAQEL +BQAwKzEXMBUGA1UEAwwORGV2ZWxvcG1lbnQgQ0ExEDAOBgNVBAoMB2Vrcy10dnMw +IBcNMjUwMTAxMDAwMDAwWhgPMjEyNTAxMDEwMDAwMDBaMEIxDzANBgNVBAMMBnJk +LXRsczEQMA4GA1UECgwHZWtzLXR2czEdMBsGA1UEBRMUMDAwMDAwMDQwMDAwMDAx +NDkwMDAwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCs0VDnzkTgWg/O +nmdlMpwuOnEe/q0n5ODL+oWxby9t2PNO3gEWc50dw4zAYqkcv6Qm3Nop47WGm5ba +/VflcftQGa/YMmXi4acA3KM8mBu6zcnIqplE0s2R2MWf6Lwl28reaBwLCgcip7lo +Xg5/tH5Q5vbu21D04LbG4s5yX84DsHUQ2k6LMIP5Ht4VD142LCahb48mfmNwLdLT +Y10AOI46TSB3pLxRR/RGImKp5/vOIC1F7Gt3FjRM5JXyJniyyQVDs0KauVDxPbb+ +DoGoZ0uOQBlGoUXCbk1ijuh3WVzsV5KnpposAQQH/qFpqa2T2seUa8lKqK5YeSQR +eJvlXPQ1AgMBAAGjVzBVMDQGA1UdEQQtMCuCCWxvY2FsaG9zdIcEfwAAAYIYdHZz +LW1vY2stYXJzLmVrcy10ZXN0Lm5sMB0GA1UdJQQWMBQGCCsGAQUFBwMBBggrBgEF +BQcDAjANBgkqhkiG9w0BAQsFAAOCAQEADZliu/yyBG4oWg7pB7klTsyk4P/8mepu +BHn9c7gYRnHiGEhTTE8K2tQS4yt6dRui90w4buR5s+zxfEVQdPYm/IQKhEQWzRdC +zWuipnHJ9Yo61Oi/rOZ4tzPFPrIPI5jxIg4gPW4EVGbg43ZPAd+ySSLroqVwNpSp +9+uDqBJoiLxVXwYQvvey+lnBXYXM3e+gK/uqFlr7lYYUB3gRAWNi0EhRvinvjjWA +uwG+e40OZ7zzzusNmL56W2lby6izQJzNjyTN1JLPFDfOsdYB/eMEFLkPyrDx+uxO +KHvdTir59MCSiEb0v2WcxQ7rfZjCWw+UucJEwZDO9EB5sq6nESi2Gg== +-----END CERTIFICATE----- diff --git a/auth-service/pkioverheid_private_root_g1.pem b/auth-service/pkioverheid_private_root_g1.pem new file mode 100644 index 000000000..6e4740ae6 --- /dev/null +++ b/auth-service/pkioverheid_private_root_g1.pem @@ -0,0 +1,32 @@ +-----BEGIN CERTIFICATE----- +MIIFhDCCA2ygAwIBAgIEAJimITANBgkqhkiG9w0BAQsFADBiMQswCQYDVQQGEwJO +TDEeMBwGA1UECgwVU3RhYXQgZGVyIE5lZGVybGFuZGVuMTMwMQYDVQQDDCpTdGFh +dCBkZXIgTmVkZXJsYW5kZW4gUHJpdmF0ZSBSb290IENBIC0gRzEwHhcNMTMxMTE0 +MTM0ODU1WhcNMjgxMTEzMjMwMDAwWjBiMQswCQYDVQQGEwJOTDEeMBwGA1UECgwV +U3RhYXQgZGVyIE5lZGVybGFuZGVuMTMwMQYDVQQDDCpTdGFhdCBkZXIgTmVkZXJs +YW5kZW4gUHJpdmF0ZSBSb290IENBIC0gRzEwggIiMA0GCSqGSIb3DQEBAQUAA4IC +DwAwggIKAoICAQDaIMh56ynwnEhE7Ey54KpX5j1XDoxbHDCgXctute55RjmG2hy6 +fuq++q/dCSsj38Pi/KYn/PN13EF05k39IRvakb0AQNVyHifNKXfta6Tzi5QcM4BK +09DB4Ckb6TdZTNUtWyEcAtRblYaVSQ4Xr5QODNqu2FGQucraVXqCIx81azlOE2Jb +Zli9AZKn94pP57A11dUYhxMsh70YosJEKVB8Ue4ROksHhb/nnOISG+2y9FD5M8u8 +jYhp00TGZGVu5z0IFgtqX0i8GmrH0ub9AWjf/iU4MWjGVRSq0cwUHEeKRj/UD9a8 +xIEn9TxIfYj+6+s4tn9dW/4PV5jc6iGJx6ExTPfOR7VHpxS4XujrZb5Ba/+oj/ON +dOfR0JSm2itCytbtjQBBL0oocIIqaqOna1cufHkcn9VleF7Zvz/8njQIpAU4J4nJ +4pE5pQ3k4ORAGNnq5R9hAqqUQGDlo3Uj8PBou0nPzQ7JNgGkN+my/lGr4rceUNK/ +8CoGnYFUH+UyFtJkvlLlEkb688/IdNdGgY+vuXCAB6xfKlJjAGChFUBb6swbNeNc +tVEdUj7Weg4Jt5gXu78C2mjs9x5lcHOgMO4ZmvYJ3Ejp4k3nNa45HOIVkYrfQrrB +HzBhR0BuReAagurcbtUjJFd7BtufGVLfU3CUn1l6u3/9eG4DGH6pq+dSKQIDAQAB +o0IwQDAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBBjAdBgNVHQ4EFgQU +Kv25Kx76w4SHBtuB/4aXdQ3rAYswDQYJKoZIhvcNAQELBQADggIBAEvpmXMOOKdQ +wUPysrsdIkGJUFF+dvmsJDiOuAqV0A1nNTooL3esvDLEZAWZwKTOwRomnHzeCfS/ +QxRKTkVX21pfrHf9ufDKykpzjl9uAILTS76FJ6//R0RTIPMrzknQpG2fCLR5DFEb +HWU/jWAxGmncfx6HQYl/azHaWbv0dhZOUjPdkGAQ6EPvHcyNU9yMkETdw0X6ioxq +zMwkGM893oBrMmtduiqIf3/H6HTXoRKAc+/DXZIq/pAc6eVMa6x43kokluaam9L7 +8yDrlHbGd2VYAr/HZ0TjDZTtI2t2/ySTb7JjC8wL8rSqxYmLpNrnhZzPW87sl2OC +FC3re3ZhtJkIHNP85jj1gqewTC7DCW6llZdB3hBzfHWby0EX2RlcwgaMfNBEV5U0 +IogccdXV+S6zWK4F+yBr0sXUrdbdMFu+g3I9CbXxt0q4eVJtoaun4M2Z+bZMqZvy +9FryBdSfhpgmJqwFz2luOhPOVCblCPhLrUeewrvuBXoZQWt1ZjuHfwJZ1dgjszVE +qwY9S0SdqCg2ZlL9s3vDIrrd3wLWrcHLQMd9gwsppNv9c7JfIJdlcZLTmF9EuL6e +CvVVrqBVqLHjva4erqYol6K/jbSfUtRCy8IlFU7LYu1KLehZKYvj3vekj3Cn08Aq +ljr/Q8Pw+OfUZTzKg4PVDQVfFqKtyosv +-----END CERTIFICATE----- diff --git a/auth-service/pkioverheid_private_services_ca_2023.pem b/auth-service/pkioverheid_private_services_ca_2023.pem new file mode 100644 index 000000000..2e125bc0a --- /dev/null +++ b/auth-service/pkioverheid_private_services_ca_2023.pem @@ -0,0 +1,42 @@ +-----BEGIN CERTIFICATE----- +MIIHaDCCBVCgAwIBAgIUHl/axOAtmxaaDCGN2qdhMvoqDgcwDQYJKoZIhvcNAQEL +BQAwZjELMAkGA1UEBhMCTkwxHjAcBgNVBAoMFVN0YWF0IGRlciBOZWRlcmxhbmRl +bjE3MDUGA1UEAwwuU3RhYXQgZGVyIE5lZGVybGFuZGVuIFByaXZhdGUgU2Vydmlj +ZXMgQ0EgLSBHMTAeFw0yMzAzMjAwOTQzMTlaFw0yODExMTIwMDAwMDBaMIGLMQsw +CQYDVQQGEwJOTDEgMB4GA1UECgwXUXVvVmFkaXMgVHJ1c3RsaW5rIEIuVi4xFzAV +BgNVBGEMDk5UUk5MLTMwMjM3NDU5MUEwPwYDVQQDDDhEaWdpQ2VydCBRdW9WYWRp +cyBQS0lvdmVyaGVpZCBQcml2YXRlIFNlcnZpY2VzIENBIC0gMjAyMzCCAiIwDQYJ +KoZIhvcNAQEBBQADggIPADCCAgoCggIBAJqOPfuVHf2PY16yWi1h1bgZdP1nAhUR +vuRtLc3gFAM32MopVQB2rq5j5m0xJdC6Y0qoiYIEGNdLHcv9CGVQPs9cIeSMeiqF +bZTqse0HSD11p6JHakCZoJ6PEAjTr1Zt2B+31U3CqJzMx+f+IesMDOvKwC6Q946o +GrEItVKtU2pAyJV3C7OH/yA00h+Z4icF5fnz6/rR0rjvhYLZwOF4cDink5zWJnvU +9ec8v/6qB6+rGLVZScGBogIVGFjs5qJT5VNA3rdj3Z+ipNoMlmcoB6I9/Sx1sH6o ++BSelGQGRpM28lt7Wds2StoZg2Y14I8kfAfjkHncDfPFtg64SkC9yCvjqWctAPnx +hy7Tt1+O66TXWJ3z7/ee0VUIzi6daHt7aU7/nxvdl5ZjxTEHaY8/R7lHHHQYjN92 +bHSP3ChHoHtlpkOYdAN6Er+EWF34HvpKO4iY3OF26VtIh+z4O342dDktkFUCIqOs +PWHqgsEwriT3SbyybCf41QzlEFmJog5dtewN/YugdVLbdDPL6zOvvZV8ETunSJqo +CHwv7Cgqf68oJI+xzK+TvBpCdbOPywcpIiN4M/LBo1B4OeM5oKe5Ll3lJzlTGtNU +C+EFQ5NIEAJxnBTsALnzpV3uiGoGyF1YVlSsBJAm5D9Fw6LtjOO7piKbPmli3T0X +O7Hk8rvMTG3LAgMBAAGjggHmMIIB4jASBgNVHRMBAf8ECDAGAQH/AgEAMB8GA1Ud +IwQYMBaAFD6vqA+HoixBexRsG/PbaNOSp0SoMFIGCCsGAQUFBwEBBEYwRDBCBggr +BgEFBQcwAoY2aHR0cDovL2NlcnQucGtpb3ZlcmhlaWQubmwvRG9tUHJpdmF0ZVNl +cnZpY2VzQ0EtRzEuY2VyMHEGA1UdIARqMGgwDAYKYIQQAYdrAQIIBDAMBgpghBAB +h2sBAggFMDYGCmCEEAGHawECCAYwKDAmBggrBgEFBQcCARYaaHR0cHM6Ly9jcHMu +cGtpb3ZlcmhlaWQubmwwCAYGBACPegECMAgGBgQAj3oBATA/BgNVHSUEODA2Bggr +BgEFBQcDAgYIKwYBBQUHAwQGCisGAQQBgjcKAwwGCisGAQQBgjcKAwQGCCsGAQUF +BwMBMCUGCCsGAQUFBwEDBBkwFzAVBggrBgEFBQcLAjAJBgcEAIvsSQECME0GA1Ud +HwRGMEQwQqBAoD6GPGh0dHA6Ly9jcmwucGtpb3ZlcmhlaWQubmwvRG9tUHJpdmF0 +ZVNlcnZpY2VzTGF0ZXN0Q1JMLUcxLmNybDAdBgNVHQ4EFgQUMQ+4zguFDzpJ3l8+ +AlioWyhAo60wDgYDVR0PAQH/BAQDAgGGMA0GCSqGSIb3DQEBCwUAA4ICAQC8qSd7 +tYts9wtD52wKSz5f55W6xbd159l4k6qLTOJnjNLlug9Cwp0ZrqQLTBBXOCRq7KzE +nNN7AHSqTMnimx1wYvZMW7HovP4sfW1KbTExubNMZX5AqHYFTrRah8ORGwtVNHnz +1a1rZaN8tBHOUBuNEPKgVHyM2ygX7Pb86im7WUbb9S1TR6vPJcXvnuswSODS58e9 +VjM1AD2GiNKPptPgJ6kQe8TNMW9I8S9CbxEV2GTMVjgKDHrAyi2SDivzTgWa0g1H +JU5J+lg8s3A9XPe8E0pUAgdGSlgSzQ8/QU1ZFgPeVNiTHoNCtH+rxmCUuBUu32Dn +X1xBY1Ui1bjgSCes6Vwek8wa52GxfNO6P08k1ZN+uZhVUacyGo8F6T14NfjFen0l +bZf1sSdEXijB9Tjx71oU+ZDF+qv7thCcC0LKHW+qxoun+FvooF60DPb06Il745NA +sOcnpIq94dvAkoXQ+9BQ9gkn6PhEL2YOfFzp0LGXehGvsOHWrljTaoC31Nfq6S1i +PaeS5e2EEc4XUzFuisMp8QeU+al3MGMNyqqLXvSRwJmoNrN69Kl52pqB9zFuHKE5 +BDiDdXYrsv2LpNr1CwIUVSlye68pINbYx1GXsjKtsUjlqoLnvEMG+tK/mBiNSE43 +1qRo7TU+WQm/mTxL4jbfLzBmekgnNXHgutnMdg== +-----END CERTIFICATE----- diff --git a/auth-service/src/bindings/http_post.rs b/auth-service/src/bindings/http_post.rs new file mode 100644 index 000000000..d508d0ceb --- /dev/null +++ b/auth-service/src/bindings/http_post.rs @@ -0,0 +1,227 @@ +use askama::Template; +use axum::{ + http::{HeaderValue, header}, + response::{IntoResponse, Response}, +}; +use axum_extra::routing::TypedPath; +use base64::{Engine, engine::general_purpose::STANDARD as BASE64}; + +use crate::error::Result; + +/// Body of the auto-submit script. Served verbatim at +/// [`AutosubmitJsPath`](crate::AutosubmitJsPath). +/// Kept external (rather than inline) so the page complies with a strict CSP +/// that does not permit `script-src 'unsafe-inline'`. +pub const AUTOSUBMIT_JS: &str = "document.getElementById('saml').submit();\n"; + +/// Inputs to [`create_post_form`]. All values are HTML-escaped by askama, so an +/// attacker-influenced value (e.g. a poisoned IdP endpoint from metadata) +/// cannot break out of its attribute and inject markup. +#[derive(Template)] +#[template(path = "post_form.html")] +struct PostFormTemplate<'a> { + destination: &'a str, + param_name: &'a str, + encoded: &'a str, + autosubmit_js_path: &'a str, +} + +/// Build a Content-Security-Policy for the auto-submit page that whitelists +/// `action_url` (the IdP endpoint) as a valid form submission target. +/// +/// The global CSP (`form-action 'self'`) would otherwise block the POST to +/// the IdP. This per-response policy is set by login/logout handlers and +/// takes precedence over the router-wide default (the outer layer only sets +/// the header if it is not already present). +/// +/// `script-src 'self'` is emitted *before* the `form-action` directive that +/// carries `action_url`: per the CSP first-occurrence-wins rule, this guarantees +/// our `script-src` cannot be displaced even if `action_url` somehow contained a +/// second `script-src` directive (`action_url` is also validated upstream in +/// [`crate::saml::idp_metadata`] to reject `;`, quotes and whitespace). +pub fn autosubmit_csp(action_url: &str) -> String { + format!( + "default-src 'none'; base-uri 'none'; script-src 'self'; \ + form-action 'self' {action_url}; frame-ancestors 'none';" + ) +} + +/// Generate the HTML auto-submit form for HTTP-POST binding. +/// +/// `destination` (the IdP endpoint) and `param_name` are HTML-escaped by the +/// template; `encoded` is base64 (no HTML-special characters). +pub fn create_post_form(destination: &str, saml_xml: &str, param_name: &str) -> Result { + Ok(PostFormTemplate { + destination, + param_name, + encoded: &BASE64.encode(saml_xml.as_bytes()), + autosubmit_js_path: crate::AutosubmitJsPath::PATH, + } + .render()?) +} + +/// Build the autosubmit HTTP-POST response that carries `saml_xml` (under +/// `param_name`) to `action_url`. Used by both the login and logout handlers, +/// which differ only in the cookie jar they prepend to it. +/// +/// The page holds a signed SAML message, so it is served with a per-response +/// CSP whitelisting `action_url` (see [`autosubmit_csp`]) plus `no-store` and +/// `DENY` framing as defense-in-depth. +pub fn autosubmit_post_response( + action_url: &str, + saml_xml: &str, + param_name: &str, +) -> Result { + let html = create_post_form(action_url, saml_xml, param_name)?; + // The IdP URL is validated upstream ([`crate::saml::idp_metadata`] rejects + // `;`, quotes and whitespace), so a URL that is not representable in a + // header value means something is badly wrong; refuse to serve the page + // rather than degrade its CSP. + let csp_value = HeaderValue::from_str(&autosubmit_csp(action_url))?; + Ok(( + [ + (header::CONTENT_TYPE, HeaderValue::from_static("text/html")), + (header::CONTENT_SECURITY_POLICY, csp_value), + (header::CACHE_CONTROL, HeaderValue::from_static("no-store")), + (header::X_FRAME_OPTIONS, HeaderValue::from_static("DENY")), + ], + html, + ) + .into_response()) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn form_contains_destination_and_encoded_value() { + let html = create_post_form( + "https://idp.example.com/sso", + "test", + "SAMLRequest", + ) + .unwrap(); + assert!(html.contains(r#"action="https://idp.example.com/sso""#)); + assert!(html.contains(r#"name="SAMLRequest""#)); + let expected_b64 = BASE64.encode(b"test"); + assert!(html.contains(&expected_b64)); + } + + #[test] + fn form_round_trips_via_base64() { + let original = "data"; + let html = create_post_form("https://x.com", original, "SAMLRequest").unwrap(); + let marker = r#"value=""#; + let start = html.find(marker).unwrap() + marker.len(); + let end = start + html[start..].find('"').unwrap(); + let encoded = &html[start..end]; + let decoded = String::from_utf8(BASE64.decode(encoded).unwrap()).unwrap(); + assert_eq!(decoded, original); + } + + #[test] + fn form_uses_external_script_not_inline() { + let html = + create_post_form("https://idp.example.com/sso", "", "SAMLRequest").unwrap(); + assert!(html.contains(&format!( + r#""#, + "", + "SAMLRequest", + ) + .unwrap(); + assert!( + !html.contains(r#""> + + diff --git a/auth-service/templates/saml/artifact_resolve.xml b/auth-service/templates/saml/artifact_resolve.xml new file mode 100644 index 000000000..23bdae4fe --- /dev/null +++ b/auth-service/templates/saml/artifact_resolve.xml @@ -0,0 +1,5 @@ + +{{ issuer }} +{{ signing_cert_base64 }} +{{ artifact }} + diff --git a/auth-service/templates/saml/authn_request.xml b/auth-service/templates/saml/authn_request.xml new file mode 100644 index 000000000..4c673c0c9 --- /dev/null +++ b/auth-service/templates/saml/authn_request.xml @@ -0,0 +1,14 @@ + +{{ issuer }} +{{ signing_cert_base64 }} + +{{ intended_audience }} +{{ service_uuid }} + +{%- if let Some(loa) = requested_loa_uri %} +{{ loa }} +{%- endif %} +{%- if let Some(ad) = preselected_ad_entity_id %} + +{%- endif %} + diff --git a/auth-service/templates/saml/dv_metadata.xml b/auth-service/templates/saml/dv_metadata.xml new file mode 100644 index 000000000..2af36afb9 --- /dev/null +++ b/auth-service/templates/saml/dv_metadata.xml @@ -0,0 +1,14 @@ + +{{ signing_cert_base64 }} + +{%- for kd in key_descriptors %} +{{ kd.key_name }}{{ kd.cert_base64 }} +{%- endfor %} + + + +{{ service_name }} +{{ service_uuid }} + + + diff --git a/auth-service/templates/saml/logout_request.xml b/auth-service/templates/saml/logout_request.xml new file mode 100644 index 000000000..e9becf2c9 --- /dev/null +++ b/auth-service/templates/saml/logout_request.xml @@ -0,0 +1,5 @@ + +{{ issuer }} +{{ signing_cert_base64 }} +{{ name_id }} + diff --git a/auth-service/templates/saml/soap_envelope.xml b/auth-service/templates/saml/soap_envelope.xml new file mode 100644 index 000000000..b3d186fda --- /dev/null +++ b/auth-service/templates/saml/soap_envelope.xml @@ -0,0 +1,2 @@ + +{{ body|safe }} diff --git a/auth-service/tests/common/mod.rs b/auth-service/tests/common/mod.rs new file mode 100644 index 000000000..99709b4c3 --- /dev/null +++ b/auth-service/tests/common/mod.rs @@ -0,0 +1,284 @@ +//! Shared scaffolding for the auth-service integration/security tests. +//! +//! A subdirectory module (`common/mod.rs`) is deliberately used so cargo does +//! not compile this as its own test binary. Each test binary pulls in only a +//! subset of these helpers, so unused items are expected per binary. +#![allow(dead_code)] + +use auth_service::{ + bindings::soap::unwrap_soap, + keys::KeyPair, + saml::{ + constants::{NS_SAML, NS_SAMLP, NS_SOAP, STATUS_SUCCESS, SUBJECT_CONFIRMATION_BEARER}, + loa::MINIMUM_LOA, + validation::{ + Claims, ValidateArtifactResponseOpts, ValidateAssertionOpts, ValidateResponseOpts, + validate_artifact_response_at, validate_assertion_at, validate_response_at, + }, + xml_parser::parse, + }, +}; +use chrono::{Duration, Utc}; +use std::path::PathBuf; + +// --------------------------------------------------------------------------- +// Test entity identifiers (re-using the library's SAML constants where one +// already exists, so the test scaffold never drifts from production values). +// --------------------------------------------------------------------------- + +pub const RD: &str = "urn:test:rd"; +pub const DV: &str = "urn:test:dv"; +pub const ACS: &str = "https://dv.example.com/acs"; +pub const SUCCESS: &str = STATUS_SUCCESS; +pub const BEARER: &str = SUBJECT_CONFIRMATION_BEARER; +pub const SAML: &str = NS_SAML; +pub const SAMLP: &str = NS_SAMLP; + +// --------------------------------------------------------------------------- +// Generic helpers +// --------------------------------------------------------------------------- + +/// Load a keypair (by fixture base name) from the committed TVS fixtures. +pub fn load_key(name: &str) -> KeyPair { + let dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("fixtures"); + let cert_pem = std::fs::read_to_string(dir.join(format!("{name}.pem"))).unwrap(); + let key_pem = std::fs::read_to_string(dir.join(format!("{name}-key.pem"))).unwrap(); + KeyPair::from_pem(cert_pem, key_pem.into()) +} + +/// A SAML timestamp (`%Y-%m-%dT%H:%M:%SZ`) at `offset` from now. +pub fn ts(offset: Duration) -> String { + (Utc::now() + offset) + .format("%Y-%m-%dT%H:%M:%SZ") + .to_string() +} + +/// The inline `ds:Signature` template (empty digest/sig) the signer fills, +/// matching what `xml_builder` embeds in real messages. +pub fn inline_signature(ref_id: &str, cert_b64: &str) -> String { + format!( + r##"{cert_b64}"## + ) +} + +/// Wrap `body` in a `soapenv:` SOAP envelope, as real TVS does. +pub fn soap_envelope(body: &str) -> String { + format!( + r#"{body}"# + ) +} + +// --------------------------------------------------------------------------- +// XSW scaffolding (shared by the two XML-Signature-Wrapping suites) +// --------------------------------------------------------------------------- + +/// A full, valid `Response` + `Assertion` for `name_id` (every check passes for +/// our DV). Namespaces are inherited from the enclosing ArtifactResponse. +pub fn response(id_suffix: &str, name_id: &str) -> String { + let issued = ts(Duration::zero()); + let nb = ts(-Duration::minutes(5)); + let noa = ts(Duration::minutes(5)); + let scd = ts(Duration::minutes(2)); + format!( + r#"{RD}{RD}{name_id}{DV}http://eidas.europa.eu/LoA/substantial"# + ) +} + +/// Outcome of [`run_chain`]: the NameID the chain accepted (`None` when any +/// stage rejected) plus every recorded validation error, so security tests can +/// assert both that a forgery was rejected *and* why. +#[derive(Debug)] +pub struct ChainResult { + pub accepted: Option, + pub errors: Vec, +} + +/// Drive the exact handler chain (single parse, node navigation) over a SOAP +/// envelope. +pub fn run_chain(soap: &str, rd_key: &KeyPair) -> ChainResult { + let mut errors = Vec::new(); + let rejected = |errors: Vec| ChainResult { + accepted: None, + errors, + }; + + let Ok(doc) = parse(soap) else { + return rejected(vec!["XML parse error".to_string()]); + }; + let root = doc.document_element(); + let Some(art_node) = unwrap_soap(&doc, root) else { + return rejected(vec!["failed to unwrap SOAP envelope".to_string()]); + }; + + let response_node = validate_artifact_response_at( + &doc, + art_node, + &ValidateArtifactResponseOpts { + trusted_keys: std::slice::from_ref(rd_key), + expected_in_response_to: "", + expected_issuer: Some(RD), + }, + &mut errors, + ); + if !errors.is_empty() { + return rejected(errors); + } + let Some(response_node) = response_node else { + return rejected(vec!["no Response extracted".to_string()]); + }; + + let assertion_node = validate_response_at( + &doc, + response_node, + &ValidateResponseOpts { + expected_destination: Some(ACS), + expected_issuer: Some(RD), + }, + &mut errors, + ); + if !errors.is_empty() { + return rejected(errors); + } + let Some(assertion_node) = assertion_node else { + return rejected(vec!["no Assertion extracted".to_string()]); + }; + + let claims = validate_assertion_at( + &doc, + assertion_node, + &ValidateAssertionOpts { + dv_entity_id: DV, + expected_recipient: ACS, + expected_issuer: Some(RD), + private_keys: &[], + minimum_loa: Some(MINIMUM_LOA), + expected_service_uuid: None, + }, + &mut errors, + ); + ChainResult { + accepted: claims.map(|c| c.name_id), + errors, + } +} + +// --------------------------------------------------------------------------- +// String-input validation wrappers (test-only). +// +// Production navigates the single parsed tree via the `_at` entry points; these +// parse-then-delegate wrappers live here (rather than in the library) so the +// crate's public API carries only the node-based validators it actually uses. +// --------------------------------------------------------------------------- + +pub struct ResponseResult { + pub valid: bool, + pub errors: Vec, + pub assertion_xml: Option, +} + +/// Parse a Response and validate it (no recipient/issuer binding). The inner +/// Assertion's source is returned whenever one was extracted, valid or not +/// (some tests assert on it for rejected documents). +pub fn validate_response(response_xml: &str) -> ResponseResult { + let doc = match parse(response_xml) { + Ok(d) => d, + Err(e) => { + return ResponseResult { + valid: false, + errors: vec![format!("XML parse error: {e}")], + assertion_xml: None, + }; + } + }; + let root = doc.document_element(); + let mut errors = Vec::new(); + let assertion = validate_response_at( + &doc, + root, + &ValidateResponseOpts { + expected_destination: None, + expected_issuer: None, + }, + &mut errors, + ); + ResponseResult { + valid: errors.is_empty(), + assertion_xml: assertion.and_then(|n| doc.node_source(n).map(str::to_string)), + errors, + } +} + +pub struct ArtifactResponseResult { + pub valid: bool, + pub errors: Vec, + pub response_xml: Option, +} + +/// Parse a SOAP-wrapped ArtifactResponse and validate it. The inner Response's +/// source is returned whenever one was extracted, valid or not (some tests +/// assert on it for rejected documents). +pub fn validate_artifact_response( + soap_xml: &str, + trusted_keys: &[KeyPair], + expected_in_response_to: &str, +) -> ArtifactResponseResult { + let doc = match parse(soap_xml) { + Ok(d) => d, + Err(e) => { + return ArtifactResponseResult { + valid: false, + errors: vec![format!("XML parse error: {e}")], + response_xml: None, + }; + } + }; + let root = doc.document_element(); + let Some(art_node) = unwrap_soap(&doc, root) else { + return ArtifactResponseResult { + valid: false, + errors: vec!["Failed to unwrap SOAP envelope".to_string()], + response_xml: None, + }; + }; + let mut errors = Vec::new(); + let response = validate_artifact_response_at( + &doc, + art_node, + &ValidateArtifactResponseOpts { + trusted_keys, + expected_in_response_to, + // The string wrapper focuses on the other checks; Issuer binding has + // its own coverage (run_chain and the dedicated integration tests). + expected_issuer: None, + }, + &mut errors, + ); + ArtifactResponseResult { + valid: errors.is_empty(), + response_xml: response.and_then(|n| doc.node_source(n).map(str::to_string)), + errors, + } +} + +/// Test-side bundle of [`validate_assertion_at`]'s two outputs. +pub struct AssertionResult { + pub errors: Vec, + pub claims: Option, +} + +/// Parse an Assertion and validate it against `opts`. +pub fn validate_assertion(assertion_xml: &str, opts: ValidateAssertionOpts<'_>) -> AssertionResult { + let doc = match parse(assertion_xml) { + Ok(d) => d, + Err(e) => { + return AssertionResult { + errors: vec![format!("XML parse error: {e}")], + claims: None, + }; + } + }; + let root = doc.document_element(); + let mut errors = Vec::new(); + let claims = validate_assertion_at(&doc, root, &opts, &mut errors); + AssertionResult { errors, claims } +} diff --git a/auth-service/tests/message_roundtrip.rs b/auth-service/tests/message_roundtrip.rs new file mode 100644 index 000000000..bf0c76f88 --- /dev/null +++ b/auth-service/tests/message_roundtrip.rs @@ -0,0 +1,172 @@ +//! Tests for outgoing SAML message building and signing. +//! +//! Every message built, signed, and verified against the signing key must +//! round-trip: render, sign, parse, and verify must all cohere. Assertions are +//! structural (not byte-exact). + +use auth_service::saml::{ + messages::{ + AuthnRequestSpec, create_artifact_resolve, create_authn_request, create_logout_request, + }, + verification::verify_xml_signature, +}; + +mod common; +use common::load_key; + +#[test] +fn authn_request_builds_signs_and_verifies() { + let key = load_key("dv-signing-1"); + let msg = create_authn_request(&AuthnRequestSpec { + entity_id: "urn:test:dv", + service_uuid: "f847dc11-ac24-47b2-84a8-a057440ce56d", + sso_url: "https://rd.example.com/sso", + signing_key: &key, + preselected_ad_entity_id: None, + acs_url: None, + }) + .expect("AuthnRequest built"); + + // Structural assertions. + assert!(msg.xml.contains("AuthnRequest")); + assert!(msg.xml.contains("Version=\"2.0\"")); + assert!(msg.xml.contains("ForceAuthn=\"true\"")); + assert!(msg.xml.contains("urn:test:dv")); + assert!(msg.xml.contains(&format!("ID=\"{}\"", msg.id))); + assert!( + !msg.xml.contains("Scoping"), + "no Scoping without a preselect" + ); + + // The enveloping signature must verify against the signing key. + let result = verify_xml_signature(&msg.xml, std::slice::from_ref(&key), None); + assert!( + result.is_valid(), + "AuthnRequest signature: {:?}", + result.errors + ); +} + +#[test] +fn authn_request_with_preselect_emits_scoping_and_verifies() { + let key = load_key("dv-signing-1"); + let ad = "urn:nl-eid-gdi:1.0:AD:00000004166909913000:entities:9002"; + let msg = create_authn_request(&AuthnRequestSpec { + entity_id: "urn:test:dv", + service_uuid: "uuid-1", + sso_url: "https://rd.example.com/sso", + signing_key: &key, + preselected_ad_entity_id: Some(ad), + acs_url: None, + }) + .expect("AuthnRequest built"); + + assert!(msg.xml.contains("")); + assert!(msg.xml.contains(&format!("ProviderID=\"{ad}\""))); + let result = verify_xml_signature(&msg.xml, std::slice::from_ref(&key), None); + assert!( + result.is_valid(), + "AuthnRequest signature: {:?}", + result.errors + ); +} + +#[test] +fn artifact_resolve_builds_signs_and_verifies() { + let key = load_key("dv-signing-1"); + let msg = create_artifact_resolve( + "AAQAAGotsbEd41l9KWDK", + "urn:test:dv", + "https://rd.example.com/ars", + &key, + ) + .expect("ArtifactResolve built"); + + assert!(msg.xml.contains("ArtifactResolve")); + assert!( + msg.xml + .contains("AAQAAGotsbEd41l9KWDK") + ); + assert!(msg.xml.contains("urn:test:dv")); + let result = verify_xml_signature(&msg.xml, std::slice::from_ref(&key), None); + assert!( + result.is_valid(), + "ArtifactResolve signature: {:?}", + result.errors + ); +} + +#[test] +fn logout_request_builds_signs_and_verifies() { + let key = load_key("dv-signing-1"); + let msg = create_logout_request( + "transient-id-abc", + "urn:test:dv", + "https://rd.example.com/slo", + &key, + ) + .expect("LogoutRequest built"); + + assert!(msg.xml.contains("LogoutRequest")); + assert!( + msg.xml + .contains("transient-id-abc") + ); + let result = verify_xml_signature(&msg.xml, std::slice::from_ref(&key), None); + assert!( + result.is_valid(), + "LogoutRequest signature: {:?}", + result.errors + ); +} + +/// Two distinct signing keys: a message signed by one must NOT verify against +/// the other (verification must not accept an untrusted key). +#[test] +fn message_signed_by_one_key_rejected_by_another() { + let signer = load_key("dv-signing-1"); + let other = load_key("rd-signing-1"); + let msg = create_authn_request(&AuthnRequestSpec { + entity_id: "urn:test:dv", + service_uuid: "uuid-1", + sso_url: "https://rd.example.com/sso", + signing_key: &signer, + preselected_ad_entity_id: None, + acs_url: None, + }) + .expect("built"); + let result = verify_xml_signature(&msg.xml, std::slice::from_ref(&other), None); + assert!( + !result.is_valid(), + "must not verify against an unrelated key" + ); +} + +/// A Test-mode AuthnRequest carrying an explicit `AssertionConsumerServiceURL` +/// must still sign and verify (the substituted attribute must not break c14n). +#[test] +fn authn_request_with_acs_url_signs_and_verifies() { + let key = load_key("dv-signing-1"); + let msg = create_authn_request(&AuthnRequestSpec { + entity_id: "urn:test:dv", + service_uuid: "uuid-1", + sso_url: "https://rd.example.com/sso", + signing_key: &key, + preselected_ad_entity_id: None, + acs_url: Some("https://pr-7.preview.example.test/saml/sp/acs"), + }) + .expect("AuthnRequest built"); + + assert!( + msg.xml.contains( + "AssertionConsumerServiceURL=\"https://pr-7.preview.example.test/saml/sp/acs\"" + ) + ); + assert!(!msg.xml.contains("AssertionConsumerServiceIndex")); + let result = verify_xml_signature(&msg.xml, std::slice::from_ref(&key), None); + assert!( + result.is_valid(), + "AuthnRequest signature: {:?}", + result.errors + ); +} diff --git a/auth-service/tests/metadata_validation.rs b/auth-service/tests/metadata_validation.rs new file mode 100644 index 000000000..f28101a6a --- /dev/null +++ b/auth-service/tests/metadata_validation.rs @@ -0,0 +1,535 @@ +//! Integration tests for IdP metadata validation. +//! +//! These tests build signed metadata using our own test certificates, then +//! verify that every validation check correctly rejects malformed input. +//! +//! Run with: cargo test --test metadata_validation + +use auth_service::{ + keys::KeyPair, + saml::{ + constants::{NS_DSIG, NS_MD}, + crypto::sign, + idp_metadata::extract_idp_keys, + verification::verify_xml_signature, + xml_parser::{find_descendant, inner_text}, + }, +}; +use secrecy::ExposeSecret; + +mod common; +use common::{inline_signature, load_key}; + +// --------------------------------------------------------------------------- +// Helpers +// --------------------------------------------------------------------------- + +/// Build a minimal IdP metadata document (unsigned). +fn build_idp_metadata(entity_id: &str, id: &str, keys: &[(&str, &str, &str)]) -> String { + let mut key_descriptors = String::new(); + for (use_attr, key_name, cert_b64) in keys { + key_descriptors.push_str(&format!( + r#"{key_name}{cert_b64}"# + )); + } + format!( + r#"{key_descriptors}"# + ) +} + +/// Insert the inline signature before `anchor` and sign it in place. +fn sign_inline(xml: &str, id: &str, signing_key: &KeyPair, anchor: &str) -> String { + let sig = inline_signature(id, &signing_key.cert_base64); + let pos = xml.find(anchor).expect("signature anchor present"); + let templated = format!("{}{}{}", &xml[..pos], sig, &xml[pos..]); + sign(&templated, signing_key.key_pem.expose_secret()).expect("signing must succeed") +} + +/// Build and sign an IdP metadata document using the given key. +fn signed_metadata(signing_key: &KeyPair) -> String { + let id = "_test_metadata_001"; + let xml = build_idp_metadata( + "urn:test:idp", + id, + &[("signing", &signing_key.key_name, &signing_key.cert_base64)], + ); + sign_inline(&xml, id, signing_key, "` so the value no longer matches +/// what was signed. Panics if the tag is absent (a broken fixture, not a pass). +fn flip_first_byte_in(xml: &str, tag: &str) -> String { + let open = format!("<{tag}>"); + let pos = xml + .find(&open) + .unwrap_or_else(|| panic!("no {tag} found in signed metadata")); + let start = pos + open.len(); + let mut out = xml.to_string(); + let c = out.as_bytes()[start]; + let replacement = if c == b'A' { b'B' } else { b'A' }; + // SAFETY: both bytes are ASCII, so the string stays valid UTF-8. + unsafe { out.as_bytes_mut()[start] = replacement }; + out +} + +/// Build and sign metadata with two signing keys (rollover scenario). +fn signed_metadata_with_rollover(key1: &KeyPair, key2: &KeyPair, signing_key: &KeyPair) -> String { + let id = "_test_metadata_rollover"; + let xml = build_idp_metadata( + "urn:test:idp", + id, + &[ + ("signing", &key1.key_name, &key1.cert_base64), + ("signing", &key2.key_name, &key2.cert_base64), + ], + ); + sign_inline(&xml, id, signing_key, "") + .expect("must have closing tag") + + "".len(); + let stripped = format!("{}{}", &xml[..start], &xml[end..]); + + let result = verify_xml_signature(&stripped, &[key], None); + assert!(!result.is_valid(), "missing signature must fail"); + assert!( + result.errors.iter().any(|e| e.contains("No ds:Signature")), + "error should mention missing signature: {:?}", + result.errors + ); +} + +#[test] +fn rejects_wrong_signing_key() { + let rd_key = load_key("rd-signing-1"); + let dv_key = load_key("dv-signing-1"); + let xml = signed_metadata(&rd_key); + + // Verify with a different key (DV key instead of RD key) + let result = verify_xml_signature(&xml, &[dv_key], None); + assert!(!result.is_valid(), "wrong key must fail verification"); +} + +#[test] +fn rejects_mismatched_cert_base64() { + let rd_key = load_key("rd-signing-1"); + let xml = signed_metadata(&rd_key); + + // Our signature template embeds X509Certificate, not KeyName. + // Providing a trusted key with a different cert_base64 must fail. + let mut wrong = rd_key.clone(); + wrong.cert_base64 = "AAAA".to_string(); + wrong.key_name = "0000000000000000000000000000000000000000".to_string(); + + let result = verify_xml_signature(&xml, &[wrong], None); + assert!(!result.is_valid(), "mismatched cert must fail"); + assert!( + result.errors.iter().any(|e| e.contains("does not match")), + "error should mention cert mismatch: {:?}", + result.errors + ); +} + +#[test] +fn rejects_empty_trusted_keys() { + let key = load_key("rd-signing-1"); + let xml = signed_metadata(&key); + + // No trusted keys at all + let result = verify_xml_signature(&xml, &[], None); + assert!(!result.is_valid(), "empty trust store must fail"); +} + +// --------------------------------------------------------------------------- +// Tests: key extraction edge cases +// --------------------------------------------------------------------------- + +#[test] +fn key_extraction_bare_key_descriptor_goes_to_both() { + // A KeyDescriptor without a `use` attribute should be usable for both signing and encryption. + let key = load_key("rd-signing-1"); + let xml = format!( + r#" + +{} +"#, + key.cert_base64 + ); + let doc = auth_service::saml::xml_parser::parse(&xml).unwrap(); + let root = doc.document_element(); + let keys = extract_idp_keys(&doc, root); + + assert_eq!(keys.signing.len(), 1, "bare key must appear in signing"); + assert_eq!( + keys.encryption.len(), + 1, + "bare key must appear in encryption" + ); + assert_eq!(keys.signing[0].key_name, keys.encryption[0].key_name); +} + +#[test] +fn key_extraction_signing_only_not_in_encryption() { + let key = load_key("rd-signing-1"); + let xml = format!( + r#" + +{} +"#, + key.cert_base64 + ); + let doc = auth_service::saml::xml_parser::parse(&xml).unwrap(); + let root = doc.document_element(); + let keys = extract_idp_keys(&doc, root); + + assert_eq!(keys.signing.len(), 1); + assert_eq!( + keys.encryption.len(), + 0, + "signing key must not appear in encryption" + ); +} + +#[test] +fn key_extraction_encryption_only_not_in_signing() { + let xml = format!( + r#" + +{} +"#, + load_key("dv-encryption-1").cert_base64 + ); + let doc = auth_service::saml::xml_parser::parse(&xml).unwrap(); + let root = doc.document_element(); + let keys = extract_idp_keys(&doc, root); + + assert_eq!( + keys.signing.len(), + 0, + "encryption key must not appear in signing" + ); + assert_eq!(keys.encryption.len(), 1); +} + +#[test] +fn key_extraction_skips_key_name_only_descriptors() { + // KeyDescriptor with KeyName but no X509Certificate should be skipped + let xml = r#" + +some-key-name +"#; + let doc = auth_service::saml::xml_parser::parse(xml).unwrap(); + let root = doc.document_element(); + let keys = extract_idp_keys(&doc, root); + + assert_eq!( + keys.signing.len(), + 0, + "KeyName-only descriptors must be skipped" + ); +} + +#[test] +fn key_extraction_handles_empty_metadata() { + let xml = r#" + +"#; + let doc = auth_service::saml::xml_parser::parse(xml).unwrap(); + let root = doc.document_element(); + let keys = extract_idp_keys(&doc, root); + + assert_eq!(keys.signing.len(), 0); + assert_eq!(keys.encryption.len(), 0); +} + +// --------------------------------------------------------------------------- +// Tests: certificate rollover +// --------------------------------------------------------------------------- + +#[test] +fn rollover_metadata_contains_both_keys() { + let key1 = load_key("rd-signing-1"); + let key2 = load_key("dv-signing-1"); // use DV key as second signing key for testing + let xml = signed_metadata_with_rollover(&key1, &key2, &key1); + + let doc = auth_service::saml::xml_parser::parse(&xml).unwrap(); + let root = doc.document_element(); + let keys = extract_idp_keys(&doc, root); + + assert_eq!(keys.signing.len(), 2, "should extract both signing keys"); + let names: Vec<&str> = keys.signing.iter().map(|k| k.key_name.as_str()).collect(); + assert!(names.contains(&key1.key_name.as_str())); + assert!(names.contains(&key2.key_name.as_str())); +} + +#[test] +fn rollover_metadata_verifies_with_either_advertised_key() { + let key1 = load_key("rd-signing-1"); + let key2 = load_key("dv-signing-1"); + + // Whichever of the two advertised keys actually signed, verification must + // succeed: that is the point of publishing both during a rollover. + for signer in [&key1, &key2] { + let xml = signed_metadata_with_rollover(&key1, &key2, signer); + let doc = auth_service::saml::xml_parser::parse(&xml).unwrap(); + let root = doc.document_element(); + let keys = extract_idp_keys(&doc, root); + let result = verify_xml_signature(&xml, &keys.signing, None); + assert!( + result.is_valid(), + "rollover metadata signed by {} must verify: {:?}", + signer.key_name, + result.errors + ); + } +} + +#[test] +fn rollover_old_key_removed_after_update() { + let key1 = load_key("rd-signing-1"); + let key2 = load_key("dv-signing-1"); + + // First metadata has key1 + key2 + let xml1 = signed_metadata_with_rollover(&key1, &key2, &key1); + let doc1 = auth_service::saml::xml_parser::parse(&xml1).unwrap(); + let root1 = doc1.document_element(); + let keys1 = extract_idp_keys(&doc1, root1); + assert_eq!(keys1.signing.len(), 2); + + // Second metadata has only key1 (key2 removed after rollover) + let xml2 = signed_metadata(&key1); + let doc2 = auth_service::saml::xml_parser::parse(&xml2).unwrap(); + let root2 = doc2.document_element(); + let keys2 = extract_idp_keys(&doc2, root2); + assert_eq!(keys2.signing.len(), 1, "old rollover key should be gone"); + assert_eq!(keys2.signing[0].key_name, key1.key_name); +} + +// --------------------------------------------------------------------------- +// Tests: key name derivation consistency +// --------------------------------------------------------------------------- + +#[test] +fn extracted_key_name_matches_derived_key_name() { + let key = load_key("rd-signing-1"); + let xml = signed_metadata(&key); + + let doc = auth_service::saml::xml_parser::parse(&xml).unwrap(); + let root = doc.document_element(); + let keys = extract_idp_keys(&doc, root); + + // The key_name derived from the extracted PEM must match the original + assert_eq!( + keys.signing[0].key_name, key.key_name, + "extracted key_name must match original" + ); +} + +#[test] +fn signature_x509cert_matches_metadata_key() { + let key = load_key("rd-signing-1"); + let xml = signed_metadata(&key); + + let doc = auth_service::saml::xml_parser::parse(&xml).unwrap(); + let root = doc.document_element(); + let sig = find_descendant(&doc, root, NS_DSIG, "Signature").expect("must have Signature"); + let x509_node = find_descendant(&doc, sig, NS_DSIG, "X509Certificate") + .expect("Signature must have X509Certificate"); + let sig_cert: String = inner_text(&doc, x509_node) + .chars() + .filter(|c: &char| !c.is_whitespace()) + .collect(); + + let keys = extract_idp_keys(&doc, root); + assert!( + keys.signing.iter().any(|k| k.cert_base64 == sig_cert), + "Signature X509Certificate must match a signing KeyDescriptor" + ); +} + +// --------------------------------------------------------------------------- +// Tests: XML parsing edge cases +// --------------------------------------------------------------------------- + +#[test] +fn rejects_non_xml_input() { + let result = verify_xml_signature("this is not XML", &[], None); + assert!(!result.is_valid()); + assert!(result.errors.iter().any(|e| e.contains("parse error"))); +} + +#[test] +fn rejects_xml_without_signature() { + let xml = r#" + +"#; + + let key = load_key("rd-signing-1"); + let result = verify_xml_signature(xml, &[key], None); + assert!(!result.is_valid()); + assert!( + result.errors.iter().any(|e| e.contains("No ds:Signature")), + "must report missing signature: {:?}", + result.errors + ); +} + +#[test] +fn rejects_tampered_key_descriptor_cert() { + let key = load_key("rd-signing-1"); + let xml = signed_metadata(&key); + + // Tamper with the X509Certificate in the KeyDescriptor (not the Signature). + // This changes the trusted cert that would be used for verification. + let tampered = xml.replacen(&key.cert_base64[..20], "AAAAAAAAAAAAAAAAAAAAAA", 1); + + // Re-extract keys from the tampered metadata; the cert is now different. + let doc = auth_service::saml::xml_parser::parse(&tampered).unwrap(); + let root = doc.document_element(); + let tampered_keys = extract_idp_keys(&doc, root); + + assert!( + !tampered_keys.signing.is_empty(), + "the tampered cert must still be extractable, so the assertion below \ + actually exercises signature verification" + ); + + // The tampered cert no longer matches the genuine signature. + let result = verify_xml_signature(&tampered, &tampered_keys.signing, None); + assert!( + !result.is_valid(), + "tampered KeyDescriptor cert should fail verification" + ); +} + +// --------------------------------------------------------------------------- +// Tests: verify with X509Certificate matching (not KeyName) +// --------------------------------------------------------------------------- + +#[test] +fn verifies_via_x509_certificate_matching() { + let key = load_key("rd-signing-1"); + // Our signature template embeds X509Certificate, not KeyName. + // A trusted key with matching cert_base64 but empty key_name should still verify + // via the X509Certificate matching path. + let trust = KeyPair { + cert_pem: key.cert_pem.clone(), + key_pem: String::new().into(), + key_name: String::new(), // won't match by KeyName, falls through to cert matching + cert_base64: key.cert_base64.clone(), + }; + + let xml = signed_metadata(&key); + let result = verify_xml_signature(&xml, &[trust], None); + assert!( + result.is_valid(), + "X509Certificate matching should verify: {:?}", + result.errors + ); +} diff --git a/auth-service/tests/tvs_metadata.rs b/auth-service/tests/tvs_metadata.rs new file mode 100644 index 000000000..b8928ac9e --- /dev/null +++ b/auth-service/tests/tvs_metadata.rs @@ -0,0 +1,138 @@ +//! Fetch and validate real TVS (Toegang Verlening Service) metadata from DICTU. +//! +//! These tests hit external URLs and require network access. +//! Run with: cargo test --test tvs_metadata -- --ignored + +use auth_service::saml::{ + constants::{NS_DSIG, NS_MD}, + idp_metadata::extract_idp_keys, + verification::verify_xml_signature, + xml_parser::{descendants_by_tag, find_descendant, inner_text}, +}; + +const TVS_PP_METADATA_URL: &str = "https://pp2.toegang.overheid.nl/kvs/rd/metadata"; +const TVS_PROD_METADATA_URL: &str = "https://rd2.toegang.overheid.nl/kvs/rd/metadata"; + +async fn fetch_metadata(url: &str) -> String { + reqwest::get(url) + .await + .unwrap_or_else(|e| panic!("Failed to fetch {url}: {e}")) + .text() + .await + .unwrap_or_else(|e| panic!("Failed to read response from {url}: {e}")) +} + +fn validate_metadata(xml: &str, url: &str) { + let doc = auth_service::saml::xml_parser::parse(xml) + .unwrap_or_else(|e| panic!("{url}: XML parse error: {e}")); + let root = doc.document_element(); + + // Root must be EntityDescriptor with an entityID + assert_eq!( + doc.local_name(root), + Some("EntityDescriptor"), + "{url}: root element is not EntityDescriptor" + ); + assert!( + doc.get_attribute(root, "entityID").is_some(), + "{url}: missing entityID attribute" + ); + + // Must contain an IDPSSODescriptor + let idp = find_descendant(&doc, root, NS_MD, "IDPSSODescriptor") + .unwrap_or_else(|| panic!("{url}: missing IDPSSODescriptor")); + + // Must expose SingleSignOnService endpoint + assert!( + find_descendant(&doc, idp, NS_MD, "SingleSignOnService").is_some(), + "{url}: missing SingleSignOnService" + ); + + // Must expose ArtifactResolutionService endpoint + assert!( + find_descendant(&doc, idp, NS_MD, "ArtifactResolutionService").is_some(), + "{url}: missing ArtifactResolutionService" + ); + + // Extract keys separated by intended use + let keys = extract_idp_keys(&doc, root); + + // Must have 1 or 2 signing certificates + assert!( + keys.signing.len() == 1 || keys.signing.len() == 2, + "{url}: expected 1 or 2 signing keys, got {}", + keys.signing.len() + ); + + // IdP metadata may have 0-2 encryption keys (typically 0: only SPs + // publish encryption keys so the IdP can encrypt assertions for them) + assert!( + keys.encryption.len() <= 2, + "{url}: expected at most 2 encryption keys, got {}", + keys.encryption.len() + ); + + // Every KeyDescriptor must have an explicit use attribute; a bare + // KeyDescriptor (use for both) would be a misconfiguration in TVS + for kd in descendants_by_tag(&doc, root, NS_MD, "KeyDescriptor") { + let use_attr = doc.get_attribute(kd, "use"); + assert!( + use_attr == Some("signing") || use_attr == Some("encryption"), + "{url}: KeyDescriptor has unexpected use attribute: {use_attr:?}" + ); + } + + // Metadata must be signed + let sig = find_descendant(&doc, root, NS_DSIG, "Signature") + .unwrap_or_else(|| panic!("{url}: metadata is not signed")); + + // TVS metadata signatures use KeyName: verify our derived key_name + // matches the KeyName in the Signature's KeyInfo + if let Some(key_name_node) = find_descendant(&doc, sig, NS_DSIG, "KeyName") { + let sig_key_name = inner_text(&doc, key_name_node); + let sig_key_name = sig_key_name.trim(); + assert!( + keys.signing.iter().any(|k| k.key_name == sig_key_name), + "{url}: Signature KeyName '{sig_key_name}' not found in signing KeyDescriptors" + ); + } + + // Verify the XML signature using ONLY the signing keys + let result = verify_xml_signature(xml, &keys.signing, None); + assert!( + result.is_valid(), + "{url}: signature verification with signing keys failed: {:?}", + result.errors + ); + + // If there are encryption-only keys, they must NOT verify the signature + let signing_thumbprints: Vec<&str> = keys.signing.iter().map(|k| k.key_name.as_str()).collect(); + let encryption_only: Vec<_> = keys + .encryption + .iter() + .filter(|k| !signing_thumbprints.contains(&k.key_name.as_str())) + .cloned() + .collect(); + + if !encryption_only.is_empty() { + let result = verify_xml_signature(xml, &encryption_only, None); + assert!( + !result.is_valid(), + "{url}: signature verification should fail with encryption-only keys" + ); + } +} + +#[tokio::test] +#[ignore] // requires network access; run with: cargo test --test tvs_metadata -- --ignored +async fn validate_preproduction_metadata() { + let xml = fetch_metadata(TVS_PP_METADATA_URL).await; + validate_metadata(&xml, TVS_PP_METADATA_URL); +} + +#[tokio::test] +#[ignore] // requires network access; run with: cargo test --test tvs_metadata -- --ignored +async fn validate_production_metadata() { + let xml = fetch_metadata(TVS_PROD_METADATA_URL).await; + validate_metadata(&xml, TVS_PROD_METADATA_URL); +} diff --git a/auth-service/tests/validation.rs b/auth-service/tests/validation.rs new file mode 100644 index 000000000..9344db2f2 --- /dev/null +++ b/auth-service/tests/validation.rs @@ -0,0 +1,1346 @@ +//! Validation tests for SAML Response, ArtifactResponse, and Assertion processing. +//! +//! Tests are organized by the eID SAML 4.4 specification sections they cover. +//! References like "§7.6.3.5 rule 2" point to ../eid-saml-4.4-requirements.md. +//! +//! The Assertion is not signed independently; its authenticity comes from the +//! enveloping RD signature on the ArtifactResponse (verified separately). These +//! tests pass `expected_issuer: None` to focus on the other content checks; the +//! Issuer binding has dedicated tests. + +use auth_service::{ + bindings::soap::unwrap_soap, + keys::KeyPair, + saml::{ + constants::*, + crypto::sign, + loa::MINIMUM_LOA, + validation::{ + ValidateArtifactResponseOpts, ValidateAssertionOpts, ValidateResponseOpts, + validate_artifact_response_at, validate_assertion_at, validate_response_at, + }, + xml_parser::parse, + }, +}; +use chrono::Duration; +use secrecy::ExposeSecret; + +mod common; +use common::{ + inline_signature, load_key, soap_envelope as soap_wrap, ts, validate_artifact_response, + validate_assertion, validate_response, +}; + +const RD_ENTITY_ID: &str = "urn:test:rd"; + +// --------------------------------------------------------------------------- +// Helpers +// --------------------------------------------------------------------------- + +/// Remove the first `...` element (with its children) from `xml`. +fn strip_element(xml: &str, tag: &str) -> String { + let open = format!("<{tag}"); + let close = format!(""); + let start = xml.find(&open).expect("element present"); + let end = xml[start..] + .find(&close) + .map(|i| start + i + close.len()) + .expect("close tag present"); + let mut out = xml.to_string(); + out.replace_range(start..end, ""); + out +} + +/// Build a complete, RD-signed `ArtifactResponse` SOAP envelope wrapping a valid +/// `Response` + outer RD `Assertion`, signed (enveloped) with `signing_key`. The +/// `` is inserted after the first ``, making it a direct +/// child of the ArtifactResponse, the only signature the SP verifies. +fn signed_artifact_response_soap(signing_key: &KeyPair) -> String { + // Model real TVS: namespaces are declared once on the ArtifactResponse root, + // and the inner Response/Assertion inherit them without redeclaring. (Strip + // the Assertion's own xmlns:saml so it, too, inherits.) + let now = ts(Duration::zero()); + // The outer RD Assertion carries the eID LoA (the eIDAS Substantial URL the + // RD emits, per §10.3) and our claims. + let outer = AssertionBuilder { + authn_class_ref: Some("http://eidas.europa.eu/LoA/substantial".into()), + ..AssertionBuilder::default() + } + .build() + .replace(&format!(r#" xmlns:saml="{NS_SAML}""#), ""); + // Inject an with the original AD (eIDAS) assertion as the LAST child, + // carrying conflicting Recipient / InResponseTo and a scheme-specific LoA. It + // must be ignored: claims come from the outer Assertion only. + let advice = format!( + r#"urn:test:eidas-adhttp://eidas.europa.eu/LoA/substantial"#, + bearer = SUBJECT_CONFIRMATION_BEARER, + scd = ts(Duration::minutes(2)), + ); + let assertion = outer.replace("", &format!("{advice}")); + let response = format!( + r#"{RD_ENTITY_ID}{assertion}"# + ); + // The RD-signed enveloping signature is inline, right after the + // ArtifactResponse's own Issuer (a direct child, the only signature the SP + // verifies); the signer fills the digest/signature. + let sig = inline_signature("_art1", &signing_key.cert_base64); + let artifact_response = format!( + r#"{RD_ENTITY_ID}{sig}{response}"# + ); + let signed = sign(&artifact_response, signing_key.key_pem.expose_secret()).unwrap(); + soap_wrap(&signed) +} + +const DV_ENTITY_ID: &str = "urn:test:dv"; +const ACS_URL: &str = "https://dv.example.com/acs"; + +/// Build a minimal but well-formed Assertion XML. +/// +/// Every parameter can be overridden; `None` omits the element entirely. +struct AssertionBuilder { + issuer: String, + name_id: Option, + subject_method: String, + scd_recipient: String, + scd_not_on_or_after: String, + conditions_not_before: String, + conditions_not_on_or_after: String, + audiences: Vec, + authn_class_ref: Option, + auth_authority: Option, + service_uuid: Option, + extra_attributes: String, +} + +impl Default for AssertionBuilder { + fn default() -> Self { + Self { + issuer: "urn:test:rd".into(), + name_id: Some("user-123".into()), + subject_method: SUBJECT_CONFIRMATION_BEARER.into(), + scd_recipient: ACS_URL.into(), + scd_not_on_or_after: ts(Duration::minutes(2)), + conditions_not_before: ts(-Duration::minutes(5)), + conditions_not_on_or_after: ts(Duration::minutes(5)), + audiences: vec![DV_ENTITY_ID.into()], + authn_class_ref: Some( + "urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport".into(), + ), + auth_authority: Some("urn:test:ad".into()), + service_uuid: Some("f847dc11-ac24-47b2-84a8-a057440ce56d".into()), + extra_attributes: String::new(), + } + } +} + +impl AssertionBuilder { + fn build(&self) -> String { + let name_id = self + .name_id + .as_ref() + .map(|v| format!(r#"{v}"#)) + .unwrap_or_default(); + + let audiences: String = self + .audiences + .iter() + .map(|a| format!("{a}")) + .collect(); + + let authn_class = self + .authn_class_ref + .as_ref() + .map(|v| format!("{v}")) + .unwrap_or_default(); + + let auth_authority = self + .auth_authority + .as_ref() + .map(|v| format!("{v}")) + .unwrap_or_default(); + + let service_uuid_attr = self + .service_uuid + .as_ref() + .map(|v| { + format!( + r#"{v}"# + ) + }) + .unwrap_or_default(); + + format!( + r#" +{issuer} + + {name_id} + + + + + + {audiences} + + + {authn_class}{auth_authority} + +{service_uuid_attr}{extra} +"#, + now = ts(Duration::zero()), + issuer = self.issuer, + method = self.subject_method, + scd_noa = self.scd_not_on_or_after, + recipient = self.scd_recipient, + cond_nb = self.conditions_not_before, + cond_noa = self.conditions_not_on_or_after, + extra = self.extra_attributes, + ) + } +} + +fn validate(xml: &str) -> common::AssertionResult { + validate_assertion( + xml, + ValidateAssertionOpts { + dv_entity_id: DV_ENTITY_ID, + expected_recipient: ACS_URL, + expected_issuer: None, + private_keys: &[], + minimum_loa: None, + expected_service_uuid: None, + }, + ) +} + +/// Like `validate`, but enforces the production `MINIMUM_LOA`. +fn validate_with_loa(xml: &str) -> common::AssertionResult { + validate_assertion( + xml, + ValidateAssertionOpts { + dv_entity_id: DV_ENTITY_ID, + expected_recipient: ACS_URL, + expected_issuer: None, + private_keys: &[], + minimum_loa: Some(MINIMUM_LOA), + expected_service_uuid: None, + }, + ) +} + +// =========================================================================== +// §7.6.2: Response validation +// =========================================================================== + +/// §7.6.2: A successful Response MUST contain an Assertion. +#[test] +fn response_success_with_assertion() { + let xml = format!( + r#"data"#, + now = ts(Duration::zero()) + ); + let r = validate_response(&xml); + assert!(r.valid, "errors: {:?}", r.errors); + assert!(r.assertion_xml.is_some()); +} + +/// §7.6.2: A successful Response without an Assertion is invalid. +#[test] +fn response_success_without_assertion() { + let xml = format!( + r#""# + ); + let r = validate_response(&xml); + assert!(!r.valid); + assert!(r.errors.iter().any(|e| e.contains("No Assertion"))); +} + +/// §7.6.2 / §7.8: Error status codes must be reported. +#[test] +fn response_responder_status() { + let xml = format!( + r#""# + ); + let r = validate_response(&xml); + assert!(!r.valid); + assert!(r.errors.iter().any(|e| e.contains("Responder"))); +} + +/// §7.8.1: Secondary StatusCode (e.g. AuthnFailed) must be reported. +#[test] +fn response_nested_status_code() { + let xml = format!( + r#""# + ); + let r = validate_response(&xml); + assert!(!r.valid); + assert!(r.errors.iter().any(|e| e.contains("AuthnFailed"))); +} + +/// §7.8.3: StatusMessage is included when authentication is cancelled. +#[test] +fn response_status_message_reported() { + let xml = format!( + r#"Authentication cancelled"# + ); + let r = validate_response(&xml); + assert!(!r.valid); + assert!( + r.errors + .iter() + .any(|e| e.contains("Authentication cancelled")), + "errors: {:?}", + r.errors + ); +} + +/// §7.6.2: Malformed XML must be rejected. +#[test] +fn response_malformed_xml() { + let r = validate_response("<<"#, + ); + let r = validate_artifact_response(&soap, &[], ""); + assert!(!r.valid); + assert!( + r.errors + .iter() + .any(|e| e.contains("Expected ArtifactResponse")), + "errors: {:?}", + r.errors + ); +} + +/// §7.6.1: InResponseTo must match the original ArtifactResolve ID. +#[test] +fn artifact_response_in_response_to_mismatch() { + let soap = soap_wrap(&format!( + r#""# + )); + let r = validate_artifact_response(&soap, &[], "_expected"); + assert!(!r.valid); + assert!( + r.errors.iter().any(|e| e.contains("InResponseTo mismatch")), + "errors: {:?}", + r.errors + ); +} + +/// §7.6.1: ArtifactResponse @Version is mandatory and MUST be 2.0. +#[test] +fn artifact_response_missing_version_rejected() { + let soap = soap_wrap(&format!( + r#""# + )); + let r = validate_artifact_response(&soap, &[], ""); + assert!(!r.valid); + assert!( + r.errors + .iter() + .any(|e| e.contains("missing the required @Version")), + "errors: {:?}", + r.errors + ); +} + +/// §7.6.1: ArtifactResponse with error status must be rejected. +#[test] +fn artifact_response_error_status() { + let soap = soap_wrap(&format!( + r#""# + )); + let r = validate_artifact_response(&soap, &[], ""); + assert!(!r.valid); + assert!( + r.errors + .iter() + .any(|e| e.contains("ArtifactResponse status")), + "errors: {:?}", + r.errors + ); +} + +/// §7.6.1: A successful ArtifactResponse without inner Response is an error. +/// Note: the "No Response" check only triggers when no other errors are present +/// (e.g. signature is valid). With an unsigned test document the signature error +/// masks this, so we only assert response_xml is None. +#[test] +fn artifact_response_success_without_inner_response() { + let soap = soap_wrap(&format!( + r#""# + )); + let r = validate_artifact_response(&soap, &[], ""); + assert!(!r.valid); + assert!(r.response_xml.is_none(), "should not extract a Response"); +} + +/// §7.6.1: Invalid SOAP envelope must be rejected. +#[test] +fn artifact_response_invalid_soap() { + let r = validate_artifact_response("", &[], ""); + assert!(!r.valid); + assert!( + r.errors.iter().any(|e| e.contains("SOAP")), + "errors: {:?}", + r.errors + ); +} + +/// §7.6.1: Malformed XML inside SOAP body must be rejected. +#[test] +fn artifact_response_malformed_xml() { + let soap = soap_wrap("<<= minimum, Issuer bound to the RD); the conflicting is ignored. + let mut assn_errors = Vec::new(); + let claims = validate_assertion_at( + &doc, + assertion_node, + &ValidateAssertionOpts { + dv_entity_id: DV_ENTITY_ID, + expected_recipient: ACS_URL, + expected_issuer: Some(RD_ENTITY_ID), + private_keys: &[], + minimum_loa: Some(MINIMUM_LOA), + expected_service_uuid: None, + }, + &mut assn_errors, + ); + assert!( + assn_errors.is_empty(), + "Assertion must validate: {assn_errors:?}" + ); + let claims = claims.expect("claims present"); + assert_eq!( + claims.service_uuid.as_deref(), + Some("f847dc11-ac24-47b2-84a8-a057440ce56d") + ); +} + +#[test] +fn signed_artifact_response_issuer_mismatch_rejected() { + // eID §7.6.1: a validly RD-signed ArtifactResponse whose Issuer is bound to a + // different EntityID than the pinned RD is rejected. + let rd_key = load_key("rd-signing-1"); + let soap = signed_artifact_response_soap(&rd_key); + + let doc = parse(&soap).unwrap(); + let root = doc.document_element(); + let art_node = unwrap_soap(&doc, root).expect("SOAP body unwrapped"); + + let mut errors = Vec::new(); + validate_artifact_response_at( + &doc, + art_node, + &ValidateArtifactResponseOpts { + trusted_keys: std::slice::from_ref(&rd_key), + expected_in_response_to: "", + expected_issuer: Some("urn:test:not-the-rd"), + }, + &mut errors, + ); + assert!( + errors + .iter() + .any(|e| e.contains("ArtifactResponse Issuer mismatch")), + "expected issuer mismatch, got: {errors:?}" + ); +} + +#[test] +fn signed_artifact_response_untrusted_signer_rejected() { + // The ArtifactResponse is RD-signed, but we present an unrelated trusted key + // (a different fixture cert): the envelope signature must NOT verify. + let rd_key = load_key("rd-signing-1"); + let other_key = load_key("dv-signing-1"); + let soap = signed_artifact_response_soap(&rd_key); + + let art = validate_artifact_response(&soap, std::slice::from_ref(&other_key), ""); + assert!( + !art.valid, + "ArtifactResponse signed by RD must not verify against an unrelated key" + ); +} + +// =========================================================================== +// §7.6.3.3: SubjectConfirmation +// =========================================================================== + +/// §7.6.3.3: `SubjectConfirmationData/@NotBefore` MUST NOT be used. A sender +/// adding it is not following the profile, and honouring it would widen the +/// ~2-minute bearer window, so it fails closed. +#[test] +fn assertion_subject_confirmation_not_before_rejected() { + let xml = AssertionBuilder::default().build().replace( + r#""#, + ts(Duration::zero()) + ), + "", + ); + let errs = validate(&no_instant).errors; + assert!( + errs.iter() + .any(|e| e.contains("AuthnStatement @AuthnInstant is missing")), + "{errs:?}" + ); + + let no_stmt = strip_element(&AssertionBuilder::default().build(), "saml:AuthnStatement"); + let errs = validate(&no_stmt).errors; + assert!( + errs.iter() + .any(|e| e.contains("missing the required AuthnStatement")), + "{errs:?}" + ); +} + +/// §9.5: Assertion with NotBefore in the future must be rejected. +#[test] +fn assertion_conditions_not_yet_valid() { + let xml = AssertionBuilder { + conditions_not_before: ts(Duration::hours(1)), + ..Default::default() + } + .build(); + let errs = validate(&xml).errors; + assert!( + errs.iter().any(|e| e.contains("not yet valid")), + "expected NotBefore error, got: {errs:?}" + ); +} + +/// §9.5: Assertion with NotOnOrAfter in the past must be rejected. +#[test] +fn assertion_conditions_expired() { + let xml = AssertionBuilder { + conditions_not_on_or_after: ts(-Duration::hours(1)), + ..Default::default() + } + .build(); + let errs = validate(&xml).errors; + assert!( + errs.iter() + .any(|e| e.contains("Assertion") && e.contains("expired")), + "expected Assertion expired error, got: {errs:?}" + ); +} + +/// §9.5: NotBefore within clock skew tolerance should be accepted. +#[test] +fn assertion_conditions_not_before_within_skew() { + // NotBefore is 10 seconds in the future, skew is 30 seconds, so valid. + let xml = AssertionBuilder { + conditions_not_before: ts(Duration::seconds(10)), + ..Default::default() + } + .build(); + let errs = validate(&xml).errors; + assert!( + !errs.iter().any(|e| e.contains("not yet valid")), + "within skew should be accepted, got: {errs:?}" + ); +} + +/// §9.5: NotOnOrAfter within clock skew tolerance should be accepted. +#[test] +fn assertion_conditions_not_on_or_after_within_skew() { + // NotOnOrAfter is 10 seconds in the past, skew is 30 seconds, so still valid. + let xml = AssertionBuilder { + conditions_not_on_or_after: ts(-Duration::seconds(10)), + ..Default::default() + } + .build(); + let errs = validate(&xml).errors; + assert!( + !errs.iter().any(|e| e.contains("expired")), + "within skew should be accepted, got: {errs:?}" + ); +} + +/// §9.5: Both NotBefore and NotOnOrAfter violations must be reported. +#[test] +fn assertion_conditions_both_violations() { + let xml = AssertionBuilder { + conditions_not_before: ts(Duration::hours(2)), + conditions_not_on_or_after: ts(-Duration::hours(2)), + ..Default::default() + } + .build(); + let errs = validate(&xml).errors; + assert!( + errs.iter().any(|e| e.contains("not yet valid")), + "expected NotBefore error, got: {errs:?}" + ); + assert!( + errs.iter().any(|e| e.contains("expired")), + "expected NotOnOrAfter error, got: {errs:?}" + ); +} + +// =========================================================================== +// §7.6.3.4: Claim extraction (NameID, AuthnContext, ServiceUUID) +// =========================================================================== + +/// §7.6.3.2: NameID is extracted from Subject. +#[test] +fn assertion_extracts_name_id() { + let xml = AssertionBuilder::default().build(); + let r = validate(&xml); + assert!( + r.errors.is_empty(), + "valid assertion should have no errors: {:?}", + r.errors + ); + let claims = r.claims.expect("claims present when no errors"); + assert_eq!(claims.name_id, "user-123"); +} + +/// §7.6.3: a Subject NameID whose Format is not the transient URI is rejected. +#[test] +fn assertion_non_transient_name_id_rejected() { + let xml = AssertionBuilder::default().build().replace( + NAMEID_TRANSIENT, + "urn:oasis:names:tc:SAML:2.0:nameid-format:persistent", + ); + let errs = validate(&xml).errors; + assert!( + errs.iter().any(|e| e.contains("Subject NameID Format")), + "expected transient-format rejection, got: {errs:?}" + ); +} + +/// §7.6.3 / SAML core §2.2.2: a Subject NameID with no Format attribute is +/// tolerated (Format defaults to "unspecified"). The TVS preprod IdP omits it, +/// and the acting identity comes from the decrypted EncryptedID, not this NameID. +#[test] +fn assertion_name_id_without_format_tolerated() { + let xml = AssertionBuilder::default() + .build() + .replace(&format!(r#" Format="{NAMEID_TRANSIENT}""#), ""); + let errs = validate(&xml).errors; + assert!( + !errs.iter().any(|e| e.contains("Subject NameID Format")), + "absent NameID Format should be tolerated, got: {errs:?}" + ); +} + +/// §7.6.3: a missing Assertion @Version fails closed. +#[test] +fn assertion_missing_version_rejected() { + let xml = AssertionBuilder::default() + .build() + .replace(r#" Version="2.0""#, ""); + let errs = validate(&xml).errors; + assert!( + errs.iter() + .any(|e| e.contains("missing the required @Version")), + "expected missing-Version rejection, got: {errs:?}" + ); +} + +/// §7.6.3 (cardinality 1): a missing Subject NameID fails closed. +#[test] +fn assertion_missing_name_id_rejected() { + let xml = AssertionBuilder { + name_id: None, + ..Default::default() + } + .build(); + let errs = validate(&xml).errors; + assert!( + errs.iter() + .any(|e| e.contains("missing the required Subject NameID")), + "expected missing-NameID rejection, got: {errs:?}" + ); +} + +/// §7.6.3.4: ServiceUUID attribute is extracted from AttributeStatement. +#[test] +fn assertion_extracts_service_uuid() { + let xml = AssertionBuilder::default().build(); + let r = validate(&xml); + assert!(r.errors.is_empty(), "valid assertion: {:?}", r.errors); + let claims = r.claims.expect("claims present when no errors"); + assert_eq!( + claims.service_uuid.as_deref(), + Some("f847dc11-ac24-47b2-84a8-a057440ce56d") + ); +} + +/// §7.6.3: Missing AuthnContextClassRef is not an error when no minimum is required. +#[test] +fn assertion_missing_authn_context_no_error() { + let xml = AssertionBuilder { + authn_class_ref: None, + auth_authority: None, + ..Default::default() + } + .build(); + let errs = validate(&xml).errors; + assert!(errs.is_empty(), "got: {errs:?}"); +} + +/// §7.6.3.2 / TVS T6: an AuthnContextClassRef below `MINIMUM_LOA` must be +/// rejected. With `MINIMUM_LOA = Low` (Midden), the default builder's +/// `PasswordProtectedTransport` (Basic) is too low. +#[test] +fn assertion_loa_below_minimum_rejected() { + let xml = AssertionBuilder::default().build(); + let errs = validate_with_loa(&xml).errors; + assert!( + errs.iter().any(|e| e.contains("LoA too low")), + "expected 'LoA too low' error, got: {errs:?}" + ); +} + +/// §7.6.3.2: an AuthnContextClassRef equal to `MINIMUM_LOA` is accepted. +#[test] +fn assertion_loa_equal_minimum_accepted() { + let xml = AssertionBuilder { + authn_class_ref: Some( + "urn:oasis:names:tc:SAML:2.0:ac:classes:MobileTwoFactorContract".into(), + ), + ..Default::default() + } + .build(); + let errs = validate_with_loa(&xml).errors; + assert!( + !errs.iter().any(|e| e.contains("LoA")), + "Low (Midden) should satisfy MINIMUM_LOA, got: {errs:?}" + ); +} + +/// §7.6.3.2: an AuthnContextClassRef above `MINIMUM_LOA` is accepted. +/// Uses the eIDAS URL spelling the RD actually emits (eID §10.3). +#[test] +fn assertion_loa_above_minimum_accepted() { + let xml = AssertionBuilder { + authn_class_ref: Some("http://eidas.europa.eu/LoA/high".into()), + ..Default::default() + } + .build(); + let errs = validate_with_loa(&xml).errors; + assert!( + !errs.iter().any(|e| e.contains("LoA")), + "High should satisfy MINIMUM_LOA, got: {errs:?}" + ); +} + +/// §7.6.3.2 / §10.3: the eIDAS Substantial URL the RD emits maps to +/// `Substantial` (above the `Low` minimum) and is accepted: this is the value a +/// real TVS eIDAS login sends. +#[test] +fn assertion_loa_eidas_substantial_accepted() { + let xml = AssertionBuilder { + authn_class_ref: Some("http://eidas.europa.eu/LoA/substantial".into()), + ..Default::default() + } + .build(); + let errs = validate_with_loa(&xml).errors; + assert!( + !errs.iter().any(|e| e.contains("LoA")), + "eIDAS Substantial should satisfy MINIMUM_LOA, got: {errs:?}" + ); +} + +/// A LoA URI not listed in eID §10.3 is rejected. +#[test] +fn assertion_loa_unrecognized_uri_rejected() { + let xml = AssertionBuilder { + authn_class_ref: Some("urn:bogus:loa".into()), + ..Default::default() + } + .build(); + let errs = validate_with_loa(&xml).errors; + assert!( + errs.iter().any(|e| e.contains("Unrecognized LoA")), + "expected 'Unrecognized LoA' error, got: {errs:?}" + ); +} + +/// §7.6.3.2: a missing AuthnContextClassRef IS an error when a minimum is required. +#[test] +fn assertion_missing_authn_context_with_minimum_rejected() { + let xml = AssertionBuilder { + authn_class_ref: None, + auth_authority: None, + ..Default::default() + } + .build(); + let errs = validate_with_loa(&xml).errors; + assert!( + errs.iter() + .any(|e| e.contains("missing the required AuthnContextClassRef")), + "expected missing-AuthnContextClassRef error, got: {errs:?}" + ); +} + +// =========================================================================== +// §7.6.3: Multiple simultaneous errors +// =========================================================================== + +/// All validation errors must be collected, not short-circuited. +#[test] +fn assertion_collects_all_errors() { + let xml = AssertionBuilder { + subject_method: "urn:wrong".into(), + scd_recipient: "https://evil.example.com".into(), + scd_not_on_or_after: ts(-Duration::hours(1)), + conditions_not_before: ts(Duration::hours(1)), + conditions_not_on_or_after: ts(-Duration::hours(1)), + audiences: vec!["urn:other".into()], + ..Default::default() + } + .build(); + let errs = validate(&xml).errors; + + assert!( + errs.iter().any(|e| e.contains("bearer")), + "missing bearer: {errs:?}" + ); + assert!( + errs.iter().any(|e| e.contains("Recipient mismatch")), + "missing recipient: {errs:?}" + ); + assert!( + errs.iter() + .any(|e| e.contains("SubjectConfirmation") && e.contains("expired")), + "missing scd expiry: {errs:?}" + ); + assert!( + errs.iter().any(|e| e.contains("not yet valid")), + "missing NotBefore: {errs:?}" + ); + assert!( + errs.iter() + .any(|e| e.contains("Assertion") && e.contains("expired")), + "missing conditions expiry: {errs:?}" + ); + assert!( + errs.iter().any(|e| e.contains("AudienceRestriction")), + "missing audience: {errs:?}" + ); +} + +/// §7.6.3.5 rule 7: an invalid assertion records errors and yields no claims. +#[test] +fn assertion_invalid_produces_no_claims() { + let xml = AssertionBuilder { + audiences: vec!["urn:wrong".into()], + ..Default::default() + } + .build(); + let r = validate(&xml); + assert!(!r.errors.is_empty()); + assert!( + r.claims.is_none(), + "invalid assertion must not produce claims" + ); +} + +// =========================================================================== +// §7.6.3: Malformed input +// =========================================================================== + +/// Malformed XML must be rejected. +#[test] +fn assertion_malformed_xml() { + let r = validate("<<"# + ); + let r = validate_response(&xml); + assert!(!r.valid); + assert!(r.errors.iter().any(|e| e.contains("Requester"))); +} + +/// §7.8.2: NoAuthnContext secondary status code. +#[test] +fn response_no_authn_context_status() { + let xml = format!( + r#""# + ); + let r = validate_response(&xml); + assert!(!r.valid); + assert!( + r.errors.iter().any(|e| e.contains("NoAuthnContext")), + "errors: {:?}", + r.errors + ); +} + +/// §7.8.2: RequestDenied secondary status code. +#[test] +fn response_request_denied_status() { + let xml = format!( + r#""# + ); + let r = validate_response(&xml); + assert!(!r.valid); + assert!( + r.errors.iter().any(|e| e.contains("RequestDenied")), + "errors: {:?}", + r.errors + ); +} + +/// §7.6.2: a non-Success status invalidates the Response even when an +/// Assertion is present. +#[test] +fn response_error_status_rejected() { + let xml = format!( + r#"data"# + ); + let r = validate_response(&xml); + assert!(!r.valid); + // Assertion XML is still extracted even on error (it's used for diagnostics), + // but the result is not valid + assert!(r.errors.iter().any(|e| e.contains("Responder"))); +} + +// =========================================================================== +// §7.6.1: inner-Response extraction from an ArtifactResponse +// =========================================================================== + +/// §7.6.1 (quoting SAML-bindings §3.6.6): "Even if the ArtifactResponse's Status +/// indicates Success, it may still not contain a Response if the artifact +/// requester is not authorized or the artifact is no longer valid." That case +/// must be an error, not a silently empty success. +#[test] +fn artifact_response_success_without_response_is_rejected() { + let soap = soap_wrap(&format!( + r#"{RD_ENTITY_ID}"#, + now = ts(Duration::zero()) + )); + let art = validate_artifact_response(&soap, &[], ""); + assert!( + art.errors + .iter() + .any(|e| e.contains("reports Success but carries no Response")), + "expected the Success-without-Response rejection, got: {:?}", + art.errors + ); +} + +/// §7.6.1: two Response children are ambiguous; we consume only the first, so an +/// appended second one must be rejected rather than ignored. +#[test] +fn artifact_response_with_two_responses_is_rejected() { + let inner = format!( + r#""#, + now = ts(Duration::zero()) + ); + let soap = soap_wrap(&format!( + r#"{RD_ENTITY_ID}{first}{second}"#, + now = ts(Duration::zero()), + first = inner.replace("{n}", "1"), + second = inner.replace("{n}", "2"), + )); + let art = validate_artifact_response(&soap, &[], ""); + assert!( + art.errors.iter().any(|e| e.contains("2 Response elements")), + "expected the multiple-Response rejection, got: {:?}", + art.errors + ); +} + +/// §7.6.1: Successful ArtifactResponse extracts inner Response XML preserving +/// namespace prefixes (required for signature verification on the inner Response). +#[test] +fn artifact_response_extracts_inner_response() { + let inner_response = format!( + r#""# + ); + let soap = soap_wrap(&format!( + r#"{inner_response}"# + )); + // Signature will fail, but Response extraction should succeed + let r = validate_artifact_response(&soap, &[], ""); + assert!( + r.response_xml.is_some(), + "inner Response must be extracted, errors: {:?}", + r.errors + ); + let resp = r.response_xml.unwrap(); + assert!( + resp.contains("samlp:Response"), + "must preserve namespace prefix" + ); +} diff --git a/auth-service/tests/xsw_exploit_check.rs b/auth-service/tests/xsw_exploit_check.rs new file mode 100644 index 000000000..ff1c5714d --- /dev/null +++ b/auth-service/tests/xsw_exploit_check.rs @@ -0,0 +1,138 @@ +//! Test for an XML Signature Wrapping bypass via XML-comment injection. +//! +//! Threat: an attacker able to mutate the (post-signature) back-channel bytes +//! without the RD signing key hides a forged Response/Assertion inside an XML +//! comment. Exclusive-c14n excludes comments from the signature digest, so the RD +//! signature still verifies. A comment-blind string slicer could carve the forged +//! element out of the comment interior, accepting an attacker-chosen NameID. +//! +//! The whole signed document is parsed exactly once and the validators navigate +//! that single tree (`child_element` plus node lookups); the parser ignores +//! comments, so the forgery is never materialized and the genuine signed element +//! is what gets processed. These tests assert the attacker NameID is rejected (the +//! genuine subject wins). + +mod common; +use common::{ + ACS, BEARER, DV, RD, SAML, SAMLP, SUCCESS, inline_signature as sig_template, load_key, + response, run_chain, soap_envelope as soap, ts, +}; + +use auth_service::saml::crypto::sign; +use chrono::Duration; +use secrecy::ExposeSecret; + +/// A genuine signed Response yields the genuine NameID. +#[test] +fn baseline_genuine_response_is_accepted() { + let rd_key = load_key("rd-signing-1"); + let genuine = response("1", "GENUINE-USER"); + let sig = sig_template("_art1", &rd_key.cert_base64); + let art = format!( + r#"{RD}{sig}{genuine}"#, + now = ts(Duration::zero()) + ); + let signed = sign(&art, rd_key.key_pem.expose_secret()).unwrap(); + let soap = soap(&signed); + let result = run_chain(&soap, &rd_key); + assert_eq!( + result.accepted.as_deref(), + Some("GENUINE-USER"), + "errors: {:?}", + result.errors + ); +} + +/// A forged Response hidden in a leading comment must not override the genuine one. +#[test] +fn comment_injection_xsw_attempt() { + let rd_key = load_key("rd-signing-1"); + let genuine = response("1", "GENUINE-USER"); + let forged = response("9", "ATTACKER-CHOSEN-VICTIM"); + + let sig = sig_template("_art1", &rd_key.cert_base64); + let art = format!( + r#"{RD}{sig}{genuine}"#, + now = ts(Duration::zero()) + ); + // Sign the GENUINE document with the real RD key. + let signed = sign(&art, rd_key.key_pem.expose_secret()).unwrap(); + + // ATTACKER (post-signing, no signing key): inject a comment carrying the forged + // Response right before the genuine one. Exclusive-c14n excludes comments, so + // the signature is unaffected. + let injected = signed.replace(&genuine, &format!("{genuine}")); + assert_ne!(injected, signed, "injection must have landed"); + let soap = soap(&injected); + + let result = run_chain(&soap, &rd_key); + assert_ne!( + result.accepted.as_deref(), + Some("ATTACKER-CHOSEN-VICTIM"), + "SECURITY: comment-injection XSW accepted the attacker NameID" + ); + // The comment is invisible to the parser, so the GENUINE subject is what the + // chain accepts: the forgery is not merely rejected, it never materializes. + assert_eq!( + result.accepted.as_deref(), + Some("GENUINE-USER"), + "the genuine subject must still be accepted, errors: {:?}", + result.errors + ); +} + +/// A forged Response that closes cleanly with NO nested Assertion, so a +/// string-slicing extractor would pair it with the sibling forged Assertion +/// below. +fn forged_response_no_assertion() -> String { + format!( + r#"{RD}"#, + issued = ts(-Duration::minutes(5)) + ) +} + +fn forged_assertion(name_id: &str) -> String { + let issued = ts(-Duration::minutes(5)); + let nb = ts(-Duration::minutes(5)); + let noa = ts(Duration::minutes(5)); + let scd = ts(Duration::minutes(2)); + format!( + r#"{RD}{name_id}{DV}http://eidas.europa.eu/LoA/substantial"# + ) +} + +/// The sibling forged-Response + forged-Assertion comment layout must also fail. +#[test] +fn comment_injection_xsw_sibling_attempt() { + let rd_key = load_key("rd-signing-1"); + let genuine = response("1", "GENUINE-USER"); + // Sibling layout: forged Response (clean close) + forged Assertion, both in a + // comment, ahead of the genuine Response. + let forged = format!( + "{}{}", + forged_response_no_assertion(), + forged_assertion("ATTACKER-CHOSEN-VICTIM") + ); + + let sig = sig_template("_art1", &rd_key.cert_base64); + let art = format!( + r#"{RD}{sig}{genuine}"#, + now = ts(Duration::zero()) + ); + let signed = sign(&art, rd_key.key_pem.expose_secret()).unwrap(); + let injected = signed.replace(&genuine, &format!("{genuine}")); + assert_ne!(injected, signed, "injection must have landed"); + + let result = run_chain(&soap(&injected), &rd_key); + assert_ne!( + result.accepted.as_deref(), + Some("ATTACKER-CHOSEN-VICTIM"), + "SECURITY: sibling comment-injection XSW accepted the attacker NameID" + ); + assert_eq!( + result.accepted.as_deref(), + Some("GENUINE-USER"), + "the genuine subject must still be accepted, errors: {:?}", + result.errors + ); +} diff --git a/auth-service/tests/xsw_parser_divergence.rs b/auth-service/tests/xsw_parser_divergence.rs new file mode 100644 index 000000000..0aca4fa19 --- /dev/null +++ b/auth-service/tests/xsw_parser_divergence.rs @@ -0,0 +1,348 @@ +//! The signed bytes are parsed three times: by `run_chain`'s tree (roxmltree), +//! again inside `verify_xml_signature`, and once more by the crypto backend +//! (`uppsala`). Any construct those parsers read differently is a signature +//! wrapping vector: the digest covers one thing and the claims come from another. +//! +//! Every case here must either be rejected or yield exactly the genuine claims. +//! Accepting an attacker-chosen NameID is the failure this suite exists to catch. + +mod common; +use common::{ + RD, SAML, SAMLP, SUCCESS, inline_signature as sig_template, load_key, response, run_chain, + soap_envelope as soap, ts, +}; + +use auth_service::{keys::KeyPair, saml::crypto::sign}; +use chrono::Duration; +use secrecy::ExposeSecret; + +const GENUINE: &str = "GENUINE-USER"; +const ATTACKER: &str = "ATTACKER-CHOSEN-VICTIM"; + +/// A genuine RD-signed ArtifactResponse, plus its unsigned template. +/// +/// `art_attrs` is spliced into the ArtifactResponse start tag and `extra_body` +/// after the Status, so a case can add ID attributes or sibling elements before +/// the RD signs (a signed document) or after (a tampered one). +fn artifact_response(art_attrs: &str, extra_body: &str, name_id: &str) -> String { + let now = ts(Duration::zero()); + let body = response("1", name_id); + let sig = sig_template("_art1", &load_key("rd-signing-1").cert_base64); + format!( + r#"{RD}{sig}{extra_body}{body}"# + ) +} + +fn signed_artifact_response(rd_key: &KeyPair, art_attrs: &str, extra_body: &str) -> String { + let xml = artifact_response(art_attrs, extra_body, GENUINE); + sign(&xml, rd_key.key_pem.expose_secret()).expect("RD signing") +} + +/// Assert the chain either rejected `soap_xml` or read the genuine NameID from +/// it. Never the attacker's, and never a partially-trusted mixture. +fn assert_rejected_or_genuine(label: &str, soap_xml: &str, rd_key: &KeyPair) { + let result = run_chain(soap_xml, rd_key); + assert_ne!( + result.accepted.as_deref(), + Some(ATTACKER), + "SECURITY [{label}]: attacker NameID accepted, errors: {:?}", + result.errors + ); + if let Some(accepted) = result.accepted.as_deref() { + assert_eq!( + accepted, GENUINE, + "[{label}]: accepted an unexpected NameID, errors: {:?}", + result.errors + ); + } +} + +/// Positive control. If this breaks, every rejection below is meaningless. +#[test] +fn baseline_signed_artifact_response_is_accepted() { + let rd_key = load_key("rd-signing-1"); + let signed = signed_artifact_response(&rd_key, "", ""); + let result = run_chain(&soap(&signed), &rd_key); + assert_eq!( + result.accepted.as_deref(), + Some(GENUINE), + "errors: {:?}", + result.errors + ); +} + +/// Two elements carrying the referenced ID make `#_art1` ambiguous: the backend +/// could digest one while the chain reads the other. +#[test] +fn duplicate_id_value_is_rejected() { + let rd_key = load_key("rd-signing-1"); + let signed = signed_artifact_response(&rd_key, "", ""); + let tampered = signed.replace( + r#""#, + r#""#, + ); + assert_ne!(tampered, signed, "tampering must apply"); + + let result = run_chain(&soap(&tampered), &rd_key); + assert!(result.accepted.is_none(), "errors: {:?}", result.errors); + assert!( + result + .errors + .iter() + .any(|e| e.contains("carry the ID") || e.contains("duplicate ID")), + "expected an ID-uniqueness rejection, got: {:?}", + result.errors + ); +} + +/// The same value under a different ID-ish attribute name is still a collision: +/// our attribute set has to match the one the backend resolves references with. +#[test] +fn duplicate_id_under_another_id_attribute_is_rejected() { + let rd_key = load_key("rd-signing-1"); + for attr in ["Id", "id", "AssertionID", "xml:id"] { + let signed = signed_artifact_response(&rd_key, "", ""); + let tampered = signed.replace( + r#""#, + &format!(r#""#), + ); + let result = run_chain(&soap(&tampered), &rd_key); + assert!( + result.accepted.is_none(), + "{attr}: a second element carrying the referenced ID must be rejected, errors: {:?}", + result.errors + ); + } +} + +/// `xml:id` is registered as an ID by the backend twice over (its attribute +/// lookup matches by local name, so `xml:id` also answers to `id`), which makes +/// it collide with itself. Any document carrying one therefore fails closed, and +/// no `xml:id` alias can make a reference resolve away from the root. +#[test] +fn xml_id_on_the_root_fails_closed() { + let rd_key = load_key("rd-signing-1"); + let signed = signed_artifact_response(&rd_key, "", ""); + let tampered = signed.replacen(r#"ID="_art1""#, r#"ID="_art1" xml:id="_alias""#, 1); + assert_ne!(tampered, signed, "tampering must apply"); + + let result = run_chain(&soap(&tampered), &rd_key); + assert!( + result.accepted.is_none(), + "an xml:id alias on the signed root must not be accepted, errors: {:?}", + result.errors + ); +} + +/// Comments are excluded from both exclusive c14n and the parsed tree, so a +/// comment splitting an identity string must not smuggle one past either. +#[test] +fn comments_inside_identity_text_do_not_forge_a_value() { + let rd_key = load_key("rd-signing-1"); + let cases = [ + ("Issuer", format!("{RD}"), format!("{RD}EXTRA")), + ( + "NameID", + format!(r#"{GENUINE}"#), + format!(r#"{GENUINE}{ATTACKER}"#), + ), + ( + "AuthnContextClassRef", + "http://eidas.europa.eu/LoA/substantial".to_string(), + "http://eidas.europa.eu/LoA/low".to_string(), + ), + ]; + + for (label, from, to) in cases { + let signed = signed_artifact_response(&rd_key, "", ""); + let tampered = signed.replacen(&from, &to, 1); + assert_ne!(tampered, signed, "[{label}] tampering must apply"); + // Tampering after signing breaks the digest, so this must be rejected; + // the point is that it is never *silently* read differently. + let result = run_chain(&soap(&tampered), &rd_key); + assert!( + result.accepted.is_none(), + "[{label}]: comment-split identity text was accepted, errors: {:?}", + result.errors + ); + } +} + +/// An identity wrapped in a child element is not that identity: `direct_text` +/// must not fold child text into `Issuer` / `NameID` / `AuthnContextClassRef`. +#[test] +fn identity_text_inside_a_child_element_is_not_read_as_the_identity() { + let rd_key = load_key("rd-signing-1"); + // Signed *with* the nested shape, so the digest is valid and the only thing + // that can reject it is the extraction rule itself. + let now = ts(Duration::zero()); + let body = response("1", GENUINE); + let sig = sig_template("_art1", &rd_key.cert_base64); + let nested_issuer = format!( + r#"{RD}{sig}{body}"# + ); + let signed = sign(&nested_issuer, rd_key.key_pem.expose_secret()).expect("RD signing"); + + let result = run_chain(&soap(&signed), &rd_key); + assert!( + result.accepted.is_none(), + "an Issuer whose text lives in a child element must not satisfy the RD binding" + ); + assert!( + result + .errors + .iter() + .any(|e| e.contains("Issuer contains child elements")), + "expected the direct-text rejection, got: {:?}", + result.errors + ); +} + +/// A DTD is the classic entity-expansion and XXE surface, and the two parsers +/// need not agree on entity handling. `allow_dtd` is off, so it never parses. +#[test] +fn doctype_with_internal_entity_is_rejected() { + let rd_key = load_key("rd-signing-1"); + let signed = signed_artifact_response(&rd_key, "", ""); + let with_doctype = format!( + r#"]>{}"#, + soap(&signed) + ); + let result = run_chain(&with_doctype, &rd_key); + assert!( + result.accepted.is_none(), + "a document with a DTD must not be processed, errors: {:?}", + result.errors + ); +} + +/// CDATA and character references are alternative spellings of text. Both +/// parsers must agree, so an identity spelled either way is never a second, +/// different identity. +#[test] +fn cdata_and_character_references_do_not_forge_an_identity() { + let rd_key = load_key("rd-signing-1"); + let spellings = [ + format!(""), + // "ATTACKER-CHOSEN-VICTIM" with the leading 'A' as a character reference. + "ATTACKER-CHOSEN-VICTIM".to_string(), + "ATTACKER-CHOSEN-VICTIM".to_string(), + ]; + for spelling in spellings { + let signed = signed_artifact_response(&rd_key, "", ""); + let tampered = signed.replacen(GENUINE, &spelling, 1); + assert_rejected_or_genuine(&format!("spelling {spelling}"), &soap(&tampered), &rd_key); + } +} + +/// Whitespace in base64 is legal and must be tolerated the same way by the +/// structural checks and the backend: either it verifies or it fails, never a +/// silent pass on a different digest. +#[test] +fn whitespace_in_signature_and_digest_values_is_not_exploitable() { + let rd_key = load_key("rd-signing-1"); + let signed = signed_artifact_response(&rd_key, "", ""); + + // Split the SignatureValue across lines, as many signers do. + let sig_start = signed.find("").unwrap() + "".len(); + let sig_end = signed.find("").unwrap(); + let value = &signed[sig_start..sig_end]; + let wrapped: String = value + .as_bytes() + .chunks(64) + .map(|c| format!("{}\n", std::str::from_utf8(c).unwrap())) + .collect(); + let reformatted = format!( + "{}\n{}{}", + &signed[..sig_start], + wrapped, + &signed[sig_end..] + ); + + let result = run_chain(&soap(&reformatted), &rd_key); + assert_ne!(result.accepted.as_deref(), Some(ATTACKER)); + assert_eq!( + result.accepted.as_deref(), + Some(GENUINE), + "a line-wrapped SignatureValue must still verify, errors: {:?}", + result.errors + ); +} + +/// A `Signature` nested inside `Advice` sits earlier in document order than the +/// enveloping one, and the backend verifies the first it finds. +#[test] +fn signature_nested_in_advice_before_the_enveloping_one_is_rejected() { + let rd_key = load_key("rd-signing-1"); + let signed = signed_artifact_response(&rd_key, "", ""); + let stolen_sig = { + let start = signed.find("").unwrap() + "".len(); + signed[start..end].to_string() + }; + // Place a copy of the genuine signature before the enveloping one. + let tampered = signed.replacen( + r#""#, + &format!(r#"{stolen_sig}"#), + 1, + ); + assert_ne!(tampered, signed, "tampering must apply"); + + let result = run_chain(&soap(&tampered), &rd_key); + assert!(result.accepted.is_none(), "errors: {:?}", result.errors); + assert!( + result.errors.iter().any(|e| e.contains("wrapping")), + "expected a wrapping rejection, got: {:?}", + result.errors + ); +} + +/// The ArtifactResponse slice is verified as a standalone document. When the RD +/// declares the namespaces on the envelope instead of the element, the inherited +/// declarations are restored, and the result must be identical to the +/// self-contained form. +#[test] +fn namespaces_declared_on_the_envelope_still_verify() { + let rd_key = load_key("rd-signing-1"); + let now = ts(Duration::zero()); + let body = response("1", GENUINE); + let sig = sig_template("_art1", &rd_key.cert_base64); + + // The ArtifactResponse uses samlp:/saml: without declaring them; the envelope + // does. The RD signs the whole envelope's inner element in that scope. + let envelope = format!( + r#"{RD}{sig}{body}"# + ); + let signed = sign(&envelope, rd_key.key_pem.expose_secret()).expect("RD signing"); + + let result = run_chain(&signed, &rd_key); + assert_eq!( + result.accepted.as_deref(), + Some(GENUINE), + "envelope-declared namespaces must verify via the reconstruction, errors: {:?}", + result.errors + ); +} + +/// The reconstruction must not rescue a forgery: same envelope-level namespaces, +/// but the NameID is changed after signing. +#[test] +fn namespace_reconstruction_does_not_rescue_a_tampered_document() { + let rd_key = load_key("rd-signing-1"); + let now = ts(Duration::zero()); + let body = response("1", GENUINE); + let sig = sig_template("_art1", &rd_key.cert_base64); + let envelope = format!( + r#"{RD}{sig}{body}"# + ); + let signed = sign(&envelope, rd_key.key_pem.expose_secret()).expect("RD signing"); + let tampered = signed.replacen(GENUINE, ATTACKER, 1); + assert_ne!(tampered, signed, "tampering must apply"); + + let result = run_chain(&tampered, &rd_key); + assert!( + result.accepted.is_none(), + "SECURITY: reconstruction accepted a tampered document, errors: {:?}", + result.errors + ); +} diff --git a/auth-service/tests/xsw_sibling_poc.rs b/auth-service/tests/xsw_sibling_poc.rs new file mode 100644 index 000000000..c157a6b06 --- /dev/null +++ b/auth-service/tests/xsw_sibling_poc.rs @@ -0,0 +1,97 @@ +//! PoC: XML Signature Wrapping via a detached (sibling) reference. +//! +//! The RD signs an ArtifactResponse with an enveloped signature referencing the +//! ArtifactResponse @ID. An attacker who controls the response bytes (front-channel +//! LogoutResponse, or a subverted back-channel) rebuilds the document so the +//! genuine Signature becomes a *sibling* of a reconstructed copy of the genuine +//! signed element, and wraps a FORGED ArtifactResponse/Response around them as the +//! new document root. The signature's reference still resolves to the sibling +//! (whose digest is unchanged) while the validators read the forged root, so +//! the chain MUST reject the document: the §7.6.1 root-coverage check requires +//! every Reference to target the consumed root element. + +mod common; +use common::{ + RD, SAML, SAMLP, SUCCESS, inline_signature as sig_template, load_key, response, run_chain, + soap_envelope as soap, ts, +}; + +use auth_service::saml::crypto::sign; +use chrono::Duration; +use secrecy::ExposeSecret; + +#[test] +fn sibling_wrapping_xsw() { + let rd_key = load_key("rd-signing-1"); + let now = ts(Duration::zero()); + + // 1. Genuine ArtifactResponse, signed by the real RD key (reference -> _art1, + // enveloped). This is what an attacker can capture (or, for metadata/SLO, is + // simply public). + let genuine_inner_body = response("1", "GENUINE-USER"); + let sig = sig_template("_art1", &rd_key.cert_base64); + let genuine = format!( + r#"{RD}{sig}{genuine_inner_body}"# + ); + let signed = sign(&genuine, rd_key.key_pem.expose_secret()).unwrap(); + + // 2. Pull the now-filled element out of the signed document. + let sig_start = signed.find("").unwrap() + "".len(); + let filled_sig = &signed[sig_start..sig_end]; + + // 3. Rebuild inner _art1 WITHOUT its Signature child (the enveloped transform + // removed the signature when computing the genuine digest, so the digest over + // this signature-less copy is identical). + let inner_no_sig = format!( + r#"{RD}{genuine_inner_body}"# + ); + + // 4. Forged outer ArtifactResponse: the moved genuine Signature (still + // referencing _art1) + a FORGED Response, with the reconstructed genuine + // _art1 as a sibling so the reference still resolves and digests cleanly. + let forged_body = response("F", "ATTACKER-CHOSEN-VICTIM"); + let outer = format!( + r#"{RD}{filled_sig}{forged_body}{inner_no_sig}"# + ); + + let result = run_chain(&soap(&outer), &rd_key); + assert_ne!( + result.accepted.as_deref(), + Some("ATTACKER-CHOSEN-VICTIM"), + "SECURITY: sibling-wrapping XSW accepted the attacker NameID" + ); + // Pin *why* it was rejected: the §7.6.1 root-coverage check, not an + // incidental fixture problem. Without this the test would still pass if the + // chain broke for an unrelated reason. + assert!( + result + .errors + .iter() + .any(|e| e.contains("does not target the signed root element")), + "expected the Reference/root-coverage rejection, got: {:?}", + result.errors + ); +} + +/// Positive control: the same builder, unwrapped, must be accepted. Guards the +/// test above from silently passing because its fixture rotted. +#[test] +fn baseline_genuine_artifact_response_is_accepted() { + let rd_key = load_key("rd-signing-1"); + let now = ts(Duration::zero()); + let genuine_inner_body = response("1", "GENUINE-USER"); + let sig = sig_template("_art1", &rd_key.cert_base64); + let genuine = format!( + r#"{RD}{sig}{genuine_inner_body}"# + ); + let signed = sign(&genuine, rd_key.key_pem.expose_secret()).unwrap(); + + let result = run_chain(&soap(&signed), &rd_key); + assert_eq!( + result.accepted.as_deref(), + Some("GENUINE-USER"), + "genuine document must be accepted, errors: {:?}", + result.errors + ); +} diff --git a/deny.toml b/deny.toml index 0c0e82e23..3d22f9ff7 100644 --- a/deny.toml +++ b/deny.toml @@ -38,8 +38,7 @@ multiple-versions = "allow" [advisories] ignore = [ - { id = "RUSTSEC-2026-0194", reason = "quick-xml <0.41 DoS (quadratic duplicate-attribute check) pulled transitively by eml-nl; no release depending on quick-xml >=0.41 available upstream yet, see https://github.com/tafia/quick-xml/issues/969" }, - { id = "RUSTSEC-2026-0195", reason = "quick-xml <0.41 DoS (unbounded namespace-declaration allocation) pulled transitively by eml-nl; no release depending on quick-xml >=0.41 available upstream yet, see https://github.com/tafia/quick-xml/issues/969" }, + { id = "RUSTSEC-2023-0071", reason = "rsa 0.9 pulled transitively by bergshamra-crypto (XML-DSig/XML-Enc); no patched release available upstream (rsa 0.10 is still a release candidate). The only private-key operation is RSA-OAEP unwrapping of an EncryptedID, which is reached only after the RD signature on the ArtifactResponse has been verified, so a chosen-ciphertext timing oracle is not reachable. Revisit when bergshamra publishes a release on a constant-time rsa." }, ] [sources] diff --git a/sigrid.yaml b/sigrid.yaml index bb1f2de86..e7f2554ef 100644 --- a/sigrid.yaml +++ b/sigrid.yaml @@ -18,7 +18,6 @@ exclude: - ".*[.]md" - ".*/deploy/.*" - ".*/development/.*" - - ".*/auth-service/.*" - ".*/docs/.*" - ".*/frontend/fonts/.*" - ".*/frontend/icons/.*"