From 80fab0367c009e5be98ba90599b389035b3b941a Mon Sep 17 00:00:00 2001 From: Dmitry Prudnikov Date: Thu, 13 Aug 2026 21:27:32 +0300 Subject: [PATCH 01/27] feat(cli): add native xmlsec1 command - Add fail-closed command parsing, capability queries, key loading, signing, verification, encryption, decryption, and key generation - Exercise the binary through process tests and unmodified upstream runners - Reconcile the compatibility ledger and document the supported surface Closes #112 --- Cargo.toml | 2 +- README.md | 18 + compatibility/libxmlsec1-1.3.13-rules.json | 31 +- compatibility/libxmlsec1-1.3.13.json | 115 ++-- docs/cli.md | 75 +++ tests/capability_ledger.rs | 51 +- tools/xmlsec1/Cargo.toml | 30 + tools/xmlsec1/README.md | 19 + tools/xmlsec1/src/args.rs | 320 +++++++++++ tools/xmlsec1/src/capabilities.rs | 75 +++ tools/xmlsec1/src/commands.rs | 638 +++++++++++++++++++++ tools/xmlsec1/src/key_material.rs | 166 ++++++ tools/xmlsec1/src/lib.rs | 32 ++ tools/xmlsec1/src/main.rs | 9 + tools/xmlsec1/tests/process_contract.rs | 361 ++++++++++++ tools/xmlsec1/tests/upstream_runner.rs | 54 ++ 16 files changed, 1939 insertions(+), 57 deletions(-) create mode 100644 docs/cli.md create mode 100644 tools/xmlsec1/Cargo.toml create mode 100644 tools/xmlsec1/README.md create mode 100644 tools/xmlsec1/src/args.rs create mode 100644 tools/xmlsec1/src/capabilities.rs create mode 100644 tools/xmlsec1/src/commands.rs create mode 100644 tools/xmlsec1/src/key_material.rs create mode 100644 tools/xmlsec1/src/lib.rs create mode 100644 tools/xmlsec1/src/main.rs create mode 100644 tools/xmlsec1/tests/process_contract.rs create mode 100644 tools/xmlsec1/tests/upstream_runner.rs diff --git a/Cargo.toml b/Cargo.toml index 2b7e7be..8bc743a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,7 +13,7 @@ categories = ["cryptography", "web-programming", "authentication"] readme = "README.md" [workspace] -members = [".", "tools/capability-ledger"] +members = [".", "tools/capability-ledger", "tools/xmlsec1"] resolver = "3" [[example]] diff --git a/README.md b/README.md index 167075f..fb0cfd9 100644 --- a/README.md +++ b/README.md @@ -20,6 +20,7 @@ XML Security in pure Rust, built to replace libxmlsec1. - **XMLDSig** — XML Digital Signatures (verify and signing pipelines, X.509 `KeyInfo`, and xmlsec1 CLI interoperability) - **XMLEnc** — XML Encryption encrypt/decrypt pipelines (direct, RSA-OAEP, and AES-KW keys) - **X.509** — Certificate-based key extraction and validation +- **Native CLI** — `xmlsec1` command surface backed by the same Rust policy and provider pipelines ## Why? @@ -65,6 +66,23 @@ complete upstream 1.3.13 public surface as generated, evidence-linked data. It separates implemented wire behavior from policy-gated compatibility, planned parity work, provider-specific differences, and the not-yet-implemented C ABI. +## Native CLI + +Install the command-line package and inspect its runtime capability registry: + +```sh +cargo install xmlsec1-cli +xmlsec1 version +xmlsec1 list-transforms +xmlsec1 list-key-data +``` + +The native binary supports sign/verify, encrypt/decrypt, AES key generation, +capability checks, libxmlsec1 option syntax, and deterministic process statuses. +Unsupported algorithms, key formats, providers, and policy controls fail closed +instead of being silently ignored. See the [CLI compatibility guide](docs/cli.md) +for commands, examples, current format coverage, and upstream runner validation. + ## XMLDSig Usage `examples/sign.rs` builds an enveloped RSA-SHA256 signature and `examples/verify.rs` diff --git a/compatibility/libxmlsec1-1.3.13-rules.json b/compatibility/libxmlsec1-1.3.13-rules.json index 00029d7..9b89d25 100644 --- a/compatibility/libxmlsec1-1.3.13-rules.json +++ b/compatibility/libxmlsec1-1.3.13-rules.json @@ -25,6 +25,10 @@ "test": "capability_ledger::backend_surface_distinguishes_provider_capabilities_from_unimplemented_apis", "description": "Only backend transform classes backed by native provider operations are provider-limited; every other backend-specific C entry point remains planned." }, + "native-cli-tests": { + "test": "capability_ledger::native_cli_claims_match_process_and_upstream_runner_tests", + "description": "Process tests cover native command parsing, exit statuses, sign/verify and encrypt/decrypt paths; the unmodified upstream DSig, Enc, and Keys runners exercise the same binary and capability registry." + }, "planned-surface": { "test": "capability_ledger::planned_surface_is_never_reported_as_supported", "description": "Unimplemented backend APIs, CLI, registry, format, URI, and donor-suite entries remain machine-readable planned work." @@ -110,11 +114,34 @@ "rationale": "libxmlsec1 registry entry points are inventoried for the future compatibility layer and are not exposed by the native API.", "evidence": "planned-surface" }, + { + "id": "native-cli-commands", + "kinds": ["cli-command"], + "outcome": "behavior-compatible", + "rationale": "The native binary implements the libxmlsec1 command spelling and dispatch contract while capability checks delimit the available algorithm and key-data subsets.", + "evidence": "native-cli-tests" + }, + { + "id": "native-cli-options", + "kinds": ["cli-option"], + "name_regex": "^--(?:aes-key|binary-data|crypto|gen-key|help|ignore-manifests|insecure|output|pkcs8-der|pkcs8-pem|privkey-der|privkey-pem|pubkey-cert-der|pubkey-cert-pem|pubkey-der|pubkey-pem|trusted-der|trusted-pem|untrusted-der|untrusted-pem|xml-data)$", + "outcome": "provider-limited", + "rationale": "The native CLI parses and executes this option for the RustCrypto-backed formats and algorithms advertised by its capability registry.", + "evidence": "native-cli-tests" + }, + { + "id": "native-cli-exit-status", + "kinds": ["cli-exit-status"], + "name_regex": "^(?:success|failure)$", + "outcome": "behavior-compatible", + "rationale": "Successful operations exit zero and parse, policy, capability, verification, cryptographic, and I/O failures exit non-zero.", + "evidence": "native-cli-tests" + }, { "id": "planned-cli-surface", - "kinds": ["cli-command", "cli-option", "cli-exit-status"], + "kinds": ["cli-option", "cli-exit-status"], "outcome": "planned", - "rationale": "A command-compatible xmlsec1 CLI is not yet shipped; commands, typed options, and exit semantics remain explicit roadmap surface.", + "rationale": "This libxmlsec1 option or special status behavior is inventoried but is not yet reproduced by the native CLI.", "evidence": "planned-surface" }, { diff --git a/compatibility/libxmlsec1-1.3.13.json b/compatibility/libxmlsec1-1.3.13.json index e1bd52a..303cec1 100644 --- a/compatibility/libxmlsec1-1.3.13.json +++ b/compatibility/libxmlsec1-1.3.13.json @@ -20,6 +20,10 @@ "test": "capability_ledger::native_algorithm_claims_match_the_rust_api", "description": "The claimed URI set is cross-checked against the native algorithm parsers and the repository's unit and interop suites." }, + "native-cli-tests": { + "test": "capability_ledger::native_cli_claims_match_process_and_upstream_runner_tests", + "description": "Process tests cover native command parsing, exit statuses, sign/verify and encrypt/decrypt paths; the unmodified upstream DSig, Enc, and Keys runners exercise the same binary and capability registry." + }, "planned-surface": { "test": "capability_ledger::planned_surface_is_never_reported_as_supported", "description": "Unimplemented backend APIs, CLI, registry, format, URI, and donor-suite entries remain machine-readable planned work." @@ -57,6 +61,21 @@ "rationale": "Deprecated C aliases are excluded until the compatibility layer has a concrete ABI contract.", "evidence": "unsupported-legacy-surface" }, + "native-cli-commands": { + "outcome": "behavior-compatible", + "rationale": "The native binary implements the libxmlsec1 command spelling and dispatch contract while capability checks delimit the available algorithm and key-data subsets.", + "evidence": "native-cli-tests" + }, + "native-cli-exit-status": { + "outcome": "behavior-compatible", + "rationale": "Successful operations exit zero and parse, policy, capability, verification, cryptographic, and I/O failures exit non-zero.", + "evidence": "native-cli-tests" + }, + "native-cli-options": { + "outcome": "provider-limited", + "rationale": "The native CLI parses and executes this option for the RustCrypto-backed formats and algorithms advertised by its capability registry.", + "evidence": "native-cli-tests" + }, "native-sha1-verification-uri": { "outcome": "behavior-compatible", "rationale": "The native Rust API provides verification-only SHA-1 reference digest compatibility; signing rejects SHA-1 independently of policy.", @@ -74,7 +93,7 @@ }, "planned-cli-surface": { "outcome": "planned", - "rationale": "A command-compatible xmlsec1 CLI is not yet shipped; commands, typed options, and exit semantics remain explicit roadmap surface.", + "rationale": "This libxmlsec1 option or special status behavior is inventoried but is not yet reproduced by the native CLI.", "evidence": "planned-surface" }, "planned-donor-families": { @@ -21578,7 +21597,7 @@ "source": "apps/xmlsec.c", "line": 3449, "detail": "if((strcmp(cmd, \"check-key-data\") == 0) || (strcmp(cmd, \"--check-key-data\") == 0)) {", - "classification": "planned-cli-surface" + "classification": "native-cli-commands" }, { "kind": "cli-command", @@ -21586,7 +21605,7 @@ "source": "apps/xmlsec.c", "line": 3461, "detail": "if((strcmp(cmd, \"check-transforms\") == 0) || (strcmp(cmd, \"--check-transforms\") == 0)) {", - "classification": "planned-cli-surface" + "classification": "native-cli-commands" }, { "kind": "cli-command", @@ -21594,7 +21613,7 @@ "source": "apps/xmlsec.c", "line": 3509, "detail": "if((strcmp(cmd, \"decrypt\") == 0) || (strcmp(cmd, \"--decrypt\") == 0)) {", - "classification": "planned-cli-surface" + "classification": "native-cli-commands" }, { "kind": "cli-command", @@ -21602,7 +21621,7 @@ "source": "apps/xmlsec.c", "line": 3499, "detail": "if((strcmp(cmd, \"encrypt\") == 0) || (strcmp(cmd, \"--encrypt\") == 0)) {", - "classification": "planned-cli-surface" + "classification": "native-cli-commands" }, { "kind": "cli-command", @@ -21610,7 +21629,7 @@ "source": "apps/xmlsec.c", "line": 3418, "detail": "if((strcmp(cmd, \"help\") == 0) || (strcmp(cmd, \"--help\") == 0)) {", - "classification": "planned-cli-surface" + "classification": "native-cli-commands" }, { "kind": "cli-command", @@ -21618,7 +21637,7 @@ "source": "apps/xmlsec.c", "line": 3423, "detail": "if((strcmp(cmd, \"help-all\") == 0) || (strcmp(cmd, \"--help-all\") == 0)) {", - "classification": "planned-cli-surface" + "classification": "native-cli-commands" }, { "kind": "cli-command", @@ -21626,7 +21645,7 @@ "source": "apps/xmlsec.c", "line": 3467, "detail": "if((strcmp(cmd, \"keys\") == 0) || (strcmp(cmd, \"--keys\") == 0)) {", - "classification": "planned-cli-surface" + "classification": "native-cli-commands" }, { "kind": "cli-command", @@ -21634,7 +21653,7 @@ "source": "apps/xmlsec.c", "line": 3443, "detail": "if((strcmp(cmd, \"list-key-data\") == 0) || (strcmp(cmd, \"--list-key-data\") == 0)) {", - "classification": "planned-cli-surface" + "classification": "native-cli-commands" }, { "kind": "cli-command", @@ -21642,7 +21661,7 @@ "source": "apps/xmlsec.c", "line": 3455, "detail": "if((strcmp(cmd, \"list-transforms\") == 0) || (strcmp(cmd, \"--list-transforms\") == 0)) {", - "classification": "planned-cli-surface" + "classification": "native-cli-commands" }, { "kind": "cli-command", @@ -21650,7 +21669,7 @@ "source": "apps/xmlsec.c", "line": 3476, "detail": "if((strcmp(cmd, \"sign\") == 0) || (strcmp(cmd, \"--sign\") == 0)) {", - "classification": "planned-cli-surface" + "classification": "native-cli-commands" }, { "kind": "cli-command", @@ -21658,7 +21677,7 @@ "source": "apps/xmlsec.c", "line": 3486, "detail": "if((strcmp(cmd, \"verify\") == 0) || (strcmp(cmd, \"--verify\") == 0)) {", - "classification": "planned-cli-surface" + "classification": "native-cli-commands" }, { "kind": "cli-command", @@ -21666,7 +21685,7 @@ "source": "apps/xmlsec.c", "line": 3438, "detail": "if((strcmp(cmd, \"version\") == 0) || (strcmp(cmd, \"--version\") == 0)) {", - "classification": "planned-cli-surface" + "classification": "native-cli-commands" }, { "kind": "cli-command", @@ -21674,7 +21693,7 @@ "source": "apps/xmlsec.c", "line": 3449, "detail": "if((strcmp(cmd, \"check-key-data\") == 0) || (strcmp(cmd, \"--check-key-data\") == 0)) {", - "classification": "planned-cli-surface" + "classification": "native-cli-commands" }, { "kind": "cli-command", @@ -21682,7 +21701,7 @@ "source": "apps/xmlsec.c", "line": 3461, "detail": "if((strcmp(cmd, \"check-transforms\") == 0) || (strcmp(cmd, \"--check-transforms\") == 0)) {", - "classification": "planned-cli-surface" + "classification": "native-cli-commands" }, { "kind": "cli-command", @@ -21690,7 +21709,7 @@ "source": "apps/xmlsec.c", "line": 3509, "detail": "if((strcmp(cmd, \"decrypt\") == 0) || (strcmp(cmd, \"--decrypt\") == 0)) {", - "classification": "planned-cli-surface" + "classification": "native-cli-commands" }, { "kind": "cli-command", @@ -21698,7 +21717,7 @@ "source": "apps/xmlsec.c", "line": 3499, "detail": "if((strcmp(cmd, \"encrypt\") == 0) || (strcmp(cmd, \"--encrypt\") == 0)) {", - "classification": "planned-cli-surface" + "classification": "native-cli-commands" }, { "kind": "cli-command", @@ -21706,7 +21725,7 @@ "source": "apps/xmlsec.c", "line": 3418, "detail": "if((strcmp(cmd, \"help\") == 0) || (strcmp(cmd, \"--help\") == 0)) {", - "classification": "planned-cli-surface" + "classification": "native-cli-commands" }, { "kind": "cli-command", @@ -21714,7 +21733,7 @@ "source": "apps/xmlsec.c", "line": 3423, "detail": "if((strcmp(cmd, \"help-all\") == 0) || (strcmp(cmd, \"--help-all\") == 0)) {", - "classification": "planned-cli-surface" + "classification": "native-cli-commands" }, { "kind": "cli-command", @@ -21722,7 +21741,7 @@ "source": "apps/xmlsec.c", "line": 3467, "detail": "if((strcmp(cmd, \"keys\") == 0) || (strcmp(cmd, \"--keys\") == 0)) {", - "classification": "planned-cli-surface" + "classification": "native-cli-commands" }, { "kind": "cli-command", @@ -21730,7 +21749,7 @@ "source": "apps/xmlsec.c", "line": 3443, "detail": "if((strcmp(cmd, \"list-key-data\") == 0) || (strcmp(cmd, \"--list-key-data\") == 0)) {", - "classification": "planned-cli-surface" + "classification": "native-cli-commands" }, { "kind": "cli-command", @@ -21738,7 +21757,7 @@ "source": "apps/xmlsec.c", "line": 3455, "detail": "if((strcmp(cmd, \"list-transforms\") == 0) || (strcmp(cmd, \"--list-transforms\") == 0)) {", - "classification": "planned-cli-surface" + "classification": "native-cli-commands" }, { "kind": "cli-command", @@ -21746,7 +21765,7 @@ "source": "apps/xmlsec.c", "line": 3476, "detail": "if((strcmp(cmd, \"sign\") == 0) || (strcmp(cmd, \"--sign\") == 0)) {", - "classification": "planned-cli-surface" + "classification": "native-cli-commands" }, { "kind": "cli-command", @@ -21754,7 +21773,7 @@ "source": "apps/xmlsec.c", "line": 3486, "detail": "if((strcmp(cmd, \"verify\") == 0) || (strcmp(cmd, \"--verify\") == 0)) {", - "classification": "planned-cli-surface" + "classification": "native-cli-commands" }, { "kind": "cli-command", @@ -21762,7 +21781,7 @@ "source": "apps/xmlsec.c", "line": 3438, "detail": "if((strcmp(cmd, \"version\") == 0) || (strcmp(cmd, \"--version\") == 0)) {", - "classification": "planned-cli-surface" + "classification": "native-cli-commands" }, { "kind": "cli-exit-status", @@ -21770,7 +21789,7 @@ "source": "apps/xmlsec.c", "line": 1338, "detail": "1 for invalid parameters, missing input, initialization, or processing failure", - "classification": "planned-cli-surface" + "classification": "native-cli-exit-status" }, { "kind": "cli-exit-status", @@ -21778,7 +21797,7 @@ "source": "apps/xmlsec.c", "line": 1430, "detail": "0", - "classification": "planned-cli-surface" + "classification": "native-cli-exit-status" }, { "kind": "cli-exit-status", @@ -21818,7 +21837,7 @@ "source": "apps/xmlsec.c", "line": 413, "detail": "static xmlSecAppCmdLineParam aesKeyParam = { xmlSecAppCmdLineTopicKeysMngr, \"--aes-key\", \"--aeskey\", \"--aes-key[:] \" \"\\n\\tload AES key from binary file \", xmlSecAppCmdLineParamTypeString, xmlSecAppCmdLineParamFlagParamNameValue | xmlSecAppCmdLineParamFlagMultipleValues, NULL };", - "classification": "planned-cli-surface" + "classification": "native-cli-options" }, { "kind": "cli-option", @@ -21834,7 +21853,7 @@ "source": "apps/xmlsec.c", "line": 883, "detail": "static xmlSecAppCmdLineParam binaryDataParam = { xmlSecAppCmdLineTopicEncEncrypt, \"--binary-data\", \"--binary\", \"--binary-data \" \"\\n\\tbinary to encrypt\", xmlSecAppCmdLineParamTypeString, xmlSecAppCmdLineParamFlagNone, NULL };", - "classification": "planned-cli-surface" + "classification": "native-cli-options" }, { "kind": "cli-option", @@ -21882,7 +21901,7 @@ "source": "apps/xmlsec.c", "line": 161, "detail": "static xmlSecAppCmdLineParam cryptoParam = { xmlSecAppCmdLineTopicCryptoConfig, \"--crypto\", NULL, \"--crypto \" \"\\n\\tthe name of the crypto engine to use from the following\" \"\\n\\tlist: openssl, mscrypto, nss, gnutls, gcrypt (if no crypto engine is\" \"\\n\\tspecified then the default one is used)\", xmlSecAppCmdLineParamTypeString, xmlSecAppCmdLineParamFlagNone, NULL };", - "classification": "planned-cli-surface" + "classification": "native-cli-options" }, { "kind": "cli-option", @@ -21978,7 +21997,7 @@ "source": "apps/xmlsec.c", "line": 265, "detail": "static xmlSecAppCmdLineParam genKeyParam = { xmlSecAppCmdLineTopicKeysMngr, \"--gen-key\", \"-g\", \"--gen-key[:] -\" \"\\n\\tgenerate new key of bits size,\" \"\\n\\tset the key name to and add the result to keys\" \"\\n\\tmanager (for example, \\\"--gen:MyKeyName rsa-1024\\\" generates\" \"\\n\\ta new 1024 bits RSA key and sets it's name to \\\"MyKeyName\\\")\", xmlSecAppCmdLineParamTypeString, xmlSecAppCmdLineParamFlagParamNameValue | xmlSecAppCmdLineParamFlagMultipleValues, NULL };", - "classification": "planned-cli-surface" + "classification": "native-cli-options" }, { "kind": "cli-option", @@ -21986,7 +22005,7 @@ "source": "apps/xmlsec.c", "line": 150, "detail": "static xmlSecAppCmdLineParam helpParam = { xmlSecAppCmdLineTopicGeneral, \"--help\", \"-h\", \"--help\" \"\\n\\tprint help information about the command\", xmlSecAppCmdLineParamTypeFlag, xmlSecAppCmdLineParamFlagNone, NULL };", - "classification": "planned-cli-surface" + "classification": "native-cli-options" }, { "kind": "cli-option", @@ -22026,7 +22045,7 @@ "source": "apps/xmlsec.c", "line": 787, "detail": "static xmlSecAppCmdLineParam ignoreManifestsParam = { xmlSecAppCmdLineTopicDSigCommon, \"--ignore-manifests\", NULL, \"--ignore-manifests\" \"\\n\\tdo not process elements\", xmlSecAppCmdLineParamTypeFlag, xmlSecAppCmdLineParamFlagNone, NULL };", - "classification": "planned-cli-surface" + "classification": "native-cli-options" }, { "kind": "cli-option", @@ -22034,7 +22053,7 @@ "source": "apps/xmlsec.c", "line": 1080, "detail": "static xmlSecAppCmdLineParam X509DontVerifyCerts = { xmlSecAppCmdLineTopicX509Certs, \"--insecure\", NULL, \"--insecure\" \"\\n\\tdo not verify certificates or CRLs\", xmlSecAppCmdLineParamTypeFlag, xmlSecAppCmdLineParamFlagNone, NULL };", - "classification": "planned-cli-surface" + "classification": "native-cli-options" }, { "kind": "cli-option", @@ -22082,7 +22101,7 @@ "source": "apps/xmlsec.c", "line": 638, "detail": "static xmlSecAppCmdLineParam outputParam = { xmlSecAppCmdLineTopicDSigCommon | xmlSecAppCmdLineTopicEncCommon, \"--output\", \"-o\", \"--output \" \"\\n\\twrite result document to file ; the can\" \"\\n\\tbe a template and include '{inputfile}' which will be repaced\" \"\\n\\twith the input filename\", xmlSecAppCmdLineParamTypeString, xmlSecAppCmdLineParamFlagNone, NULL };", - "classification": "planned-cli-surface" + "classification": "native-cli-options" }, { "kind": "cli-option", @@ -22114,7 +22133,7 @@ "source": "apps/xmlsec.c", "line": 326, "detail": "static xmlSecAppCmdLineParam pkcs8DerParam = { xmlSecAppCmdLineTopicKeysMngr, \"--pkcs8-der\", \"--privkey-p8-der\", \"--pkcs8-der[:] [,[,[...]]]\" \"\\n\\tload private key from PKCS8 DER file and DER certificates\" \"\\n\\tthat verify this key\", xmlSecAppCmdLineParamTypeStringList, xmlSecAppCmdLineParamFlagParamNameValue | xmlSecAppCmdLineParamFlagMultipleValues, NULL };", - "classification": "planned-cli-surface" + "classification": "native-cli-options" }, { "kind": "cli-option", @@ -22122,7 +22141,7 @@ "source": "apps/xmlsec.c", "line": 314, "detail": "static xmlSecAppCmdLineParam pkcs8PemParam = { xmlSecAppCmdLineTopicKeysMngr, \"--pkcs8-pem\", \"--privkey-p8-pem\", \"--pkcs8-pem[:] [,[,[...]]]\" \"\\n\\tload private key from PKCS8 PEM file and PEM certificates\" \"\\n\\tthat verify this key\", xmlSecAppCmdLineParamTypeStringList, xmlSecAppCmdLineParamFlagParamNameValue | xmlSecAppCmdLineParamFlagMultipleValues, NULL };", - "classification": "planned-cli-surface" + "classification": "native-cli-options" }, { "kind": "cli-option", @@ -22154,7 +22173,7 @@ "source": "apps/xmlsec.c", "line": 302, "detail": "static xmlSecAppCmdLineParam privkeyDerParam = { xmlSecAppCmdLineTopicKeysMngr, \"--privkey-der\", NULL, \"--privkey-der[:] [,[,[...]]]\" \"\\n\\tload private key from DER file and certificates\" \"\\n\\tthat verify this key\", xmlSecAppCmdLineParamTypeStringList, xmlSecAppCmdLineParamFlagParamNameValue | xmlSecAppCmdLineParamFlagMultipleValues, NULL };", - "classification": "planned-cli-surface" + "classification": "native-cli-options" }, { "kind": "cli-option", @@ -22178,7 +22197,7 @@ "source": "apps/xmlsec.c", "line": 290, "detail": "static xmlSecAppCmdLineParam privkeyParam = { xmlSecAppCmdLineTopicKeysMngr, \"--privkey-pem\", \"--privkey\", \"--privkey-pem[:] [,[,[...]]]\" \"\\n\\tload private key from PEM file and certificates\" \"\\n\\tthat verify this key\", xmlSecAppCmdLineParamTypeStringList, xmlSecAppCmdLineParamFlagParamNameValue | xmlSecAppCmdLineParamFlagMultipleValues, NULL };", - "classification": "planned-cli-surface" + "classification": "native-cli-options" }, { "kind": "cli-option", @@ -22186,7 +22205,7 @@ "source": "apps/xmlsec.c", "line": 946, "detail": "static xmlSecAppCmdLineParam pubkeyCertDerParam = { xmlSecAppCmdLineTopicKeysMngr, \"--pubkey-cert-der\", NULL, \"--pubkey-cert-der[:] \" \"\\n\\tload public key from DER cert file\", xmlSecAppCmdLineParamTypeStringList, xmlSecAppCmdLineParamFlagParamNameValue | xmlSecAppCmdLineParamFlagMultipleValues, NULL };", - "classification": "planned-cli-surface" + "classification": "native-cli-options" }, { "kind": "cli-option", @@ -22194,7 +22213,7 @@ "source": "apps/xmlsec.c", "line": 935, "detail": "static xmlSecAppCmdLineParam pubkeyCertParam = { xmlSecAppCmdLineTopicKeysMngr, \"--pubkey-cert-pem\", \"--pubkey-cert\", \"--pubkey-cert-pem[:] \" \"\\n\\tload public key from PEM cert file\", xmlSecAppCmdLineParamTypeStringList, xmlSecAppCmdLineParamFlagParamNameValue | xmlSecAppCmdLineParamFlagMultipleValues, NULL };", - "classification": "planned-cli-surface" + "classification": "native-cli-options" }, { "kind": "cli-option", @@ -22202,7 +22221,7 @@ "source": "apps/xmlsec.c", "line": 375, "detail": "static xmlSecAppCmdLineParam pubkeyDerParam = { xmlSecAppCmdLineTopicKeysMngr, \"--pubkey-der\", NULL, \"--pubkey-der[:] \" \"\\n\\tload public key from DER file\", xmlSecAppCmdLineParamTypeStringList, xmlSecAppCmdLineParamFlagParamNameValue | xmlSecAppCmdLineParamFlagMultipleValues, NULL };", - "classification": "planned-cli-surface" + "classification": "native-cli-options" }, { "kind": "cli-option", @@ -22226,7 +22245,7 @@ "source": "apps/xmlsec.c", "line": 364, "detail": "static xmlSecAppCmdLineParam pubkeyParam = { xmlSecAppCmdLineTopicKeysMngr, \"--pubkey-pem\", \"--pubkey\", \"--pubkey-pem[:] \" \"\\n\\tload public key from PEM file\", xmlSecAppCmdLineParamTypeStringList, xmlSecAppCmdLineParamFlagParamNameValue | xmlSecAppCmdLineParamFlagMultipleValues, NULL };", - "classification": "planned-cli-surface" + "classification": "native-cli-options" }, { "kind": "cli-option", @@ -22290,7 +22309,7 @@ "source": "apps/xmlsec.c", "line": 968, "detail": "static xmlSecAppCmdLineParam trustedDerParam = { xmlSecAppCmdLineTopicX509Certs, \"--trusted-der\", NULL, \"--trusted-der \" \"\\n\\tload trusted (root) certificate from DER file \", xmlSecAppCmdLineParamTypeString, xmlSecAppCmdLineParamFlagMultipleValues, NULL };", - "classification": "planned-cli-surface" + "classification": "native-cli-options" }, { "kind": "cli-option", @@ -22298,7 +22317,7 @@ "source": "apps/xmlsec.c", "line": 957, "detail": "static xmlSecAppCmdLineParam trustedParam = { xmlSecAppCmdLineTopicX509Certs, \"--trusted-pem\", \"--trusted\", \"--trusted-pem \" \"\\n\\tload trusted (root) certificate from PEM file \", xmlSecAppCmdLineParamTypeString, xmlSecAppCmdLineParamFlagMultipleValues, NULL };", - "classification": "planned-cli-surface" + "classification": "native-cli-options" }, { "kind": "cli-option", @@ -22306,7 +22325,7 @@ "source": "apps/xmlsec.c", "line": 990, "detail": "static xmlSecAppCmdLineParam untrustedDerParam = { xmlSecAppCmdLineTopicX509Certs, \"--untrusted-der\", NULL, \"--untrusted-der \" \"\\n\\tload untrusted certificate from DER file \", xmlSecAppCmdLineParamTypeString, xmlSecAppCmdLineParamFlagMultipleValues, NULL };", - "classification": "planned-cli-surface" + "classification": "native-cli-options" }, { "kind": "cli-option", @@ -22314,7 +22333,7 @@ "source": "apps/xmlsec.c", "line": 979, "detail": "static xmlSecAppCmdLineParam untrustedParam = { xmlSecAppCmdLineTopicX509Certs, \"--untrusted-pem\", \"--untrusted\", \"--untrusted-pem \" \"\\n\\tload untrusted certificate from PEM file \", xmlSecAppCmdLineParamTypeString, xmlSecAppCmdLineParamFlagMultipleValues, NULL };", - "classification": "planned-cli-surface" + "classification": "native-cli-options" }, { "kind": "cli-option", @@ -22370,7 +22389,7 @@ "source": "apps/xmlsec.c", "line": 894, "detail": "static xmlSecAppCmdLineParam xmlDataParam = { xmlSecAppCmdLineTopicEncEncrypt, \"--xml-data\", NULL, \"--xml-data \" \"\\n\\tXML to encrypt\", xmlSecAppCmdLineParamTypeString, xmlSecAppCmdLineParamFlagNone, NULL };", - "classification": "planned-cli-surface" + "classification": "native-cli-options" }, { "kind": "cli-option", diff --git a/docs/cli.md b/docs/cli.md new file mode 100644 index 0000000..ee7efd7 --- /dev/null +++ b/docs/cli.md @@ -0,0 +1,75 @@ +# Native `xmlsec1` CLI + +The workspace ships a pure-Rust `xmlsec1` binary for applications and test +harnesses that use libxmlsec1 through its process interface. It delegates XML +Security operations to the same `xml-sec` policy and provider pipelines as the +library; it does not bind to libxml2, OpenSSL, or the libxmlsec1 C ABI. + +```sh +cargo install xmlsec1-cli +xmlsec1 version +xmlsec1 list-transforms +xmlsec1 list-key-data +``` + +## Commands + +The binary recognizes libxmlsec1's command names and leading-dash aliases for +`sign`, `verify`, `encrypt`, `decrypt`, `keys`, `list-transforms`, +`check-transforms`, `list-key-data`, `check-key-data`, `help`, and `version`. +Successful operations exit zero. Invalid arguments, unavailable capabilities, +policy violations, invalid signatures, decryption failures, and I/O errors exit +non-zero. + +Capability checks and runtime dispatch use one registry. A transform or key-data +class absent from `list-*` is not silently substituted and causes `check-*` to +fail. Backend selection is equally strict: `--crypto rustcrypto` and +`--crypto default` select the built-in provider; other backend names do not +fall back to RustCrypto. + +## Examples + +Sign an existing XMLDSig template and verify it with an explicit public key: + +```sh +xmlsec1 sign --privkey-pem signing-key.pem --output signed.xml template.xml +xmlsec1 verify --pubkey-pem signing-key.pub.pem signed.xml +``` + +Encrypt and decrypt binary data with a direct AES key: + +```sh +xmlsec1 encrypt --aeskey:content content.key \ + --binary-data plaintext.bin --output encrypted.xml encrypted-data.tmpl +xmlsec1 decrypt --aeskey:content content.key \ + --output plaintext.bin encrypted.xml +``` + +Files passed through `--aeskey` use libxmlsec1's binary-key contract: their +bytes are consumed verbatim rather than guessed to be Base64 text. `decrypt` +accepts both standalone `EncryptedData` and encrypted elements embedded in a +larger XML document; `--node-id` selects an embedded `EncryptedData` by `Id`. + +Generate an AES key store using the upstream command shape: + +```sh +xmlsec1 keys --gen-key:content aes-256 keys.xml +``` + +## Compatibility boundary + +The command and status surface is available now, while individual key formats, +algorithms, selectors, and policy controls remain capability-limited. Current +private-key loading accepts unencrypted PKCS#8 RSA, P-256, and P-384 keys; +public verification accepts SubjectPublicKeyInfo and X.509 certificates; direct +XMLEnc keys accept AES-128/256; RSA-OAEP uses RSA public/private keys. Encrypted +PKCS#8, PKCS#12, platform crypto stores, external DTDs, implicit network access, +and unsupported CLI policy knobs fail rather than weakening policy or falling +back. + +The integration suite invokes the checked-in libxmlsec1 1.3.13 +`testDSig.sh`, `testEnc.sh`, and `testKeys.sh` files without modification or a +Python translation layer. Covered runner cases include signature failure +classification, an AES-GCM decrypt/encrypt/decrypt cycle, and AES key-store +generation. The generated [compatibility ledger](compatibility-ledger.md) +records implemented commands and options separately from planned surface. diff --git a/tests/capability_ledger.rs b/tests/capability_ledger.rs index b6a6c7c..13daa42 100644 --- a/tests/capability_ledger.rs +++ b/tests/capability_ledger.rs @@ -161,7 +161,7 @@ fn complete_surface_categories_are_stable() { "https://github.com/lsh123/xmlsec" ); assert_eq!(ledger.generated_by, "xml-sec-capability-ledger/2"); - assert_eq!(ledger.classifications.len(), 13); + assert_eq!(ledger.classifications.len(), 16); assert_eq!(ledger.availability.len(), 427); let counts = ledger @@ -338,10 +338,11 @@ fn native_algorithm_claims_match_the_rust_api() { .items .iter() .filter(|item| { - matches!( - classification(&ledger, item).outcome.as_str(), - "behavior-compatible" | "compatibility-profile-only" - ) + item.kind == "algorithm-uri" + && matches!( + classification(&ledger, item).outcome.as_str(), + "behavior-compatible" | "compatibility-profile-only" + ) }) .collect(); assert_eq!(claims.len(), 41); @@ -760,13 +761,51 @@ fn planned_surface_is_never_reported_as_supported() { .filter(|item| classification(&ledger, item).outcome == "planned") .collect(); assert!(!planned.is_empty()); - assert!(planned.iter().any(|item| item.kind == "cli-command")); assert!(planned.iter().any(|item| item.kind == "cli-option")); assert!(planned.iter().any(|item| item.kind == "algorithm-uri")); assert!(planned.iter().any(|item| item.kind == "test-family")); assert!(planned.iter().any(|item| item.kind == "registry")); } +#[test] +fn native_cli_claims_match_process_and_upstream_runner_tests() { + // Commands are complete dispatch entries; individual options remain explicit + // when their format or policy mapping has not been implemented yet. + let ledger = ledger(); + for command in [ + "sign", + "verify", + "encrypt", + "decrypt", + "keys", + "check-transforms", + "check-key-data", + ] { + let item = ledger + .items + .iter() + .find(|item| item.kind == "cli-command" && item.name == command) + .unwrap_or_else(|| panic!("missing CLI command {command}")); + assert_eq!(classification(&ledger, item).outcome, "behavior-compatible"); + } + for option in ["--output", "--aes-key", "--privkey-pem", "--pubkey-pem"] { + let item = ledger + .items + .iter() + .find(|item| item.kind == "cli-option" && item.name == option) + .unwrap_or_else(|| panic!("missing CLI option {option}")); + assert_eq!(classification(&ledger, item).outcome, "provider-limited"); + } + for status in ["success", "failure"] { + let item = ledger + .items + .iter() + .find(|item| item.kind == "cli-exit-status" && item.name == status) + .unwrap_or_else(|| panic!("missing CLI status {status}")); + assert_eq!(classification(&ledger, item).outcome, "behavior-compatible"); + } +} + #[test] fn deprecated_surface_is_explicitly_unsupported() { // Deprecated aliases stay visible without adding compatibility shims prematurely. diff --git a/tools/xmlsec1/Cargo.toml b/tools/xmlsec1/Cargo.toml new file mode 100644 index 0000000..c27f875 --- /dev/null +++ b/tools/xmlsec1/Cargo.toml @@ -0,0 +1,30 @@ +[package] +name = "xmlsec1-cli" +version = "0.1.11" +edition = "2024" +rust-version = "1.92" +license = "Apache-2.0" +description = "Pure Rust xmlsec1-compatible command-line interface" +repository = "https://github.com/structured-world/xml-sec" +homepage = "https://github.com/structured-world/xml-sec" +readme = "README.md" +keywords = ["xml", "xmldsig", "xmlenc", "xmlsec"] +categories = ["command-line-utilities", "cryptography"] +exclude = ["tests/"] + +[[bin]] +name = "xmlsec1" +path = "src/main.rs" + +[dependencies] +base64 = "0.23" +getrandom = { version = "0.4", features = ["sys_rng"] } +quick-xml = "0.41" +roxmltree = "0.21" +rsa = { package = "sad-rsa", version = "0.2.3", features = ["sha1", "sha2"] } +thiserror = "2" +x509-parser = "0.18" +xml-sec = { version = "0.1.11", path = "../..", features = ["xmldsig", "xmlenc", "c14n"] } + +[dev-dependencies] +tempfile = "3" diff --git a/tools/xmlsec1/README.md b/tools/xmlsec1/README.md new file mode 100644 index 0000000..8af7945 --- /dev/null +++ b/tools/xmlsec1/README.md @@ -0,0 +1,19 @@ +# xmlsec1-cli + +Pure-Rust `xmlsec1` command-line interface backed by the +[`xml-sec`](https://crates.io/crates/xml-sec) XMLDSig, XMLEnc, policy, and +cryptographic-provider pipelines. + +```sh +cargo install xmlsec1-cli +xmlsec1 version +xmlsec1 list-transforms +``` + +See the repository's [CLI compatibility guide](https://github.com/structured-world/xml-sec/blob/main/docs/cli.md) for command +examples, supported key formats, fail-closed behavior, and upstream runner +coverage. + +`--aeskey` files are raw binary key material. Decryption accepts standalone +`EncryptedData` and performs in-document replacement, optionally selected by +`--node-id`. diff --git a/tools/xmlsec1/src/args.rs b/tools/xmlsec1/src/args.rs new file mode 100644 index 0000000..6b9f20c --- /dev/null +++ b/tools/xmlsec1/src/args.rs @@ -0,0 +1,320 @@ +use std::{collections::BTreeMap, ffi::OsString, fmt}; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Command { + Help, + HelpAll, + HelpDsig, + HelpEnc, + HelpKeys, + HelpX509, + Version, + ListKeyData, + CheckKeyData, + ListTransforms, + CheckTransforms, + Keys, + Sign, + Verify, + SignTemplate, + Encrypt, + Decrypt, +} + +impl Command { + fn parse(value: &str) -> Option { + let value = value.strip_prefix("--").unwrap_or(value); + Some(match value { + "help" | "-h" | "-?" => Self::Help, + "help-all" => Self::HelpAll, + "help-dsig" => Self::HelpDsig, + "help-enc" => Self::HelpEnc, + "help-keys" => Self::HelpKeys, + "help-x509" => Self::HelpX509, + "version" => Self::Version, + "list-key-data" | "list-key-data-klasses" => Self::ListKeyData, + "check-key-data" | "check-key-data-klass" => Self::CheckKeyData, + "list-transforms" => Self::ListTransforms, + "check-transforms" => Self::CheckTransforms, + "keys" => Self::Keys, + "sign" => Self::Sign, + "verify" => Self::Verify, + "sign-tmpl" | "sign-template" => Self::SignTemplate, + "encrypt" => Self::Encrypt, + "decrypt" => Self::Decrypt, + _ => return None, + }) + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct OptionValue { + pub name: String, + pub parameter: Option, + pub value: Option, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct Invocation { + pub command: Command, + pub options: BTreeMap>, + pub positional: Vec, +} + +#[derive(Debug, thiserror::Error, PartialEq, Eq)] +pub enum ParseError { + #[error("command is required")] + MissingCommand, + #[error("unknown command: {0}")] + UnknownCommand(String), + #[error("option {0} requires a value")] + MissingOptionValue(String), + #[error("unsupported option: {0}")] + UnsupportedOption(String), + #[error("arguments are not valid UTF-8")] + NonUtf8, +} + +#[derive(Clone, Copy)] +enum Arity { + Flag, + Value, +} + +impl Invocation { + pub fn parse(args: impl IntoIterator) -> Result { + let mut args = args.into_iter(); + let _program = args.next(); + let command_text = args.next().ok_or(ParseError::MissingCommand)?; + let command_text = command_text + .into_string() + .map_err(|_| ParseError::NonUtf8)?; + let command = Command::parse(&command_text) + .ok_or_else(|| ParseError::UnknownCommand(command_text.clone()))?; + let remaining = args + .map(|arg| arg.into_string().map_err(|_| ParseError::NonUtf8)) + .collect::, _>>()?; + let mut options = BTreeMap::>::new(); + let mut positional = Vec::new(); + let mut index = 0; + let mut options_finished = false; + while index < remaining.len() { + let argument = &remaining[index]; + if argument == "--" { + options_finished = true; + index += 1; + continue; + } + if options_finished || !argument.starts_with('-') { + positional.push(argument.clone()); + options_finished = true; + index += 1; + continue; + } + let stripped = argument.trim_start_matches('-'); + let (raw_name, parameter) = stripped + .split_once(':') + .map_or((stripped, None), |(name, parameter)| { + (name, Some(parameter.to_owned())) + }); + let name = canonical_option(raw_name) + .ok_or_else(|| ParseError::UnsupportedOption(argument.clone()))?; + let value = match option_arity(name) { + Arity::Flag => None, + Arity::Value => { + index += 1; + Some( + remaining + .get(index) + .filter(|value| !value.starts_with('-')) + .cloned() + .ok_or_else(|| ParseError::MissingOptionValue(argument.clone()))?, + ) + } + }; + options + .entry(name.to_owned()) + .or_default() + .push(OptionValue { + name: name.to_owned(), + parameter, + value, + }); + index += 1; + } + Ok(Self { + command, + options, + positional, + }) + } + + pub fn flag(&self, name: &str) -> bool { + self.options.contains_key(name) + } + + pub fn last_value(&self, name: &str) -> Option<&str> { + self.options + .get(name) + .and_then(|values| values.last()) + .and_then(|entry| entry.value.as_deref()) + } + + pub fn values(&self, name: &str) -> impl Iterator { + self.options.get(name).into_iter().flatten() + } +} + +fn canonical_option(name: &str) -> Option<&'static str> { + Some(match name { + "o" | "output" => "output", + "crypto" => "crypto", + "crypto-config" => "crypto-config", + "verbose" => "verbose", + "print-crypto-library-errors" => "print-crypto-library-errors", + "print-debug" => "print-debug", + "print-xml-debug" => "print-xml-debug", + "repeat" => "repeat", + "keys-file" => "keys-file", + "gen-key" => "gen-key", + "privkey" | "privkey-pem" => "privkey-pem", + "privkey-der" => "privkey-der", + "pkcs8-pem" => "pkcs8-pem", + "pkcs8-der" => "pkcs8-der", + "pubkey" | "pubkey-pem" => "pubkey-pem", + "pubkey-der" => "pubkey-der", + "pubkey-cert-pem" => "pubkey-cert-pem", + "pubkey-cert-der" => "pubkey-cert-der", + "trusted-pem" | "trusted" => "trusted-pem", + "trusted-der" => "trusted-der", + "untrusted-pem" | "untrusted" => "untrusted-pem", + "untrusted-der" => "untrusted-der", + "aes-key" | "aeskey" => "aes-key", + "hmac-key" | "hmackey" => "hmac-key", + "pwd" => "pwd", + "enabled-key-data" => "enabled-key-data", + "enabled-reference-uris" => "enabled-reference-uris", + "enabled-retrieval-uris" => "enabled-retrieval-uris", + "enabled-cipher-reference-uris" => "enabled-cipher-reference-uris", + "ignore-manifests" => "ignore-manifests", + "lax-key-search" => "lax-key-search", + "verify-keys" => "verify-keys", + "verify-crls" => "verify-crls", + "X509-skip-time-checks" => "X509-skip-time-checks", + "X509-skip-strict-checks" => "X509-skip-strict-checks", + "insecure" => "insecure", + "verification-time" | "verification-gmt-time" => "verification-time", + "depth" => "depth", + "node-id" => "node-id", + "node-name" => "node-name", + "node-xpath" => "node-xpath", + "id-attr" => "id-attr", + "add-id-attr" => "add-id-attr", + "binary-data" => "binary-data", + "xml-data" => "xml-data", + "session-key" => "session-key", + "url-map" => "url-map", + "enable-asn1-signatures-hack" => "enable-asn1-signatures-hack", + "help" => "help", + _ => return None, + }) +} + +fn option_arity(name: &str) -> Arity { + match name { + "verbose" + | "print-crypto-library-errors" + | "print-debug" + | "print-xml-debug" + | "ignore-manifests" + | "lax-key-search" + | "verify-keys" + | "verify-crls" + | "X509-skip-time-checks" + | "X509-skip-strict-checks" + | "insecure" + | "enable-asn1-signatures-hack" + | "help" => Arity::Flag, + _ => Arity::Value, + } +} + +impl fmt::Display for Command { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(formatter, "{:?}", self) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + fn parse(arguments: &[&str]) -> Result { + Invocation::parse(arguments.iter().map(OsString::from)) + } + + #[test] + fn parses_alias_named_and_repeated_options() { + let parsed = parse(&[ + "xmlsec1", + "sign-tmpl", + "-o", + "signed.xml", + "--privkey-pem:signer", + "key.pem", + "--trusted-pem", + "root-a.pem", + "--trusted-pem", + "root-b.pem", + "input.xml", + ]) + .expect("valid donor-shaped arguments must parse"); + assert_eq!(parsed.command, Command::SignTemplate); + assert_eq!(parsed.last_value("output"), Some("signed.xml")); + assert_eq!(parsed.values("trusted-pem").count(), 2); + assert_eq!( + parsed + .values("privkey-pem") + .next() + .unwrap() + .parameter + .as_deref(), + Some("signer") + ); + assert_eq!(parsed.positional, ["input.xml"]); + } + + #[test] + fn parses_the_donor_leading_dash_command_aliases() { + assert_eq!( + parse(&["xmlsec1", "--verify", "input.xml"]) + .unwrap() + .command, + Command::Verify + ); + assert_eq!( + parse(&["xmlsec1", "--list-transforms"]).unwrap().command, + Command::ListTransforms + ); + } + + #[test] + fn rejects_options_after_input_like_the_donor_parser() { + let parsed = parse(&["xmlsec1", "verify", "input.xml", "--verbose"]) + .expect("the donor treats trailing options as filenames"); + assert_eq!(parsed.positional, ["input.xml", "--verbose"]); + assert!(!parsed.flag("verbose")); + } + + #[test] + fn rejects_unknown_and_missing_option_values() { + assert!(matches!( + parse(&["xmlsec1", "verify", "--made-up"]), + Err(ParseError::UnsupportedOption(_)) + )); + assert!(matches!( + parse(&["xmlsec1", "verify", "--output"]), + Err(ParseError::MissingOptionValue(_)) + )); + } +} diff --git a/tools/xmlsec1/src/capabilities.rs b/tools/xmlsec1/src/capabilities.rs new file mode 100644 index 0000000..6f970ac --- /dev/null +++ b/tools/xmlsec1/src/capabilities.rs @@ -0,0 +1,75 @@ +use std::io::Write; + +pub const TRANSFORMS: &[&str] = &[ + "base64", + "enveloped-signature", + "c14n", + "c14n-with-comments", + "c14n11", + "c14n11-with-comments", + "exc-c14n", + "exc-c14n-with-comments", + "xpath", + "xpath2", + "dsa-sha1", + "ecdsa-sha256", + "ecdsa-sha384", + "rsa-sha1", + "rsa-sha256", + "rsa-sha384", + "rsa-sha512", + "sha1", + "sha256", + "sha384", + "sha512", + "aes128-cbc", + "aes256-cbc", + "aes128-gcm", + "aes256-gcm", + "rsa-oaep-enc11", +]; + +// Key-data names describe complete CLI loading paths, not provider primitives. +pub const KEY_DATA: &[&str] = &[ + "key-value", + "der-encoded-key-value", + "aes", + "rsa", + "ec", + "x509", + "raw-x509-cert", +]; + +pub fn list(label: &str, values: &[&str], output: &mut dyn Write) -> std::io::Result<()> { + writeln!(output, "Registered {label}:")?; + for (index, value) in values.iter().enumerate() { + if index > 0 { + write!(output, ",")?; + } + write!(output, "\"{value}\"")?; + } + writeln!(output) +} + +pub fn contains_all(values: &[&str], requested: &[String]) -> bool { + requested + .iter() + .flat_map(|value| value.split(',')) + .all(|value| values.contains(&value)) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn checks_comma_separated_and_repeated_capabilities() { + assert!(contains_all( + TRANSFORMS, + &["c14n,rsa-sha256".into(), "sha256".into()] + )); + assert!(!contains_all(TRANSFORMS, &["xslt".into()])); + assert!(contains_all(TRANSFORMS, &["rsa-oaep-enc11".into()])); + assert!(!contains_all(KEY_DATA, &["key-name".into()])); + } +} diff --git a/tools/xmlsec1/src/commands.rs b/tools/xmlsec1/src/commands.rs new file mode 100644 index 0000000..0a040a0 --- /dev/null +++ b/tools/xmlsec1/src/commands.rs @@ -0,0 +1,638 @@ +use std::{collections::HashSet, fs, io::Write}; + +use roxmltree::Document; +use xml_sec::{ + policy::{EncryptionPolicy, SigningPolicy, VerificationPolicy}, + provider::default_provider, + xmldsig::{ + DefaultKeyResolver, DsigStatus, KeyResolverConfig, SignContext, SignatureAlgorithm, + UriTypeSet, VerifyContext, X509CertificateKeyInfoWriter, + }, + xmlenc::{ + DataEncryptionAlgorithm, DecryptContext, DecryptedContent, DecryptionKeyResolver, + EncryptedDataBuilder, EncryptedDataType, PrivateKeyDecryptor, SymmetricKeyDecryptor, + }, +}; + +use crate::{ + Command, Invocation, + capabilities::{self, KEY_DATA, TRANSFORMS}, + key_material, +}; + +const GENERIC_OPTIONS: &[&str] = &[ + // These options only select this fixed backend or control diagnostics; none + // authorizes the core library to discover configuration or external data. + "crypto", + "crypto-config", + "verbose", + "print-crypto-library-errors", + "print-debug", + "print-xml-debug", + "help", +]; + +#[derive(Debug, thiserror::Error)] +pub enum CommandError { + #[error("{0}")] + Usage(String), + #[error("unsupported option for this command: --{0}")] + UnsupportedOption(String), + #[error("unsupported crypto provider: {0}")] + UnsupportedProvider(String), + #[error("I/O error for {path}: {source}")] + Io { + path: String, + source: std::io::Error, + }, + #[error(transparent)] + Key(#[from] key_material::KeyMaterialError), + #[error("XML signature operation failed: {0}")] + Signature(String), + #[error("signature is invalid")] + InvalidSignature, + #[error("XML encryption operation failed: {0}")] + Encryption(String), + #[error("requested capability is not available")] + CapabilityUnavailable, +} + +pub fn execute( + invocation: Invocation, + stdout: &mut dyn Write, + _stderr: &mut dyn Write, +) -> Result<(), CommandError> { + if invocation.flag("help") { + return help(stdout); + } + validate_provider(&invocation)?; + match invocation.command { + Command::Help + | Command::HelpAll + | Command::HelpDsig + | Command::HelpEnc + | Command::HelpKeys + | Command::HelpX509 => help(stdout), + Command::Version => writeln!(stdout, "xmlsec1 1.3.13 (rustcrypto)").map_err(stdout_error), + Command::ListTransforms => { + validate_options(&invocation, &[])?; + capabilities::list("transform klasses", TRANSFORMS, stdout).map_err(stdout_error) + } + Command::CheckTransforms => { + validate_options(&invocation, &[])?; + if capabilities::contains_all(TRANSFORMS, &invocation.positional) { + Ok(()) + } else { + Err(CommandError::CapabilityUnavailable) + } + } + Command::ListKeyData => { + validate_options(&invocation, &[])?; + capabilities::list("key data klasses", KEY_DATA, stdout).map_err(stdout_error) + } + Command::CheckKeyData => { + validate_options(&invocation, &[])?; + if capabilities::contains_all(KEY_DATA, &invocation.positional) { + Ok(()) + } else { + Err(CommandError::CapabilityUnavailable) + } + } + Command::Keys => keys(&invocation, stdout), + Command::Sign | Command::SignTemplate => sign(&invocation, stdout), + Command::Verify => verify(&invocation, stdout), + Command::Encrypt => encrypt(&invocation, stdout), + Command::Decrypt => decrypt(&invocation, stdout), + } +} + +fn help(output: &mut dyn Write) -> Result<(), CommandError> { + writeln!( + output, + "Usage: xmlsec1 [options] [files]\n\ + Commands: sign verify encrypt decrypt keys list-transforms check-transforms \ + list-key-data check-key-data" + ) + .map_err(stdout_error) +} + +fn validate_provider(invocation: &Invocation) -> Result<(), CommandError> { + if let Some(provider) = invocation.last_value("crypto") + && !matches!(provider, "rustcrypto" | "default") + { + return Err(CommandError::UnsupportedProvider(provider.to_owned())); + } + Ok(()) +} + +fn validate_options(invocation: &Invocation, command_options: &[&str]) -> Result<(), CommandError> { + for name in invocation.options.keys() { + if !GENERIC_OPTIONS.contains(&name.as_str()) && !command_options.contains(&name.as_str()) { + return Err(CommandError::UnsupportedOption(name.clone())); + } + } + Ok(()) +} + +fn input_path(invocation: &Invocation) -> Result<&str, CommandError> { + if invocation.positional.len() != 1 { + return Err(CommandError::Usage(format!( + "{} expects exactly one input file", + invocation.command + ))); + } + Ok(&invocation.positional[0]) +} + +fn read_input(invocation: &Invocation) -> Result { + let path = input_path(invocation)?; + fs::read_to_string(path).map_err(|source| CommandError::Io { + path: path.to_owned(), + source, + }) +} + +fn write_output( + invocation: &Invocation, + bytes: &[u8], + stdout: &mut dyn Write, +) -> Result<(), CommandError> { + if let Some(path) = invocation.last_value("output") { + fs::write(path, bytes).map_err(|source| CommandError::Io { + path: path.to_owned(), + source, + }) + } else { + stdout.write_all(bytes).map_err(stdout_error) + } +} + +fn sign(invocation: &Invocation, stdout: &mut dyn Write) -> Result<(), CommandError> { + validate_options( + invocation, + &[ + "output", + "privkey-pem", + "privkey-der", + "pkcs8-pem", + "pkcs8-der", + "pwd", + "lax-key-search", + "node-id", + "node-name", + "node-xpath", + "id-attr", + "add-id-attr", + ], + )?; + reject_unimplemented_selectors(invocation, &[])?; + if invocation.last_value("pwd").is_some() { + return Err(CommandError::UnsupportedOption("pwd".into())); + } + let key_option = ["privkey-pem", "privkey-der", "pkcs8-pem", "pkcs8-der"] + .into_iter() + .find_map(|name| invocation.values(name).next()); + let key_option = key_option.ok_or_else(|| { + CommandError::Usage("sign requires --privkey-pem or --pkcs8-pem/der".into()) + })?; + let value = key_option.value.as_deref().unwrap_or_default(); + let mut files = value.split(','); + let key_path = files.next().unwrap_or_default(); + let certificate_path = files.next(); + if files.next().is_some() { + return Err(CommandError::Usage( + "private key accepts at most one certificate path".into(), + )); + } + let xml = read_input(invocation)?; + let key = key_material::load_signing_key(key_path)?; + let policy = SigningPolicy::default(); + let signed = if let Some(certificate_path) = certificate_path { + let certificate = key_material::read_text(certificate_path)?; + let writer = X509CertificateKeyInfoWriter::from_pem(&certificate) + .map_err(|error| CommandError::Signature(error.to_string()))?; + SignContext::new(key.as_ref()) + .policy(policy) + .key_info_writer(&writer) + .sign_template(&xml) + } else { + SignContext::new(key.as_ref()) + .policy(policy) + .sign_template(&xml) + } + .map_err(|error| CommandError::Signature(error.to_string()))?; + write_output(invocation, signed.as_bytes(), stdout) +} + +fn verification_policy(invocation: &Invocation) -> VerificationPolicy { + let mut policy = VerificationPolicy { + process_manifests: !invocation.flag("ignore-manifests"), + reference_uri_types: UriTypeSet::ALL, + retrieval_uri_types: UriTypeSet::ALL, + ..VerificationPolicy::default() + }; + policy.key_trust.allowed_legacy_signature_algorithms = HashSet::from([ + SignatureAlgorithm::RsaSha1, + SignatureAlgorithm::DsaSha1, + SignatureAlgorithm::HmacSha1, + ]); + policy.key_trust.check_crls = invocation.flag("verify-crls"); + policy.key_trust.verify_x509_chains = !invocation.flag("insecure") + && (invocation.values("trusted-pem").next().is_some() + || invocation.values("trusted-der").next().is_some()); + policy +} + +fn verify(invocation: &Invocation, stdout: &mut dyn Write) -> Result<(), CommandError> { + validate_options( + invocation, + &[ + "pubkey-pem", + "pubkey-der", + "pubkey-cert-pem", + "pubkey-cert-der", + "trusted-pem", + "trusted-der", + "untrusted-pem", + "untrusted-der", + "enabled-reference-uris", + "enabled-retrieval-uris", + "ignore-manifests", + "lax-key-search", + "verify-crls", + "X509-skip-time-checks", + "insecure", + "verification-time", + "depth", + "node-id", + "node-name", + "node-xpath", + "id-attr", + "add-id-attr", + "url-map", + ], + )?; + reject_unimplemented_selectors(invocation, &[])?; + reject_unimplemented_verification_policy(invocation)?; + let direct_path = ["pubkey-pem", "pubkey-der"] + .into_iter() + .find_map(|name| invocation.last_value(name)); + // With an explicit public key there is no key-manager search to relax. + // Reject the flag on resolver-backed paths until its semantics exist. + if invocation.flag("lax-key-search") && direct_path.is_none() { + return Err(CommandError::UnsupportedOption("lax-key-search".into())); + } + let xml = read_input(invocation)?; + let algorithm = key_material::signature_algorithm(&xml)?; + let policy = verification_policy(invocation); + let result = if let Some(path) = direct_path { + let key = key_material::load_verification_key(path, algorithm)?; + VerifyContext::new().policy(policy).key(&key).verify(&xml) + } else { + let mut config = KeyResolverConfig::default(); + for name in [ + "pubkey-cert-pem", + "pubkey-cert-der", + "untrusted-pem", + "untrusted-der", + ] { + for option in invocation.values(name) { + config.lookup_certs.push(key_material::load_certificate( + option.value.as_deref().unwrap_or_default(), + )?); + } + } + for name in ["trusted-pem", "trusted-der"] { + for option in invocation.values(name) { + config.trusted_certs.push(key_material::load_certificate( + option.value.as_deref().unwrap_or_default(), + )?); + } + } + let resolver = DefaultKeyResolver::new(config); + VerifyContext::new() + .policy(policy) + .key_resolver(&resolver) + .verify(&xml) + } + .map_err(|error| CommandError::Signature(error.to_string()))?; + if result.status != DsigStatus::Valid + || result + .manifest_references + .iter() + .any(|reference| reference.status != DsigStatus::Valid) + { + return Err(CommandError::InvalidSignature); + } + if invocation.flag("print-debug") || invocation.flag("print-xml-debug") { + writeln!(stdout, "Status: valid").map_err(stdout_error)?; + } + Ok(()) +} + +fn encrypt(invocation: &Invocation, stdout: &mut dyn Write) -> Result<(), CommandError> { + validate_options( + invocation, + &[ + "output", + "binary-data", + "xml-data", + "aes-key", + "pubkey-pem", + "pubkey-der", + "lax-key-search", + "node-id", + "node-name", + "node-xpath", + "id-attr", + "add-id-attr", + ], + )?; + reject_unimplemented_selectors(invocation, &[])?; + let template = read_input(invocation)?; + let (algorithm, encrypted_type) = encryption_template(&template)?; + let mut builder = EncryptedDataBuilder::new(algorithm).policy(EncryptionPolicy::default()); + if let Some(option) = invocation.values("aes-key").next() { + let key = key_material::load_symmetric( + option.value.as_deref().unwrap_or_default(), + Some(algorithm.key_len()), + )?; + builder = builder.direct_key(key); + if let Some(name) = option.parameter.as_deref() { + builder = builder.direct_key_name(name); + } + } else if let Some(path) = ["pubkey-pem", "pubkey-der"] + .into_iter() + .find_map(|name| invocation.last_value(name)) + { + builder = builder.recipient_rsa_oaep(key_material::load_rsa_public(path)?); + } else { + return Err(CommandError::Usage( + "encrypt requires --aes-key or --pubkey-pem".into(), + )); + } + builder = builder.encryption_type(encrypted_type); + let result = if let Some(path) = invocation.last_value("binary-data") { + let data = key_material::read(path)?; + builder.encrypt_binary(&data) + } else if let Some(path) = invocation.last_value("xml-data") { + let data = key_material::read_text(path)?; + builder.encrypt_xml(&data) + } else { + return Err(CommandError::Usage( + "encrypt requires --binary-data or --xml-data".into(), + )); + } + .map_err(|error| CommandError::Encryption(error.to_string()))?; + write_output(invocation, result.encrypted_data_xml.as_bytes(), stdout) +} + +fn decrypt(invocation: &Invocation, stdout: &mut dyn Write) -> Result<(), CommandError> { + validate_options( + invocation, + &[ + "output", + "aes-key", + "privkey-pem", + "privkey-der", + "pkcs8-pem", + "pkcs8-der", + "pwd", + "lax-key-search", + "node-id", + "node-name", + "node-xpath", + "id-attr", + "add-id-attr", + ], + )?; + reject_unimplemented_selectors(invocation, &["node-id"])?; + if invocation.last_value("pwd").is_some() { + return Err(CommandError::UnsupportedOption("pwd".into())); + } + let xml = read_input(invocation)?; + let encrypted_data_id = invocation.last_value("node-id"); + let bytes = if let Some(option) = invocation.values("aes-key").next() { + let key = key_material::load_symmetric(option.value.as_deref().unwrap_or_default(), None)?; + decrypt_input(&SymmetricKeyDecryptor::new(key), &xml, encrypted_data_id)? + } else if let Some(path) = ["privkey-pem", "privkey-der", "pkcs8-pem", "pkcs8-der"] + .into_iter() + .find_map(|name| invocation.last_value(name)) + { + let resolver = PrivateKeyDecryptor::new(key_material::load_rsa_private(path)?); + decrypt_input(&resolver, &xml, encrypted_data_id)? + } else { + return Err(CommandError::Usage( + "decrypt requires --aes-key or an RSA private key".into(), + )); + }; + write_output(invocation, &bytes, stdout) +} + +fn decrypt_input( + resolver: &dyn DecryptionKeyResolver, + xml: &str, + encrypted_data_id: Option<&str>, +) -> Result, CommandError> { + let document = + Document::parse(xml).map_err(|error| CommandError::Encryption(error.to_string()))?; + let standalone = document + .root_element() + .has_tag_name(("http://www.w3.org/2001/04/xmlenc#", "EncryptedData")); + let context = DecryptContext::new(resolver); + if standalone && encrypted_data_id.is_none() { + return context + .decrypt(xml) + .map(|content| match content { + DecryptedContent::Xml(xml) => xml.into_bytes(), + DecryptedContent::Bytes(bytes) => bytes, + }) + .map_err(|error| CommandError::Encryption(error.to_string())); + } + context + .decrypt_document(xml, encrypted_data_id) + .map(String::into_bytes) + .map_err(|error| CommandError::Encryption(error.to_string())) +} + +fn encryption_template( + xml: &str, +) -> Result<(DataEncryptionAlgorithm, EncryptedDataType), CommandError> { + let document = + Document::parse(xml).map_err(|error| CommandError::Encryption(error.to_string()))?; + let encrypted_data = document + .descendants() + .find(|node| node.is_element() && node.tag_name().name() == "EncryptedData") + .ok_or_else(|| CommandError::Encryption("template has no EncryptedData".into()))?; + let method = encrypted_data + .children() + .find(|node| node.is_element() && node.tag_name().name() == "EncryptionMethod") + .and_then(|node| node.attribute("Algorithm")) + .ok_or_else(|| CommandError::Encryption("template has no encryption algorithm".into()))?; + let algorithm = DataEncryptionAlgorithm::from_uri(method) + .map_err(|error| CommandError::Encryption(error.to_string()))?; + let encrypted_type = match encrypted_data.attribute("Type") { + Some("http://www.w3.org/2001/04/xmlenc#Content") => EncryptedDataType::Content, + _ => EncryptedDataType::Element, + }; + Ok((algorithm, encrypted_type)) +} + +fn keys(invocation: &Invocation, stdout: &mut dyn Write) -> Result<(), CommandError> { + validate_options(invocation, &["gen-key"])?; + let generated = invocation.values("gen-key").collect::>(); + if generated.is_empty() { + return Err(CommandError::Usage( + "keys requires --gen-key:name algorithm".into(), + )); + } + let mut entries = String::new(); + for generated in generated { + let name = generated + .parameter + .as_deref() + .ok_or_else(|| CommandError::Usage("--gen-key requires a key name".into()))?; + let algorithm = generated.value.as_deref().unwrap_or_default(); + let size = match algorithm { + "aes-128" => 16, + "aes-192" => 24, + "aes-256" => 32, + _ => return Err(CommandError::CapabilityUnavailable), + }; + let mut key = vec![0_u8; size]; + default_provider() + .fill_random(&mut key) + .map_err(|error| CommandError::Encryption(error.to_string()))?; + let encoded = base64::Engine::encode(&base64::engine::general_purpose::STANDARD, key); + let name = quick_xml::escape::escape(name); + entries.push_str(&format!( + "\n\ + {name}\n\ + \n\ + {encoded}\n\ + \n\ + \n" + )); + } + let document = format!( + "\n\n\ + {entries}\n" + ); + if invocation.positional.len() > 1 { + return Err(CommandError::Usage( + "keys accepts at most one key-store path".into(), + )); + } + if let Some(path) = invocation.positional.first() { + fs::write(path, document.as_bytes()).map_err(|source| CommandError::Io { + path: path.clone(), + source, + }) + } else { + stdout.write_all(document.as_bytes()).map_err(stdout_error) + } +} + +fn reject_unimplemented_selectors( + invocation: &Invocation, + supported: &[&str], +) -> Result<(), CommandError> { + for name in [ + "node-id", + "node-name", + "node-xpath", + "id-attr", + "add-id-attr", + ] { + if !supported.contains(&name) && invocation.options.contains_key(name) { + return Err(CommandError::UnsupportedOption(name.into())); + } + } + Ok(()) +} + +fn reject_unimplemented_verification_policy(invocation: &Invocation) -> Result<(), CommandError> { + for name in [ + "enabled-reference-uris", + "enabled-retrieval-uris", + "X509-skip-time-checks", + "verification-time", + "depth", + "url-map", + ] { + if invocation.options.contains_key(name) { + return Err(CommandError::UnsupportedOption(name.into())); + } + } + Ok(()) +} + +fn stdout_error(source: std::io::Error) -> CommandError { + CommandError::Io { + path: "stdout".into(), + source, + } +} + +#[cfg(test)] +mod tests { + use std::ffi::OsString; + + use super::*; + + fn invocation(arguments: &[&str]) -> Invocation { + Invocation::parse(arguments.iter().map(OsString::from)).unwrap() + } + + #[test] + fn capability_checks_fail_closed() { + let mut output = Vec::new(); + assert!( + execute( + invocation(&["xmlsec1", "check-transforms", "c14n", "rsa-sha256"]), + &mut output, + &mut Vec::new() + ) + .is_ok() + ); + assert!(matches!( + execute( + invocation(&["xmlsec1", "check-transforms", "xslt"]), + &mut output, + &mut Vec::new() + ), + Err(CommandError::CapabilityUnavailable) + )); + } + + #[test] + fn unsupported_provider_never_falls_back() { + let error = execute( + invocation(&["xmlsec1", "version", "--crypto", "openssl"]), + &mut Vec::new(), + &mut Vec::new(), + ) + .unwrap_err(); + assert!(matches!(error, CommandError::UnsupportedProvider(_))); + } + + #[test] + fn command_help_is_an_action_and_semantic_no_ops_fail_closed() { + let mut output = Vec::new(); + execute( + invocation(&["xmlsec1", "verify", "--help"]), + &mut output, + &mut Vec::new(), + ) + .unwrap(); + assert!(String::from_utf8(output).unwrap().starts_with("Usage:")); + + let error = execute( + invocation(&["xmlsec1", "verify", "--lax-key-search", "input.xml"]), + &mut Vec::new(), + &mut Vec::new(), + ) + .unwrap_err(); + assert!(matches!(error, CommandError::UnsupportedOption(_))); + } +} diff --git a/tools/xmlsec1/src/key_material.rs b/tools/xmlsec1/src/key_material.rs new file mode 100644 index 0000000..e9b1a01 --- /dev/null +++ b/tools/xmlsec1/src/key_material.rs @@ -0,0 +1,166 @@ +use std::fs; + +use roxmltree::Document; +use rsa::{ + RsaPrivateKey, RsaPublicKey, + pkcs1::{DecodeRsaPrivateKey as _, DecodeRsaPublicKey as _}, + pkcs8::{DecodePrivateKey as _, DecodePublicKey as _}, +}; +use xml_sec::xmldsig::{ + EcdsaP256SigningKey, EcdsaP384SigningKey, RsaSigningKey, SignatureAlgorithm, SigningKey, + VerificationKey, find_signature_node, parse_signed_info, +}; + +#[derive(Debug, thiserror::Error)] +pub enum KeyMaterialError { + #[error("failed to read key file {path}: {source}")] + Read { + path: String, + source: std::io::Error, + }, + #[error("invalid PEM key in {0}")] + InvalidPem(String), + #[error("unsupported private key in {0}")] + UnsupportedPrivateKey(String), + #[error("unsupported public key in {0}")] + UnsupportedPublicKey(String), + #[error("signature template does not contain a valid SignedInfo")] + MissingSignedInfo, + #[error("invalid XML signature: {0}")] + Signature(String), + #[error("invalid symmetric key length: expected {expected} bytes, got {actual}")] + SymmetricLength { expected: usize, actual: usize }, +} + +pub fn read(path: &str) -> Result, KeyMaterialError> { + fs::read(path).map_err(|source| KeyMaterialError::Read { + path: path.to_owned(), + source, + }) +} + +pub fn read_text(path: &str) -> Result { + String::from_utf8(read(path)?).map_err(|_| KeyMaterialError::InvalidPem(path.to_owned())) +} + +pub fn signature_algorithm(xml: &str) -> Result { + let document = + Document::parse(xml).map_err(|error| KeyMaterialError::Signature(error.to_string()))?; + let signature = find_signature_node(&document).ok_or(KeyMaterialError::MissingSignedInfo)?; + let signed_info = signature + .children() + .find(|node| node.is_element() && node.tag_name().name() == "SignedInfo") + .ok_or(KeyMaterialError::MissingSignedInfo)?; + parse_signed_info(signed_info) + .map(|info| info.signature_method) + .map_err(|error| KeyMaterialError::Signature(error.to_string())) +} + +pub fn load_signing_key(path: &str) -> Result, KeyMaterialError> { + let bytes = read(path)?; + if let Ok(text) = std::str::from_utf8(&bytes) { + if let Ok(key) = RsaSigningKey::from_pkcs8_pem(text) { + return Ok(Box::new(key)); + } + if let Ok(key) = EcdsaP256SigningKey::from_pkcs8_pem(text) { + return Ok(Box::new(key)); + } + if let Ok(key) = EcdsaP384SigningKey::from_pkcs8_pem(text) { + return Ok(Box::new(key)); + } + } + if let Ok(key) = RsaSigningKey::from_pkcs8_der(&bytes) { + return Ok(Box::new(key)); + } + if let Ok(key) = EcdsaP256SigningKey::from_pkcs8_der(&bytes) { + return Ok(Box::new(key)); + } + if let Ok(key) = EcdsaP384SigningKey::from_pkcs8_der(&bytes) { + return Ok(Box::new(key)); + } + Err(KeyMaterialError::UnsupportedPrivateKey(path.to_owned())) +} + +pub fn load_verification_key( + path: &str, + algorithm: SignatureAlgorithm, +) -> Result { + let bytes = read(path)?; + let public_key_bytes = if let Ok(text) = std::str::from_utf8(&bytes) { + parse_pem(text, "PUBLIC KEY")? + } else { + bytes + }; + Ok(VerificationKey { + algorithm, + public_key_bytes, + certificate_der: None, + name: None, + }) +} + +pub fn load_certificate(path: &str) -> Result, KeyMaterialError> { + let bytes = read(path)?; + if let Ok(text) = std::str::from_utf8(&bytes) { + parse_pem(text, "CERTIFICATE") + } else { + Ok(bytes) + } +} + +fn parse_pem(text: &str, expected_label: &str) -> Result, KeyMaterialError> { + let (rest, pem) = x509_parser::pem::parse_x509_pem(text.as_bytes()) + .map_err(|_| KeyMaterialError::InvalidPem(expected_label.to_owned()))?; + if !rest.iter().all(u8::is_ascii_whitespace) || pem.label != expected_label { + return Err(KeyMaterialError::InvalidPem(expected_label.to_owned())); + } + Ok(pem.contents) +} + +pub fn load_rsa_private(path: &str) -> Result { + let bytes = read(path)?; + if let Ok(text) = std::str::from_utf8(&bytes) { + if let Ok(key) = RsaPrivateKey::from_pkcs8_pem(text) { + return Ok(key); + } + if let Ok(key) = RsaPrivateKey::from_pkcs1_pem(text) { + return Ok(key); + } + } + RsaPrivateKey::from_pkcs8_der(&bytes) + .or_else(|_| RsaPrivateKey::from_pkcs1_der(&bytes)) + .map_err(|_| KeyMaterialError::UnsupportedPrivateKey(path.to_owned())) +} + +pub fn load_rsa_public(path: &str) -> Result { + let bytes = read(path)?; + if let Ok(text) = std::str::from_utf8(&bytes) { + if let Ok(key) = RsaPublicKey::from_public_key_pem(text) { + return Ok(key); + } + if let Ok(key) = RsaPublicKey::from_pkcs1_pem(text) { + return Ok(key); + } + if let Ok(private) = RsaPrivateKey::from_pkcs8_pem(text) { + return Ok(private.to_public_key()); + } + } + RsaPublicKey::from_public_key_der(&bytes) + .or_else(|_| RsaPublicKey::from_pkcs1_der(&bytes)) + .map_err(|_| KeyMaterialError::UnsupportedPublicKey(path.to_owned())) +} + +pub fn load_symmetric(path: &str, expected: Option) -> Result, KeyMaterialError> { + // libxmlsec1's binary-key options consume the file verbatim. In particular, + // ASCII bytes must not be guessed to be a textual Base64 representation. + let key = read(path)?; + if let Some(expected) = expected + && key.len() != expected + { + return Err(KeyMaterialError::SymmetricLength { + expected, + actual: key.len(), + }); + } + Ok(key) +} diff --git a/tools/xmlsec1/src/lib.rs b/tools/xmlsec1/src/lib.rs new file mode 100644 index 0000000..3c2b0bd --- /dev/null +++ b/tools/xmlsec1/src/lib.rs @@ -0,0 +1,32 @@ +//! Native command-line compatibility surface for libxmlsec1 automation. + +mod args; +mod capabilities; +mod commands; +mod key_material; + +use std::{ffi::OsString, io::Write, process::ExitCode}; + +pub use args::{Command, Invocation, OptionValue, ParseError}; + +/// Parse and execute one `xmlsec1` process invocation. +pub fn run( + args: impl IntoIterator, + stdout: &mut dyn Write, + stderr: &mut dyn Write, +) -> ExitCode { + let invocation = match Invocation::parse(args) { + Ok(invocation) => invocation, + Err(error) => { + let _ = writeln!(stderr, "Error: {error}"); + return ExitCode::FAILURE; + } + }; + match commands::execute(invocation, stdout, stderr) { + Ok(()) => ExitCode::SUCCESS, + Err(error) => { + let _ = writeln!(stderr, "Error: {error}"); + ExitCode::FAILURE + } + } +} diff --git a/tools/xmlsec1/src/main.rs b/tools/xmlsec1/src/main.rs new file mode 100644 index 0000000..295e700 --- /dev/null +++ b/tools/xmlsec1/src/main.rs @@ -0,0 +1,9 @@ +use std::process::ExitCode; + +fn main() -> ExitCode { + xmlsec1_cli::run( + std::env::args_os(), + &mut std::io::stdout(), + &mut std::io::stderr(), + ) +} diff --git a/tools/xmlsec1/tests/process_contract.rs b/tools/xmlsec1/tests/process_contract.rs new file mode 100644 index 0000000..8f30633 --- /dev/null +++ b/tools/xmlsec1/tests/process_contract.rs @@ -0,0 +1,361 @@ +use std::{fs, path::Path, process::Command}; + +fn binary() -> &'static str { + env!("CARGO_BIN_EXE_xmlsec1") +} + +fn project_root() -> &'static Path { + Path::new(env!("CARGO_MANIFEST_DIR")) + .parent() + .and_then(Path::parent) + .unwrap() +} + +#[test] +fn signs_verifies_and_rejects_tampering_through_process_api() { + // Exercise the process boundary and prove a post-signature content change + // is classified as invalid rather than accepted or reported as a CLI error. + let temp = tempfile::tempdir().unwrap(); + let template = project_root() + .join("tests/fixtures/xmldsig/aleksey-xmldsig-01/enveloping-sha256-rsa-sha256.tmpl"); + let private_key = project_root().join("tests/fixtures/keys/rsa/rsa-4096-key.pem"); + let public_key = project_root().join("tests/fixtures/keys/rsa/rsa-4096-pubkey.pem"); + let signed = temp.path().join("signed.xml"); + + let sign = Command::new(binary()) + .args(["sign", "--privkey-pem"]) + .arg(&private_key) + .args(["--output"]) + .arg(&signed) + .arg(&template) + .output() + .unwrap(); + assert!( + sign.status.success(), + "{}", + String::from_utf8_lossy(&sign.stderr) + ); + + let verify = Command::new(binary()) + .args(["verify", "--pubkey-pem"]) + .arg(&public_key) + .arg(&signed) + .output() + .unwrap(); + assert!( + verify.status.success(), + "{}", + String::from_utf8_lossy(&verify.stderr) + ); + + let tampered = temp.path().join("tampered.xml"); + let xml = fs::read_to_string(&signed).unwrap(); + fs::write(&tampered, xml.replace("some text", "tampered text")).unwrap(); + let rejected = Command::new(binary()) + .args(["verify", "--pubkey-pem"]) + .arg(&public_key) + .arg(&tampered) + .output() + .unwrap(); + assert!(!rejected.status.success()); + assert!(String::from_utf8_lossy(&rejected.stderr).contains("invalid")); +} + +#[test] +fn encrypts_decrypts_and_rejects_wrong_symmetric_key() { + // A reciprocal binary round trip must preserve non-UTF-8 bytes, while an + // authenticated GCM decrypt with the wrong key must fail. + let temp = tempfile::tempdir().unwrap(); + let template = temp.path().join("template.xml"); + let plaintext = temp.path().join("plaintext.bin"); + let key = temp.path().join("key.bin"); + let wrong_key = temp.path().join("wrong-key.bin"); + let encrypted = temp.path().join("encrypted.xml"); + let decrypted = temp.path().join("decrypted.bin"); + fs::write( + &template, + r#" + +"#, + ) + .unwrap(); + fs::write(&plaintext, b"process-level binary payload\0\xff").unwrap(); + // xmlsec1 treats --aeskey input as raw bytes even when all bytes happen to + // be valid Base64 characters. + fs::write(&key, b"0123456789abcdef").unwrap(); + fs::write(&wrong_key, b"fedcba9876543210").unwrap(); + + let encrypt = Command::new(binary()) + .args(["encrypt", "--aeskey:content"]) + .arg(&key) + .args(["--binary-data"]) + .arg(&plaintext) + .args(["--output"]) + .arg(&encrypted) + .arg(&template) + .output() + .unwrap(); + assert!( + encrypt.status.success(), + "{}", + String::from_utf8_lossy(&encrypt.stderr) + ); + + let decrypt = Command::new(binary()) + .args(["decrypt", "--aeskey"]) + .arg(&key) + .args(["--output"]) + .arg(&decrypted) + .arg(&encrypted) + .output() + .unwrap(); + assert!( + decrypt.status.success(), + "{}", + String::from_utf8_lossy(&decrypt.stderr) + ); + assert_eq!(fs::read(&decrypted).unwrap(), fs::read(&plaintext).unwrap()); + + let rejected = Command::new(binary()) + .args(["decrypt", "--aeskey"]) + .arg(&wrong_key) + .arg(&encrypted) + .output() + .unwrap(); + assert!(!rejected.status.success()); +} + +#[test] +fn encrypts_and_decrypts_with_an_rsa_oaep_recipient() { + // The advertised RSA path must emit XML Encryption 1.1 OAEP and unwrap its + // generated content key through a separate CLI invocation. + let temp = tempfile::tempdir().unwrap(); + let template = temp.path().join("template.xml"); + let plaintext = temp.path().join("plaintext.bin"); + let encrypted = temp.path().join("encrypted.xml"); + let private_key = project_root().join("tests/fixtures/keys/rsa/rsa-4096-key.pem"); + let public_key = project_root().join("tests/fixtures/keys/rsa/rsa-4096-pubkey.pem"); + fs::write( + &template, + r#" + +"#, + ) + .unwrap(); + fs::write(&plaintext, b"RSA recipient payload").unwrap(); + + let encrypt = Command::new(binary()) + .args(["encrypt", "--pubkey-pem"]) + .arg(&public_key) + .args(["--binary-data"]) + .arg(&plaintext) + .args(["--output"]) + .arg(&encrypted) + .arg(&template) + .output() + .unwrap(); + assert!( + encrypt.status.success(), + "{}", + String::from_utf8_lossy(&encrypt.stderr) + ); + assert!( + fs::read_to_string(&encrypted) + .unwrap() + .contains("http://www.w3.org/2009/xmlenc11#rsa-oaep") + ); + + let decrypt = Command::new(binary()) + .args(["decrypt", "--privkey-pem"]) + .arg(&private_key) + .arg(&encrypted) + .output() + .unwrap(); + assert!( + decrypt.status.success(), + "{}", + String::from_utf8_lossy(&decrypt.stderr) + ); + assert_eq!(decrypt.stdout, fs::read(&plaintext).unwrap()); +} + +#[test] +fn decrypts_encrypted_data_embedded_in_a_document() { + // libxmlsec1 decrypt replaces EncryptedData in its containing document; a + // standalone-only implementation would reject this process contract. + let temp = tempfile::tempdir().unwrap(); + let template = temp.path().join("template.xml"); + let plaintext = temp.path().join("plaintext.xml"); + let key = temp.path().join("key.bin"); + let encrypted = temp.path().join("encrypted.xml"); + let document = temp.path().join("document.xml"); + fs::write( + &template, + r#" + +"#, + ) + .unwrap(); + fs::write(&plaintext, "document payload").unwrap(); + fs::write(&key, b"0123456789abcdef").unwrap(); + + let encrypt = Command::new(binary()) + .args(["encrypt", "--aeskey:content"]) + .arg(&key) + .args(["--xml-data"]) + .arg(&plaintext) + .args(["--output"]) + .arg(&encrypted) + .arg(&template) + .output() + .unwrap(); + assert!(encrypt.status.success()); + fs::write( + &document, + format!( + "{}", + fs::read_to_string(&encrypted).unwrap() + ), + ) + .unwrap(); + + let decrypted = Command::new(binary()) + .args(["decrypt", "--aeskey"]) + .arg(&key) + .arg(&document) + .output() + .unwrap(); + assert!( + decrypted.status.success(), + "{}", + String::from_utf8_lossy(&decrypted.stderr) + ); + let output = String::from_utf8(decrypted.stdout).unwrap(); + let parsed = roxmltree::Document::parse(&output).unwrap(); + assert_eq!(parsed.root_element().tag_name().name(), "root"); + assert_eq!( + parsed + .descendants() + .find(|node| node.has_tag_name("secret")) + .and_then(|node| node.text()), + Some("document payload") + ); +} + +#[test] +fn generated_key_store_uses_the_libxmlsec1_xml_shape() { + // Validate namespaces and element layout, not just well-formedness, because + // libxmlsec1's key manager depends on this exact interoperable structure. + let temp = tempfile::tempdir().unwrap(); + let key_store = temp.path().join("keys.xml"); + let generated = Command::new(binary()) + .args(["keys", "--gen-key:integration<&", "aes-128"]) + .arg(&key_store) + .output() + .unwrap(); + assert!( + generated.status.success(), + "{}", + String::from_utf8_lossy(&generated.stderr) + ); + + let xml = fs::read_to_string(key_store).unwrap(); + let document = roxmltree::Document::parse(&xml).unwrap(); + let elements = document + .descendants() + .filter(|node| node.is_element()) + .map(|node| (node.tag_name().namespace(), node.tag_name().name())) + .collect::>(); + assert_eq!( + elements, + vec![ + (Some("http://www.aleksey.com/xmlsec/2002"), "Keys"), + (Some("http://www.w3.org/2000/09/xmldsig#"), "KeyInfo"), + (Some("http://www.w3.org/2000/09/xmldsig#"), "KeyName"), + (Some("http://www.w3.org/2000/09/xmldsig#"), "KeyValue"), + (Some("http://www.aleksey.com/xmlsec/2002"), "AESKeyValue"), + ] + ); + assert_eq!( + document + .descendants() + .find(|node| node.has_tag_name(("http://www.w3.org/2000/09/xmldsig#", "KeyName"))) + .and_then(|node| node.text()), + Some("integration<&") + ); +} + +#[test] +fn generated_key_store_contains_every_requested_key() { + // Repeated --gen-key options are independent requests and must never be + // silently collapsed to the first parsed value. + let generated = Command::new(binary()) + .args([ + "keys", + "--gen-key:first", + "aes-128", + "--gen-key:second", + "aes-256", + ]) + .output() + .unwrap(); + assert!(generated.status.success()); + let xml = String::from_utf8(generated.stdout).unwrap(); + let document = roxmltree::Document::parse(&xml).unwrap(); + let names = document + .descendants() + .filter(|node| node.has_tag_name(("http://www.w3.org/2000/09/xmldsig#", "KeyName"))) + .filter_map(|node| node.text()) + .collect::>(); + assert_eq!(names, ["first", "second"]); +} + +#[test] +fn reports_capabilities_and_process_failures_deterministically() { + // Cover parser, capability, malformed-input, and output-path failures at + // the executable boundary where automation observes only status and stderr. + let temp = tempfile::tempdir().unwrap(); + let malformed = temp.path().join("malformed.xml"); + fs::write(&malformed, "").unwrap(); + + assert!( + Command::new(binary()) + .args(["check-transforms", "c14n", "rsa-sha256"]) + .status() + .unwrap() + .success() + ); + assert!( + !Command::new(binary()) + .args(["check-transforms", "xslt"]) + .status() + .unwrap() + .success() + ); + let invalid_xml = Command::new(binary()) + .args(["verify", "--pubkey-pem", "missing.pem"]) + .arg(&malformed) + .output() + .unwrap(); + assert!(!invalid_xml.status.success()); + assert!(String::from_utf8_lossy(&invalid_xml.stderr).contains("signature")); + assert!( + !Command::new(binary()) + .args(["verify", "--unknown-option", "missing.xml"]) + .status() + .unwrap() + .success() + ); + assert!( + !Command::new(binary()) + .args([ + "keys", + "--gen-key:test", + "aes-128", + "/missing/output/keys.xml" + ]) + .status() + .unwrap() + .success() + ); +} diff --git a/tools/xmlsec1/tests/upstream_runner.rs b/tools/xmlsec1/tests/upstream_runner.rs new file mode 100644 index 0000000..48759c1 --- /dev/null +++ b/tools/xmlsec1/tests/upstream_runner.rs @@ -0,0 +1,54 @@ +use std::{path::Path, process::Command}; + +fn root() -> &'static Path { + Path::new(env!("CARGO_MANIFEST_DIR")) + .parent() + .and_then(Path::parent) + .unwrap() +} + +fn run_upstream(script: &str, selected_test: &str) { + let tests = root().join("donors/xmlsec/tests"); + let output = Command::new(tests.join("testrun.sh")) + .arg(tests.join(script)) + .arg("rustcrypto") + .arg(&tests) + .arg(env!("CARGO_BIN_EXE_xmlsec1")) + .arg("der") + .env("XMLSEC_TEST_NAME", selected_test) + .env("XMLSEC_TEST_REPRODUCIBLE", "1") + .output() + .expect("the checked-in upstream runner must execute"); + assert!( + output.status.success(), + "{script} failed\nstdout:\n{}\nstderr:\n{}", + String::from_utf8_lossy(&output.stdout), + String::from_utf8_lossy(&output.stderr) + ); + let stdout = String::from_utf8_lossy(&output.stdout); + assert!(stdout.contains("TOTAL OK:"), "runner summary is missing"); + assert!( + stdout.contains("TOTAL FAILED: 0"), + "runner reported a failed operation" + ); +} + +#[test] +fn unmodified_dsig_runner_observes_failure_status() { + // This upstream negative vector proves that digest tampering reaches the + // native process and is reported with the status expected by testrun.sh. + run_upstream("testDSig.sh", "signature-rsa-enveloped-bad-digest-val"); +} + +#[test] +fn unmodified_enc_runner_round_trips_aes_gcm() { + run_upstream( + "testEnc.sh", + "xmlenc11-interop-2012/xenc11-example-AES128-GCM", + ); +} + +#[test] +fn unmodified_keys_runner_generates_aes_key_store() { + run_upstream("testKeys.sh", "test-aes128"); +} From 91b687626a16db22dcbbd62a2e082afbdf1bfa59 Mon Sep 17 00:00:00 2001 From: Dmitry Prudnikov Date: Fri, 14 Aug 2026 00:08:49 +0300 Subject: [PATCH 02/27] fix(cli): harden compatibility contracts --- .github/workflows/ci.yml | 17 +- README.md | 7 +- compatibility/libxmlsec1-1.3.13-rules.json | 2 +- compatibility/libxmlsec1-1.3.13.json | 2 +- docs/cli.md | 32 +- scripts/import-xmlsec1-cli-fixtures.sh | 65 + scripts/install-xmlsec1.sh | 37 +- tests/capability_ledger.rs | 37 +- tools/xmlsec1/README.md | 4 +- tools/xmlsec1/src/args.rs | 117 +- tools/xmlsec1/src/capabilities.rs | 32 +- tools/xmlsec1/src/commands.rs | 571 +++- tools/xmlsec1/src/key_material.rs | 183 +- .../tests/fixtures/upstream/DONOR_COMMIT | 1 + .../phaos-xmldsig-three/certs/rsa-ca-cert.der | Bin 0 -> 722 bytes ...signature-rsa-enveloped-bad-digest-val.xml | 6 + .../tests/fixtures/upstream/testDSig.sh | 2668 +++++++++++++++++ .../tests/fixtures/upstream/testEnc.sh | 2027 +++++++++++++ .../tests/fixtures/upstream/testKeys.sh | 613 ++++ .../tests/fixtures/upstream/testrun.sh | 590 ++++ .../xenc11-example-AES128-GCM.data | 1 + .../xenc11-example-AES128-GCM.key | 1 + .../xenc11-example-AES128-GCM.tmpl | 14 + .../xenc11-example-AES128-GCM.xml | 16 + tools/xmlsec1/tests/process_contract.rs | 382 ++- tools/xmlsec1/tests/upstream_runner.rs | 9 +- 26 files changed, 7264 insertions(+), 170 deletions(-) create mode 100755 scripts/import-xmlsec1-cli-fixtures.sh create mode 100644 tools/xmlsec1/tests/fixtures/upstream/DONOR_COMMIT create mode 100644 tools/xmlsec1/tests/fixtures/upstream/phaos-xmldsig-three/certs/rsa-ca-cert.der create mode 100644 tools/xmlsec1/tests/fixtures/upstream/phaos-xmldsig-three/signature-rsa-enveloped-bad-digest-val.xml create mode 100755 tools/xmlsec1/tests/fixtures/upstream/testDSig.sh create mode 100755 tools/xmlsec1/tests/fixtures/upstream/testEnc.sh create mode 100755 tools/xmlsec1/tests/fixtures/upstream/testKeys.sh create mode 100755 tools/xmlsec1/tests/fixtures/upstream/testrun.sh create mode 100644 tools/xmlsec1/tests/fixtures/upstream/xmlenc11-interop-2012/xenc11-example-AES128-GCM.data create mode 100644 tools/xmlsec1/tests/fixtures/upstream/xmlenc11-interop-2012/xenc11-example-AES128-GCM.key create mode 100644 tools/xmlsec1/tests/fixtures/upstream/xmlenc11-interop-2012/xenc11-example-AES128-GCM.tmpl create mode 100644 tools/xmlsec1/tests/fixtures/upstream/xmlenc11-interop-2012/xenc11-example-AES128-GCM.xml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1b354aa..def5158 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -13,6 +13,7 @@ env: RUSTFLAGS: -Dwarnings XMLSEC1_PREFIX: ${{ github.workspace }}/.tools/xmlsec1-1.3.13 XMLSEC1_BIN: ${{ github.workspace }}/.tools/xmlsec1-1.3.13/bin/xmlsec1 + XMLSEC1_SOURCE_DIR: ${{ github.workspace }}/donors/xmlsec LD_LIBRARY_PATH: ${{ github.workspace }}/.tools/xmlsec1-1.3.13/lib jobs: @@ -38,6 +39,8 @@ jobs: with: toolchain: "1.92.0" - uses: Swatinem/rust-cache@v2 + - name: Check native CLI upstream fixture snapshot + run: scripts/import-xmlsec1-cli-fixtures.sh --check - run: >- cargo run -p xml-sec-capability-ledger -- check donors/xmlsec @@ -79,13 +82,25 @@ jobs: - uses: actions/checkout@v7 with: persist-credentials: false + - name: Read pinned donor revision + id: test-donor-revision + run: | + commit="$(cat compatibility/libxmlsec1-1.3.13-donor-commit.txt)" + [[ "$commit" =~ ^[0-9a-f]{40}$ ]] + echo "commit=$commit" >> "$GITHUB_OUTPUT" + - uses: actions/checkout@v7 + with: + repository: lsh123/xmlsec + ref: ${{ steps.test-donor-revision.outputs.commit }} + path: donors/xmlsec + persist-credentials: false - uses: dtolnay/rust-toolchain@stable with: toolchain: ${{ matrix.rust }} - uses: taiki-e/install-action@nextest - name: Refresh apt package index run: sudo apt-get update - - name: Build pinned xmlsec1 for XMLDSig interop tests + - name: Build pinned xmlsec1 for external-oracle tests run: | sudo apt-get install --yes autoconf automake build-essential libltdl-dev libssl-dev libtool libxml2-dev pkg-config scripts/install-xmlsec1.sh diff --git a/README.md b/README.md index fb0cfd9..17b31b3 100644 --- a/README.md +++ b/README.md @@ -77,8 +77,11 @@ xmlsec1 list-transforms xmlsec1 list-key-data ``` -The native binary supports sign/verify, encrypt/decrypt, AES key generation, -capability checks, libxmlsec1 option syntax, and deterministic process statuses. +The native binary supports sign/verify, template-preserving encrypt/decrypt, +AES key generation, capability checks, libxmlsec1 key aliases and option syntax, +and deterministic process statuses. Its process tests run a minimal checked-in +snapshot of the unmodified upstream DSig, Enc, and Keys runners without network +access or a system `xmlsec1` installation. Unsupported algorithms, key formats, providers, and policy controls fail closed instead of being silently ignored. See the [CLI compatibility guide](docs/cli.md) for commands, examples, current format coverage, and upstream runner validation. diff --git a/compatibility/libxmlsec1-1.3.13-rules.json b/compatibility/libxmlsec1-1.3.13-rules.json index 9b89d25..67b453f 100644 --- a/compatibility/libxmlsec1-1.3.13-rules.json +++ b/compatibility/libxmlsec1-1.3.13-rules.json @@ -124,7 +124,7 @@ { "id": "native-cli-options", "kinds": ["cli-option"], - "name_regex": "^--(?:aes-key|binary-data|crypto|gen-key|help|ignore-manifests|insecure|output|pkcs8-der|pkcs8-pem|privkey-der|privkey-pem|pubkey-cert-der|pubkey-cert-pem|pubkey-der|pubkey-pem|trusted-der|trusted-pem|untrusted-der|untrusted-pem|xml-data)$", + "name_regex": "^--(?:aes-key|binary-data|crypto|gen-key|help|ignore-manifests|insecure|node-id|output|pkcs8-der|pkcs8-pem|privkey-der|privkey-pem|pubkey-cert-der|pubkey-cert-pem|pubkey-der|pubkey-pem|trusted-der|trusted-pem|untrusted-der|untrusted-pem|xml-data)$", "outcome": "provider-limited", "rationale": "The native CLI parses and executes this option for the RustCrypto-backed formats and algorithms advertised by its capability registry.", "evidence": "native-cli-tests" diff --git a/compatibility/libxmlsec1-1.3.13.json b/compatibility/libxmlsec1-1.3.13.json index 303cec1..02b3218 100644 --- a/compatibility/libxmlsec1-1.3.13.json +++ b/compatibility/libxmlsec1-1.3.13.json @@ -22077,7 +22077,7 @@ "source": "apps/xmlsec.c", "line": 652, "detail": "static xmlSecAppCmdLineParam nodeIdParam = { xmlSecAppCmdLineTopicDSigCommon | xmlSecAppCmdLineTopicEncCommon, \"--node-id\", NULL, \"--node-id \" \"\\n\\tset the operation start point to the node with given \", xmlSecAppCmdLineParamTypeString, xmlSecAppCmdLineParamFlagNone, NULL };", - "classification": "planned-cli-surface" + "classification": "native-cli-options" }, { "kind": "cli-option", diff --git a/docs/cli.md b/docs/cli.md index ee7efd7..d33dc72 100644 --- a/docs/cli.md +++ b/docs/cli.md @@ -25,7 +25,10 @@ Capability checks and runtime dispatch use one registry. A transform or key-data class absent from `list-*` is not silently substituted and causes `check-*` to fail. Backend selection is equally strict: `--crypto rustcrypto` and `--crypto default` select the built-in provider; other backend names do not -fall back to RustCrypto. +fall back to RustCrypto. The upstream runners pass `--crypto-config` for every +backend. RustCrypto accepts an absent or empty configuration directory because +it has no external backend configuration; a non-empty path is rejected rather +than ignored. ## Examples @@ -49,6 +52,9 @@ Files passed through `--aeskey` use libxmlsec1's binary-key contract: their bytes are consumed verbatim rather than guessed to be Base64 text. `decrypt` accepts both standalone `EncryptedData` and encrypted elements embedded in a larger XML document; `--node-id` selects an embedded `EncryptedData` by `Id`. +Encryption preserves the template's `Id`, `Type`, `MimeType`, `KeyInfo`, +`EncryptionProperties`, and RSA-OAEP parameters while replacing only the +cryptographic `CipherValue` payloads. Generate an AES key store using the upstream command shape: @@ -60,16 +66,30 @@ xmlsec1 keys --gen-key:content aes-256 keys.xml The command and status surface is available now, while individual key formats, algorithms, selectors, and policy controls remain capability-limited. Current -private-key loading accepts unencrypted PKCS#8 RSA, P-256, and P-384 keys; -public verification accepts SubjectPublicKeyInfo and X.509 certificates; direct -XMLEnc keys accept AES-128/256; RSA-OAEP uses RSA public/private keys. Encrypted +private-key loading accepts unencrypted PKCS#8 RSA, P-256, and P-384 plus +PKCS#1 RSA in PEM or DER; `--privkey-p8-pem` and `--privkey-p8-der` are accepted +as upstream PKCS#8 aliases. Public verification accepts SubjectPublicKeyInfo, +PKCS#1 RSA public keys, and X.509 certificates. Explicit certificate options +pin verification to that certificate's public key instead of permitting an +embedded `KeyInfo` to select another identity. Direct XMLEnc keys accept +AES-128/256; RSA-OAEP supports both the XMLEnc 1.0 `rsa-oaep-mgf1p` and XMLEnc +1.1 parameter contracts. Encrypted PKCS#8, PKCS#12, platform crypto stores, external DTDs, implicit network access, and unsupported CLI policy knobs fail rather than weakening policy or falling back. -The integration suite invokes the checked-in libxmlsec1 1.3.13 +Filesystem arguments remain native `OsString` values, so Unix paths are not +required to be UTF-8. Values immediately following valued options are consumed +verbatim, including names beginning with `-`, matching the upstream parser. + +The integration suite invokes a minimal checked-in snapshot of libxmlsec1 1.3.13 `testDSig.sh`, `testEnc.sh`, and `testKeys.sh` files without modification or a Python translation layer. Covered runner cases include signature failure classification, an AES-GCM decrypt/encrypt/decrypt cycle, and AES key-store -generation. The generated [compatibility ledger](compatibility-ledger.md) +generation. `scripts/import-xmlsec1-cli-fixtures.sh` refreshes only those +scripts and selected vectors from the pinned donor commit; CI verifies the +snapshot in the donor-backed ledger job. The native CLI package tests require +no network checkout or system `xmlsec1`; separate workspace interoperability +tests still build the pinned C implementation as an external oracle. The generated +[compatibility ledger](compatibility-ledger.md) records implemented commands and options separately from planned surface. diff --git a/scripts/import-xmlsec1-cli-fixtures.sh b/scripts/import-xmlsec1-cli-fixtures.sh new file mode 100755 index 0000000..cdd13cd --- /dev/null +++ b/scripts/import-xmlsec1-cli-fixtures.sh @@ -0,0 +1,65 @@ +#!/usr/bin/env bash +set -euo pipefail + +repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +donor_tests="${XMLSEC_DONOR_ROOT:-$repo_root/donors/xmlsec/tests}" +target="$repo_root/tools/xmlsec1/tests/fixtures/upstream" +mode="${1:-import}" +if [[ "$mode" != "import" && "$mode" != "--check" ]]; then + printf 'usage: %s [--check]\n' "$0" >&2 + exit 2 +fi +mkdir -p "$(dirname "$target")" +staging="$(mktemp -d "${target}.import.XXXXXX")" + +cleanup() { + rm -rf "$staging" +} +trap cleanup EXIT + +assets=( + "testrun.sh" + "testDSig.sh" + "testEnc.sh" + "testKeys.sh" + "phaos-xmldsig-three/signature-rsa-enveloped-bad-digest-val.xml" + "phaos-xmldsig-three/certs/rsa-ca-cert.der" + "xmlenc11-interop-2012/xenc11-example-AES128-GCM.xml" + "xmlenc11-interop-2012/xenc11-example-AES128-GCM.tmpl" + "xmlenc11-interop-2012/xenc11-example-AES128-GCM.data" + "xmlenc11-interop-2012/xenc11-example-AES128-GCM.key" +) + +for asset in "${assets[@]}"; do + source="$donor_tests/$asset" + if [[ ! -f "$source" ]]; then + printf 'pinned donor asset is missing: %s\n' "$source" >&2 + exit 1 + fi + mkdir -p "$staging/$(dirname "$asset")" + mode=0644 + if [[ "$asset" == *.sh ]]; then + mode=0755 + fi + install -m "$mode" "$source" "$staging/$asset" +done + +printf '%s\n' "$(<"$repo_root/compatibility/libxmlsec1-1.3.13-donor-commit.txt")" \ + > "$staging/DONOR_COMMIT" + +backup="${target}.backup.$$" +if [[ "$mode" == "--check" ]]; then + diff --recursive --brief "$target" "$staging" + exit +fi +if [[ -e "$target" ]]; then + mv "$target" "$backup" +fi +if mv "$staging" "$target"; then + rm -rf "$backup" +else + if [[ -e "$backup" ]]; then + mv "$backup" "$target" + fi + exit 1 +fi diff --git a/scripts/install-xmlsec1.sh b/scripts/install-xmlsec1.sh index c128360..c9d5e46 100755 --- a/scripts/install-xmlsec1.sh +++ b/scripts/install-xmlsec1.sh @@ -83,16 +83,35 @@ source_dir="$work_dir/xmlsec" build_dir="$work_dir/build" stage_dir="$work_dir/stage" -git init "$source_dir" -git -C "$source_dir" remote add origin "$XMLSEC1_REPOSITORY" -git -C "$source_dir" fetch --depth=1 origin "$XMLSEC1_COMMIT" -fetched_commit="$(git -C "$source_dir" rev-parse FETCH_HEAD)" -if [[ "$fetched_commit" != "$XMLSEC1_COMMIT" ]]; then - printf 'xmlsec1 source revision mismatch: expected %s, got %s\n' \ - "$XMLSEC1_COMMIT" "$fetched_commit" >&2 - exit 1 +if [[ -n "${XMLSEC1_SOURCE_DIR:-}" ]]; then + local_source_dir="$XMLSEC1_SOURCE_DIR" + if [[ "$local_source_dir" != /* || ! -d "$local_source_dir/.git" ]]; then + printf 'XMLSEC1_SOURCE_DIR must be an absolute git checkout: %s\n' "$local_source_dir" >&2 + exit 1 + fi + source_commit="$(git -C "$local_source_dir" rev-parse HEAD)" + if [[ "$source_commit" != "$XMLSEC1_COMMIT" ]]; then + printf 'xmlsec1 source revision mismatch: expected %s, got %s\n' \ + "$XMLSEC1_COMMIT" "$source_commit" >&2 + exit 1 + fi + mkdir -p "$source_dir" + # Configure/autoreconf writes generated files into the source tree. Export + # the verified commit so a CI checkout remains immutable and reusable by + # ledger and fixture checks. + git -C "$local_source_dir" archive "$XMLSEC1_COMMIT" | tar -x -C "$source_dir" +else + git init "$source_dir" + git -C "$source_dir" remote add origin "$XMLSEC1_REPOSITORY" + git -C "$source_dir" fetch --depth=1 origin "$XMLSEC1_COMMIT" + fetched_commit="$(git -C "$source_dir" rev-parse FETCH_HEAD)" + if [[ "$fetched_commit" != "$XMLSEC1_COMMIT" ]]; then + printf 'xmlsec1 source revision mismatch: expected %s, got %s\n' \ + "$XMLSEC1_COMMIT" "$fetched_commit" >&2 + exit 1 + fi + git -C "$source_dir" checkout --detach "$XMLSEC1_COMMIT" fi -git -C "$source_dir" checkout --detach "$XMLSEC1_COMMIT" mkdir -p "$build_dir" "$stage_dir" OBJ_DIR="$build_dir" "$source_dir/autogen.sh" \ diff --git a/tests/capability_ledger.rs b/tests/capability_ledger.rs index 13daa42..7ae29a0 100644 --- a/tests/capability_ledger.rs +++ b/tests/capability_ledger.rs @@ -788,7 +788,30 @@ fn native_cli_claims_match_process_and_upstream_runner_tests() { .unwrap_or_else(|| panic!("missing CLI command {command}")); assert_eq!(classification(&ledger, item).outcome, "behavior-compatible"); } - for option in ["--output", "--aes-key", "--privkey-pem", "--pubkey-pem"] { + for option in [ + "--aes-key", + "--binary-data", + "--crypto", + "--gen-key", + "--help", + "--ignore-manifests", + "--insecure", + "--node-id", + "--output", + "--pkcs8-der", + "--pkcs8-pem", + "--privkey-der", + "--privkey-pem", + "--pubkey-cert-der", + "--pubkey-cert-pem", + "--pubkey-der", + "--pubkey-pem", + "--trusted-der", + "--trusted-pem", + "--untrusted-der", + "--untrusted-pem", + "--xml-data", + ] { let item = ledger .items .iter() @@ -796,13 +819,21 @@ fn native_cli_claims_match_process_and_upstream_runner_tests() { .unwrap_or_else(|| panic!("missing CLI option {option}")); assert_eq!(classification(&ledger, item).outcome, "provider-limited"); } - for status in ["success", "failure"] { + for (status, outcome, code) in [ + ("success", "behavior-compatible", "0"), + ("failure", "behavior-compatible", "1"), + ("unknown-command", "planned", "0"), + ] { let item = ledger .items .iter() .find(|item| item.kind == "cli-exit-status" && item.name == status) .unwrap_or_else(|| panic!("missing CLI status {status}")); - assert_eq!(classification(&ledger, item).outcome, "behavior-compatible"); + assert_eq!(classification(&ledger, item).outcome, outcome); + assert!( + item.detail.contains(code), + "{status} must record exit code {code}" + ); } } diff --git a/tools/xmlsec1/README.md b/tools/xmlsec1/README.md index 8af7945..cb9b18d 100644 --- a/tools/xmlsec1/README.md +++ b/tools/xmlsec1/README.md @@ -16,4 +16,6 @@ coverage. `--aeskey` files are raw binary key material. Decryption accepts standalone `EncryptedData` and performs in-document replacement, optionally selected by -`--node-id`. +`--node-id`. Encryption retains template metadata and RSA-OAEP parameters; +PKCS#1 RSA and PKCS#8/SPKI/X.509 PEM or DER key material is normalized into the +same core signing, verification, and encryption pipelines. diff --git a/tools/xmlsec1/src/args.rs b/tools/xmlsec1/src/args.rs index 6b9f20c..3c8e760 100644 --- a/tools/xmlsec1/src/args.rs +++ b/tools/xmlsec1/src/args.rs @@ -1,4 +1,8 @@ -use std::{collections::BTreeMap, ffi::OsString, fmt}; +use std::{ + collections::BTreeMap, + ffi::{OsStr, OsString}, + fmt, +}; #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum Command { @@ -51,14 +55,14 @@ impl Command { pub struct OptionValue { pub name: String, pub parameter: Option, - pub value: Option, + pub value: Option, } #[derive(Debug, Clone, PartialEq, Eq)] pub struct Invocation { pub command: Command, pub options: BTreeMap>, - pub positional: Vec, + pub positional: Vec, } #[derive(Debug, thiserror::Error, PartialEq, Eq)] @@ -91,47 +95,44 @@ impl Invocation { .map_err(|_| ParseError::NonUtf8)?; let command = Command::parse(&command_text) .ok_or_else(|| ParseError::UnknownCommand(command_text.clone()))?; - let remaining = args - .map(|arg| arg.into_string().map_err(|_| ParseError::NonUtf8)) - .collect::, _>>()?; + let remaining = args.collect::>(); let mut options = BTreeMap::>::new(); let mut positional = Vec::new(); let mut index = 0; let mut options_finished = false; while index < remaining.len() { let argument = &remaining[index]; - if argument == "--" { + if argument == OsStr::new("--") { options_finished = true; index += 1; continue; } - if options_finished || !argument.starts_with('-') { + let option_text = argument.to_str(); + if options_finished || !option_text.is_some_and(|value| value.starts_with('-')) { positional.push(argument.clone()); options_finished = true; index += 1; continue; } - let stripped = argument.trim_start_matches('-'); + let argument_text = option_text.ok_or(ParseError::NonUtf8)?; + let stripped = argument_text.trim_start_matches('-'); let (raw_name, parameter) = stripped .split_once(':') .map_or((stripped, None), |(name, parameter)| { (name, Some(parameter.to_owned())) }); let name = canonical_option(raw_name) - .ok_or_else(|| ParseError::UnsupportedOption(argument.clone()))?; - let value = match option_arity(name) { - Arity::Flag => None, - Arity::Value => { - index += 1; - Some( - remaining - .get(index) - .filter(|value| !value.starts_with('-')) - .cloned() - .ok_or_else(|| ParseError::MissingOptionValue(argument.clone()))?, - ) - } - }; + .ok_or_else(|| ParseError::UnsupportedOption(argument_text.to_owned()))?; + let value = + match option_arity(name) { + Arity::Flag => None, + Arity::Value => { + index += 1; + Some(remaining.get(index).cloned().ok_or_else(|| { + ParseError::MissingOptionValue(argument_text.to_owned()) + })?) + } + }; options .entry(name.to_owned()) .or_default() @@ -153,7 +154,7 @@ impl Invocation { self.options.contains_key(name) } - pub fn last_value(&self, name: &str) -> Option<&str> { + pub fn last_value(&self, name: &str) -> Option<&OsStr> { self.options .get(name) .and_then(|values| values.last()) @@ -176,11 +177,11 @@ fn canonical_option(name: &str) -> Option<&'static str> { "print-xml-debug" => "print-xml-debug", "repeat" => "repeat", "keys-file" => "keys-file", - "gen-key" => "gen-key", + "g" | "gen-key" => "gen-key", "privkey" | "privkey-pem" => "privkey-pem", "privkey-der" => "privkey-der", - "pkcs8-pem" => "pkcs8-pem", - "pkcs8-der" => "pkcs8-der", + "pkcs8-pem" | "privkey-p8-pem" => "pkcs8-pem", + "pkcs8-der" | "privkey-p8-der" => "pkcs8-der", "pubkey" | "pubkey-pem" => "pubkey-pem", "pubkey-der" => "pubkey-der", "pubkey-cert-pem" => "pubkey-cert-pem", @@ -270,7 +271,7 @@ mod tests { ]) .expect("valid donor-shaped arguments must parse"); assert_eq!(parsed.command, Command::SignTemplate); - assert_eq!(parsed.last_value("output"), Some("signed.xml")); + assert_eq!(parsed.last_value("output"), Some(OsStr::new("signed.xml"))); assert_eq!(parsed.values("trusted-pem").count(), 2); assert_eq!( parsed @@ -281,7 +282,7 @@ mod tests { .as_deref(), Some("signer") ); - assert_eq!(parsed.positional, ["input.xml"]); + assert_eq!(parsed.positional, [OsString::from("input.xml")]); } #[test] @@ -302,7 +303,10 @@ mod tests { fn rejects_options_after_input_like_the_donor_parser() { let parsed = parse(&["xmlsec1", "verify", "input.xml", "--verbose"]) .expect("the donor treats trailing options as filenames"); - assert_eq!(parsed.positional, ["input.xml", "--verbose"]); + assert_eq!( + parsed.positional, + [OsString::from("input.xml"), OsString::from("--verbose")] + ); assert!(!parsed.flag("verbose")); } @@ -317,4 +321,57 @@ mod tests { Err(ParseError::MissingOptionValue(_)) )); } + + #[test] + fn consumes_dash_prefixed_option_values_verbatim() { + // Option arity, not the first byte of its value, determines parsing. + let parsed = parse(&["xmlsec1", "verify", "--pubkey-pem", "-key.pem", "input.xml"]) + .expect("the donor consumes the argument after a valued option"); + assert_eq!( + parsed.last_value("pubkey-pem"), + Some(OsStr::new("-key.pem")) + ); + } + + #[test] + fn recognizes_donor_key_generation_and_pkcs8_aliases() { + // These spellings are used by unmodified donor automation. + let generated = parse(&["xmlsec1", "keys", "-g:session", "aes-128"]) + .expect("short key-generation alias must parse"); + assert_eq!(generated.values("gen-key").count(), 1); + + for alias in ["--privkey-p8-pem", "--privkey-p8-der"] { + let parsed = parse(&["xmlsec1", "sign", alias, "key.p8", "template.xml"]) + .expect("PKCS#8 donor alias must parse"); + assert_eq!( + parsed + .values(if alias.ends_with("pem") { + "pkcs8-pem" + } else { + "pkcs8-der" + }) + .count(), + 1 + ); + } + } + + #[cfg(unix)] + #[test] + fn preserves_non_utf8_filesystem_arguments() { + // Unix paths are opaque bytes and must not pass through String. + use std::os::unix::ffi::OsStringExt as _; + + let path = OsString::from_vec(vec![b'i', b'n', 0xff]); + let parsed = Invocation::parse([ + OsString::from("xmlsec1"), + OsString::from("verify"), + OsString::from("--pubkey-pem"), + path.clone(), + path.clone(), + ]) + .expect("filesystem arguments are opaque bytes on Unix"); + assert_eq!(parsed.last_value("pubkey-pem"), Some(path.as_os_str())); + assert_eq!(parsed.positional, [path]); + } } diff --git a/tools/xmlsec1/src/capabilities.rs b/tools/xmlsec1/src/capabilities.rs index 6f970ac..068e485 100644 --- a/tools/xmlsec1/src/capabilities.rs +++ b/tools/xmlsec1/src/capabilities.rs @@ -1,4 +1,4 @@ -use std::io::Write; +use std::{ffi::OsString, io::Write}; pub const TRANSFORMS: &[&str] = &[ "base64", @@ -26,6 +26,7 @@ pub const TRANSFORMS: &[&str] = &[ "aes256-cbc", "aes128-gcm", "aes256-gcm", + "rsa-oaep-mgf1p", "rsa-oaep-enc11", ]; @@ -51,11 +52,15 @@ pub fn list(label: &str, values: &[&str], output: &mut dyn Write) -> std::io::Re writeln!(output) } -pub fn contains_all(values: &[&str], requested: &[String]) -> bool { +pub fn all_requested_available(values: &[&str], requested: &[OsString]) -> bool { + // libxmlsec1 treats an empty check as a vacuously successful query. Keep + // that process contract distinct from fail-closed handling of unknown names. requested .iter() - .flat_map(|value| value.split(',')) + .map(|value| value.to_str()) + .flat_map(|value| value.into_iter().flat_map(|value| value.split(','))) .all(|value| values.contains(&value)) + && requested.iter().all(|value| value.to_str().is_some()) } #[cfg(test)] @@ -64,12 +69,25 @@ mod tests { #[test] fn checks_comma_separated_and_repeated_capabilities() { - assert!(contains_all( + assert!(all_requested_available( TRANSFORMS, &["c14n,rsa-sha256".into(), "sha256".into()] )); - assert!(!contains_all(TRANSFORMS, &["xslt".into()])); - assert!(contains_all(TRANSFORMS, &["rsa-oaep-enc11".into()])); - assert!(!contains_all(KEY_DATA, &["key-name".into()])); + assert!(!all_requested_available(TRANSFORMS, &["xslt".into()])); + assert!(all_requested_available( + TRANSFORMS, + &["rsa-oaep-enc11".into()] + )); + assert!(all_requested_available( + TRANSFORMS, + &["rsa-oaep-mgf1p".into()] + )); + assert!(!all_requested_available(KEY_DATA, &["key-name".into()])); + } + + #[test] + fn empty_queries_match_the_donor_vacuous_success_contract() { + // No requested names means no missing capabilities in libxmlsec1. + assert!(all_requested_available(TRANSFORMS, &[])); } } diff --git a/tools/xmlsec1/src/commands.rs b/tools/xmlsec1/src/commands.rs index 0a040a0..1539b9f 100644 --- a/tools/xmlsec1/src/commands.rs +++ b/tools/xmlsec1/src/commands.rs @@ -1,8 +1,14 @@ -use std::{collections::HashSet, fs, io::Write}; +use std::{ + collections::HashSet, + ffi::OsStr, + fs::{self, File, OpenOptions}, + io::{Read, Write}, + path::{Path, PathBuf}, +}; use roxmltree::Document; use xml_sec::{ - policy::{EncryptionPolicy, SigningPolicy, VerificationPolicy}, + policy::{DecryptionPolicy, EncryptionPolicy, SigningPolicy, VerificationPolicy}, provider::default_provider, xmldsig::{ DefaultKeyResolver, DsigStatus, KeyResolverConfig, SignContext, SignatureAlgorithm, @@ -10,7 +16,8 @@ use xml_sec::{ }, xmlenc::{ DataEncryptionAlgorithm, DecryptContext, DecryptedContent, DecryptionKeyResolver, - EncryptedDataBuilder, EncryptedDataType, PrivateKeyDecryptor, SymmetricKeyDecryptor, + EncryptedDataBuilder, EncryptedDataType, EncryptionRecipient, KeyTransportAlgorithm, + OaepDigestAlgorithm, PrivateKeyDecryptor, RsaOaepParameters, SymmetricKeyDecryptor, }, }; @@ -31,6 +38,9 @@ const GENERIC_OPTIONS: &[&str] = &[ "print-xml-debug", "help", ]; +const XMLDSIG_NS: &str = "http://www.w3.org/2000/09/xmldsig#"; +const XMLENC_NS: &str = "http://www.w3.org/2001/04/xmlenc#"; +const XMLENC11_NS: &str = "http://www.w3.org/2009/xmlenc11#"; #[derive(Debug, thiserror::Error)] pub enum CommandError { @@ -40,11 +50,15 @@ pub enum CommandError { UnsupportedOption(String), #[error("unsupported crypto provider: {0}")] UnsupportedProvider(String), - #[error("I/O error for {path}: {source}")] + #[error("I/O error for {}: {source}", path.display())] Io { - path: String, + path: PathBuf, source: std::io::Error, }, + #[error("input XML exceeds policy limit of {maximum} bytes")] + InputTooLarge { maximum: usize }, + #[error("input XML is not valid UTF-8")] + InvalidUtf8Input, #[error(transparent)] Key(#[from] key_material::KeyMaterialError), #[error("XML signature operation failed: {0}")] @@ -66,6 +80,7 @@ pub fn execute( return help(stdout); } validate_provider(&invocation)?; + validate_crypto_config(&invocation)?; match invocation.command { Command::Help | Command::HelpAll @@ -80,7 +95,7 @@ pub fn execute( } Command::CheckTransforms => { validate_options(&invocation, &[])?; - if capabilities::contains_all(TRANSFORMS, &invocation.positional) { + if capabilities::all_requested_available(TRANSFORMS, &invocation.positional) { Ok(()) } else { Err(CommandError::CapabilityUnavailable) @@ -92,7 +107,7 @@ pub fn execute( } Command::CheckKeyData => { validate_options(&invocation, &[])?; - if capabilities::contains_all(KEY_DATA, &invocation.positional) { + if capabilities::all_requested_available(KEY_DATA, &invocation.positional) { Ok(()) } else { Err(CommandError::CapabilityUnavailable) @@ -117,7 +132,7 @@ fn help(output: &mut dyn Write) -> Result<(), CommandError> { } fn validate_provider(invocation: &Invocation) -> Result<(), CommandError> { - if let Some(provider) = invocation.last_value("crypto") + if let Some(provider) = option_text(invocation, "crypto")? && !matches!(provider, "rustcrypto" | "default") { return Err(CommandError::UnsupportedProvider(provider.to_owned())); @@ -125,6 +140,31 @@ fn validate_provider(invocation: &Invocation) -> Result<(), CommandError> { Ok(()) } +fn validate_crypto_config(invocation: &Invocation) -> Result<(), CommandError> { + let Some(path) = invocation.last_value("crypto-config") else { + return Ok(()); + }; + let path = Path::new(path); + if !path.exists() { + // The upstream runners always pass their backend-specific config path; + // for providers without external configuration that path is absent. + return Ok(()); + } + let empty_directory = path.is_dir() + && fs::read_dir(path) + .map_err(|source| CommandError::Io { + path: path.to_owned(), + source, + })? + .next() + .is_none(); + if empty_directory { + Ok(()) + } else { + Err(CommandError::UnsupportedOption("crypto-config".into())) + } +} + fn validate_options(invocation: &Invocation, command_options: &[&str]) -> Result<(), CommandError> { for name in invocation.options.keys() { if !GENERIC_OPTIONS.contains(&name.as_str()) && !command_options.contains(&name.as_str()) { @@ -134,7 +174,7 @@ fn validate_options(invocation: &Invocation, command_options: &[&str]) -> Result Ok(()) } -fn input_path(invocation: &Invocation) -> Result<&str, CommandError> { +fn input_path(invocation: &Invocation) -> Result<&OsStr, CommandError> { if invocation.positional.len() != 1 { return Err(CommandError::Usage(format!( "{} expects exactly one input file", @@ -144,12 +184,35 @@ fn input_path(invocation: &Invocation) -> Result<&str, CommandError> { Ok(&invocation.positional[0]) } -fn read_input(invocation: &Invocation) -> Result { +fn read_input(invocation: &Invocation, maximum: usize) -> Result { let path = input_path(invocation)?; - fs::read_to_string(path).map_err(|source| CommandError::Io { - path: path.to_owned(), - source, - }) + let mut bytes = Vec::with_capacity(maximum.min(64 * 1024)); + if path == OsStr::new("-") { + std::io::stdin() + .lock() + .take(maximum.saturating_add(1) as u64) + .read_to_end(&mut bytes) + .map_err(|source| CommandError::Io { + path: PathBuf::from("stdin"), + source, + })?; + } else { + File::open(path) + .map_err(|source| CommandError::Io { + path: PathBuf::from(path), + source, + })? + .take(maximum.saturating_add(1) as u64) + .read_to_end(&mut bytes) + .map_err(|source| CommandError::Io { + path: PathBuf::from(path), + source, + })?; + } + if bytes.len() > maximum { + return Err(CommandError::InputTooLarge { maximum }); + } + String::from_utf8(bytes).map_err(|_| CommandError::InvalidUtf8Input) } fn write_output( @@ -159,7 +222,7 @@ fn write_output( ) -> Result<(), CommandError> { if let Some(path) = invocation.last_value("output") { fs::write(path, bytes).map_err(|source| CommandError::Io { - path: path.to_owned(), + path: PathBuf::from(path), source, }) } else { @@ -167,6 +230,28 @@ fn write_output( } } +fn option_text<'a>( + invocation: &'a Invocation, + name: &str, +) -> Result, CommandError> { + invocation + .last_value(name) + .map(|value| { + value + .to_str() + .ok_or_else(|| CommandError::Usage(format!("--{name} value must be valid UTF-8"))) + }) + .transpose() +} + +fn option_value_text(option: &crate::OptionValue) -> Result<&str, CommandError> { + option + .value + .as_deref() + .and_then(OsStr::to_str) + .ok_or_else(|| CommandError::Usage(format!("--{} value must be valid UTF-8", option.name))) +} + fn sign(invocation: &Invocation, stdout: &mut dyn Write) -> Result<(), CommandError> { validate_options( invocation, @@ -189,28 +274,19 @@ fn sign(invocation: &Invocation, stdout: &mut dyn Write) -> Result<(), CommandEr if invocation.last_value("pwd").is_some() { return Err(CommandError::UnsupportedOption("pwd".into())); } - let key_option = ["privkey-pem", "privkey-der", "pkcs8-pem", "pkcs8-der"] - .into_iter() - .find_map(|name| invocation.values(name).next()); - let key_option = key_option.ok_or_else(|| { - CommandError::Usage("sign requires --privkey-pem or --pkcs8-pem/der".into()) - })?; + let policy = SigningPolicy::default(); + let xml = read_input(invocation, policy.resources.max_xml_document_bytes)?; + let (key_option, certificate_is_der) = select_signing_key(invocation, &xml)?; let value = key_option.value.as_deref().unwrap_or_default(); - let mut files = value.split(','); - let key_path = files.next().unwrap_or_default(); - let certificate_path = files.next(); - if files.next().is_some() { - return Err(CommandError::Usage( - "private key accepts at most one certificate path".into(), - )); - } - let xml = read_input(invocation)?; + let (key_path, certificate_path) = split_key_and_certificate(value)?; let key = key_material::load_signing_key(key_path)?; - let policy = SigningPolicy::default(); let signed = if let Some(certificate_path) = certificate_path { - let certificate = key_material::read_text(certificate_path)?; - let writer = X509CertificateKeyInfoWriter::from_pem(&certificate) - .map_err(|error| CommandError::Signature(error.to_string()))?; + let writer = if certificate_is_der { + X509CertificateKeyInfoWriter::from_der(&key_material::read(certificate_path)?) + } else { + X509CertificateKeyInfoWriter::from_pem(&key_material::read_text(certificate_path)?) + } + .map_err(|error| CommandError::Signature(error.to_string()))?; SignContext::new(key.as_ref()) .policy(policy) .key_info_writer(&writer) @@ -224,7 +300,89 @@ fn sign(invocation: &Invocation, stdout: &mut dyn Write) -> Result<(), CommandEr write_output(invocation, signed.as_bytes(), stdout) } -fn verification_policy(invocation: &Invocation) -> VerificationPolicy { +fn select_signing_key<'a>( + invocation: &'a Invocation, + xml: &str, +) -> Result<(&'a crate::OptionValue, bool), CommandError> { + let mut keys = Vec::new(); + for (name, certificate_is_der) in [ + ("privkey-pem", false), + ("privkey-der", true), + ("pkcs8-pem", false), + ("pkcs8-der", true), + ] { + keys.extend(invocation.values(name).map(|key| (key, certificate_is_der))); + } + if keys.is_empty() { + return Err(CommandError::Usage( + "sign requires --privkey-pem or --pkcs8-pem/der".into(), + )); + } + if let [selected] = keys.as_slice() { + return Ok(*selected); + } + let requested_name = template_key_name(xml)?; + if let Some(requested_name) = requested_name { + let matching = keys + .into_iter() + .filter(|(key, _)| key.parameter.as_deref() == Some(requested_name.as_str())) + .collect::>(); + return match matching.as_slice() { + [selected] => Ok(*selected), + [] => Err(CommandError::Usage(format!( + "signature template requests unknown KeyName {requested_name}" + ))), + _ => Err(CommandError::Usage(format!( + "multiple private keys use KeyName {requested_name}" + ))), + }; + } + Err(CommandError::Usage( + "multiple private keys require a template KeyName and named options".into(), + )) +} + +fn template_key_name(xml: &str) -> Result, CommandError> { + let document = + Document::parse(xml).map_err(|error| CommandError::Signature(error.to_string()))?; + let signature = document + .descendants() + .find(|node| node.has_tag_name((XMLDSIG_NS, "Signature"))); + Ok(signature.and_then(|signature| { + signature + .children() + .find(|node| node.has_tag_name((XMLDSIG_NS, "KeyInfo"))) + .and_then(|key_info| { + key_info + .children() + .find(|node| node.has_tag_name((XMLDSIG_NS, "KeyName"))) + }) + .and_then(|key_name| key_name.text()) + .map(str::to_owned) + })) +} + +fn split_key_and_certificate(value: &OsStr) -> Result<(&OsStr, Option<&OsStr>), CommandError> { + let bytes = value.as_encoded_bytes(); + let Some(separator) = bytes.iter().position(|byte| *byte == b',') else { + return Ok((value, None)); + }; + if bytes[separator + 1..].contains(&b',') { + return Err(CommandError::Usage( + "private key accepts at most one certificate path".into(), + )); + } + // Splitting at an ASCII byte preserves encoded-byte boundaries on every + // platform covered by OsStr's encoded-byte contract. + let key = unsafe { OsStr::from_encoded_bytes_unchecked(&bytes[..separator]) }; + let certificate = unsafe { OsStr::from_encoded_bytes_unchecked(&bytes[separator + 1..]) }; + Ok((key, Some(certificate))) +} + +fn xmlsec_compatibility_verification_policy(invocation: &Invocation) -> VerificationPolicy { + // Running the xmlsec1-compatible binary is the explicit compatibility + // boundary: donor verification accepts legacy signatures, while the core + // library's default policy and every signing path remain secure by default. let mut policy = VerificationPolicy { process_manifests: !invocation.flag("ignore-manifests"), reference_uri_types: UriTypeSet::ALL, @@ -274,20 +432,42 @@ fn verify(invocation: &Invocation, stdout: &mut dyn Write) -> Result<(), Command )?; reject_unimplemented_selectors(invocation, &[])?; reject_unimplemented_verification_policy(invocation)?; - let direct_path = ["pubkey-pem", "pubkey-der"] + let direct_keys = ["pubkey-pem", "pubkey-der"] + .into_iter() + .flat_map(|name| invocation.values(name)) + .collect::>(); + let explicit_certificates = ["pubkey-cert-pem", "pubkey-cert-der"] .into_iter() - .find_map(|name| invocation.last_value(name)); + .flat_map(|name| invocation.values(name)) + .collect::>(); + if direct_keys.len() + explicit_certificates.len() > 1 { + return Err(CommandError::Usage( + "verify accepts exactly one explicit public key or certificate".into(), + )); + } + let direct_path = direct_keys + .first() + .and_then(|option| option.value.as_deref()); // With an explicit public key there is no key-manager search to relax. // Reject the flag on resolver-backed paths until its semantics exist. - if invocation.flag("lax-key-search") && direct_path.is_none() { + if invocation.flag("lax-key-search") + && direct_path.is_none() + && explicit_certificates.is_empty() + { return Err(CommandError::UnsupportedOption("lax-key-search".into())); } - let xml = read_input(invocation)?; + let policy = xmlsec_compatibility_verification_policy(invocation); + let xml = read_input(invocation, policy.resources.max_xml_document_bytes)?; let algorithm = key_material::signature_algorithm(&xml)?; - let policy = verification_policy(invocation); let result = if let Some(path) = direct_path { let key = key_material::load_verification_key(path, algorithm)?; VerifyContext::new().policy(policy).key(&key).verify(&xml) + } else if let [certificate] = explicit_certificates.as_slice() { + let key = key_material::load_certificate_verification_key( + certificate.value.as_deref().unwrap_or_default(), + algorithm, + )?; + VerifyContext::new().policy(policy).key(&key).verify(&xml) } else { let mut config = KeyResolverConfig::default(); for name in [ @@ -349,10 +529,22 @@ fn encrypt(invocation: &Invocation, stdout: &mut dyn Write) -> Result<(), Comman ], )?; reject_unimplemented_selectors(invocation, &[])?; - let template = read_input(invocation)?; + let policy = EncryptionPolicy::default(); + let maximum_document_bytes = policy.resources.max_xml_document_bytes; + let template = read_input(invocation, policy.resources.max_xml_document_bytes)?; let (algorithm, encrypted_type) = encryption_template(&template)?; - let mut builder = EncryptedDataBuilder::new(algorithm).policy(EncryptionPolicy::default()); - if let Some(option) = invocation.values("aes-key").next() { + let mut builder = EncryptedDataBuilder::new(algorithm).policy(policy); + let aes_keys = invocation.values("aes-key").collect::>(); + let public_keys = ["pubkey-pem", "pubkey-der"] + .into_iter() + .flat_map(|name| invocation.values(name)) + .collect::>(); + if aes_keys.len() + public_keys.len() > 1 { + return Err(CommandError::Usage( + "encrypt accepts exactly one AES key or RSA public key".into(), + )); + } + if let [option] = aes_keys.as_slice() { let key = key_material::load_symmetric( option.value.as_deref().unwrap_or_default(), Some(algorithm.key_len()), @@ -361,11 +553,13 @@ fn encrypt(invocation: &Invocation, stdout: &mut dyn Write) -> Result<(), Comman if let Some(name) = option.parameter.as_deref() { builder = builder.direct_key_name(name); } - } else if let Some(path) = ["pubkey-pem", "pubkey-der"] - .into_iter() - .find_map(|name| invocation.last_value(name)) - { - builder = builder.recipient_rsa_oaep(key_material::load_rsa_public(path)?); + } else if let [option] = public_keys.as_slice() { + let path = option.value.as_deref().unwrap_or_default(); + let mut recipient = EncryptionRecipient::rsa_oaep(key_material::load_rsa_public(path)?); + if let Some(parameters) = template_oaep_parameters(&template)? { + recipient = recipient.oaep_parameters(parameters); + } + builder = builder.add_recipient(recipient); } else { return Err(CommandError::Usage( "encrypt requires --aes-key or --pubkey-pem".into(), @@ -384,7 +578,183 @@ fn encrypt(invocation: &Invocation, stdout: &mut dyn Write) -> Result<(), Comman )); } .map_err(|error| CommandError::Encryption(error.to_string()))?; - write_output(invocation, result.encrypted_data_xml.as_bytes(), stdout) + let rendered = apply_encryption_template(&template, &result.encrypted_data_xml)?; + if rendered.len() > maximum_document_bytes { + return Err(CommandError::Encryption( + "encrypted template output exceeds XML document policy".into(), + )); + } + write_output(invocation, rendered.as_bytes(), stdout) +} + +fn template_oaep_parameters(xml: &str) -> Result, CommandError> { + let document = + Document::parse(xml).map_err(|error| CommandError::Encryption(error.to_string()))?; + let Some(method) = document + .descendants() + .find(|node| node.has_tag_name((XMLENC_NS, "EncryptedKey"))) + .and_then(|key| { + key.children() + .find(|node| node.has_tag_name((XMLENC_NS, "EncryptionMethod"))) + }) + else { + return Ok(None); + }; + let algorithm = method + .attribute("Algorithm") + .ok_or_else(|| CommandError::Encryption("EncryptedKey has no algorithm".into()))?; + let transport = KeyTransportAlgorithm::from_uri(algorithm) + .map_err(|error| CommandError::Encryption(error.to_string()))?; + let digest = method + .children() + .find(|node| node.has_tag_name((XMLDSIG_NS, "DigestMethod"))) + .and_then(|node| node.attribute("Algorithm")); + let mgf = method + .children() + .find(|node| node.has_tag_name((XMLENC11_NS, "MGF"))) + .and_then(|node| node.attribute("Algorithm")); + let digest = oaep_digest_from_uri(digest.unwrap_or(OaepDigestAlgorithm::Sha1.uri()))?; + let mgf_digest = if transport == KeyTransportAlgorithm::RsaOaepMgf1p { + OaepDigestAlgorithm::Sha1 + } else { + oaep_mgf_from_uri(mgf.unwrap_or(OaepDigestAlgorithm::Sha1.mgf_uri()))? + }; + let label = method + .children() + .find(|node| node.has_tag_name((XMLENC_NS, "OAEPparams"))) + .and_then(|node| node.text()) + .map_or_else( + || Ok(Vec::new()), + |encoded| { + base64::Engine::decode( + &base64::engine::general_purpose::STANDARD, + encoded.split_ascii_whitespace().collect::(), + ) + .map_err(|error| CommandError::Encryption(format!("invalid OAEPparams: {error}"))) + }, + )?; + Ok(Some(RsaOaepParameters { + algorithm: transport, + digest, + mgf_digest, + label, + })) +} + +fn oaep_digest_from_uri(uri: &str) -> Result { + [ + OaepDigestAlgorithm::Sha1, + OaepDigestAlgorithm::Sha256, + OaepDigestAlgorithm::Sha384, + OaepDigestAlgorithm::Sha512, + ] + .into_iter() + .find(|digest| digest.uri() == uri) + .ok_or_else(|| CommandError::Encryption(format!("unsupported OAEP digest: {uri}"))) +} + +fn oaep_mgf_from_uri(uri: &str) -> Result { + [ + OaepDigestAlgorithm::Sha1, + OaepDigestAlgorithm::Sha256, + OaepDigestAlgorithm::Sha384, + OaepDigestAlgorithm::Sha512, + ] + .into_iter() + .find(|digest| digest.mgf_uri() == uri) + .ok_or_else(|| CommandError::Encryption(format!("unsupported OAEP MGF: {uri}"))) +} + +fn apply_encryption_template(template: &str, generated: &str) -> Result { + let template_document = + Document::parse(template).map_err(|error| CommandError::Encryption(error.to_string()))?; + let generated_document = + Document::parse(generated).map_err(|error| CommandError::Encryption(error.to_string()))?; + let template_data = template_document + .descendants() + .find(|node| node.has_tag_name((XMLENC_NS, "EncryptedData"))) + .ok_or_else(|| CommandError::Encryption("template has no EncryptedData".into()))?; + let generated_data = generated_document.root_element(); + let template_cipher = encrypted_data_cipher_value(template_data) + .ok_or_else(|| CommandError::Encryption("template has no CipherValue".into()))?; + let generated_cipher = encrypted_data_cipher_value(generated_data) + .ok_or_else(|| CommandError::Encryption("generated data has no CipherValue".into()))?; + let mut replacements = vec![( + template_cipher.range(), + standalone_cipher_value(generated_cipher), + )]; + + let template_key_info = direct_child_element(template_data, XMLDSIG_NS, "KeyInfo"); + let generated_key_info = direct_child_element(generated_data, XMLDSIG_NS, "KeyInfo"); + match (template_key_info, generated_key_info) { + (Some(template_key_info), Some(generated_key_info)) => { + let template_values = template_key_info + .descendants() + .filter(|node| node.has_tag_name((XMLENC_NS, "CipherValue"))) + .collect::>(); + let generated_values = generated_key_info + .descendants() + .filter(|node| node.has_tag_name((XMLENC_NS, "CipherValue"))) + .collect::>(); + if template_values.len() != generated_values.len() { + return Err(CommandError::Encryption( + "template KeyInfo does not contain one CipherValue per generated recipient" + .into(), + )); + } + replacements.extend(template_values.into_iter().zip(generated_values).map( + |(template_value, generated_value)| { + ( + template_value.range(), + standalone_cipher_value(generated_value), + ) + }, + )); + } + (None, Some(generated_key_info)) => { + let cipher_data = direct_child_element(template_data, XMLENC_NS, "CipherData") + .ok_or_else(|| CommandError::Encryption("template has no CipherData".into()))?; + let key_info = generated[generated_key_info.range()].replacen( + " {} + } + replacements.sort_by_key(|(range, _)| std::cmp::Reverse(range.start)); + let mut output = template.to_owned(); + for (range, replacement) in replacements { + output.replace_range(range, &replacement); + } + Ok(output) +} + +fn standalone_cipher_value(node: roxmltree::Node<'_, '_>) -> String { + format!( + "{}", + node.text().unwrap_or_default() + ) +} + +fn direct_child_element<'a, 'input>( + node: roxmltree::Node<'a, 'input>, + namespace: &str, + name: &str, +) -> Option> { + node.children() + .find(|child| child.has_tag_name((namespace, name))) +} + +fn encrypted_data_cipher_value<'a, 'input>( + data: roxmltree::Node<'a, 'input>, +) -> Option> { + direct_child_element(data, XMLENC_NS, "CipherData") + .and_then(|cipher| direct_child_element(cipher, XMLENC_NS, "CipherValue")) } fn decrypt(invocation: &Invocation, stdout: &mut dyn Write) -> Result<(), CommandError> { @@ -410,17 +780,31 @@ fn decrypt(invocation: &Invocation, stdout: &mut dyn Write) -> Result<(), Comman if invocation.last_value("pwd").is_some() { return Err(CommandError::UnsupportedOption("pwd".into())); } - let xml = read_input(invocation)?; - let encrypted_data_id = invocation.last_value("node-id"); - let bytes = if let Some(option) = invocation.values("aes-key").next() { - let key = key_material::load_symmetric(option.value.as_deref().unwrap_or_default(), None)?; - decrypt_input(&SymmetricKeyDecryptor::new(key), &xml, encrypted_data_id)? - } else if let Some(path) = ["privkey-pem", "privkey-der", "pkcs8-pem", "pkcs8-der"] + let policy = DecryptionPolicy::default(); + let xml = read_input(invocation, policy.resources.max_xml_document_bytes)?; + let encrypted_data_id = option_text(invocation, "node-id")?; + let aes_keys = invocation.values("aes-key").collect::>(); + let private_keys = ["privkey-pem", "privkey-der", "pkcs8-pem", "pkcs8-der"] .into_iter() - .find_map(|name| invocation.last_value(name)) - { + .flat_map(|name| invocation.values(name)) + .collect::>(); + if aes_keys.len() + private_keys.len() > 1 { + return Err(CommandError::Usage( + "decrypt accepts exactly one AES key or RSA private key".into(), + )); + } + let bytes = if let [option] = aes_keys.as_slice() { + let key = key_material::load_symmetric(option.value.as_deref().unwrap_or_default(), None)?; + decrypt_input( + &SymmetricKeyDecryptor::new(key), + &xml, + encrypted_data_id, + policy, + )? + } else if let [option] = private_keys.as_slice() { + let path = option.value.as_deref().unwrap_or_default(); let resolver = PrivateKeyDecryptor::new(key_material::load_rsa_private(path)?); - decrypt_input(&resolver, &xml, encrypted_data_id)? + decrypt_input(&resolver, &xml, encrypted_data_id, policy)? } else { return Err(CommandError::Usage( "decrypt requires --aes-key or an RSA private key".into(), @@ -433,13 +817,14 @@ fn decrypt_input( resolver: &dyn DecryptionKeyResolver, xml: &str, encrypted_data_id: Option<&str>, + policy: DecryptionPolicy, ) -> Result, CommandError> { let document = Document::parse(xml).map_err(|error| CommandError::Encryption(error.to_string()))?; let standalone = document .root_element() .has_tag_name(("http://www.w3.org/2001/04/xmlenc#", "EncryptedData")); - let context = DecryptContext::new(resolver); + let context = DecryptContext::new(resolver).policy(policy); if standalone && encrypted_data_id.is_none() { return context .decrypt(xml) @@ -462,18 +847,23 @@ fn encryption_template( Document::parse(xml).map_err(|error| CommandError::Encryption(error.to_string()))?; let encrypted_data = document .descendants() - .find(|node| node.is_element() && node.tag_name().name() == "EncryptedData") + .find(|node| node.has_tag_name((XMLENC_NS, "EncryptedData"))) .ok_or_else(|| CommandError::Encryption("template has no EncryptedData".into()))?; let method = encrypted_data .children() - .find(|node| node.is_element() && node.tag_name().name() == "EncryptionMethod") + .find(|node| node.has_tag_name((XMLENC_NS, "EncryptionMethod"))) .and_then(|node| node.attribute("Algorithm")) .ok_or_else(|| CommandError::Encryption("template has no encryption algorithm".into()))?; let algorithm = DataEncryptionAlgorithm::from_uri(method) .map_err(|error| CommandError::Encryption(error.to_string()))?; let encrypted_type = match encrypted_data.attribute("Type") { + None | Some("http://www.w3.org/2001/04/xmlenc#Element") => EncryptedDataType::Element, Some("http://www.w3.org/2001/04/xmlenc#Content") => EncryptedDataType::Content, - _ => EncryptedDataType::Element, + Some(other) => { + return Err(CommandError::Encryption(format!( + "unsupported EncryptedData Type: {other}" + ))); + } }; Ok((algorithm, encrypted_type)) } @@ -492,7 +882,7 @@ fn keys(invocation: &Invocation, stdout: &mut dyn Write) -> Result<(), CommandEr .parameter .as_deref() .ok_or_else(|| CommandError::Usage("--gen-key requires a key name".into()))?; - let algorithm = generated.value.as_deref().unwrap_or_default(); + let algorithm = option_value_text(generated)?; let size = match algorithm { "aes-128" => 16, "aes-192" => 24, @@ -524,15 +914,39 @@ fn keys(invocation: &Invocation, stdout: &mut dyn Write) -> Result<(), CommandEr )); } if let Some(path) = invocation.positional.first() { - fs::write(path, document.as_bytes()).map_err(|source| CommandError::Io { - path: path.clone(), - source, - }) + write_secret_file(path, document.as_bytes()) } else { stdout.write_all(document.as_bytes()).map_err(stdout_error) } } +fn write_secret_file(path: &OsStr, bytes: &[u8]) -> Result<(), CommandError> { + let mut options = OpenOptions::new(); + options.write(true).create(true).truncate(true); + #[cfg(unix)] + { + use std::os::unix::fs::OpenOptionsExt as _; + options.mode(0o600); + } + let mut file = options.open(path).map_err(|source| CommandError::Io { + path: PathBuf::from(path), + source, + })?; + #[cfg(unix)] + { + use std::os::unix::fs::PermissionsExt as _; + file.set_permissions(fs::Permissions::from_mode(0o600)) + .map_err(|source| CommandError::Io { + path: PathBuf::from(path), + source, + })?; + } + file.write_all(bytes).map_err(|source| CommandError::Io { + path: PathBuf::from(path), + source, + }) +} + fn reject_unimplemented_selectors( invocation: &Invocation, supported: &[&str], @@ -569,7 +983,7 @@ fn reject_unimplemented_verification_policy(invocation: &Invocation) -> Result<( fn stdout_error(source: std::io::Error) -> CommandError { CommandError::Io { - path: "stdout".into(), + path: PathBuf::from("stdout"), source, } } @@ -585,7 +999,7 @@ mod tests { } #[test] - fn capability_checks_fail_closed() { + fn capability_checks_reject_unknown_names() { let mut output = Vec::new(); assert!( execute( @@ -635,4 +1049,23 @@ mod tests { .unwrap_err(); assert!(matches!(error, CommandError::UnsupportedOption(_))); } + + #[test] + fn input_reader_enforces_the_compiled_policy_limit_before_parsing() { + // The reader must stop at maximum + 1 rather than allocating an entire + // attacker-controlled XML file before the operation policy sees it. + let temp = tempfile::tempdir().unwrap(); + let path = temp.path().join("oversized.xml"); + fs::write(&path, b"").unwrap(); + let invocation = Invocation::parse([ + OsString::from("xmlsec1"), + OsString::from("verify"), + path.into_os_string(), + ]) + .unwrap(); + assert!(matches!( + read_input(&invocation, 4), + Err(CommandError::InputTooLarge { maximum: 4 }) + )); + } } diff --git a/tools/xmlsec1/src/key_material.rs b/tools/xmlsec1/src/key_material.rs index e9b1a01..a782ebd 100644 --- a/tools/xmlsec1/src/key_material.rs +++ b/tools/xmlsec1/src/key_material.rs @@ -1,11 +1,17 @@ -use std::fs; +use std::{ + fs, + path::{Path, PathBuf}, +}; use roxmltree::Document; use rsa::{ RsaPrivateKey, RsaPublicKey, pkcs1::{DecodeRsaPrivateKey as _, DecodeRsaPublicKey as _}, - pkcs8::{DecodePrivateKey as _, DecodePublicKey as _}, + pkcs8::{ + DecodePrivateKey as _, DecodePublicKey as _, EncodePrivateKey as _, EncodePublicKey as _, + }, }; +use x509_parser::prelude::FromDer as _; use xml_sec::xmldsig::{ EcdsaP256SigningKey, EcdsaP384SigningKey, RsaSigningKey, SignatureAlgorithm, SigningKey, VerificationKey, find_signature_node, parse_signed_info, @@ -15,15 +21,17 @@ use xml_sec::xmldsig::{ pub enum KeyMaterialError { #[error("failed to read key file {path}: {source}")] Read { - path: String, + path: PathBuf, source: std::io::Error, }, - #[error("invalid PEM key in {0}")] - InvalidPem(String), - #[error("unsupported private key in {0}")] - UnsupportedPrivateKey(String), - #[error("unsupported public key in {0}")] - UnsupportedPublicKey(String), + #[error("invalid PEM key in {}", .0.display())] + InvalidPem(PathBuf), + #[error("unsupported private key in {}", .0.display())] + UnsupportedPrivateKey(PathBuf), + #[error("unsupported public key in {}", .0.display())] + UnsupportedPublicKey(PathBuf), + #[error("invalid X.509 certificate in {}", .0.display())] + InvalidCertificate(PathBuf), #[error("signature template does not contain a valid SignedInfo")] MissingSignedInfo, #[error("invalid XML signature: {0}")] @@ -32,14 +40,16 @@ pub enum KeyMaterialError { SymmetricLength { expected: usize, actual: usize }, } -pub fn read(path: &str) -> Result, KeyMaterialError> { +pub fn read(path: impl AsRef) -> Result, KeyMaterialError> { + let path = path.as_ref(); fs::read(path).map_err(|source| KeyMaterialError::Read { path: path.to_owned(), source, }) } -pub fn read_text(path: &str) -> Result { +pub fn read_text(path: impl AsRef) -> Result { + let path = path.as_ref(); String::from_utf8(read(path)?).map_err(|_| KeyMaterialError::InvalidPem(path.to_owned())) } @@ -56,7 +66,8 @@ pub fn signature_algorithm(xml: &str) -> Result Result, KeyMaterialError> { +pub fn load_signing_key(path: impl AsRef) -> Result, KeyMaterialError> { + let path = path.as_ref(); let bytes = read(path)?; if let Ok(text) = std::str::from_utf8(&bytes) { if let Ok(key) = RsaSigningKey::from_pkcs8_pem(text) { @@ -78,19 +89,51 @@ pub fn load_signing_key(path: &str) -> Result, KeyMaterialEr if let Ok(key) = EcdsaP384SigningKey::from_pkcs8_der(&bytes) { return Ok(Box::new(key)); } + let rsa = std::str::from_utf8(&bytes) + .ok() + .and_then(|text| RsaPrivateKey::from_pkcs1_pem(text).ok()) + .or_else(|| RsaPrivateKey::from_pkcs1_der(&bytes).ok()); + if let Some(rsa) = rsa { + let der = rsa + .to_pkcs8_der() + .map_err(|_| KeyMaterialError::UnsupportedPrivateKey(path.to_owned()))?; + return RsaSigningKey::from_pkcs8_der(der.as_bytes()) + .map(|key| Box::new(key) as Box) + .map_err(|_| KeyMaterialError::UnsupportedPrivateKey(path.to_owned())); + } Err(KeyMaterialError::UnsupportedPrivateKey(path.to_owned())) } pub fn load_verification_key( - path: &str, + path: impl AsRef, algorithm: SignatureAlgorithm, ) -> Result { + let path = path.as_ref(); let bytes = read(path)?; let public_key_bytes = if let Ok(text) = std::str::from_utf8(&bytes) { - parse_pem(text, "PUBLIC KEY")? - } else { + if let Ok(spki) = parse_pem(text, "PUBLIC KEY", path) { + spki + } else if let Ok(key) = RsaPublicKey::from_pkcs1_pem(text) { + key.to_public_key_der() + .map_err(|_| KeyMaterialError::UnsupportedPublicKey(path.to_owned()))? + .as_bytes() + .to_vec() + } else { + return Err(KeyMaterialError::UnsupportedPublicKey(path.to_owned())); + } + } else if valid_spki(&bytes) { bytes + } else if let Ok(key) = RsaPublicKey::from_pkcs1_der(&bytes) { + key.to_public_key_der() + .map_err(|_| KeyMaterialError::UnsupportedPublicKey(path.to_owned()))? + .as_bytes() + .to_vec() + } else { + return Err(KeyMaterialError::UnsupportedPublicKey(path.to_owned())); }; + if !valid_spki(&public_key_bytes) { + return Err(KeyMaterialError::UnsupportedPublicKey(path.to_owned())); + } Ok(VerificationKey { algorithm, public_key_bytes, @@ -99,25 +142,53 @@ pub fn load_verification_key( }) } -pub fn load_certificate(path: &str) -> Result, KeyMaterialError> { +fn valid_spki(bytes: &[u8]) -> bool { + x509_parser::x509::SubjectPublicKeyInfo::from_der(bytes).is_ok_and(|(rest, _)| rest.is_empty()) +} + +pub fn load_certificate(path: impl AsRef) -> Result, KeyMaterialError> { + let path = path.as_ref(); let bytes = read(path)?; - if let Ok(text) = std::str::from_utf8(&bytes) { - parse_pem(text, "CERTIFICATE") + let der = if let Ok(text) = std::str::from_utf8(&bytes) { + parse_pem(text, "CERTIFICATE", path)? } else { - Ok(bytes) + bytes + }; + let (rest, _) = x509_parser::certificate::X509Certificate::from_der(&der) + .map_err(|_| KeyMaterialError::InvalidCertificate(path.to_owned()))?; + if !rest.is_empty() { + return Err(KeyMaterialError::InvalidCertificate(path.to_owned())); } + Ok(der) } -fn parse_pem(text: &str, expected_label: &str) -> Result, KeyMaterialError> { +pub fn load_certificate_verification_key( + path: impl AsRef, + algorithm: SignatureAlgorithm, +) -> Result { + let path = path.as_ref(); + let certificate_der = load_certificate(path)?; + let (_, certificate) = x509_parser::certificate::X509Certificate::from_der(&certificate_der) + .map_err(|_| KeyMaterialError::InvalidCertificate(path.to_owned()))?; + Ok(VerificationKey { + algorithm, + public_key_bytes: certificate.public_key().raw.to_vec(), + certificate_der: Some(certificate_der), + name: None, + }) +} + +fn parse_pem(text: &str, expected_label: &str, path: &Path) -> Result, KeyMaterialError> { let (rest, pem) = x509_parser::pem::parse_x509_pem(text.as_bytes()) - .map_err(|_| KeyMaterialError::InvalidPem(expected_label.to_owned()))?; + .map_err(|_| KeyMaterialError::InvalidPem(path.to_owned()))?; if !rest.iter().all(u8::is_ascii_whitespace) || pem.label != expected_label { - return Err(KeyMaterialError::InvalidPem(expected_label.to_owned())); + return Err(KeyMaterialError::InvalidPem(path.to_owned())); } Ok(pem.contents) } -pub fn load_rsa_private(path: &str) -> Result { +pub fn load_rsa_private(path: impl AsRef) -> Result { + let path = path.as_ref(); let bytes = read(path)?; if let Ok(text) = std::str::from_utf8(&bytes) { if let Ok(key) = RsaPrivateKey::from_pkcs8_pem(text) { @@ -132,7 +203,8 @@ pub fn load_rsa_private(path: &str) -> Result { .map_err(|_| KeyMaterialError::UnsupportedPrivateKey(path.to_owned())) } -pub fn load_rsa_public(path: &str) -> Result { +pub fn load_rsa_public(path: impl AsRef) -> Result { + let path = path.as_ref(); let bytes = read(path)?; if let Ok(text) = std::str::from_utf8(&bytes) { if let Ok(key) = RsaPublicKey::from_public_key_pem(text) { @@ -150,7 +222,10 @@ pub fn load_rsa_public(path: &str) -> Result { .map_err(|_| KeyMaterialError::UnsupportedPublicKey(path.to_owned())) } -pub fn load_symmetric(path: &str, expected: Option) -> Result, KeyMaterialError> { +pub fn load_symmetric( + path: impl AsRef, + expected: Option, +) -> Result, KeyMaterialError> { // libxmlsec1's binary-key options consume the file verbatim. In particular, // ASCII bytes must not be guessed to be a textual Base64 representation. let key = read(path)?; @@ -164,3 +239,61 @@ pub fn load_symmetric(path: &str, expected: Option) -> Result, Ke } Ok(key) } + +#[cfg(test)] +mod tests { + use rsa::{ + pkcs1::{EncodeRsaPrivateKey as _, EncodeRsaPublicKey as _}, + pkcs8::DecodePrivateKey as _, + }; + + use super::*; + + fn fixture(path: &str) -> PathBuf { + Path::new(env!("CARGO_MANIFEST_DIR")) + .parent() + .and_then(Path::parent) + .unwrap() + .join(path) + } + + #[test] + fn normalizes_pkcs1_private_and_public_keys() { + // PKCS#1 is a donor-supported RSA container. The CLI normalizes it to + // the core's PKCS#8/SPKI contracts instead of duplicating crypto code. + let original = RsaPrivateKey::from_pkcs8_pem( + &fs::read_to_string(fixture("tests/fixtures/keys/rsa/rsa-2048-key.pem")).unwrap(), + ) + .unwrap(); + let temp = tempfile::tempdir().unwrap(); + let private = temp.path().join("private.pem"); + let public = temp.path().join("public.der"); + fs::write(&private, original.to_pkcs1_pem(Default::default()).unwrap()).unwrap(); + fs::write( + &public, + original.to_public_key().to_pkcs1_der().unwrap().as_bytes(), + ) + .unwrap(); + + load_signing_key(&private).expect("PKCS#1 private key must normalize"); + let key = load_verification_key(&public, SignatureAlgorithm::RsaSha256) + .expect("PKCS#1 public key must normalize"); + RsaPublicKey::from_public_key_der(&key.public_key_bytes) + .expect("verification key must use SPKI DER"); + } + + #[test] + fn malformed_pem_error_names_the_source_path() { + // Diagnostics must identify the failing file rather than a PEM label. + let temp = tempfile::tempdir().unwrap(); + let path = temp.path().join("broken-key.pem"); + fs::write( + &path, + "-----BEGIN PUBLIC KEY-----\ninvalid\n-----END PUBLIC KEY-----", + ) + .unwrap(); + let error = load_verification_key(&path, SignatureAlgorithm::RsaSha256).unwrap_err(); + assert!(error.to_string().contains(path.to_str().unwrap())); + assert!(!error.to_string().contains("in PUBLIC KEY")); + } +} diff --git a/tools/xmlsec1/tests/fixtures/upstream/DONOR_COMMIT b/tools/xmlsec1/tests/fixtures/upstream/DONOR_COMMIT new file mode 100644 index 0000000..aba30af --- /dev/null +++ b/tools/xmlsec1/tests/fixtures/upstream/DONOR_COMMIT @@ -0,0 +1 @@ +5fdd47dc35753438bdc38b6e96c1a3805c67a483 diff --git a/tools/xmlsec1/tests/fixtures/upstream/phaos-xmldsig-three/certs/rsa-ca-cert.der b/tools/xmlsec1/tests/fixtures/upstream/phaos-xmldsig-three/certs/rsa-ca-cert.der new file mode 100644 index 0000000000000000000000000000000000000000..7bd9a2ca7faaa24012fbe1c93b1d6b429e998a16 GIT binary patch literal 722 zcmXqLVmfEg#ALpJnTe5!iJ7r&fdMZYr&gOs+jm|@Miy2EgBn9_15P&PP!={}rqEzR zK?8meheMddFST4DGQTJrCd7^;BxxWHQpF`K5Rj3WU#t+4nw*iBpOc?nX((bK1d`$w z=620X&&*3rEy~PGHxx4v0f{mT^M<4rmnb+pDrf`+J8BxpiSrs58JHMZ8d@5d8kt0i z^BNi$8W|fJm>K{<)BwA#aXxZrF|sl+H}*0ZGk-> zfpYiD%+0?X!kV1cs97wWoH}93tc|T*X??%XYEM}v>{2sPG`*!~J5Qv;#-mx9o^pBI z_kA8KCz_b2v7gfW4~$F(#>H_4G2jT36=r1o&%$cJ45T0e{9plQX7*zY29m(Amlb3Y zG7xCwD*;B8Ljfq(^pf*)4P-%5d@N!tBF3D77YeqvMOl2_{XMhm7_l#uz%~@yHmpGZ>hu))_(*K7( zt}JFNnsNBmw7qwnH&p+Vw>hEexq~^lIv`*R-}3Z^%`YTsjkL<=pP93>pgCVsq}+kq mvEi4e?!Ii+AlbkfZi#{Aw?9QkK54$F<9TdV_SR#+7XSe33+2-Q literal 0 HcmV?d00001 diff --git a/tools/xmlsec1/tests/fixtures/upstream/phaos-xmldsig-three/signature-rsa-enveloped-bad-digest-val.xml b/tools/xmlsec1/tests/fixtures/upstream/phaos-xmldsig-three/signature-rsa-enveloped-bad-digest-val.xml new file mode 100644 index 0000000..f9bf744 --- /dev/null +++ b/tools/xmlsec1/tests/fixtures/upstream/phaos-xmldsig-three/signature-rsa-enveloped-bad-digest-val.xml @@ -0,0 +1,6 @@ + + + Alfonso Soriano + 2B + New York Yankees +nM52V/bzRd0VE3EwShWtsBzTEDc=fbye4Xm//RPUTsLd1dwJPo0gPZYX6gVYCEB/gz2348EARNk/nCCch1fFfpuqAGMKg4ayVC0yWkUyE5V4QB33jaGlh9wuNQSjxs6TIvFwSsT+0ioDgVgFv0gVeasbyNL4rFEHuAWL8QKwDT9L6b2wUvJC90DmpBs9GMR2jTZIWlM=MIIC0DCCAjmgAwIBAgIDD0JBMA0GCSqGSIb3DQEBBAUAMHwxCzAJBgNVBAYTAlVTMREwDwYDVQQIEwhOZXcgWW9yazERMA8GA1UEBxMITmV3IFlvcmsxGTAXBgNVBAoTEFBoYW9zIFRlY2hub2xvZ3kxFDASBgNVBAsTC0VuZ2luZWVyaW5nMRYwFAYDVQQDEw1UZXN0IENBIChSU0EpMB4XDTAyMDQyOTE5MTY0MFoXDTEyMDQyNjE5MTY0MFowgYAxCzAJBgNVBAYTAlVTMREwDwYDVQQIEwhOZXcgWW9yazERMA8GA1UEBxMITmV3IFlvcmsxGTAXBgNVBAoTEFBoYW9zIFRlY2hub2xvZ3kxFDASBgNVBAsTC0VuZ2luZWVyaW5nMRowGAYDVQQDExFUZXN0IENsaWVudCAoUlNBKTCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEAgIb6nAB9oS/AI5jIj6WymvQhRxiMlE07G4abmMliYi5zWzvaFE2tnU+RZIBgtoXcgDEIU/vsLQut7nzCn9mHxC8JEaV4D4U91j64AyZakShqJw7qjJfqUxxPL0yJv2oFiouPDjGuJ9JPi0NrsZq+yfWfM54s4b9SNkcOIVMybZUCAwEAAaNbMFkwDAYDVR0TAQH/BAIwADAPBgNVHQ8BAf8EBQMDB9gAMBkGA1UdEQQSMBCBDnRlY2hAcGhhb3MuY29tMB0GA1UdDgQWBBQT58rBCxPmVLeZaYGRqVROnQlFbzANBgkqhkiG9w0BAQQFAAOBgQCxbCovFST25t+ryN1RipqozxJQcguKfeCwbfgBNobzcRvoW0kSIf7zi4mtQajDM0NfslFF51/dex5Rn64HmFFshSwSvQQMyf5Cfaqv2XQ60OXq6nAFG6WbHoge6RqfIez2MWDLoSB6plsjKtMmL3mcybBhROtX5GGuLx1NtfhNFQ==CN=Test CA (RSA),OU=Engineering,O=Phaos Technology,L=New York,ST=New York,C=US1000001CN=Test Client (RSA),OU=Engineering,O=Phaos Technology,L=New York,ST=New York,C=USE+fKwQsT5lS3mWmBkalUTp0JRW8= \ No newline at end of file diff --git a/tools/xmlsec1/tests/fixtures/upstream/testDSig.sh b/tools/xmlsec1/tests/fixtures/upstream/testDSig.sh new file mode 100755 index 0000000..bc61052 --- /dev/null +++ b/tools/xmlsec1/tests/fixtures/upstream/testDSig.sh @@ -0,0 +1,2668 @@ +#!/bin/sh +# +# This script needs to be called from testrun.sh script +# + +# ensure this script is called from testrun.sh +if [ -z "$xmlsec_app" -o -z "$xmlsec_params" ]; then + echo "This script needs to be called from testrun.sh script" + exit 1 +fi + +# Setup URL to files mapping for offline testing, if tests are run against online +# then some tests might fail. +if [ -z "$XMLSEC_TEST_ONLINE" ]; then + url_map_xml_stylesheet_2005="--url-map:http://www.w3.org/TR/xml-stylesheet $topfolder/external-data/xml-stylesheet-2005" + url_map_xml_stylesheet_b64_2005="--url-map:http://www.w3.org/Signature/2002/04/xml-stylesheet.b64 $topfolder/external-data/xml-stylesheet-2005.b64" + url_map_xml_stylesheet_2018="--url-map:http://www.w3.org/TR/xml-stylesheet $topfolder/external-data/xml-stylesheet-2018" + url_map_rfc3161="--url-map:http://www.ietf.org/rfc/rfc3161.txt $topfolder/external-data/rfc3161.txt" +else + url_map_xml_stylesheet_2005="" + url_map_xml_stylesheet_b64_2005="" + url_map_xml_stylesheet_2018="" + url_map_rfc3161="" +fi + +########################################################################## +########################################################################## +########################################################################## +if [ -z "$XMLSEC_TEST_REPRODUCIBLE" ]; then + echo "--- testDSig started for xmlsec-$crypto library ($timestamp)" +fi +echo "--- LD_LIBRARY_PATH=$LD_LIBRARY_PATH" +echo "--- LTDL_LIBRARY_PATH=$LTDL_LIBRARY_PATH" +if [ -z "$XMLSEC_TEST_REPRODUCIBLE" ]; then + echo "--- log file is $logfile" +fi +echo "--- testDSig started for xmlsec-$crypto library ($timestamp)" >> $logfile +echo "--- LD_LIBRARY_PATH=$LD_LIBRARY_PATH" >> $logfile +echo "--- LTDL_LIBRARY_PATH=$LTDL_LIBRARY_PATH" >> $logfile + + +########################################################################## +########################################################################## +########################################################################## +# +# DSig test function +# +execDSigTest() { + execDSigTestWithCryptoConfig "$1" "$2" "$3" "$4" "$5" "$6" "$7" "$8" "" +} + + +execDSigTestWithCryptoConfig() { + expected_res="$1" + folder="$2" + filename="$3" + req_transforms="$4" + req_key_data="$5" + params1="$6" + params2="$7" + params3="$8" + crypto_config="$9" + failures=0 + + if [ -n "$XMLSEC_TEST_NAME" -a "$XMLSEC_TEST_NAME" != "$filename" ]; then + return + fi + + # prepare + setupTest + + # check params + if [ "z$expected_res" != "z$res_success" -a "z$expected_res" != "z$res_fail" ] ; then + echo " Bad parameter: expected_res=$expected_res" + tearDownTest + return + fi + if [ "z$crypto_config" = "z" ] ; then + crypto_config="$default_crypto_config" + fi + + # starting test + if [ -n "$folder" ] ; then + cd $topfolder/$folder + full_file=$filename + echo "Test: $folder/$filename $extra_message" + echo "Test: $folder/$filename in folder " `pwd` " $extra_message -- expected $expected_res" > $curlogfile + else + full_file=$topfolder/$filename + echo "Test: $filename $extra_message" + echo "Test: $folder/$filename $extra_message -- $expected_res" > $curlogfile + fi + extra_message="" + + # check transforms + if [ -n "$req_transforms" ] ; then + printf " Checking required transforms " + echo "$extra_vars $xmlsec_app check-transforms --crypto-config $crypto_config $xmlsec_params $req_transforms" >> $curlogfile + $xmlsec_app check-transforms $xmlsec_params --crypto-config $crypto_config $req_transforms >> $curlogfile 2>> $curlogfile + printCheckStatus $? + res=$? + if [ $res -ne 0 ]; then + cat $curlogfile >> $logfile + tearDownTest + return + fi + fi + + # check key data + if [ -n "$req_key_data" ] ; then + printf " Checking required key data " + echo "$extra_vars $xmlsec_app check-key-data $xmlsec_params --crypto-config $crypto_config $req_key_data" >> $curlogfile + $xmlsec_app check-key-data $xmlsec_params --crypto-config $crypto_config $req_key_data >> $curlogfile 2>> $curlogfile + printCheckStatus $? + res=$? + if [ $res -ne 0 ]; then + cat $curlogfile >> $logfile + tearDownTest + return + fi + fi + + # run tests + xml_verification_failed="no" + if [ -n "$params1" ] ; then + printf " Verify existing signature " + echo "$extra_vars $VALGRIND $xmlsec_app verify --X509-skip-strict-checks $xmlsec_params --crypto-config $crypto_config $params1 $full_file.xml" >> $curlogfile + $VALGRIND $xmlsec_app verify --X509-skip-strict-checks $xmlsec_params --crypto-config $crypto_config $params1 $full_file.xml >> $curlogfile 2>> $curlogfile + printRes $expected_res $? + if [ $? -ne 0 ]; then + xml_verification_failed="yes" + failures=`expr $failures + 1` + fi + fi + + if [ -n "$params2" -a -z "$PERF_TEST" ] ; then + printf " Create new signature " + echo "$extra_vars $VALGRIND $xmlsec_app sign $xmlsec_params --crypto-config $crypto_config $params2 --output $tmpfile $full_file.tmpl" >> $curlogfile + $VALGRIND $xmlsec_app sign $xmlsec_params --crypto-config $crypto_config $params2 --output $tmpfile $full_file.tmpl >> $curlogfile 2>> $curlogfile + printRes $res_success $? + if [ $? -ne 0 ]; then + failures=`expr $failures + 1` + fi + fi + + # update existing signature if verification failed + if [ "z$XMLSEC_TEST_UPDATE_XML_ON_FAILURE" = "zyes" -a "z$xml_verification_failed" = "zyes" ] ; then + printf " Update existing signature " + echo "cp $tmpfile $full_file.xml" >> $curlogfile 2>> $curlogfile + cp $tmpfile $full_file.xml + printRes $res_success $? + if [ $? -ne 0 ]; then + failures=`expr $failures + 1` + fi + fi + + if [ -n "$params3" -a -z "$PERF_TEST" ] ; then + printf " Verify new signature " + echo "$extra_vars $VALGRIND $xmlsec_app verify --X509-skip-strict-checks $xmlsec_params --crypto-config $crypto_config $params3 $tmpfile" >> $curlogfile + $VALGRIND $xmlsec_app verify --X509-skip-strict-checks $xmlsec_params --crypto-config $crypto_config $params3 $tmpfile >> $curlogfile 2>> $curlogfile + printRes $res_success $? + if [ $? -ne 0 ]; then + failures=`expr $failures + 1` + fi + fi + + # save logs + cat $curlogfile >> $logfile + if [ $failures -ne 0 ] ; then + cat $curlogfile >> $failedlogfile + fi + + # cleanup + tearDownTest +} + + +execDSigPrintXmlDebugTest() { + folder="$1" + filename="$2" + req_transforms="$3" + req_key_data="$4" + params1="$5" + crypto_config="$6" + failures=0 + test_name="$filename (with --print-xml-debug)" + + if [ -n "$XMLSEC_TEST_NAME" -a "$XMLSEC_TEST_NAME" != "$test_name" ]; then + return + fi + + # prepare + setupTest + + if [ "z$crypto_config" = "z" ] ; then + crypto_config="$default_crypto_config" + fi + + # starting test + if [ -n "$folder" ] ; then + cd $topfolder/$folder + full_file=$filename + echo "Test: $folder/$test_name $extra_message" + echo "Test: $folder/$test_name in folder " `pwd` " $extra_message -- expected $res_success" > $curlogfile + else + full_file=$topfolder/$filename + echo "Test: $test_name $extra_message" + echo "Test: $test_name $extra_message -- $res_success" > $curlogfile + fi + extra_message="" + + # check transforms + if [ -n "$req_transforms" ] ; then + printf " Checking required transforms " + echo "$extra_vars $xmlsec_app check-transforms --crypto-config $crypto_config $xmlsec_params $req_transforms" >> $curlogfile + $xmlsec_app check-transforms $xmlsec_params --crypto-config $crypto_config $req_transforms >> $curlogfile 2>> $curlogfile + res=$? + + printCheckStatus $? + if [ $res -ne 0 ]; then + cat $curlogfile >> $logfile + tearDownTest + return + fi + fi + + # check key data + if [ -n "$req_key_data" ] ; then + printf " Checking required key data " + echo "$extra_vars $xmlsec_app check-key-data $xmlsec_params --crypto-config $crypto_config $req_key_data" >> $curlogfile + $xmlsec_app check-key-data $xmlsec_params --crypto-config $crypto_config $req_key_data >> $curlogfile 2>> $curlogfile + res=$? + + printCheckStatus $? + if [ $res -ne 0 ]; then + cat $curlogfile >> $logfile + tearDownTest + return + fi + fi + + # run test + rm -f $tmpfile $tmpfile.2 + if [ -n "$params1" ] ; then + printf " Verify with --print-xml-debug " + echo "$extra_vars $VALGRIND $xmlsec_app verify --X509-skip-strict-checks $xmlsec_params --print-xml-debug --crypto-config $crypto_config $params1 $full_file.xml > $tmpfile.2" >> $curlogfile + $VALGRIND $xmlsec_app verify --X509-skip-strict-checks $xmlsec_params --print-xml-debug --crypto-config $crypto_config $params1 $full_file.xml > $tmpfile.2 2>> $curlogfile + res=$? + + printRes $expected_res $? + if [ $? -ne 0 ]; then + failures=`expr $failures + 1` + cat $curlogfile >> $logfile + cat $curlogfile >> $failedlogfile + tearDownTest + return + fi + fi + + # check xmllint availability for --print-xml-debug test + if command -v xmllint >/dev/null 2>&1 ; then + printf " Verify --print-xml-debug output with xmllint " + echo "xmllint --noout $tmpfile.2" >> $curlogfile + xmllint --noout $tmpfile.2 >> $curlogfile 2>> $curlogfile + + res=$? + + printCheckStatus $? + if [ $res -ne 0 ]; then + failures=`expr $failures + 1` + cat $curlogfile >> $logfile + cat $curlogfile >> $failedlogfile + tearDownTest + return + fi + else + printf " Checking for xmllint availability " + echo "Skipping test: xmllint is not available" >> $curlogfile + printCheckStatus 1 + cat $curlogfile >> $logfile + cat $curlogfile >> $failedlogfile + tearDownTest + return + fi + + # save logs + cat $curlogfile >> $logfile + + # cleanup + tearDownTest +} + +########################################################################## +########################################################################## +########################################################################## +echo "--------- Positive Testing ----------" + +########################################################################## +# +# xmldsig11-interop-2011 (https://www.w3.org/TR/2012/NOTE-xmldsig-core1-interop-20121113/) +# +########################################################################## + +# HMAC +# None of the tests include KeyInfo so we use "--lax-key-search" for *any* hmac key +execDSigTest $res_success \ + "xmldsig11-interop-2012" \ + "signature-enveloping-hmac-sha1-truncated40" \ + "c14n sha1 hmac-sha1" \ + "" \ + "--lax-key-search --hmackey keys/hmackey.bin --hmac-min-out-len 40" + +execDSigTest $res_success \ + "xmldsig11-interop-2012" \ + "signature-enveloping-hmac-sha1-truncated160" \ + "c14n sha1 hmac-sha1" \ + "" \ + "--lax-key-search --hmackey keys/hmackey.bin" + +execDSigTest $res_success \ + "xmldsig11-interop-2012" \ + "signature-enveloping-hmac-sha224" \ + "c14n sha1 hmac-sha224" \ + "" \ + "--lax-key-search --hmackey keys/hmackey.bin" + +execDSigTest $res_success \ + "xmldsig11-interop-2012" \ + "signature-enveloping-hmac-sha224" \ + "c14n sha1 hmac-sha224" \ + "" \ + "--lax-key-search --hmackey keys/hmackey.bin" + +execDSigTest $res_success \ + "xmldsig11-interop-2012" \ + "signature-enveloping-hmac-sha256" \ + "c14n sha1 hmac-sha256" \ + "" \ + "--lax-key-search --hmackey keys/hmackey.bin" + +execDSigTest $res_success \ + "xmldsig11-interop-2012" \ + "signature-enveloping-hmac-sha384" \ + "c14n sha1 hmac-sha384" \ + "" \ + "--lax-key-search --hmackey keys/hmackey.bin" + +execDSigTest $res_success \ + "xmldsig11-interop-2012" \ + "signature-enveloping-hmac-sha512" \ + "c14n sha1 hmac-sha512" \ + "" \ + "--lax-key-search --hmackey keys/hmackey.bin" + +# ECDSA + +# Diabled tests with PublicKey X,Y components (RFC4050, not part XMLDSig 1.1 spec): +# signature-enveloping-p256_sha1_4050.xml +# signature-enveloping-p256_sha512_4050.xml +# signature-enveloping-p384_sha384_4050.xml +# signature-enveloping-p521_sha256_4050.xml +# signature-enveloping-p256_sha256_4050.xml +# signature-enveloping-p384_sha1_4050.xml +# signature-enveloping-p384_sha512_4050.xml +# signature-enveloping-p521_sha384_4050.xml +# signature-enveloping-p256_sha384_4050.xml +# signature-enveloping-p384_sha256_4050.xml +# signature-enveloping-p521_sha1_4050.xml +# signature-enveloping-p521_sha512_4050.xml + +execDSigTest $res_success \ + "xmldsig11-interop-2012" \ + "signature-enveloping-p256_sha1" \ + "c14n sha1 ecdsa-sha1" \ + "key-value ec" \ + "--enabled-key-data key-value,ec" \ + "--enabled-key-data key-name,key-value,ec $priv_key_option:key-p256 $topfolder/keys/ec/ec-prime256v1-key.$priv_key_format --pwd secret123" \ + "--enabled-key-data key-value,ec" + + +execDSigTest $res_success \ + "xmldsig11-interop-2012" \ + "signature-enveloping-p256_sha224" \ + "c14n sha1 ecdsa-sha224" \ + "key-value ec" \ + "--enabled-key-data key-value,ec" \ + "--enabled-key-data key-name,key-value,ec $priv_key_option:key-p256 $topfolder/keys/ec/ec-prime256v1-key.$priv_key_format --pwd secret123" \ + "--enabled-key-data key-value,ec" + +execDSigTest $res_success \ + "xmldsig11-interop-2012" \ + "signature-enveloping-p256_sha256" \ + "c14n sha1 ecdsa-sha256" \ + "key-value ec" \ + "--enabled-key-data key-value,ec" \ + "--enabled-key-data key-name,key-value,ec $priv_key_option:key-p256 $topfolder/keys/ec/ec-prime256v1-key.$priv_key_format --pwd secret123" \ + "--enabled-key-data key-value,ec" + +execDSigTest $res_success \ + "xmldsig11-interop-2012" \ + "signature-enveloping-p256_sha384" \ + "c14n sha1 ecdsa-sha384" \ + "key-value ec" \ + "--enabled-key-data key-value,ec" \ + "--enabled-key-data key-name,key-value,ec $priv_key_option:key-p256 $topfolder/keys/ec/ec-prime256v1-key.$priv_key_format --pwd secret123" \ + "--enabled-key-data key-value,ec" + +execDSigTest $res_success \ + "xmldsig11-interop-2012" \ + "signature-enveloping-p256_sha512" \ + "c14n sha1 ecdsa-sha512" \ + "key-value ec" \ + "--enabled-key-data key-value,ec" \ + "--enabled-key-data key-name,key-value,ec $priv_key_option:key-p256 $topfolder/keys/ec/ec-prime256v1-key.$priv_key_format --pwd secret123" \ + "--enabled-key-data key-value,ec" + +execDSigTest $res_success \ + "xmldsig11-interop-2012" \ + "signature-enveloping-p384_sha1" \ + "c14n sha1 ecdsa-sha1" \ + "key-value ec" \ + "--enabled-key-data key-value,ec" \ + "--enabled-key-data key-name,key-value,ec $priv_key_option:key-p384 $topfolder/keys/ec/ec-prime384v1-key.$priv_key_format --pwd secret123" \ + "--enabled-key-data key-value,ec" + +execDSigTest $res_success \ + "xmldsig11-interop-2012" \ + "signature-enveloping-p384_sha224" \ + "c14n sha1 ecdsa-sha224" \ + "key-value ec" \ + "--enabled-key-data key-value,ec" \ + "--enabled-key-data key-name,key-value,ec $priv_key_option:key-p384 $topfolder/keys/ec/ec-prime384v1-key.$priv_key_format --pwd secret123" \ + "--enabled-key-data key-value,ec" + +execDSigTest $res_success \ + "xmldsig11-interop-2012" \ + "signature-enveloping-p384_sha256" \ + "c14n sha1 ecdsa-sha256" \ + "key-value ec" \ + "--enabled-key-data key-value,ec" \ + "--enabled-key-data key-name,key-value,ec $priv_key_option:key-p384 $topfolder/keys/ec/ec-prime384v1-key.$priv_key_format --pwd secret123" \ + "--enabled-key-data key-value,ec" + +execDSigTest $res_success \ + "xmldsig11-interop-2012" \ + "signature-enveloping-p384_sha384" \ + "c14n sha1 ecdsa-sha384" \ + "key-value ec" \ + "--enabled-key-data key-value,ec" \ + "--enabled-key-data key-name,key-value,ec $priv_key_option:key-p384 $topfolder/keys/ec/ec-prime384v1-key.$priv_key_format --pwd secret123" \ + "--enabled-key-data key-value,ec" + +execDSigTest $res_success \ + "xmldsig11-interop-2012" \ + "signature-enveloping-p384_sha512" \ + "c14n sha1 ecdsa-sha512" \ + "key-value ec" \ + "--enabled-key-data key-value,ec" \ + "--enabled-key-data key-name,key-value,ec $priv_key_option:key-p384 $topfolder/keys/ec/ec-prime384v1-key.$priv_key_format --pwd secret123" \ + "--enabled-key-data key-value,ec" + +execDSigTest $res_success \ + "xmldsig11-interop-2012" \ + "signature-enveloping-p521_sha1" \ + "c14n sha1 ecdsa-sha1" \ + "key-value ec" \ + "--enabled-key-data key-value,ec" \ + "--enabled-key-data key-name,key-value,ec $priv_key_option:TestKeyName-ec-prime521v1 $topfolder/keys/ec/ec-prime521v1-key.$priv_key_format --pwd secret123" \ + "--enabled-key-data key-value,ec" + +execDSigTest $res_success \ + "xmldsig11-interop-2012" \ + "signature-enveloping-p521_sha224" \ + "c14n sha1 ecdsa-sha224" \ + "key-value ec" \ + "--enabled-key-data key-value,ec" \ + "--enabled-key-data key-name,key-value,ec $priv_key_option:TestKeyName-ec-prime521v1 $topfolder/keys/ec/ec-prime521v1-key.$priv_key_format --pwd secret123" \ + "--enabled-key-data key-value,ec" + +execDSigTest $res_success \ + "xmldsig11-interop-2012" \ + "signature-enveloping-p521_sha256" \ + "c14n sha1 ecdsa-sha256" \ + "key-value ec" \ + "--enabled-key-data key-value,ec" \ + "--enabled-key-data key-name,key-value,ec $priv_key_option:TestKeyName-ec-prime521v1 $topfolder/keys/ec/ec-prime521v1-key.$priv_key_format --pwd secret123" \ + "--enabled-key-data key-value,ec" + +execDSigTest $res_success \ + "xmldsig11-interop-2012" \ + "signature-enveloping-p521_sha384" \ + "c14n sha1 ecdsa-sha384" \ + "key-value ec" \ + "--enabled-key-data key-value,ec" \ + "--enabled-key-data key-name,key-value,ec $priv_key_option:TestKeyName-ec-prime521v1 $topfolder/keys/ec/ec-prime521v1-key.$priv_key_format --pwd secret123" \ + "--enabled-key-data key-value,ec" + +execDSigTest $res_success \ + "xmldsig11-interop-2012" \ + "signature-enveloping-p521_sha512" \ + "c14n sha1 ecdsa-sha512" \ + "key-value ec" \ + "--enabled-key-data key-value,ec" \ + "--enabled-key-data key-name,key-value,ec $priv_key_option:TestKeyName-ec-prime521v1 $topfolder/keys/ec/ec-prime521v1-key.$priv_key_format --pwd secret123" \ + "--enabled-key-data key-value,ec" + +# RSA +execDSigTest $res_success \ + "xmldsig11-interop-2012" \ + "signature-enveloping-rsa-sha224" \ + "c14n sha1 rsa-sha224" \ + "rsa" \ + "--enabled-key-data key-value,rsa" + +execDSigTest $res_success \ + "xmldsig11-interop-2012" \ + "signature-enveloping-rsa-sha256" \ + "c14n sha1 rsa-sha256" \ + "rsa" \ + "--enabled-key-data key-value,rsa" + +execDSigTest $res_success \ + "xmldsig11-interop-2012" \ + "signature-enveloping-rsa_sha384" \ + "c14n sha1 rsa-sha256" \ + "rsa" \ + "--enabled-key-data key-value,rsa" + +execDSigTest $res_success \ + "xmldsig11-interop-2012" \ + "signature-enveloping-rsa_sha512" \ + "c14n sha1 rsa-sha512" \ + "rsa" \ + "--enabled-key-data key-value,rsa" + +execDSigTest $res_success \ + "xmldsig11-interop-2012" \ + "signature-enveloping-sha224-rsa_sha256" \ + "c14n sha224 rsa-sha256" \ + "rsa" \ + "--enabled-key-data key-value,rsa" + +execDSigTest $res_success \ + "xmldsig11-interop-2012" \ + "signature-enveloping-sha256-rsa-sha256" \ + "c14n sha256 rsa-sha256" \ + "rsa" \ + "--enabled-key-data key-value,rsa" + +execDSigTest $res_success \ + "xmldsig11-interop-2012" \ + "signature-enveloping-sha384-rsa_sha256" \ + "c14n sha384 rsa-sha256" \ + "rsa" \ + "--enabled-key-data key-value,rsa" + +execDSigTest $res_success \ + "xmldsig11-interop-2012" \ + "signature-enveloping-sha512-rsa_sha256" \ + "c14n sha512 rsa-sha256" \ + "rsa" \ + "--enabled-key-data key-value,rsa" + +# KeyInfoReference +execDSigTest $res_success \ + "xmldsig11-interop-2012" \ + "signature-enveloping-keyinforeference-rsa" \ + "c14n sha256 rsa-sha256" \ + "key-info-reference key-name key-value rsa" \ + "--enabled-key-data key-info-reference,key-name,key-value,rsa" \ + "$priv_key_option:TestKeyName-rsa-4096 $topfolder/keys/rsa/rsa-4096-key$priv_key_suffix.$priv_key_format --pwd secret123" \ + "--enabled-key-data key-info-reference,key-name,rsa $pub_key_option:TestKeyName-rsa-4096 $topfolder/keys/rsa/rsa-4096-pubkey$rsa_pub_key_suffix.$pub_key_format" + +# DEREncodedKeyValue +execDSigTest $res_success \ + "xmldsig11-interop-2012" \ + "signature-enveloping-derencoded-rsa" \ + "c14n sha256 rsa-sha256" \ + "der-encoded-key-value rsa" \ + "--enabled-key-data der-encoded-key-value,rsa" \ + "--enabled-key-data der-encoded-key-value,key-name,rsa $priv_key_option:TestKeyName-rsa-4096 $topfolder/keys/rsa/rsa-4096-key$priv_key_suffix.$priv_key_format --pwd secret123" \ + "--enabled-key-data der-encoded-key-value,rsa" + +execDSigTest $res_success \ + "xmldsig11-interop-2012" \ + "signature-enveloping-derencoded-ec" \ + "c14n sha256 ecdsa-sha256" \ + "der-encoded-key-value ec" \ + "--enabled-key-data der-encoded-key-value,ec" \ + "--enabled-key-data der-encoded-key-value,key-name,ec $priv_key_option:secp256r1 $topfolder/keys/ec/ec-prime256v1-key.$priv_key_format --pwd secret123" \ + "--enabled-key-data der-encoded-key-value,ec" + +execDSigTest $res_success \ + "" \ + "aleksey-xmldsig-01/enveloping-sha1-dsa1024-der-encoded-key-value" \ + "sha1 dsa-sha1" \ + "der-encoded-key-value dsa" \ + "--enabled-key-data der-encoded-key-value,dsa" \ + "--enabled-key-data der-encoded-key-value,key-name,dsa $priv_key_option:TestKeyName-dsa-1024 $topfolder/keys/dsa/dsa-1024-key$priv_key_suffix.$priv_key_format --pwd secret123" \ + "--enabled-key-data der-encoded-key-value,dsa" + +execDSigTest $res_success \ + "" \ + "aleksey-xmldsig-01/enveloping-sha256-dsa2048-der-encoded-key-value" \ + "sha256 dsa-sha256" \ + "der-encoded-key-value dsa" \ + "--enabled-key-data der-encoded-key-value,dsa" \ + "--enabled-key-data der-encoded-key-value,key-name,dsa $priv_key_option:TestKeyName-dsa-2048 $topfolder/keys/dsa/dsa-2048-key$priv_key_suffix.$priv_key_format --pwd secret123" \ + "--enabled-key-data der-encoded-key-value,dsa" + +execDSigTest $res_success \ + "" \ + "aleksey-xmldsig-01/enveloping-sha256-rsa2048-der-encoded-key-value" \ + "sha256 rsa-sha256" \ + "der-encoded-key-value rsa" \ + "--enabled-key-data der-encoded-key-value,rsa" \ + "--enabled-key-data der-encoded-key-value,key-name,rsa $priv_key_option:TestKeyName-rsa-2048 $topfolder/keys/rsa/rsa-2048-key$priv_key_suffix.$priv_key_format --pwd secret123" \ + "--enabled-key-data der-encoded-key-value,rsa" + +execDSigTest $res_success \ + "" \ + "aleksey-xmldsig-01/enveloping-sha256-rsa4096-der-encoded-key-value" \ + "sha256 rsa-sha256" \ + "der-encoded-key-value rsa" \ + "--enabled-key-data der-encoded-key-value,rsa" \ + "--enabled-key-data der-encoded-key-value,key-name,rsa $priv_key_option:TestKeyName-rsa-4096 $topfolder/keys/rsa/rsa-4096-key$priv_key_suffix.$priv_key_format --pwd secret123" \ + "--enabled-key-data der-encoded-key-value,rsa" + +execDSigTest $res_success \ + "" \ + "aleksey-xmldsig-01/enveloping-sha256-rsa2048-retrieval-method-rsa-key-value" \ + "sha256 rsa-sha256" \ + "retrieval-method rsa" \ + "--enabled-key-data retrieval-method,key-name,rsa" \ + "--enabled-key-data retrieval-method,key-name,rsa $priv_key_option:TestKeyName-rsa-2048 $topfolder/keys/rsa/rsa-2048-key$priv_key_suffix.$priv_key_format --pwd secret123" \ + "--enabled-key-data retrieval-method,key-name,rsa" + +extra_message="Negative test: RSA is disabled, should fail" +execDSigTest $res_fail \ + "" \ + "aleksey-xmldsig-01/enveloping-sha256-rsa2048-retrieval-method-rsa-key-value" \ + "sha256 rsa-sha256" \ + "retrieval-method rsa" \ + "--enabled-key-data retrieval-method,key-name" + +extra_message="Negative test: KeyValue (including RSA is disabled by default), should fail" +execDSigTest $res_fail \ + "" \ + "aleksey-xmldsig-01/enveloping-sha256-rsa2048-retrieval-method-rsa-key-value" \ + "sha256 rsa-sha256" \ + "retrieval-method rsa" \ + " " + +execDSigTest $res_success \ + "" \ + "aleksey-xmldsig-01/enveloping-sha256-rsa2048-retrieval-method-x509-data" \ + "sha256 rsa-sha256" \ + "retrieval-method x509" \ + "--trusted-$cert_format $topfolder/keys/cacert.$cert_format --enabled-key-data retrieval-method,key-name,x509" \ + "--enabled-key-data key-name $priv_key_option:TestKeyName-rsa-2048 $topfolder/keys/rsa/rsa-2048-key$priv_key_suffix.$priv_key_format --pwd secret123" \ + "--trusted-$cert_format $topfolder/keys/cacert.$cert_format --enabled-key-data retrieval-method,key-name,x509" + +extra_message="Negative test: cert is not trusted, should fail" +execDSigTest $res_fail \ + "" \ + "aleksey-xmldsig-01/enveloping-sha256-rsa2048-retrieval-method-x509-data" \ + "sha256 rsa-sha256" \ + "retrieval-method x509" \ + "--enabled-key-data retrieval-method,key-name,x509" + +execDSigTest $res_success \ + "" \ + "aleksey-xmldsig-01/enveloping-sha256-ec-prime256v1-der-encoded-key-value" \ + "sha256 ecdsa-sha256" \ + "der-encoded-key-value ec" \ + "--enabled-key-data der-encoded-key-value,ec" \ + "--enabled-key-data der-encoded-key-value,key-name,ec $priv_key_option:TestKeyName-ec-prime256v1 $topfolder/keys/ec/ec-prime256v1-key$priv_key_suffix.$priv_key_format --pwd secret123" \ + "--enabled-key-data der-encoded-key-value,ec" + +execDSigTest $res_success \ + "" \ + "aleksey-xmldsig-01/enveloping-sha384-ec-prime384v1-der-encoded-key-value" \ + "sha384 ecdsa-sha384" \ + "der-encoded-key-value ec" \ + "--enabled-key-data der-encoded-key-value,ec" \ + "--enabled-key-data der-encoded-key-value,key-name,ec $priv_key_option:TestKeyName-ec-prime384v1 $topfolder/keys/ec/ec-prime384v1-key$priv_key_suffix.$priv_key_format --pwd secret123" \ + "--enabled-key-data der-encoded-key-value,ec" + +execDSigTest $res_success \ + "" \ + "aleksey-xmldsig-01/enveloping-sha512-ec-prime521v1-der-encoded-key-value" \ + "sha512 ecdsa-sha512" \ + "der-encoded-key-value ec" \ + "--enabled-key-data der-encoded-key-value,ec" \ + "--enabled-key-data der-encoded-key-value,key-name,ec $priv_key_option:TestKeyName-ec-prime521v1 $topfolder/keys/ec/ec-prime521v1-key$priv_key_suffix.$priv_key_format --pwd secret123" \ + "--enabled-key-data der-encoded-key-value,ec" + +execDSigTest $res_success \ + "" \ + "aleksey-xmldsig-01/enveloping-sha256-eddsa-ed25519-der-encoded-key-value" \ + "sha256 eddsa-ed25519" \ + "der-encoded-key-value eddsa" \ + "--enabled-key-data der-encoded-key-value,eddsa" \ + "--enabled-key-data der-encoded-key-value,key-name,eddsa $eddsa_priv_key_option:TestKeyName-eddsa-ed25519 $topfolder/keys/eddsa/eddsa-ed25519-key.$eddsa_priv_key_format --pwd secret123" \ + "--enabled-key-data der-encoded-key-value,eddsa" + +execDSigTest $res_success \ + "" \ + "aleksey-xmldsig-01/enveloping-sha256-eddsa-ed448-der-encoded-key-value" \ + "sha256 eddsa-ed448" \ + "der-encoded-key-value eddsa" \ + "--enabled-key-data der-encoded-key-value,eddsa" \ + "--enabled-key-data der-encoded-key-value,key-name,eddsa $eddsa_priv_key_option:TestKeyName-eddsa-ed448 $topfolder/keys/eddsa/eddsa-ed448-key.$eddsa_priv_key_format --pwd secret123" \ + "--enabled-key-data der-encoded-key-value,eddsa" + + +if [ "z$xmlsec_feature_x509_data_lookup" = "zyes" ] ; then + execDSigTest $res_success \ + "xmldsig11-interop-2012" \ + "signature-enveloping-x509digest-rsa" \ + "c14n sha256 rsa-sha256" \ + "x509" \ + "--enabled-key-data x509 --pubkey-cert-der ./keys/rsa-key.crt" \ + "--enabled-key-data x509 --pkcs12 $topfolder/keys/rsa/rsa-4096-key.p12 --pwd secret123" \ + "--enabled-key-data x509 --pubkey-cert-der $topfolder/keys/rsa/rsa-4096-cert.der" +fi + + +########################################################################## +# +# xmldsig2ed-tests +# +# http://www.w3.org/TR/xmldsig2ed-tests/ +# +# No KeyInfo so use --lax-key-search option +# +########################################################################## + +execDSigTest $res_success \ + "xmldsig2ed-tests" \ + "defCan-1" \ + "c14n11 sha1 hmac-sha1" \ + "hmac" \ + "--lax-key-search --hmackey $topfolder/keys/hmackey.bin" \ + "--hmackey:TeskKeyName-Hmac $topfolder/keys/hmackey.bin" \ + "--hmackey:TeskKeyName-Hmac $topfolder/keys/hmackey.bin" + +execDSigTest $res_success \ + "xmldsig2ed-tests" \ + "defCan-2" \ + "c14n11 xslt xpath sha1 hmac-sha1" \ + "hmac" \ + "--lax-key-search --hmackey $topfolder/keys/hmackey.bin" \ + "--hmackey:TeskKeyName-Hmac $topfolder/keys/hmackey.bin" \ + "--hmackey:TeskKeyName-Hmac $topfolder/keys/hmackey.bin" + +# +# differences in XSLT transform output, tbd +# +# execDSigTest $res_success \ +# "xmldsig2ed-tests" \ +# "defCan-3" \ +# "c14n11 xslt xpath sha1 hmac-sha1" \ +# "hmac" \ +# "--hmackey $topfolder/keys/hmackey.bin" \ +# "--hmackey $topfolder/keys/hmackey.bin" \ +# "--hmackey $topfolder/keys/hmackey.bin" +# + +execDSigTest $res_success \ + "xmldsig2ed-tests" \ + "xpointer-1-SUN" \ + "c14n11 xpointer sha1 hmac-sha1" \ + "hmac" \ + "--lax-key-search --hmackey $topfolder/keys/hmackey.bin" + +execDSigTest $res_success \ + "xmldsig2ed-tests" \ + "xpointer-2-SUN" \ + "c14n11 xpointer sha1 hmac-sha1" \ + "hmac" \ + "--lax-key-search --hmackey $topfolder/keys/hmackey.bin" + +execDSigTest $res_success \ + "xmldsig2ed-tests" \ + "xpointer-3-SUN" \ + "c14n11 xpointer sha1 hmac-sha1" \ + "hmac" \ + "--lax-key-search --hmackey $topfolder/keys/hmackey.bin" + +execDSigTest $res_success \ + "xmldsig2ed-tests" \ + "xpointer-4-SUN" \ + "c14n11 xpointer sha1 hmac-sha1" \ + "hmac" \ + "--lax-key-search --hmackey $topfolder/keys/hmackey.bin" + +execDSigTest $res_success \ + "xmldsig2ed-tests" \ + "xpointer-5-SUN" \ + "c14n11 xpointer sha1 hmac-sha1" \ + "hmac" \ + "--lax-key-search --hmackey $topfolder/keys/hmackey.bin" + +execDSigTest $res_success \ + "xmldsig2ed-tests" \ + "xpointer-6-SUN" \ + "c14n11 xpointer sha1 hmac-sha1" \ + "hmac" \ + "--lax-key-search --hmackey $topfolder/keys/hmackey.bin" + +########################################################################## +# +# aleksey-xmldsig-01 +# +########################################################################## + + + +# These tests verify certificates lookup, keys lookup is tested in XMLEnc.sh +if [ "z$xmlsec_feature_x509_data_lookup" = "zyes" ] ; then + execDSigTest $res_success \ + "" \ + "aleksey-xmldsig-01/enveloped-x509-subjectname" \ + "sha512 rsa-sha512" \ + "rsa x509" \ + "--untrusted-$cert_format $topfolder/keys/rsa/rsa-4096-cert.$cert_format --untrusted-$cert_format $topfolder/keys/ca2cert.$cert_format --trusted-$cert_format $topfolder/keys/cacert.$cert_format --enabled-key-data x509" \ + "$priv_key_option $topfolder/keys/rsa/rsa-4096-key$priv_key_suffix.$priv_key_format --pwd secret123" \ + "--untrusted-$cert_format $topfolder/keys/rsa/rsa-4096-cert.$cert_format --untrusted-$cert_format $topfolder/keys/ca2cert.$cert_format --trusted-$cert_format $topfolder/keys/cacert.$cert_format --enabled-key-data x509" + + execDSigTest $res_success \ + "" \ + "aleksey-xmldsig-01/enveloped-x509-issuerserial" \ + "sha512 rsa-sha512" \ + "rsa x509" \ + "--untrusted-$cert_format $topfolder/keys/rsa/rsa-4096-cert.$cert_format --untrusted-$cert_format $topfolder/keys/ca2cert.$cert_format --trusted-$cert_format $topfolder/keys/cacert.$cert_format --enabled-key-data x509" \ + "$priv_key_option $topfolder/keys/rsa/rsa-4096-key$priv_key_suffix.$priv_key_format --pwd secret123" \ + "--untrusted-$cert_format $topfolder/keys/rsa/rsa-4096-cert.$cert_format --untrusted-$cert_format $topfolder/keys/ca2cert.$cert_format --trusted-$cert_format $topfolder/keys/cacert.$cert_format --enabled-key-data x509" + + execDSigTest $res_success \ + "" \ + "aleksey-xmldsig-01/enveloped-x509-ski" \ + "sha512 rsa-sha512" \ + "rsa x509" \ + "--untrusted-$cert_format $topfolder/keys/rsa/rsa-4096-cert.$cert_format --untrusted-$cert_format $topfolder/keys/ca2cert.$cert_format --trusted-$cert_format $topfolder/keys/cacert.$cert_format --enabled-key-data x509" \ + "$priv_key_option $topfolder/keys/rsa/rsa-4096-key$priv_key_suffix.$priv_key_format --pwd secret123" \ + "--untrusted-$cert_format $topfolder/keys/rsa/rsa-4096-cert.$cert_format --untrusted-$cert_format $topfolder/keys/ca2cert.$cert_format --trusted-$cert_format $topfolder/keys/cacert.$cert_format --enabled-key-data x509" +fi + +if [ "z$xmlsec_feature_x509_data_lookup_digest_sha1" = "zyes" ] ; then + execDSigTest $res_success \ + "" \ + "aleksey-xmldsig-01/enveloped-x509-digest-sha1" \ + "sha1 rsa-sha1" \ + "rsa x509" \ + "--untrusted-$cert_format $topfolder/keys/rsa/rsa-4096-cert.$cert_format --untrusted-$cert_format $topfolder/keys/ca2cert.$cert_format --trusted-$cert_format $topfolder/keys/cacert.$cert_format --enabled-key-data x509" \ + "--lax-key-search $priv_key_option $topfolder/keys/rsa/rsa-4096-key$priv_key_suffix.$priv_key_format --pwd secret123" \ + "--untrusted-$cert_format $topfolder/keys/rsa/rsa-4096-cert.$cert_format --untrusted-$cert_format $topfolder/keys/ca2cert.$cert_format --trusted-$cert_format $topfolder/keys/cacert.$cert_format --enabled-key-data x509" +fi +if [ "z$xmlsec_feature_x509_data_lookup_digest_sha224" = "zyes" ] ; then + execDSigTest $res_success \ + "" \ + "aleksey-xmldsig-01/enveloped-x509-digest-sha224" \ + "sha224 rsa-sha224" \ + "rsa x509" \ + "--untrusted-$cert_format $topfolder/keys/rsa/rsa-4096-cert.$cert_format --untrusted-$cert_format $topfolder/keys/ca2cert.$cert_format --trusted-$cert_format $topfolder/keys/cacert.$cert_format --enabled-key-data x509" \ + "$priv_key_option $topfolder/keys/rsa/rsa-4096-key$priv_key_suffix.$priv_key_format --pwd secret123" \ + "--untrusted-$cert_format $topfolder/keys/rsa/rsa-4096-cert.$cert_format --untrusted-$cert_format $topfolder/keys/ca2cert.$cert_format --trusted-$cert_format $topfolder/keys/cacert.$cert_format --enabled-key-data x509" +fi +if [ "z$xmlsec_feature_x509_data_lookup_digest_sha256" = "zyes" ] ; then + execDSigTest $res_success \ + "" \ + "aleksey-xmldsig-01/enveloped-x509-digest-sha256" \ + "sha256 rsa-sha256" \ + "rsa x509" \ + "--untrusted-$cert_format $topfolder/keys/rsa/rsa-4096-cert.$cert_format --untrusted-$cert_format $topfolder/keys/ca2cert.$cert_format --trusted-$cert_format $topfolder/keys/cacert.$cert_format --enabled-key-data x509" \ + "--lax-key-search $priv_key_option $topfolder/keys/rsa/rsa-4096-key$priv_key_suffix.$priv_key_format --pwd secret123" \ + "--untrusted-$cert_format $topfolder/keys/rsa/rsa-4096-cert.$cert_format --untrusted-$cert_format $topfolder/keys/ca2cert.$cert_format --trusted-$cert_format $topfolder/keys/cacert.$cert_format --enabled-key-data x509" +fi +if [ "z$xmlsec_feature_x509_data_lookup_digest_sha384" = "zyes" ] ; then + execDSigTest $res_success \ + "" \ + "aleksey-xmldsig-01/enveloped-x509-digest-sha384" \ + "sha384 rsa-sha384" \ + "rsa x509" \ + "--untrusted-$cert_format $topfolder/keys/rsa/rsa-4096-cert.$cert_format --untrusted-$cert_format $topfolder/keys/ca2cert.$cert_format --trusted-$cert_format $topfolder/keys/cacert.$cert_format --enabled-key-data x509" \ + "$priv_key_option $topfolder/keys/rsa/rsa-4096-key$priv_key_suffix.$priv_key_format --pwd secret123" \ + "--untrusted-$cert_format $topfolder/keys/rsa/rsa-4096-cert.$cert_format --untrusted-$cert_format $topfolder/keys/ca2cert.$cert_format --trusted-$cert_format $topfolder/keys/cacert.$cert_format --enabled-key-data x509" +fi +if [ "z$xmlsec_feature_x509_data_lookup_digest_sha512" = "zyes" ] ; then + execDSigTest $res_success \ + "" \ + "aleksey-xmldsig-01/enveloped-x509-digest-sha512" \ + "sha512 rsa-sha512" \ + "rsa x509" \ + "--untrusted-$cert_format $topfolder/keys/rsa/rsa-4096-cert.$cert_format --untrusted-$cert_format $topfolder/keys/ca2cert.$cert_format --trusted-$cert_format $topfolder/keys/cacert.$cert_format --enabled-key-data x509" \ + "$priv_key_option $topfolder/keys/rsa/rsa-4096-key$priv_key_suffix.$priv_key_format --pwd secret123" \ + "--untrusted-$cert_format $topfolder/keys/rsa/rsa-4096-cert.$cert_format --untrusted-$cert_format $topfolder/keys/ca2cert.$cert_format --trusted-$cert_format $topfolder/keys/cacert.$cert_format --enabled-key-data x509" +fi +if [ "z$xmlsec_feature_x509_data_lookup_digest_sha3" = "zyes" ] ; then + execDSigTest $res_success \ + "" \ + "aleksey-xmldsig-01/enveloped-x509-digest-sha3_224" \ + "sha3-224 sha256 rsa-sha256" \ + "rsa x509" \ + "--untrusted-$cert_format $topfolder/keys/rsa/rsa-4096-cert.$cert_format --untrusted-$cert_format $topfolder/keys/ca2cert.$cert_format --trusted-$cert_format $topfolder/keys/cacert.$cert_format --enabled-key-data x509" \ + "$priv_key_option $topfolder/keys/rsa/rsa-4096-key$priv_key_suffix.$priv_key_format --pwd secret123 --untrusted-$cert_format $topfolder/keys/rsa/rsa-4096-cert.$cert_format --untrusted-$cert_format $topfolder/keys/ca2cert.$cert_format --trusted-$cert_format $topfolder/keys/cacert.$cert_format --enabled-key-data x509" \ + "--untrusted-$cert_format $topfolder/keys/rsa/rsa-4096-cert.$cert_format --untrusted-$cert_format $topfolder/keys/ca2cert.$cert_format --trusted-$cert_format $topfolder/keys/cacert.$cert_format --enabled-key-data x509" + + execDSigTest $res_success \ + "" \ + "aleksey-xmldsig-01/enveloped-x509-digest-sha3_256" \ + "sha3-256 sha256 rsa-sha256" \ + "rsa x509" \ + "--untrusted-$cert_format $topfolder/keys/rsa/rsa-4096-cert.$cert_format --untrusted-$cert_format $topfolder/keys/ca2cert.$cert_format --trusted-$cert_format $topfolder/keys/cacert.$cert_format --enabled-key-data x509" \ + "$priv_key_option $topfolder/keys/rsa/rsa-4096-key$priv_key_suffix.$priv_key_format --pwd secret123 --untrusted-$cert_format $topfolder/keys/rsa/rsa-4096-cert.$cert_format --untrusted-$cert_format $topfolder/keys/ca2cert.$cert_format --trusted-$cert_format $topfolder/keys/cacert.$cert_format --enabled-key-data x509" \ + "--untrusted-$cert_format $topfolder/keys/rsa/rsa-4096-cert.$cert_format --untrusted-$cert_format $topfolder/keys/ca2cert.$cert_format --trusted-$cert_format $topfolder/keys/cacert.$cert_format --enabled-key-data x509" + + execDSigTest $res_success \ + "" \ + "aleksey-xmldsig-01/enveloped-x509-digest-sha3_384" \ + "sha3-384 sha256 rsa-sha256" \ + "rsa x509" \ + "--untrusted-$cert_format $topfolder/keys/rsa/rsa-4096-cert.$cert_format --untrusted-$cert_format $topfolder/keys/ca2cert.$cert_format --trusted-$cert_format $topfolder/keys/cacert.$cert_format --enabled-key-data x509" \ + "$priv_key_option $topfolder/keys/rsa/rsa-4096-key$priv_key_suffix.$priv_key_format --pwd secret123 --untrusted-$cert_format $topfolder/keys/rsa/rsa-4096-cert.$cert_format --untrusted-$cert_format $topfolder/keys/ca2cert.$cert_format --trusted-$cert_format $topfolder/keys/cacert.$cert_format --enabled-key-data x509" \ + "--untrusted-$cert_format $topfolder/keys/rsa/rsa-4096-cert.$cert_format --untrusted-$cert_format $topfolder/keys/ca2cert.$cert_format --trusted-$cert_format $topfolder/keys/cacert.$cert_format --enabled-key-data x509" + + execDSigTest $res_success \ + "" \ + "aleksey-xmldsig-01/enveloped-x509-digest-sha3_512" \ + "sha3-512 sha256 rsa-sha256" \ + "rsa x509" \ + "--untrusted-$cert_format $topfolder/keys/rsa/rsa-4096-cert.$cert_format --untrusted-$cert_format $topfolder/keys/ca2cert.$cert_format --trusted-$cert_format $topfolder/keys/cacert.$cert_format --enabled-key-data x509" \ + "$priv_key_option $topfolder/keys/rsa/rsa-4096-key$priv_key_suffix.$priv_key_format --pwd secret123 --untrusted-$cert_format $topfolder/keys/rsa/rsa-4096-cert.$cert_format --untrusted-$cert_format $topfolder/keys/ca2cert.$cert_format --trusted-$cert_format $topfolder/keys/cacert.$cert_format --enabled-key-data x509" \ + "--untrusted-$cert_format $topfolder/keys/rsa/rsa-4096-cert.$cert_format --untrusted-$cert_format $topfolder/keys/ca2cert.$cert_format --trusted-$cert_format $topfolder/keys/cacert.$cert_format --enabled-key-data x509" +fi + + +if [ "z$xmlsec_feature_nssdb_lookup" = "zyes" ] ; then + extra_message="Signature cert lookup in NSS DB" + execDSigTestWithCryptoConfig $res_success \ + "" \ + "aleksey-xmldsig-01/enveloped-x509-subjectname" \ + "sha512 rsa-sha512" \ + "rsa x509" \ + "--untrusted-$cert_format $topfolder/keys/rsa/rsa-4096-cert.$cert_format --untrusted-$cert_format $topfolder/keys/ca2cert.$cert_format --trusted-$cert_format $topfolder/keys/cacert.$cert_format --enabled-key-data x509" \ + "--insecure" \ + "--untrusted-$cert_format $topfolder/keys/rsa/rsa-4096-cert.$cert_format --untrusted-$cert_format $topfolder/keys/ca2cert.$cert_format --trusted-$cert_format $topfolder/keys/cacert.$cert_format --enabled-key-data x509" \ + "$topfolder/keys/nssdb" + + extra_message="Signature cert lookup in NSS DB" + execDSigTestWithCryptoConfig $res_success \ + "" \ + "aleksey-xmldsig-01/enveloped-x509-issuerserial" \ + "sha512 rsa-sha512" \ + "rsa x509" \ + "--untrusted-$cert_format $topfolder/keys/rsa/rsa-4096-cert.$cert_format --untrusted-$cert_format $topfolder/keys/ca2cert.$cert_format --trusted-$cert_format $topfolder/keys/cacert.$cert_format --enabled-key-data x509" \ + "--insecure" \ + "--untrusted-$cert_format $topfolder/keys/rsa/rsa-4096-cert.$cert_format --untrusted-$cert_format $topfolder/keys/ca2cert.$cert_format --trusted-$cert_format $topfolder/keys/cacert.$cert_format --enabled-key-data x509" \ + "$topfolder/keys/nssdb" +fi + +execDSigTest $res_success \ + "" \ + "aleksey-xmldsig-01/signature-two-keynames" \ + "sha1 rsa-sha1" \ + "rsa x509" \ + "$pub_key_option:key2 $topfolder/keys/rsa/rsa-2048-pubkey$rsa_pub_key_suffix.$pub_key_format $url_map_xml_stylesheet_2018" \ + "$priv_key_option:key2 $topfolder/keys/rsa/rsa-2048-key.$priv_key_format --pwd secret123 $url_map_xml_stylesheet_2018" \ + "$pub_key_option:key2 $topfolder/keys/rsa/rsa-2048-pubkey$rsa_pub_key_suffix.$pub_key_format $url_map_xml_stylesheet_2018" + +execDSigTest $res_success \ + "" \ + "aleksey-xmldsig-01/enveloping-dsa-x509chain" \ + "sha1 dsa-sha1" \ + "dsa x509" \ + "--trusted-$cert_format $topfolder/keys/cacert.$cert_format --enabled-key-data x509" \ + "$priv_key_option:TestKeyName-dsa-1024 $topfolder/keys/dsa/dsa-1024-key.$priv_key_format --pwd secret123" \ + "--trusted-$cert_format $topfolder/keys/cacert.$cert_format --enabled-key-data x509" + +execDSigTest $res_success \ + "" \ + "aleksey-xmldsig-01/enveloping-rsa-x509chain" \ + "sha1 rsa-sha1" \ + "rsa x509" \ + "--trusted-$cert_format $topfolder/keys/cacert.$cert_format --enabled-key-data x509" \ + "$priv_key_option:TestKeyName-rsa-2048 $topfolder/keys/rsa/rsa-2048-key.$priv_key_format --pwd secret123" \ + "--trusted-$cert_format $topfolder/keys/cacert.$cert_format --enabled-key-data x509" + +execDSigTest $res_success \ + "" \ + "aleksey-xmldsig-01/enveloping-md5-hmac-md5" \ + "md5 hmac-md5" \ + "hmac" \ + "--lax-key-search --hmackey $topfolder/keys/hmackey.bin" \ + "--hmackey:TeskKeyName-Hmac $topfolder/keys/hmackey.bin" \ + "--hmackey:TeskKeyName-Hmac $topfolder/keys/hmackey.bin" + +execDSigTest $res_success \ + "" \ + "aleksey-xmldsig-01/enveloping-md5-hmac-md5-64" \ + "md5 hmac-md5" \ + "hmac" \ + "--lax-key-search --hmackey $topfolder/keys/hmackey.bin" \ + "--hmackey:TeskKeyName-Hmac $topfolder/keys/hmackey.bin" \ + "--hmackey:TeskKeyName-Hmac $topfolder/keys/hmackey.bin" + +execDSigTest $res_success \ + "" \ + "aleksey-xmldsig-01/enveloping-ripemd160-hmac-ripemd160" \ + "ripemd160 hmac-ripemd160" \ + "hmac" \ + "--lax-key-search --hmackey $topfolder/keys/hmackey.bin" \ + "--hmackey:TeskKeyName-Hmac $topfolder/keys/hmackey.bin" \ + "--hmackey:TeskKeyName-Hmac $topfolder/keys/hmackey.bin" + +execDSigTest $res_success \ + "" \ + "aleksey-xmldsig-01/enveloping-ripemd160-hmac-ripemd160-64" \ + "ripemd160 hmac-ripemd160" \ + "hmac" \ + "--lax-key-search --hmackey $topfolder/keys/hmackey.bin" \ + "--hmackey:TeskKeyName-Hmac $topfolder/keys/hmackey.bin" \ + "--hmackey:TeskKeyName-Hmac $topfolder/keys/hmackey.bin" + +execDSigTest $res_success \ + "" \ + "aleksey-xmldsig-01/xpointer-hmac" \ + "xpointer sha1 hmac-sha1" \ + "hmac" \ + "--lax-key-search --hmackey $topfolder/keys/hmackey.bin" \ + "--hmackey:TeskKeyName-Hmac $topfolder/keys/hmackey.bin" \ + "--hmackey:TeskKeyName-Hmac $topfolder/keys/hmackey.bin" + +execDSigTest $res_success \ + "" \ + "aleksey-xmldsig-01/enveloping-sha1-hmac-sha1" \ + "sha1 hmac-sha1" \ + "hmac" \ + "--lax-key-search --hmackey $topfolder/keys/hmackey.bin" \ + "--hmackey:TeskKeyName-Hmac $topfolder/keys/hmackey.bin" \ + "--hmackey:TeskKeyName-Hmac $topfolder/keys/hmackey.bin" + +execDSigTest $res_success \ + "" \ + "aleksey-xmldsig-01/enveloping-sha1-hmac-sha1-64" \ + "sha1 hmac-sha1" \ + "hmac" \ + "--lax-key-search --hmackey $topfolder/keys/hmackey.bin" \ + "--hmackey:TeskKeyName-Hmac $topfolder/keys/hmackey.bin" \ + "--hmackey:TeskKeyName-Hmac $topfolder/keys/hmackey.bin" + +execDSigTest $res_success \ + "" \ + "aleksey-xmldsig-01/enveloping-sha224-hmac-sha224" \ + "sha224 hmac-sha224" \ + "hmac" \ + "--lax-key-search --hmackey $topfolder/keys/hmackey.bin" \ + "--hmackey:TeskKeyName-Hmac $topfolder/keys/hmackey.bin" \ + "--hmackey:TeskKeyName-Hmac $topfolder/keys/hmackey.bin" + +execDSigTest $res_success \ + "" \ + "aleksey-xmldsig-01/enveloping-sha224-hmac-sha224-64" \ + "sha224 hmac-sha224" \ + "hmac" \ + "--lax-key-search --hmackey $topfolder/keys/hmackey.bin" \ + "--hmackey:TeskKeyName-Hmac $topfolder/keys/hmackey.bin" \ + "--hmackey:TeskKeyName-Hmac $topfolder/keys/hmackey.bin" + +execDSigTest $res_success \ + "" \ + "aleksey-xmldsig-01/enveloping-sha256-hmac-sha256" \ + "sha256 hmac-sha256" \ + "hmac" \ + "--lax-key-search --hmackey $topfolder/keys/hmackey.bin" \ + "--hmackey:TeskKeyName-Hmac $topfolder/keys/hmackey.bin" \ + "--hmackey:TeskKeyName-Hmac $topfolder/keys/hmackey.bin" + +execDSigTest $res_success \ + "" \ + "aleksey-xmldsig-01/enveloping-sha256-hmac-sha256-64" \ + "sha256 hmac-sha256" \ + "hmac" \ + "--lax-key-search --hmackey $topfolder/keys/hmackey.bin" \ + "--hmackey:TeskKeyName-Hmac $topfolder/keys/hmackey.bin" \ + "--hmackey:TeskKeyName-Hmac $topfolder/keys/hmackey.bin" + +execDSigTest $res_success \ + "" \ + "aleksey-xmldsig-01/enveloping-sha384-hmac-sha384" \ + "sha384 hmac-sha384" \ + "hmac" \ + "--lax-key-search --hmackey $topfolder/keys/hmackey.bin" \ + "--hmackey:TeskKeyName-Hmac $topfolder/keys/hmackey.bin" \ + "--hmackey:TeskKeyName-Hmac $topfolder/keys/hmackey.bin" + +execDSigTest $res_success \ + "" \ + "aleksey-xmldsig-01/enveloping-sha384-hmac-sha384-64" \ + "sha384 hmac-sha384" \ + "hmac" \ + "--lax-key-search --hmackey $topfolder/keys/hmackey.bin" \ + "--hmackey:TeskKeyName-Hmac $topfolder/keys/hmackey.bin" \ + "--hmackey:TeskKeyName-Hmac $topfolder/keys/hmackey.bin" + +execDSigTest $res_success \ + "" \ + "aleksey-xmldsig-01/enveloping-sha512-hmac-sha512" \ + "sha512 hmac-sha512" \ + "hmac" \ + "--lax-key-search --hmackey $topfolder/keys/hmackey.bin" \ + "--hmackey:TeskKeyName-Hmac $topfolder/keys/hmackey.bin" \ + "--hmackey:TeskKeyName-Hmac $topfolder/keys/hmackey.bin" + +execDSigTest $res_success \ + "" \ + "aleksey-xmldsig-01/enveloping-sha512-hmac-sha512-64" \ + "sha512 hmac-sha512" \ + "hmac" \ + "--lax-key-search --hmackey $topfolder/keys/hmackey.bin" \ + "--hmackey:TeskKeyName-Hmac $topfolder/keys/hmackey.bin" \ + "--hmackey:TeskKeyName-Hmac $topfolder/keys/hmackey.bin" + +execDSigTest $res_success \ + "" \ + "aleksey-xmldsig-01/enveloping-md5-rsa-md5" \ + "md5 rsa-md5" \ + "rsa x509" \ + "--trusted-$cert_format $topfolder/keys/cacert.$cert_format --enabled-key-data x509" \ + "$priv_key_option:TestKeyName-rsa-2048 $topfolder/keys/rsa/rsa-2048-key.$priv_key_format --pwd secret123" \ + "--trusted-$cert_format $topfolder/keys/cacert.$cert_format --enabled-key-data x509" + +execDSigTest $res_success \ + "" \ + "aleksey-xmldsig-01/enveloping-ripemd160-rsa-ripemd160" \ + "ripemd160 rsa-ripemd160" \ + "rsa x509" \ + "--trusted-$cert_format $topfolder/keys/cacert.$cert_format --enabled-key-data x509" \ + "$priv_key_option:TestKeyName-rsa-2048 $topfolder/keys/rsa/rsa-2048-key.$priv_key_format --pwd secret123" \ + "--trusted-$cert_format $topfolder/keys/cacert.$cert_format --enabled-key-data x509" + +execDSigTest $res_success \ + "" \ + "aleksey-xmldsig-01/enveloping-sha1-rsa-sha1" \ + "sha1 rsa-sha1" \ + "rsa x509" \ + "--trusted-$cert_format $topfolder/keys/cacert.$cert_format --enabled-key-data x509" \ + "$priv_key_option:TestKeyName-rsa-2048 $topfolder/keys/rsa/rsa-2048-key.$priv_key_format --pwd secret123" \ + "--trusted-$cert_format $topfolder/keys/cacert.$cert_format --enabled-key-data x509" + +execDSigTest $res_success \ + "" \ + "aleksey-xmldsig-01/enveloping-sha224-rsa-sha224" \ + "sha224 rsa-sha224" \ + "rsa x509" \ + "--trusted-$cert_format $topfolder/keys/cacert.$cert_format --enabled-key-data x509" \ + "$priv_key_option:TestKeyName-rsa-2048 $topfolder/keys/rsa/rsa-2048-key$priv_key_suffix.$priv_key_format --pwd secret123" \ + "--trusted-$cert_format $topfolder/keys/cacert.$cert_format --enabled-key-data x509" + +execDSigTest $res_success \ + "" \ + "aleksey-xmldsig-01/enveloping-sha256-rsa-sha256" \ + "sha256 rsa-sha256" \ + "rsa x509" \ + "--trusted-$cert_format $topfolder/keys/cacert.$cert_format --enabled-key-data x509" \ + "$priv_key_option:TestKeyName-rsa-2048 $topfolder/keys/rsa/rsa-2048-key$priv_key_suffix.$priv_key_format --pwd secret123" \ + "--trusted-$cert_format $topfolder/keys/cacert.$cert_format --enabled-key-data x509" + +execDSigTest $res_success \ + "aleksey-xmldsig-01" \ + "enveloping-sha256-rsa-sha256-relationship" \ + "sha256 rsa-sha256 relationship" \ + "rsa x509" \ + "--trusted-$cert_format $topfolder/keys/cacert.$cert_format --enabled-key-data x509" \ + "$priv_key_option:TestKeyName-rsa-2048 $topfolder/keys/rsa/rsa-2048-key$priv_key_suffix.$priv_key_format --pwd secret123" \ + "--trusted-$cert_format $topfolder/keys/cacert.$cert_format --enabled-key-data x509" + +execDSigTest $res_success \ + "" \ + "aleksey-xmldsig-01/enveloping-sha384-rsa-sha384" \ + "sha384 rsa-sha384" \ + "rsa x509" \ + "--trusted-$cert_format $topfolder/keys/cacert.$cert_format --enabled-key-data x509" \ + "$priv_key_option:TestKeyName-rsa-4096 $topfolder/keys/rsa/rsa-4096-key$priv_key_suffix.$priv_key_format --pwd secret123" \ + "--trusted-$cert_format $topfolder/keys/cacert.$cert_format --enabled-key-data x509" + +execDSigTest $res_success \ + "" \ + "aleksey-xmldsig-01/enveloping-sha512-rsa-sha512" \ + "sha512 rsa-sha512" \ + "rsa x509" \ + "--trusted-$cert_format $topfolder/keys/cacert.$cert_format --enabled-key-data x509" \ + "$priv_key_option:TestKeyName-rsa-4096 $topfolder/keys/rsa/rsa-4096-key$priv_key_suffix.$priv_key_format --pwd secret123" \ + "--trusted-$cert_format $topfolder/keys/cacert.$cert_format --enabled-key-data x509" + +execDSigTest $res_success \ + "" \ + "aleksey-xmldsig-01/enveloped-sha224-rsa-pss-sha224" \ + "sha224 rsa-pss-sha224" \ + "rsa" \ + "$pub_key_option:TestKeyName-rsa-4096 $topfolder/keys/rsa/rsa-4096-pubkey$rsa_pub_key_suffix.$pub_key_format" \ + "$priv_key_option:TestKeyName-rsa-4096 $topfolder/keys/rsa/rsa-4096-key$priv_key_suffix.$priv_key_format --pwd secret123" \ + "$pub_key_option:TestKeyName-rsa-4096 $topfolder/keys/rsa/rsa-4096-pubkey$rsa_pub_key_suffix.$pub_key_format" + +execDSigTest $res_success \ + "" \ + "aleksey-xmldsig-01/enveloped-sha256-rsa-pss-sha256" \ + "sha256 rsa-pss-sha256" \ + "rsa" \ + "$pub_key_option:TestKeyName-rsa-4096 $topfolder/keys/rsa/rsa-4096-pubkey$rsa_pub_key_suffix.$pub_key_format" \ + "$priv_key_option:TestKeyName-rsa-4096 $topfolder/keys/rsa/rsa-4096-key$priv_key_suffix.$priv_key_format --pwd secret123" \ + "$pub_key_option:TestKeyName-rsa-4096 $topfolder/keys/rsa/rsa-4096-pubkey$rsa_pub_key_suffix.$pub_key_format" + +execDSigTest $res_success \ + "" \ + "aleksey-xmldsig-01/enveloped-sha384-rsa-pss-sha384" \ + "sha384 rsa-pss-sha384" \ + "rsa" \ + "$pub_key_option:TestKeyName-rsa-4096 $topfolder/keys/rsa/rsa-4096-pubkey$rsa_pub_key_suffix.$pub_key_format" \ + "$priv_key_option:TestKeyName-rsa-4096 $topfolder/keys/rsa/rsa-4096-key$priv_key_suffix.$priv_key_format --pwd secret123" \ + "$pub_key_option:TestKeyName-rsa-4096 $topfolder/keys/rsa/rsa-4096-pubkey$rsa_pub_key_suffix.$pub_key_format" + +execDSigTest $res_success \ + "" \ + "aleksey-xmldsig-01/enveloped-sha512-rsa-pss-sha512" \ + "sha512 rsa-pss-sha512" \ + "rsa" \ + "$pub_key_option:TestKeyName-rsa-4096 $topfolder/keys/rsa/rsa-4096-pubkey$rsa_pub_key_suffix.$pub_key_format" \ + "$priv_key_option:TestKeyName-rsa-4096 $topfolder/keys/rsa/rsa-4096-key$priv_key_suffix.$priv_key_format --pwd secret123" \ + "$pub_key_option:TestKeyName-rsa-4096 $topfolder/keys/rsa/rsa-4096-pubkey$rsa_pub_key_suffix.$pub_key_format" + + +execDSigTest $res_success \ + "" \ + "aleksey-xmldsig-01/enveloped-sha3_224-rsa-pss-sha3_224" \ + "sha3-224 rsa-pss-sha3-224" \ + "rsa" \ + "$pub_key_option:TestKeyName-rsa-4096 $topfolder/keys/rsa/rsa-4096-pubkey$rsa_pub_key_suffix.$pub_key_format" \ + "$priv_key_option:TestKeyName-rsa-4096 $topfolder/keys/rsa/rsa-4096-key$priv_key_suffix.$priv_key_format --pwd secret123" \ + "$pub_key_option:TestKeyName-rsa-4096 $topfolder/keys/rsa/rsa-4096-pubkey$rsa_pub_key_suffix.$pub_key_format" + +execDSigTest $res_success \ + "" \ + "aleksey-xmldsig-01/enveloped-sha3_256-rsa-pss-sha3_256" \ + "sha3-256 rsa-pss-sha3-256" \ + "rsa" \ + "$pub_key_option:TestKeyName-rsa-4096 $topfolder/keys/rsa/rsa-4096-pubkey$rsa_pub_key_suffix.$pub_key_format" \ + "$priv_key_option:TestKeyName-rsa-4096 $topfolder/keys/rsa/rsa-4096-key$priv_key_suffix.$priv_key_format --pwd secret123" \ + "$pub_key_option:TestKeyName-rsa-4096 $topfolder/keys/rsa/rsa-4096-pubkey$rsa_pub_key_suffix.$pub_key_format" + +execDSigTest $res_success \ + "" \ + "aleksey-xmldsig-01/enveloped-sha3_384-rsa-pss-sha3_384" \ + "sha3-384 rsa-pss-sha3-384" \ + "rsa" \ + "$pub_key_option:TestKeyName-rsa-4096 $topfolder/keys/rsa/rsa-4096-pubkey$rsa_pub_key_suffix.$pub_key_format" \ + "$priv_key_option:TestKeyName-rsa-4096 $topfolder/keys/rsa/rsa-4096-key$priv_key_suffix.$priv_key_format --pwd secret123" \ + "$pub_key_option:TestKeyName-rsa-4096 $topfolder/keys/rsa/rsa-4096-pubkey$rsa_pub_key_suffix.$pub_key_format" + +execDSigTest $res_success \ + "" \ + "aleksey-xmldsig-01/enveloped-sha3_512-rsa-pss-sha3_512" \ + "sha3-512 rsa-pss-sha3-512" \ + "rsa" \ + "$pub_key_option:TestKeyName-rsa-4096 $topfolder/keys/rsa/rsa-4096-pubkey$rsa_pub_key_suffix.$pub_key_format" \ + "$priv_key_option:TestKeyName-rsa-4096 $topfolder/keys/rsa/rsa-4096-key$priv_key_suffix.$priv_key_format --pwd secret123" \ + "$pub_key_option:TestKeyName-rsa-4096 $topfolder/keys/rsa/rsa-4096-pubkey$rsa_pub_key_suffix.$pub_key_format" + + +execDSigTest $res_success \ + "" \ + "aleksey-xmldsig-01/enveloping-rsa-pss-sha1" \ + "sha1 rsa-pss-sha1" \ + "rsa x509" \ + "--trusted-$cert_format $topfolder/keys/cacert.$cert_format --enabled-key-data x509" \ + "$priv_key_option:TestKeyName-rsa-4096 $topfolder/keys/rsa/rsa-4096-key$priv_key_suffix.$priv_key_format --pwd secret123" \ + "--trusted-$cert_format $topfolder/keys/cacert.$cert_format --enabled-key-data x509" + +execDSigTest $res_success \ + "" \ + "aleksey-xmldsig-01/enveloping-rsa-pss-sha224" \ + "sha224 rsa-pss-sha224" \ + "rsa x509" \ + "--trusted-$cert_format $topfolder/keys/cacert.$cert_format --enabled-key-data x509" \ + "$priv_key_option:TestKeyName-rsa-4096 $topfolder/keys/rsa/rsa-4096-key$priv_key_suffix.$priv_key_format --pwd secret123" \ + "--trusted-$cert_format $topfolder/keys/cacert.$cert_format --enabled-key-data x509" + +execDSigTest $res_success \ + "" \ + "aleksey-xmldsig-01/enveloping-rsa-pss-sha256" \ + "sha256 rsa-pss-sha256" \ + "rsa x509" \ + "--trusted-$cert_format $topfolder/keys/cacert.$cert_format --enabled-key-data x509" \ + "$priv_key_option:TestKeyName-rsa-4096 $topfolder/keys/rsa/rsa-4096-key$priv_key_suffix.$priv_key_format --pwd secret123" \ + "--trusted-$cert_format $topfolder/keys/cacert.$cert_format --enabled-key-data x509" + +execDSigTest $res_success \ + "" \ + "aleksey-xmldsig-01/enveloping-rsa-pss-sha384" \ + "sha384 rsa-pss-sha384" \ + "rsa x509" \ + "--trusted-$cert_format $topfolder/keys/cacert.$cert_format --enabled-key-data x509" \ + "$priv_key_option:TestKeyName-rsa-4096 $topfolder/keys/rsa/rsa-4096-key$priv_key_suffix.$priv_key_format --pwd secret123" \ + "--trusted-$cert_format $topfolder/keys/cacert.$cert_format --enabled-key-data x509" + +execDSigTest $res_success \ + "" \ + "aleksey-xmldsig-01/enveloping-rsa-pss-sha512" \ + "sha512 rsa-pss-sha512" \ + "rsa x509" \ + "--trusted-$cert_format $topfolder/keys/cacert.$cert_format --enabled-key-data x509" \ + "$priv_key_option:TestKeyName-rsa-4096 $topfolder/keys/rsa/rsa-4096-key$priv_key_suffix.$priv_key_format --pwd secret123" \ + "--trusted-$cert_format $topfolder/keys/cacert.$cert_format --enabled-key-data x509" + + +execDSigTest $res_success \ + "" \ + "aleksey-xmldsig-01/enveloping-rsa-pss-sha3_224" \ + "sha3-224 rsa-pss-sha3-224" \ + "rsa x509" \ + "--trusted-$cert_format $topfolder/keys/cacert.$cert_format --enabled-key-data x509" \ + "$priv_key_option:TestKeyName-rsa-4096 $topfolder/keys/rsa/rsa-4096-key$priv_key_suffix.$priv_key_format --pwd secret123" \ + "--trusted-$cert_format $topfolder/keys/cacert.$cert_format --enabled-key-data x509" + +execDSigTest $res_success \ + "" \ + "aleksey-xmldsig-01/enveloping-rsa-pss-sha3_256" \ + "sha3-256 rsa-pss-sha3-256" \ + "rsa x509" \ + "--trusted-$cert_format $topfolder/keys/cacert.$cert_format --enabled-key-data x509" \ + "$priv_key_option:TestKeyName-rsa-4096 $topfolder/keys/rsa/rsa-4096-key$priv_key_suffix.$priv_key_format --pwd secret123" \ + "--trusted-$cert_format $topfolder/keys/cacert.$cert_format --enabled-key-data x509" + +execDSigTest $res_success \ + "" \ + "aleksey-xmldsig-01/enveloping-rsa-pss-sha3_384" \ + "sha3-384 rsa-pss-sha3-384" \ + "rsa x509" \ + "--trusted-$cert_format $topfolder/keys/cacert.$cert_format --enabled-key-data x509" \ + "$priv_key_option:TestKeyName-rsa-4096 $topfolder/keys/rsa/rsa-4096-key$priv_key_suffix.$priv_key_format --pwd secret123" \ + "--trusted-$cert_format $topfolder/keys/cacert.$cert_format --enabled-key-data x509" + +execDSigTest $res_success \ + "" \ + "aleksey-xmldsig-01/enveloping-rsa-pss-sha3_512" \ + "sha3-512 rsa-pss-sha3-512" \ + "rsa x509" \ + "--trusted-$cert_format $topfolder/keys/cacert.$cert_format --enabled-key-data x509" \ + "$priv_key_option:TestKeyName-rsa-4096 $topfolder/keys/rsa/rsa-4096-key$priv_key_suffix.$priv_key_format --pwd secret123" \ + "--trusted-$cert_format $topfolder/keys/cacert.$cert_format --enabled-key-data x509" + + +execDSigTest $res_success \ + "" \ + "aleksey-xmldsig-01/enveloping-sha256-dsa2048-sha256" \ + "sha256 dsa-sha256" \ + "dsa x509" \ + "--trusted-$cert_format $topfolder/keys/cacert.$cert_format --enabled-key-data x509" \ + "$priv_key_option:TestKeyName-dsa-2048 $topfolder/keys/dsa/dsa-2048-key$priv_key_suffix.$priv_key_format --pwd secret123" \ + "--trusted-$cert_format $topfolder/keys/cacert.$cert_format --enabled-key-data x509" + +execDSigTest $res_success \ + "" \ + "aleksey-xmldsig-01/enveloping-sha256-dsa3072-sha256" \ + "sha256 dsa-sha256" \ + "dsa x509" \ + "--trusted-$cert_format $topfolder/keys/cacert.$cert_format --enabled-key-data x509" \ + "$priv_key_option:TestKeyName-dsa-3072 $topfolder/keys/dsa/dsa-3072-key$priv_key_suffix.$priv_key_format --pwd secret123" \ + "--trusted-$cert_format $topfolder/keys/cacert.$cert_format --enabled-key-data x509" + +execDSigTest $res_success \ + "" \ + "aleksey-xmldsig-01/enveloped-sha1-dsa-sha1" \ + "sha1 dsa-sha1" \ + "" \ + "$pub_key_option:TestKeyName-dsa-1024 $topfolder/keys/dsa/dsa-1024-pubkey.$pub_key_format" \ + "$priv_key_option:TestKeyName-dsa-1024 $topfolder/keys/dsa/dsa-1024-key.$priv_key_format --pwd secret123" \ + "$pub_key_option:TestKeyName-dsa-1024 $topfolder/keys/dsa/dsa-1024-pubkey.$pub_key_format" + + +execDSigTest $res_success \ + "" \ + "aleksey-xmldsig-01/enveloped-sha1-ecdsa-sha1" \ + "sha1 ecdsa-sha1" \ + "" \ + "$pub_key_option:TestKeyName-ec-prime256v1 $topfolder/keys/ec/ec-prime256v1-pubkey.$pub_key_format" \ + "$priv_key_option:TestKeyName-ec-prime256v1 $topfolder/keys/ec/ec-prime256v1-key.$priv_key_format --pwd secret123" \ + "$pub_key_option:TestKeyName-ec-prime256v1 $topfolder/keys/ec/ec-prime256v1-pubkey.$pub_key_format" + + +execDSigTest $res_success \ + "" \ + "aleksey-xmldsig-01/enveloped-ripemd160-ecdsa-ripemd160" \ + "ripemd160 ecdsa-ripemd160" \ + "ec" \ + "$pub_key_option:TestKeyName-ec-prime256v1 $topfolder/keys/ec/ec-prime256v1-pubkey.$pub_key_format" \ + "$priv_key_option:TestKeyName-ec-prime256v1 $topfolder/keys/ec/ec-prime256v1-key.$priv_key_format --pwd secret123" \ + "$pub_key_option:TestKeyName-ec-prime256v1 $topfolder/keys/ec/ec-prime256v1-pubkey.$pub_key_format" + + +execDSigTest $res_success \ + "" \ + "aleksey-xmldsig-01/enveloped-sha1-rsa-sha1" \ + "sha1 rsa-sha1" \ + "" \ + "$pub_key_option:TestKeyName-rsa-4096 $topfolder/keys/rsa/rsa-4096-pubkey$rsa_pub_key_suffix.$pub_key_format" \ + "$priv_key_option:TestKeyName-rsa-4096 $topfolder/keys/rsa/rsa-4096-key.$priv_key_format --pwd secret123" \ + "$pub_key_option:TestKeyName-rsa-4096 $topfolder/keys/rsa/rsa-4096-pubkey$rsa_pub_key_suffix.$pub_key_format" + + +if [ "z$xmlsec_feature_nssdb_lookup" = "zyes" ] ; then + # this test expects "rsa-4096-key" in the NSS DB + extra_message="Lookup key in NSS DB" + execDSigTestWithCryptoConfig $res_success \ + "" \ + "aleksey-xmldsig-01/enveloped-sha1-rsa-sha1" \ + "sha1 rsa-sha1" \ + "" \ + "" \ + "--enabled-key-data key-name,rsa" \ + "--enabled-key-data key-name,rsa" \ + "$topfolder/keys/nssdb" +fi + +# verify that XML debug output is correct and contains the expected elements and values +execDSigPrintXmlDebugTest \ + "" \ + "aleksey-xmldsig-01/enveloped-sha1-rsa-sha1" \ + "sha1 rsa-sha1" \ + "" \ + "$pub_key_option:TestKeyName-rsa-4096 $topfolder/keys/rsa/rsa-4096-pubkey$rsa_pub_key_suffix.$pub_key_format" + + +execDSigTest $res_success \ + "" \ + "aleksey-xmldsig-01/enveloped-sha224-ecdsa-sha224" \ + "sha224 ecdsa-sha224" \ + "ec" \ + "$pub_key_option:TestKeyName-ec-prime256v1 $topfolder/keys/ec/ec-prime256v1-pubkey.$pub_key_format" \ + "$priv_key_option:TestKeyName-ec-prime256v1 $topfolder/keys/ec/ec-prime256v1-key.$priv_key_format --pwd secret123" \ + "$pub_key_option:TestKeyName-ec-prime256v1 $topfolder/keys/ec/ec-prime256v1-pubkey.$pub_key_format" + + +execDSigTest $res_success \ + "" \ + "aleksey-xmldsig-01/enveloped-sha256-ecdsa-sha256" \ + "sha256 ecdsa-sha256" \ + "ec" \ + "$pub_key_option:TestKeyName-ec-prime256v1 $topfolder/keys/ec/ec-prime256v1-pubkey.$pub_key_format" \ + "$priv_key_option:TestKeyName-ec-prime256v1 $topfolder/keys/ec/ec-prime256v1-key.$priv_key_format --pwd secret123" \ + "$pub_key_option:TestKeyName-ec-prime256v1 $topfolder/keys/ec/ec-prime256v1-pubkey.$pub_key_format" + + +execDSigTest $res_success \ + "" \ + "aleksey-xmldsig-01/enveloped-sha384-ecdsa-sha384" \ + "sha384 ecdsa-sha384" \ + "ec" \ + "$pub_key_option:TestKeyName-ec-prime521v1 $topfolder/keys/ec/ec-prime521v1-pubkey.$pub_key_format" \ + "$priv_key_option:TestKeyName-ec-prime521v1 $topfolder/keys/ec/ec-prime521v1-key.$priv_key_format --pwd secret123" \ + "$pub_key_option:TestKeyName-ec-prime521v1 $topfolder/keys/ec/ec-prime521v1-pubkey.$pub_key_format" + +execDSigTest $res_success \ + "" \ + "aleksey-xmldsig-01/enveloped-sha512-ecdsa-sha512" \ + "sha512 ecdsa-sha512" \ + "ec" \ + "$pub_key_option:TestKeyName-ec-prime521v1 $topfolder/keys/ec/ec-prime521v1-pubkey.$pub_key_format" \ + "$priv_key_option:TestKeyName-ec-prime521v1 $topfolder/keys/ec/ec-prime521v1-key.$priv_key_format --pwd secret123" \ + "$pub_key_option:TestKeyName-ec-prime521v1 $topfolder/keys/ec/ec-prime521v1-pubkey.$pub_key_format" + + +execDSigTest $res_success \ + "" \ + "aleksey-xmldsig-01/enveloped-sha3_224-ecdsa-sha3_224" \ + "sha3-224 ecdsa-sha3-224" \ + "ec" \ + "$pub_key_option:TestKeyName-ec-prime256v1 $topfolder/keys/ec/ec-prime256v1-pubkey.$pub_key_format" \ + "$priv_key_option:TestKeyName-ec-prime256v1 $topfolder/keys/ec/ec-prime256v1-key.$priv_key_format --pwd secret123" \ + "$pub_key_option:TestKeyName-ec-prime256v1 $topfolder/keys/ec/ec-prime256v1-pubkey.$pub_key_format" + +execDSigTest $res_success \ + "" \ + "aleksey-xmldsig-01/enveloped-sha3_256-ecdsa-sha3_256" \ + "sha3-256 ecdsa-sha3-256" \ + "ec" \ + "$pub_key_option:TestKeyName-ec-prime256v1 $topfolder/keys/ec/ec-prime256v1-pubkey.$pub_key_format" \ + "$priv_key_option:TestKeyName-ec-prime256v1 $topfolder/keys/ec/ec-prime256v1-key.$priv_key_format --pwd secret123" \ + "$pub_key_option:TestKeyName-ec-prime256v1 $topfolder/keys/ec/ec-prime256v1-pubkey.$pub_key_format" + +execDSigTest $res_success \ + "" \ + "aleksey-xmldsig-01/enveloped-sha3_384-ecdsa-sha3_384" \ + "sha3-384 ecdsa-sha3-384" \ + "ec" \ + "$pub_key_option:TestKeyName-ec-prime521v1 $topfolder/keys/ec/ec-prime521v1-pubkey.$pub_key_format" \ + "$priv_key_option:TestKeyName-ec-prime521v1 $topfolder/keys/ec/ec-prime521v1-key.$priv_key_format --pwd secret123" \ + "$pub_key_option:TestKeyName-ec-prime521v1 $topfolder/keys/ec/ec-prime521v1-pubkey.$pub_key_format" + +execDSigTest $res_success \ + "" \ + "aleksey-xmldsig-01/enveloped-sha3_512-ecdsa-sha3_512" \ + "sha3-512 ecdsa-sha3-512" \ + "ec" \ + "$pub_key_option:TestKeyName-ec-prime521v1 $topfolder/keys/ec/ec-prime521v1-pubkey.$pub_key_format" \ + "$priv_key_option:TestKeyName-ec-prime521v1 $topfolder/keys/ec/ec-prime521v1-key.$priv_key_format --pwd secret123" \ + "$pub_key_option:TestKeyName-ec-prime521v1 $topfolder/keys/ec/ec-prime521v1-pubkey.$pub_key_format" + + +execDSigTest $res_success \ + "" \ + "aleksey-xmldsig-01/enveloping-sha1-ecdsa-sha1" \ + "sha1 ecdsa-sha1" \ + "ec x509" \ + "--trusted-$cert_format $topfolder/keys/cacert.$cert_format --enabled-key-data x509" \ + "$priv_key_option:TestKeyName-ec-prime256v1 $topfolder/keys/ec/ec-prime256v1-key.$priv_key_format --pwd secret123" \ + "--trusted-$cert_format $topfolder/keys/cacert.$cert_format --enabled-key-data x509" + +execDSigTest $res_success \ + "" \ + "aleksey-xmldsig-01/enveloping-sha256-ecdsa-sha256" \ + "sha256 ecdsa-sha256" \ + "ec x509" \ + "--trusted-$cert_format $topfolder/keys/cacert.$cert_format --enabled-key-data x509" \ + "$priv_key_option:TestKeyName-ec-prime256v1 $topfolder/keys/ec/ec-prime256v1-key.$priv_key_format --pwd secret123" \ + "--trusted-$cert_format $topfolder/keys/cacert.$cert_format --enabled-key-data x509" + +execDSigTest $res_success \ + "" \ + "aleksey-xmldsig-01/enveloping-sha384-ecdsa-sha384" \ + "sha384 ecdsa-sha384" \ + "ec x509" \ + "--trusted-$cert_format $topfolder/keys/cacert.$cert_format --enabled-key-data x509" \ + "$priv_key_option:TestKeyName-ec-prime521v1 $topfolder/keys/ec/ec-prime521v1-key.$priv_key_format --pwd secret123" \ + "--trusted-$cert_format $topfolder/keys/cacert.$cert_format --enabled-key-data x509" + +execDSigTest $res_success \ + "" \ + "aleksey-xmldsig-01/enveloping-sha512-ecdsa-sha512" \ + "sha512 ecdsa-sha512" \ + "ec x509" \ + "--trusted-$cert_format $topfolder/keys/cacert.$cert_format --enabled-key-data x509" \ + "$priv_key_option:TestKeyName-ec-prime521v1 $topfolder/keys/ec/ec-prime521v1-key.$priv_key_format --pwd secret123" \ + "--trusted-$cert_format $topfolder/keys/cacert.$cert_format --enabled-key-data x509" + +# see issue https://github.com/lsh123/xmlsec/issues/228 +execDSigTest $res_success \ + "" \ + "aleksey-xmldsig-01/enveloped-ecdsa-java-bug" \ + "sha512 ecdsa-sha512" \ + "ec x509" \ + "--trusted-$cert_format $topfolder/keys/enveloped-ecdsa-java-bug-cert.$cert_format --enabled-key-data x509 --verification-gmt-time 2019-01-01+00:00:00" + +# see issue https://github.com/lsh123/xmlsec/issues/941 (another java bug) +execDSigTest $res_success \ + "" \ + "aleksey-xmldsig-01/enveloping-sha256-ecdsa-sha256_padded" \ + "sha256 ecdsa-sha256" \ + "ec x509" \ + "--insecure --enabled-key-data x509" + +execDSigTest $res_success \ + "" \ + "aleksey-xmldsig-01/x509data-test" \ + "xpath2 sha1 rsa-sha1" \ + "rsa x509" \ + "--trusted-$cert_format $topfolder/keys/cacert.$cert_format" \ + "$priv_key_option:TestKeyName-rsa-2048 $topfolder/keys/rsa/rsa-2048-key.$priv_key_format --pwd secret123" \ + "--trusted-$cert_format $topfolder/keys/cacert.$cert_format" + +# verify that XML debug output is correct and contains the expected elements and values +execDSigPrintXmlDebugTest \ + "" \ + "aleksey-xmldsig-01/x509data-test" \ + "xpath2 sha1 rsa-sha1" \ + "rsa x509" \ + "--trusted-$cert_format $topfolder/keys/cacert.$cert_format" + +execDSigTest $res_success \ + "" \ + "aleksey-xmldsig-01/x509data-sn-test" \ + "xpath2 sha1 rsa-sha1" \ + "rsa x509" \ + "--trusted-$cert_format $topfolder/keys/cacert.$cert_format --untrusted-$cert_format $topfolder/keys/ca2cert.$cert_format --untrusted-$cert_format $topfolder/keys/rsa/rsa-2048-cert.$cert_format --enabled-key-data x509" \ + "$priv_key_option:TestKeyName-rsa-2048 $topfolder/keys/rsa/rsa-2048-key.$priv_key_format --pwd secret123" \ + "--trusted-$cert_format $topfolder/keys/cacert.$cert_format --untrusted-$cert_format $topfolder/keys/ca2cert.$cert_format --untrusted-$cert_format $topfolder/keys/rsa/rsa-2048-cert.$cert_format --enabled-key-data x509" + +if [ "z$xmlsec_feature_asn1_signatures" = "zyes" ] ; then + execDSigTest $res_success \ + "" \ + "aleksey-xmldsig-01/enveloped-sha256-ecdsa-sha256-with-asn1" \ + "sha256 ecdsa-sha256" \ + "ec" \ + "--enable-asn1-signatures-hack $pub_key_option:TestKeyName-ec-prime256v1 $topfolder/keys/ec/ec-prime256v1-pubkey.$pub_key_format" \ + "--enable-asn1-signatures-hack $priv_key_option:TestKeyName-ec-prime256v1 $topfolder/keys/ec/ec-prime256v1-key.$priv_key_format --pwd secret123" \ + "--enable-asn1-signatures-hack $pub_key_option:TestKeyName-ec-prime256v1 $topfolder/keys/ec/ec-prime256v1-pubkey.$pub_key_format" + + execDSigTest $res_success \ + "" \ + "aleksey-xmldsig-01/enveloping-sha512-ecdsa-sha512-with-asn1" \ + "sha512 ecdsa-sha512" \ + "ec x509" \ + "--enable-asn1-signatures-hack --trusted-$cert_format $topfolder/keys/cacert.$cert_format --enabled-key-data x509" \ + "--enable-asn1-signatures-hack $priv_key_option:TestKeyName-ec-prime521v1 $topfolder/keys/ec/ec-prime521v1-key.$priv_key_format --pwd secret123" \ + "--enable-asn1-signatures-hack --trusted-$cert_format $topfolder/keys/cacert.$cert_format --enabled-key-data x509" + +fi + +### ML-DSA +execDSigTest $res_success \ + "" \ + "aleksey-xmldsig-01/enveloped-sha512-mldsa44" \ + "sha512 ml-dsa-44" \ + "ml-dsa" \ + "$pub_key_option:TestKeyName-ml-dsa-44 $topfolder/keys/ml-dsa/ml-dsa-44-pubkey.$pub_key_format" \ + "$priv_key_option:TestKeyName-ml-dsa-44 $topfolder/keys/ml-dsa/ml-dsa-44-key.$priv_key_format --pwd secret123" \ + "$pub_key_option:TestKeyName-ml-dsa-44 $topfolder/keys/ml-dsa/ml-dsa-44-pubkey.$pub_key_format" + +if [ "z$xmlsec_feature_context_string" = "zyes" ] ; then + execDSigTest $res_success \ + "" \ + "aleksey-xmldsig-01/enveloped-sha512-mldsa44-with-context-string" \ + "sha512 ml-dsa-44" \ + "ml-dsa" \ + "$pub_key_option:TestKeyName-ml-dsa-44 $topfolder/keys/ml-dsa/ml-dsa-44-pubkey.$pub_key_format" \ + "$priv_key_option:TestKeyName-ml-dsa-44 $topfolder/keys/ml-dsa/ml-dsa-44-key.$priv_key_format --pwd secret123" \ + "$pub_key_option:TestKeyName-ml-dsa-44 $topfolder/keys/ml-dsa/ml-dsa-44-pubkey.$pub_key_format" +fi + +execDSigTest $res_success \ + "" \ + "aleksey-xmldsig-01/enveloped-sha512-mldsa65" \ + "sha512 ml-dsa-65" \ + "ml-dsa" \ + "$pub_key_option:TestKeyName-ml-dsa-65 $topfolder/keys/ml-dsa/ml-dsa-65-pubkey.$pub_key_format" \ + "$priv_key_option:TestKeyName-ml-dsa-65 $topfolder/keys/ml-dsa/ml-dsa-65-key.$priv_key_format --pwd secret123" \ + "$pub_key_option:TestKeyName-ml-dsa-65 $topfolder/keys/ml-dsa/ml-dsa-65-pubkey.$pub_key_format" + +if [ "z$xmlsec_feature_context_string" = "zyes" ] ; then + execDSigTest $res_success \ + "" \ + "aleksey-xmldsig-01/enveloped-sha512-mldsa65-with-context-string" \ + "sha512 ml-dsa-65" \ + "ml-dsa" \ + "$pub_key_option:TestKeyName-ml-dsa-65 $topfolder/keys/ml-dsa/ml-dsa-65-pubkey.$pub_key_format" \ + "$priv_key_option:TestKeyName-ml-dsa-65 $topfolder/keys/ml-dsa/ml-dsa-65-key.$priv_key_format --pwd secret123" \ + "$pub_key_option:TestKeyName-ml-dsa-65 $topfolder/keys/ml-dsa/ml-dsa-65-pubkey.$pub_key_format" +fi + +execDSigTest $res_success \ + "" \ + "aleksey-xmldsig-01/enveloped-sha512-mldsa87" \ + "sha512 ml-dsa-87" \ + "ml-dsa" \ + "$pub_key_option:TestKeyName-ml-dsa-87 $topfolder/keys/ml-dsa/ml-dsa-87-pubkey.$pub_key_format" \ + "$priv_key_option:TestKeyName-ml-dsa-87 $topfolder/keys/ml-dsa/ml-dsa-87-key.$priv_key_format --pwd secret123" \ + "$pub_key_option:TestKeyName-ml-dsa-87 $topfolder/keys/ml-dsa/ml-dsa-87-pubkey.$pub_key_format" + +if [ "z$xmlsec_feature_context_string" = "zyes" ] ; then + execDSigTest $res_success \ + "" \ + "aleksey-xmldsig-01/enveloped-sha512-mldsa87-with-context-string" \ + "sha512 ml-dsa-87" \ + "ml-dsa" \ + "$pub_key_option:TestKeyName-ml-dsa-87 $topfolder/keys/ml-dsa/ml-dsa-87-pubkey.$pub_key_format" \ + "$priv_key_option:TestKeyName-ml-dsa-87 $topfolder/keys/ml-dsa/ml-dsa-87-key.$priv_key_format --pwd secret123" \ + "$pub_key_option:TestKeyName-ml-dsa-87 $topfolder/keys/ml-dsa/ml-dsa-87-pubkey.$pub_key_format" +fi + + +## SLH-DSA +execDSigTest $res_success \ + "" \ + "aleksey-xmldsig-01/enveloped-sha512-slhdsa-sha2-128f" \ + "sha512 slh-dsa-sha2-128f" \ + "slh-dsa" \ + "$pub_key_option:TestKeyName-slh-dsa-sha2-128f $topfolder/keys/slh-dsa/slh-dsa-sha2-128f-pubkey.$pub_key_format" \ + "$priv_key_option:TestKeyName-slh-dsa-sha2-128f $topfolder/keys/slh-dsa/slh-dsa-sha2-128f-key.$priv_key_format --pwd secret123" \ + "$pub_key_option:TestKeyName-slh-dsa-sha2-128f $topfolder/keys/slh-dsa/slh-dsa-sha2-128f-pubkey.$pub_key_format" + +if [ "z$xmlsec_feature_context_string" = "zyes" ] ; then + execDSigTest $res_success \ + "" \ + "aleksey-xmldsig-01/enveloped-sha512-slhdsa-sha2-128f-with-context-string" \ + "sha512 slh-dsa-sha2-128f" \ + "slh-dsa" \ + "$pub_key_option:TestKeyName-slh-dsa-sha2-128f $topfolder/keys/slh-dsa/slh-dsa-sha2-128f-pubkey.$pub_key_format" \ + "$priv_key_option:TestKeyName-slh-dsa-sha2-128f $topfolder/keys/slh-dsa/slh-dsa-sha2-128f-key.$priv_key_format --pwd secret123" \ + "$pub_key_option:TestKeyName-slh-dsa-sha2-128f $topfolder/keys/slh-dsa/slh-dsa-sha2-128f-pubkey.$pub_key_format" +fi + + +execDSigTest $res_success \ + "" \ + "aleksey-xmldsig-01/enveloped-sha512-slhdsa-sha2-128s" \ + "sha512 slh-dsa-sha2-128s" \ + "slh-dsa" \ + "$pub_key_option:TestKeyName-slh-dsa-sha2-128s $topfolder/keys/slh-dsa/slh-dsa-sha2-128s-pubkey.$pub_key_format" \ + "$priv_key_option:TestKeyName-slh-dsa-sha2-128s $topfolder/keys/slh-dsa/slh-dsa-sha2-128s-key.$priv_key_format --pwd secret123" \ + "$pub_key_option:TestKeyName-slh-dsa-sha2-128s $topfolder/keys/slh-dsa/slh-dsa-sha2-128s-pubkey.$pub_key_format" + +if [ "z$xmlsec_feature_context_string" = "zyes" ] ; then + execDSigTest $res_success \ + "" \ + "aleksey-xmldsig-01/enveloped-sha512-slhdsa-sha2-128s-with-context-string" \ + "sha512 slh-dsa-sha2-128s" \ + "slh-dsa" \ + "$pub_key_option:TestKeyName-slh-dsa-sha2-128s $topfolder/keys/slh-dsa/slh-dsa-sha2-128s-pubkey.$pub_key_format" \ + "$priv_key_option:TestKeyName-slh-dsa-sha2-128s $topfolder/keys/slh-dsa/slh-dsa-sha2-128s-key.$priv_key_format --pwd secret123" \ + "$pub_key_option:TestKeyName-slh-dsa-sha2-128s $topfolder/keys/slh-dsa/slh-dsa-sha2-128s-pubkey.$pub_key_format" +fi + + +execDSigTest $res_success \ + "" \ + "aleksey-xmldsig-01/enveloped-sha512-slhdsa-sha2-192f" \ + "sha512 slh-dsa-sha2-192f" \ + "slh-dsa" \ + "$pub_key_option:TestKeyName-slh-dsa-sha2-192f $topfolder/keys/slh-dsa/slh-dsa-sha2-192f-pubkey.$pub_key_format" \ + "$priv_key_option:TestKeyName-slh-dsa-sha2-192f $topfolder/keys/slh-dsa/slh-dsa-sha2-192f-key.$priv_key_format --pwd secret123" \ + "$pub_key_option:TestKeyName-slh-dsa-sha2-192f $topfolder/keys/slh-dsa/slh-dsa-sha2-192f-pubkey.$pub_key_format" + +if [ "z$xmlsec_feature_context_string" = "zyes" ] ; then + execDSigTest $res_success \ + "" \ + "aleksey-xmldsig-01/enveloped-sha512-slhdsa-sha2-192f-with-context-string" \ + "sha512 slh-dsa-sha2-192f" \ + "slh-dsa" \ + "$pub_key_option:TestKeyName-slh-dsa-sha2-192f $topfolder/keys/slh-dsa/slh-dsa-sha2-192f-pubkey.$pub_key_format" \ + "$priv_key_option:TestKeyName-slh-dsa-sha2-192f $topfolder/keys/slh-dsa/slh-dsa-sha2-192f-key.$priv_key_format --pwd secret123" \ + "$pub_key_option:TestKeyName-slh-dsa-sha2-192f $topfolder/keys/slh-dsa/slh-dsa-sha2-192f-pubkey.$pub_key_format" +fi + +execDSigTest $res_success \ + "" \ + "aleksey-xmldsig-01/enveloped-sha512-slhdsa-sha2-192s" \ + "sha512 slh-dsa-sha2-192s" \ + "slh-dsa" \ + "$pub_key_option:TestKeyName-slh-dsa-sha2-192s $topfolder/keys/slh-dsa/slh-dsa-sha2-192s-pubkey.$pub_key_format" \ + "$priv_key_option:TestKeyName-slh-dsa-sha2-192s $topfolder/keys/slh-dsa/slh-dsa-sha2-192s-key.$priv_key_format --pwd secret123" \ + "$pub_key_option:TestKeyName-slh-dsa-sha2-192s $topfolder/keys/slh-dsa/slh-dsa-sha2-192s-pubkey.$pub_key_format" + +if [ "z$xmlsec_feature_context_string" = "zyes" ] ; then + execDSigTest $res_success \ + "" \ + "aleksey-xmldsig-01/enveloped-sha512-slhdsa-sha2-192s-with-context-string" \ + "sha512 slh-dsa-sha2-192s" \ + "slh-dsa" \ + "$pub_key_option:TestKeyName-slh-dsa-sha2-192s $topfolder/keys/slh-dsa/slh-dsa-sha2-192s-pubkey.$pub_key_format" \ + "$priv_key_option:TestKeyName-slh-dsa-sha2-192s $topfolder/keys/slh-dsa/slh-dsa-sha2-192s-key.$priv_key_format --pwd secret123" \ + "$pub_key_option:TestKeyName-slh-dsa-sha2-192s $topfolder/keys/slh-dsa/slh-dsa-sha2-192s-pubkey.$pub_key_format" +fi + +execDSigTest $res_success \ + "" \ + "aleksey-xmldsig-01/enveloped-sha512-slhdsa-sha2-256f" \ + "sha512 slh-dsa-sha2-256f" \ + "slh-dsa" \ + "$pub_key_option:TestKeyName-slh-dsa-sha2-256f $topfolder/keys/slh-dsa/slh-dsa-sha2-256f-pubkey.$pub_key_format" \ + "$priv_key_option:TestKeyName-slh-dsa-sha2-256f $topfolder/keys/slh-dsa/slh-dsa-sha2-256f-key.$priv_key_format --pwd secret123" \ + "$pub_key_option:TestKeyName-slh-dsa-sha2-256f $topfolder/keys/slh-dsa/slh-dsa-sha2-256f-pubkey.$pub_key_format" + +if [ "z$xmlsec_feature_context_string" = "zyes" ] ; then + execDSigTest $res_success \ + "" \ + "aleksey-xmldsig-01/enveloped-sha512-slhdsa-sha2-256f-with-context-string" \ + "sha512 slh-dsa-sha2-256f" \ + "slh-dsa" \ + "$pub_key_option:TestKeyName-slh-dsa-sha2-256f $topfolder/keys/slh-dsa/slh-dsa-sha2-256f-pubkey.$pub_key_format" \ + "$priv_key_option:TestKeyName-slh-dsa-sha2-256f $topfolder/keys/slh-dsa/slh-dsa-sha2-256f-key.$priv_key_format --pwd secret123" \ + "$pub_key_option:TestKeyName-slh-dsa-sha2-256f $topfolder/keys/slh-dsa/slh-dsa-sha2-256f-pubkey.$pub_key_format" +fi + +execDSigTest $res_success \ + "" \ + "aleksey-xmldsig-01/enveloped-sha512-slhdsa-sha2-256s" \ + "sha512 slh-dsa-sha2-256s" \ + "slh-dsa" \ + "$pub_key_option:TestKeyName-slh-dsa-sha2-256s $topfolder/keys/slh-dsa/slh-dsa-sha2-256s-pubkey.$pub_key_format" \ + "$priv_key_option:TestKeyName-slh-dsa-sha2-256s $topfolder/keys/slh-dsa/slh-dsa-sha2-256s-key.$priv_key_format --pwd secret123" \ + "$pub_key_option:TestKeyName-slh-dsa-sha2-256s $topfolder/keys/slh-dsa/slh-dsa-sha2-256s-pubkey.$pub_key_format" + +if [ "z$xmlsec_feature_context_string" = "zyes" ] ; then + execDSigTest $res_success \ + "" \ + "aleksey-xmldsig-01/enveloped-sha512-slhdsa-sha2-256s-with-context-string" \ + "sha512 slh-dsa-sha2-256s" \ + "slh-dsa" \ + "$pub_key_option:TestKeyName-slh-dsa-sha2-256s $topfolder/keys/slh-dsa/slh-dsa-sha2-256s-pubkey.$pub_key_format" \ + "$priv_key_option:TestKeyName-slh-dsa-sha2-256s $topfolder/keys/slh-dsa/slh-dsa-sha2-256s-key.$priv_key_format --pwd secret123" \ + "$pub_key_option:TestKeyName-slh-dsa-sha2-256s $topfolder/keys/slh-dsa/slh-dsa-sha2-256s-pubkey.$pub_key_format" +fi + + +## ML-KEM (HMAC signature with encapsulated key) +if [ "z$xmlsec_feature_ml_kem" = "zyes" ] ; then +execDSigTest $res_success \ + "" \ + "aleksey-xmldsig-01/enveloping-sha256-hmac-sha256-em-ml-kem-512" \ + "sha256 hmac-sha256 ml-kem-512" \ + "hmac ml-kem" \ + "$mlkem_priv_key_option:TestKeyName-ml-kem-512 $topfolder/keys/ml-kem/ml-kem-512-key.$mlkem_priv_key_format --pwd secret123 --enabled-key-data key-name,encapsulation-mechanism" \ + "$mlkem_pub_key_option:TestKeyName-ml-kem-512 $topfolder/keys/ml-kem/ml-kem-512-pubkey.$mlkem_pub_key_format --enabled-key-data key-name,encapsulation-mechanism" \ + "$mlkem_priv_key_option:TestKeyName-ml-kem-512 $topfolder/keys/ml-kem/ml-kem-512-key.$mlkem_priv_key_format --pwd secret123 --enabled-key-data key-name,encapsulation-mechanism" + +execDSigTest $res_success \ + "" \ + "aleksey-xmldsig-01/enveloping-sha256-hmac-sha256-em-ml-kem-768" \ + "sha256 hmac-sha256 ml-kem-768" \ + "hmac ml-kem" \ + "$mlkem_priv_key_option:TestKeyName-ml-kem-768 $topfolder/keys/ml-kem/ml-kem-768-key.$mlkem_priv_key_format --pwd secret123 --enabled-key-data key-name,encapsulation-mechanism" \ + "$mlkem_pub_key_option:TestKeyName-ml-kem-768 $topfolder/keys/ml-kem/ml-kem-768-pubkey.$mlkem_pub_key_format --enabled-key-data key-name,encapsulation-mechanism" \ + "$mlkem_priv_key_option:TestKeyName-ml-kem-768 $topfolder/keys/ml-kem/ml-kem-768-key.$mlkem_priv_key_format --pwd secret123 --enabled-key-data key-name,encapsulation-mechanism" + +execDSigTest $res_success \ + "" \ + "aleksey-xmldsig-01/enveloping-sha256-hmac-sha256-em-ml-kem-1024" \ + "sha256 hmac-sha256 ml-kem-1024" \ + "hmac ml-kem" \ + "$mlkem_priv_key_option:TestKeyName-ml-kem-1024 $topfolder/keys/ml-kem/ml-kem-1024-key.$mlkem_priv_key_format --pwd secret123 --enabled-key-data key-name,encapsulation-mechanism" \ + "$mlkem_pub_key_option:TestKeyName-ml-kem-1024 $topfolder/keys/ml-kem/ml-kem-1024-pubkey.$mlkem_pub_key_format --enabled-key-data key-name,encapsulation-mechanism" \ + "$mlkem_priv_key_option:TestKeyName-ml-kem-1024 $topfolder/keys/ml-kem/ml-kem-1024-key.$mlkem_priv_key_format --pwd secret123 --enabled-key-data key-name,encapsulation-mechanism" +fi # xmlsec_feature_ml_kem + + +## EdDSA +execDSigTest $res_success \ + "" \ + "aleksey-xmldsig-01/enveloped-sha256-eddsa-ed25519" \ + "sha256 eddsa-ed25519" \ + "eddsa" \ + "$pub_key_option:TestKeyName-eddsa-ed25519 $topfolder/keys/eddsa/eddsa-ed25519-pubkey.$pub_key_format" \ + "$eddsa_priv_key_option:TestKeyName-eddsa-ed25519 $topfolder/keys/eddsa/eddsa-ed25519-key.$eddsa_priv_key_format --pwd secret123" \ + "$pub_key_option:TestKeyName-eddsa-ed25519 $topfolder/keys/eddsa/eddsa-ed25519-pubkey.$pub_key_format" + +execDSigTest $res_success \ + "" \ + "aleksey-xmldsig-01/enveloped-sha256-eddsa-ed25519ph" \ + "sha256 eddsa-ed25519ph" \ + "eddsa" \ + "$pub_key_option:TestKeyName-eddsa-ed25519 $topfolder/keys/eddsa/eddsa-ed25519-pubkey.$pub_key_format" \ + "$eddsa_priv_key_option:TestKeyName-eddsa-ed25519 $topfolder/keys/eddsa/eddsa-ed25519-key.$eddsa_priv_key_format --pwd secret123" \ + "$pub_key_option:TestKeyName-eddsa-ed25519 $topfolder/keys/eddsa/eddsa-ed25519-pubkey.$pub_key_format" + +# context string is required for Ed25519ctx so no point in checking feature flag +execDSigTest $res_success \ + "" \ + "aleksey-xmldsig-01/enveloped-sha256-eddsa-ed25519ctx-with-context-string" \ + "sha256 eddsa-ed25519ctx" \ + "eddsa" \ + "$pub_key_option:TestKeyName-eddsa-ed25519 $topfolder/keys/eddsa/eddsa-ed25519-pubkey.$pub_key_format" \ + "$eddsa_priv_key_option:TestKeyName-eddsa-ed25519 $topfolder/keys/eddsa/eddsa-ed25519-key.$eddsa_priv_key_format --pwd secret123" \ + "$pub_key_option:TestKeyName-eddsa-ed25519 $topfolder/keys/eddsa/eddsa-ed25519-pubkey.$pub_key_format" + +if [ "z$xmlsec_feature_context_string" = "zyes" ] ; then + + execDSigTest $res_success \ + "" \ + "aleksey-xmldsig-01/enveloped-sha256-eddsa-ed25519ph-with-context-string" \ + "sha256 eddsa-ed25519ph" \ + "eddsa" \ + "$pub_key_option:TestKeyName-eddsa-ed25519 $topfolder/keys/eddsa/eddsa-ed25519-pubkey.$pub_key_format" \ + "$eddsa_priv_key_option:TestKeyName-eddsa-ed25519 $topfolder/keys/eddsa/eddsa-ed25519-key.$eddsa_priv_key_format --pwd secret123" \ + "$pub_key_option:TestKeyName-eddsa-ed25519 $topfolder/keys/eddsa/eddsa-ed25519-pubkey.$pub_key_format" +fi + +execDSigTest $res_success \ + "" \ + "aleksey-xmldsig-01/enveloped-sha256-eddsa-ed448" \ + "sha256 eddsa-ed448" \ + "eddsa" \ + "$pub_key_option:TestKeyName-eddsa-ed448 $topfolder/keys/eddsa/eddsa-ed448-pubkey.$pub_key_format" \ + "$eddsa_priv_key_option:TestKeyName-eddsa-ed448 $topfolder/keys/eddsa/eddsa-ed448-key.$eddsa_priv_key_format --pwd secret123" \ + "$pub_key_option:TestKeyName-eddsa-ed448 $topfolder/keys/eddsa/eddsa-ed448-pubkey.$pub_key_format" + +execDSigTest $res_success \ + "" \ + "aleksey-xmldsig-01/enveloped-sha256-eddsa-ed448ph" \ + "sha256 eddsa-ed448ph" \ + "eddsa" \ + "$pub_key_option:TestKeyName-eddsa-ed448 $topfolder/keys/eddsa/eddsa-ed448-pubkey.$pub_key_format" \ + "$eddsa_priv_key_option:TestKeyName-eddsa-ed448 $topfolder/keys/eddsa/eddsa-ed448-key.$eddsa_priv_key_format --pwd secret123" \ + "$pub_key_option:TestKeyName-eddsa-ed448 $topfolder/keys/eddsa/eddsa-ed448-pubkey.$pub_key_format" + +if [ "z$xmlsec_feature_context_string" = "zyes" ] ; then + execDSigTest $res_success \ + "" \ + "aleksey-xmldsig-01/enveloped-sha256-eddsa-ed448ph-with-context-string" \ + "sha256 eddsa-ed448ph" \ + "eddsa" \ + "$pub_key_option:TestKeyName-eddsa-ed448 $topfolder/keys/eddsa/eddsa-ed448-pubkey.$pub_key_format" \ + "$eddsa_priv_key_option:TestKeyName-eddsa-ed448 $topfolder/keys/eddsa/eddsa-ed448-key.$eddsa_priv_key_format --pwd secret123" \ + "$pub_key_option:TestKeyName-eddsa-ed448 $topfolder/keys/eddsa/eddsa-ed448-pubkey.$pub_key_format" +fi + + +########################################################################## +########################################################################## +########################################################################## +echo "--------- Certificate verification testing ----------" + +# +# To generate output with an expired cert run the following command +# +# xmlsec1 sign --pkcs12 ./tests/keys/rsa/rsa-expired-key.p12 --pwd secret123 --output ./tests/aleksey-xmldsig-01/enveloping-expired-cert.xml ./tests/aleksey-xmldsig-01/enveloping-expired-cert.tmpl +# + +# This should fail: expired cert +extra_message="Negative test: expired cert" +execDSigTest $res_fail \ + "" \ + "aleksey-xmldsig-01/enveloping-expired-cert" \ + "sha1 rsa-sha1" \ + "rsa x509" \ + "--trusted-$cert_format $topfolder/keys/cacert.$cert_format --enabled-key-data x509" + +# Expired cert but there is verification time overwrite +extra_message="Expired cert but there is verification timestamp overwrite" +execDSigTest $res_success \ + "" \ + "aleksey-xmldsig-01/enveloping-expired-cert" \ + "sha1 rsa-sha1" \ + "rsa x509" \ + "--trusted-$cert_format $topfolder/keys/cacert.$cert_format --enabled-key-data x509 --verification-gmt-time 2026-03-15+00:00:00" + +if [ "z$xmlsec_feature_cert_check_skip_time" = "zyes" ] ; then + extra_message="Expired cert but we skip timestamp checks" + execDSigTest $res_success \ + "" \ + "aleksey-xmldsig-01/enveloping-expired-cert" \ + "sha1 rsa-sha1" \ + "rsa x509" \ + "--trusted-$cert_format $topfolder/keys/cacert.$cert_format --enabled-key-data x509 --X509-skip-time-checks" +fi + +# 'Verify existing signature' MUST fail here, as --trusted-... is not passed. +# If this passes, that's a bug. Note that we need to cleanup NSS certs DB +# since it automaticall stores trusted certs +extra_message="Missing trusted cert " +execDSigTest $res_fail \ + "aleksey-xmldsig-01" \ + "enveloping-sha256-rsa-sha256" \ + "sha256 rsa-sha256" \ + "rsa x509" \ + "--enabled-key-data x509" + +# This is the same, but due to --insecure it must pass. +# If this fails, that means avoiding the certificate verification doesn't +# happen correctly +extra_message="Negative test: missing trusted cert but there is --insecure bypass" +execDSigTest $res_success \ + "aleksey-xmldsig-01" \ + "enveloping-sha256-rsa-sha256" \ + "sha256 rsa-sha256" \ + "rsa x509" \ + "--enabled-key-data x509 --insecure" + + + +# Test was created using the following command: +# xmlsec1 sign --crypto openssl --lax-key-search --privkey-pem tests/keys/same-subj-key1.pem,tests/keys/same-subj-cert1.pem --output tests/aleksey-xmldsig-01/enveloped-x509-same-subj-cert.xml tests/aleksey-xmldsig-01/enveloped-x509-same-subj-cert.tmpl + +# this should succeeed with good cert +extra_message="Cert chain is good" +execDSigTest $res_success \ + "" \ + "aleksey-xmldsig-01/enveloped-x509-same-subj-cert" \ + "sha256 rsa-sha256" \ + "x509" \ + "--trusted-$cert_format $topfolder/keys/same-subj-cert1.$cert_format --enabled-key-data x509" + +# this should fail: Same subject but wrong cert +extra_message="Negative test: Same subject but wrong cert" +execDSigTest $res_fail \ + "" \ + "aleksey-xmldsig-01/enveloped-x509-same-subj-cert" \ + "sha256 rsa-sha256" \ + "x509" \ + "--trusted-$cert_format $topfolder/keys/same-subj-cert2.$cert_format --enabled-key-data x509" + +# this should succeeed with both good (cert1) and bad (cert2) certs present (simulating key rotation) +extra_message="Cert chain is good: both good (cert1) and bad (cert2) certs present" +execDSigTest $res_success \ + "" \ + "aleksey-xmldsig-01/enveloped-x509-same-subj-cert" \ + "sha256 rsa-sha256" \ + "x509" \ + "--trusted-$cert_format $topfolder/keys/same-subj-cert1.$cert_format --trusted-$cert_format $topfolder/keys/same-subj-cert2.$cert_format --enabled-key-data x509" + +# this should succeeed with both bad (cert2) and good (cert1) certs present (simulating key rotation) +extra_message="Cert chain is good: both bad (cert2) and good (cert1) certs present" +execDSigTest $res_success \ + "" \ + "aleksey-xmldsig-01/enveloped-x509-same-subj-cert" \ + "sha256 rsa-sha256" \ + "x509" \ + "--trusted-$cert_format $topfolder/keys/same-subj-cert2.$cert_format --trusted-$cert_format $topfolder/keys/same-subj-cert1.$cert_format --enabled-key-data x509" + + +# Test was created using the following command: +# xmlsec1 sign --lax-key-search --privkey-pem tests/keys/rsa/rsa-2048-key.pem,tests/keys/rsa/rsa-2048-cert.pem --output tests/aleksey-xmldsig-01/enveloped-x509-missing-cert.xml tests/aleksey-xmldsig-01/enveloped-x509-missing-cert.tmpl +# + +# this should succeeed with both intermidiate and trusted certs provided +extra_message="Cert chain is good: both intermidiate and trusted certs provided" +execDSigTest $res_success \ + "" \ + "aleksey-xmldsig-01/enveloped-x509-missing-cert" \ + "sha256 rsa-sha256" \ + "x509" \ + "--untrusted-$cert_format $topfolder/keys/ca2cert.$cert_format --trusted-$cert_format $topfolder/keys/cacert.$cert_format --enabled-key-data x509" + +# this should succeeed too because we bypass all cert checks with --insecure mode +extra_message="Cert chain is missing but there is --insecure bypass" +execDSigTest $res_success \ + "" \ + "aleksey-xmldsig-01/enveloped-x509-missing-cert" \ + "sha256 rsa-sha256" \ + "x509" \ + "--insecure --enabled-key-data x509" + +# this should fail: missing intermidiate cert (ca2cert) +extra_message="Negative test: Missing intermidiate cert" +execDSigTest $res_fail \ + "" \ + "aleksey-xmldsig-01/enveloped-x509-missing-cert" \ + "sha256 rsa-sha256" \ + "x509" \ + "--trusted-$cert_format $topfolder/keys/cacert.$cert_format --enabled-key-data x509" + +# this should fail: wront trusted cert (rsa-4096-cert) +extra_message="Negative test: Wront trusted cert" +execDSigTest $res_fail \ + "" \ + "aleksey-xmldsig-01/enveloped-x509-missing-cert" \ + "sha256 rsa-sha256" \ + "x509" \ + "--untrusted-$cert_format $topfolder/keys/ca2cert.$cert_format --trusted-$cert_format $topfolder/keys/rsa/rsa-4096-cert.$cert_format --enabled-key-data x509" + +if [ "z$xmlsec_feature_crl_load" = "zyes" ] ; then + # this should fail because there is a CRL for the cert used for signing + extra_message="Negative test: CRL present" + execDSigTest $res_fail \ + "" \ + "aleksey-xmldsig-01/enveloped-x509-missing-cert" \ + "sha256 rsa-sha256" \ + "x509" \ + "--verification-gmt-time 2023-04-01+00:00:00 --untrusted-$cert_format $topfolder/keys/ca2cert.$cert_format --trusted-$cert_format $topfolder/keys/cacert.$cert_format --crl-$cert_format $topfolder/keys/rsa/rsa-2048-cert-revoked-crl.$cert_format --enabled-key-data x509" + + # this should fail because while CRL is past due, it's still better than nothing + extra_message="Negative test: CRL is past due" + execDSigTest $res_fail \ + "" \ + "aleksey-xmldsig-01/enveloped-x509-missing-cert" \ + "sha256 rsa-sha256" \ + "x509" \ + "--verification-gmt-time 2023-05-01+00:00:00 --untrusted-$cert_format $topfolder/keys/ca2cert.$cert_format --trusted-$cert_format $topfolder/keys/cacert.$cert_format --crl-$cert_format $topfolder/keys/rsa/rsa-2048-cert-revoked-crl.$cert_format --enabled-key-data x509" + + # NSS / GnuTLS doesn't allow CRL verification by time (https://github.com/lsh123/xmlsec/issues/579) + if [ "z$xmlsec_feature_crl_check_skip_time" = "zyes" ] ; then + # this should succeeed because CRL is not valid yet + extra_message="CRL is not valid yet" + execDSigTest $res_success \ + "" \ + "aleksey-xmldsig-01/enveloped-x509-missing-cert" \ + "sha256 rsa-sha256" \ + "x509" \ + "--verification-gmt-time 2026-03-10+00:00:00 --untrusted-$cert_format $topfolder/keys/ca2cert.$cert_format --trusted-$cert_format $topfolder/keys/cacert.$cert_format --crl-$cert_format $topfolder/keys/rsa/rsa-2048-cert-revoked-crl.$cert_format --enabled-key-data x509" + fi + + # this should succeeed too because we bypass all cert checks with --insecure mode + extra_message="CRL is present but there is --insecure bypass" + execDSigTest $res_success \ + "" \ + "aleksey-xmldsig-01/enveloped-x509-missing-cert" \ + "sha256 rsa-sha256" \ + "x509" \ + "--insecure --crl-$cert_format $topfolder/keys/rsa/rsa-2048-cert-revoked-crl.$cert_format --enabled-key-data x509" + +fi + +if [ "z$xmlsec_feature_crl_verification" = "zyes" ] ; then + extra_message="Verify CRL: this should succeed because CRL is not verified" + execDSigTest $res_success \ + "" \ + "aleksey-xmldsig-01/enveloped-x509-subjectname" \ + "sha512 rsa-sha512" \ + "rsa x509" \ + "--verification-gmt-time 2026-03-10+00:00:00 --crl-$cert_format $topfolder/keys/rsa/rsa-2048-cert-revoked-crl.$cert_format --untrusted-$cert_format $topfolder/keys/rsa/rsa-4096-cert.$cert_format --untrusted-$cert_format $topfolder/keys/ca2cert.$cert_format --trusted-$cert_format $topfolder/keys/cacert.$cert_format --enabled-key-data x509" + + # GnuTLS doesn't allow CRL verification by time (https://github.com/lsh123/xmlsec/issues/579) + if [ "z$xmlsec_feature_crl_check_skip_time" = "zyes" ] ; then + extra_message="Verify CRL: this should succeed because CRL is valid" + execDSigTest $res_success \ + "" \ + "aleksey-xmldsig-01/enveloped-x509-subjectname" \ + "sha512 rsa-sha512" \ + "rsa x509" \ + "--verify-crls --verification-gmt-time 2026-03-20+00:00:00 --crl-$cert_format $topfolder/keys/rsa/rsa-2048-cert-revoked-crl.$cert_format --untrusted-$cert_format $topfolder/keys/rsa/rsa-4096-cert.$cert_format --untrusted-$cert_format $topfolder/keys/ca2cert.$cert_format --trusted-$cert_format $topfolder/keys/cacert.$cert_format --enabled-key-data x509" + fi + + # this should fail because CRL is past due + extra_message="Verify CRL: this should fail becaused CRL is past due" + execDSigTest $res_fail \ + "" \ + "aleksey-xmldsig-01/enveloped-x509-subjectname" \ + "sha512 rsa-sha512" \ + "rsa x509" \ + "--verify-crls --verification-gmt-time 2026-05-01+00:00:00 --crl-$cert_format $topfolder/keys/rsa/rsa-2048-cert-revoked-crl.$cert_format --untrusted-$cert_format $topfolder/keys/rsa/rsa-4096-cert.$cert_format --untrusted-$cert_format $topfolder/keys/ca2cert.$cert_format --trusted-$cert_format $topfolder/keys/cacert.$cert_format --enabled-key-data x509" + + # this should succeed because --insecure overwrites all verifications + extra_message="Verify CRL: this should succeed because --insecure bypasses all verifications" + execDSigTest $res_success \ + "" \ + "aleksey-xmldsig-01/enveloped-x509-subjectname" \ + "sha512 rsa-sha512" \ + "rsa x509" \ + "--insecure --verify-crls --verification-gmt-time 2026-03-01+00:00:00 --crl-$cert_format $topfolder/keys/rsa/rsa-2048-cert-revoked-crl.$cert_format --untrusted-$cert_format $topfolder/keys/rsa/rsa-4096-cert.$cert_format --untrusted-$cert_format $topfolder/keys/ca2cert.$cert_format --trusted-$cert_format $topfolder/keys/cacert.$cert_format --enabled-key-data x509" + +fi + + + +if [ "z$xmlsec_feature_key_check" = "zyes" ] ; then + # this should succeeed because key verification is not requested (no --verify-keys option) + extra_message="Successfully use key without verification" + execDSigTest $res_success \ + "" \ + "aleksey-xmldsig-01/enveloped-sha1-rsa-sha1" \ + "sha1 rsa-sha1" \ + "x509" \ + "--pubkey-cert-$cert_format:TestKeyName-rsa-4096 $topfolder/keys/rsa/rsa-4096-cert.$cert_format --enabled-key-data key-name" + + # this should fail because key cannot be verified without certificates + extra_message="Negative test: key cannot be verified" + execDSigTest $res_fail \ + "" \ + "aleksey-xmldsig-01/enveloped-sha1-rsa-sha1" \ + "sha1 rsa-sha1" \ + "x509" \ + "--verify-keys --pubkey-cert-$cert_format:TestKeyName-rsa-4096 $topfolder/keys/rsa/rsa-4096-cert.$cert_format --enabled-key-data key-name" + + # this should fail because key cannot be verified at specified time + extra_message="Negative test: key cannot be verified (cert is not yet valid)" + execDSigTest $res_fail \ + "" \ + "aleksey-xmldsig-01/enveloped-sha1-rsa-sha1" \ + "sha1 rsa-sha1" \ + "x509" \ + "--verify-keys --verification-gmt-time 1980-01-01+00:00:00 --pubkey-cert-$cert_format:TestKeyName-rsa-4096 $topfolder/keys/rsa/rsa-4096-cert.$cert_format --untrusted-$cert_format $topfolder/keys/ca2cert.$cert_format --trusted-$cert_format $topfolder/keys/cacert.$cert_format --enabled-key-data key-name" + + # this should succeeed because key can be verified + extra_message="Successfully verify key" + execDSigTest $res_success \ + "" \ + "aleksey-xmldsig-01/enveloped-sha1-rsa-sha1" \ + "sha1 rsa-sha1" \ + "x509" \ + "--verify-keys --pubkey-cert-$cert_format:TestKeyName-rsa-4096 $topfolder/keys/rsa/rsa-4096-cert.$cert_format --untrusted-$cert_format $topfolder/keys/ca2cert.$cert_format --trusted-$cert_format $topfolder/keys/cacert.$cert_format --enabled-key-data key-name" + +fi + + + +########################################################################## +# +# merlin-xmldsig-twenty-three +# +########################################################################## +execDSigTest $res_success \ + "" \ + "merlin-xmldsig-twenty-three/signature-enveloped-dsa" \ + "enveloped-signature sha1 dsa-sha1" \ + "dsa" \ + "--enabled-key-data key-value,key-name,dsa" \ + "--enabled-key-data key-value,key-name,dsa $priv_key_option:TestKeyName-dsa-1024 $topfolder/keys/dsa/dsa-1024-key.$priv_key_format --pwd secret123" \ + "--enabled-key-data key-value,key-name,dsa" + +execDSigTest $res_success \ + "" \ + "merlin-xmldsig-twenty-three/signature-enveloping-dsa" \ + "sha1 dsa-sha1" \ + "dsa" \ + "--enabled-key-data key-value,key-name,dsa" \ + "--enabled-key-data key-value,key-name,dsa $priv_key_option:TestKeyName-dsa-1024 $topfolder/keys/dsa/dsa-1024-key.$priv_key_format --pwd secret123" \ + "--enabled-key-data key-value,key-name,dsa" + +execDSigTest $res_success \ + "" \ + "merlin-xmldsig-twenty-three/signature-enveloping-b64-dsa" \ + "base64 sha1 dsa-sha1" \ + "dsa" \ + "--enabled-key-data key-value,key-name,dsa" \ + "--enabled-key-data key-value,key-name,dsa $priv_key_option:TestKeyName-dsa-1024 $topfolder/keys/dsa/dsa-1024-key.$priv_key_format --pwd secret123" \ + "--enabled-key-data key-value,key-name,dsa" + +execDSigTest $res_success \ + "" \ + "merlin-xmldsig-twenty-three/signature-enveloping-hmac-sha1-40" \ + "sha1 hmac-sha1" \ + "hmac" \ + "--lax-key-search --hmackey $topfolder/keys/hmackey.bin" \ + "--hmackey:TeskKeyName-Hmac $topfolder/keys/hmackey.bin" \ + "--hmackey:TeskKeyName-Hmac $topfolder/keys/hmackey.bin" + +execDSigTest $res_success \ + "" \ + "merlin-xmldsig-twenty-three/signature-enveloping-hmac-sha1" \ + "sha1 hmac-sha1" \ + "hmac" \ + "--lax-key-search --hmackey $topfolder/keys/hmackey.bin" \ + "--hmackey:TeskKeyName-Hmac $topfolder/keys/hmackey.bin" \ + "--hmackey:TeskKeyName-Hmac $topfolder/keys/hmackey.bin" + +execDSigTest $res_success \ + "" \ + "merlin-xmldsig-twenty-three/signature-enveloping-rsa" \ + "sha1 rsa-sha1" \ + "rsa" \ + "--enabled-key-data key-value,key-name,rsa" \ + "--enabled-key-data key-value,key-name,rsa $priv_key_option:TestKeyName-rsa-2048 $topfolder/keys/rsa/rsa-2048-key.$priv_key_format --pwd secret123" \ + "--enabled-key-data key-value,key-name,rsa" + +execDSigTest $res_success \ + "" \ + "merlin-xmldsig-twenty-three/signature-external-b64-dsa" \ + "base64 sha1 dsa-sha1" \ + "dsa" \ + "--enabled-key-data key-value,key-name,dsa $url_map_xml_stylesheet_b64_2005" \ + "--enabled-key-data key-value,key-name,dsa $priv_key_option:TestKeyName-dsa-1024 $topfolder/keys/dsa/dsa-1024-key.$priv_key_format --pwd secret123 $url_map_xml_stylesheet_b64_2005" \ + "--enabled-key-data key-value,key-name,dsa $url_map_xml_stylesheet_b64_2005" + +execDSigTest $res_success \ + "" \ + "merlin-xmldsig-twenty-three/signature-external-dsa" \ + "sha1 dsa-sha1" \ + "dsa" \ + "$url_map_xml_stylesheet_2005 --enabled-key-data key-value,key-name,dsa" \ + "$url_map_xml_stylesheet_2005 --enabled-key-data key-value,key-name,dsa $priv_key_option:TestKeyName-dsa-1024 $topfolder/keys/dsa/dsa-1024-key.$priv_key_format --pwd secret123" \ + "$url_map_xml_stylesheet_2005 --enabled-key-data key-value,key-name,dsa" + +execDSigTest $res_success \ + "" \ + "merlin-xmldsig-twenty-three/signature-keyname" \ + "sha1 dsa-sha1" \ + "dsa x509" \ + "$url_map_xml_stylesheet_2005 --pubkey-cert-$cert_format:Lugh $topfolder/merlin-xmldsig-twenty-three/certs/lugh-cert.$cert_format" \ + "$url_map_xml_stylesheet_2005 $priv_key_option:TestKeyName-dsa-1024 $topfolder/keys/dsa/dsa-1024-key.$priv_key_format --pwd secret123" \ + "$url_map_xml_stylesheet_2005 $pub_key_option:TestKeyName-dsa-1024 $topfolder/keys/dsa/dsa-1024-pubkey$rsa_pub_key_suffix.$pub_key_format" + +execDSigTest $res_success \ + "" \ + "merlin-xmldsig-twenty-three/signature-x509-crt" \ + "sha1 dsa-sha1" \ + "dsa x509" \ + "--trusted-$cert_format $topfolder/merlin-xmldsig-twenty-three/certs/ca.$cert_format --verification-gmt-time 2005-01-01+10:00:00 $url_map_xml_stylesheet_2005" \ + "$priv_key_option:TestKeyName-dsa-1024 $topfolder/keys/dsa/dsa-1024-key.$priv_key_format --pwd secret123 $url_map_xml_stylesheet_2005"\ + "--trusted-$cert_format $topfolder/keys/cacert.$cert_format $url_map_xml_stylesheet_2005" + +extra_message="Negative test: CRL is present" +execDSigTest $res_fail \ + "" \ + "merlin-xmldsig-twenty-three/signature-x509-crt-crl" \ + "sha1 rsa-sha1" \ + "rsa x509" \ + "--trusted-$cert_format $topfolder/merlin-xmldsig-twenty-three/certs/ca.$cert_format $url_map_xml_stylesheet_2018" + + +execDSigTest $res_success \ + "" \ + "merlin-xmldsig-twenty-three/signature-x509-sn" \ + "sha1 dsa-sha1" \ + "dsa x509" \ + "--trusted-$cert_format $topfolder/merlin-xmldsig-twenty-three/certs/ca.$cert_format --untrusted-$cert_format $topfolder/merlin-xmldsig-twenty-three/certs/badb.$cert_format --verification-gmt-time 2005-01-01+10:00:00 $url_map_xml_stylesheet_2005" \ + "$priv_key_option:TestKeyName-dsa-1024 $topfolder/keys/dsa/dsa-1024-key.$priv_key_format --pwd secret123 $url_map_xml_stylesheet_2005"\ + "--trusted-$cert_format $topfolder/keys/cacert.$cert_format $url_map_xml_stylesheet_2005" + +execDSigTest $res_success \ + "" \ + "merlin-xmldsig-twenty-three/signature-x509-is" \ + "sha1 dsa-sha1" \ + "dsa x509" \ + "--trusted-$cert_format $topfolder/merlin-xmldsig-twenty-three/certs/ca.$cert_format --untrusted-$cert_format $topfolder/merlin-xmldsig-twenty-three/certs/macha.$cert_format --verification-gmt-time 2005-01-01+10:00:00 $url_map_xml_stylesheet_2005" \ + "$priv_key_option:TestKeyName-dsa-1024 $topfolder/keys/dsa/dsa-1024-key.$priv_key_format --pwd secret123 $url_map_xml_stylesheet_2005"\ + "--trusted-$cert_format $topfolder/keys/cacert.$cert_format $url_map_xml_stylesheet_2005" + +execDSigTest $res_success \ + "" \ + "merlin-xmldsig-twenty-three/signature-x509-ski" \ + "sha1 dsa-sha1" \ + "dsa x509" \ + "--trusted-$cert_format $topfolder/merlin-xmldsig-twenty-three/certs/ca.$cert_format --untrusted-$cert_format $topfolder/merlin-xmldsig-twenty-three/certs/nemain.$cert_format --verification-gmt-time 2005-01-01+10:00:00 $url_map_xml_stylesheet_2005" \ + "$priv_key_option:TestKeyName-dsa-1024 $topfolder/keys/dsa/dsa-1024-key.$priv_key_format --pwd secret123 $url_map_xml_stylesheet_2005"\ + "--trusted-$cert_format $topfolder/keys/cacert.$cert_format $url_map_xml_stylesheet_2005" + +execDSigTest $res_success \ + "" \ + "merlin-xmldsig-twenty-three/signature-retrievalmethod-rawx509crt" \ + "sha1 dsa-sha1" \ + "dsa x509" \ + "--trusted-$cert_format $topfolder/merlin-xmldsig-twenty-three/certs/ca.$cert_format --untrusted-$cert_format $topfolder/merlin-xmldsig-twenty-three/certs/nemain.$cert_format --verification-gmt-time 2005-01-01+10:00:00 $url_map_xml_stylesheet_2005" \ + "--lax-key-search $priv_key_option:TestKeyName-dsa-1024 $topfolder/keys/dsa/dsa-1024-key.$priv_key_format --pwd secret123 $url_map_xml_stylesheet_2005"\ + "--trusted-$cert_format $topfolder/keys/cacert.$cert_format --trusted-$cert_format $topfolder/keys/ca2cert.$cert_format $url_map_xml_stylesheet_2005" + +execDSigTest $res_success \ + "" \ + "merlin-xmldsig-twenty-three/signature" \ + "base64 xpath xslt enveloped-signature c14n-with-comments sha1 dsa-sha1" \ + "dsa x509" \ + "--trusted-$cert_format $topfolder/merlin-xmldsig-twenty-three/certs/merlin.$cert_format --verification-gmt-time 2005-01-01+10:00:00 $url_map_xml_stylesheet_2005 $url_map_xml_stylesheet_b64_2005" \ + "--lax-key-search $priv_key_option:TestKeyName-dsa-1024 $topfolder/keys/dsa/dsa-1024-key.$priv_key_format --pwd secret123 $url_map_xml_stylesheet_2005 $url_map_xml_stylesheet_b64_2005" \ + "--trusted-$cert_format $topfolder/keys/cacert.$cert_format --untrusted-$cert_format $topfolder/keys/ca2cert.$cert_format $url_map_xml_stylesheet_2005 $url_map_xml_stylesheet_b64_2005" + + +########################################################################## +# +# merlin-xmlenc-five +# +# While the main operation is signature (and this is why we have these +# tests here instead of testEnc.sh), these tests check the encryption +# key transport/wrapper algorightms +# +########################################################################## +execDSigTest $res_success \ + "" \ + "merlin-xmlenc-five/encsig-ripemd160-hmac-ripemd160-kw-tripledes" \ + "ripemd160 hmac-ripemd160 kw-tripledes" \ + "hmac des" \ + "--keys-file $topfolder/merlin-xmlenc-five/keys.xml $url_map_xml_stylesheet_2005" \ + "--session-key hmac-192 --keys-file $topfolder/merlin-xmlenc-five/keys.xml $url_map_xml_stylesheet_2005" \ + "--keys-file $topfolder/merlin-xmlenc-five/keys.xml $url_map_xml_stylesheet_2005" + +execDSigTest $res_success \ + "" \ + "merlin-xmlenc-five/encsig-sha256-hmac-sha256-kw-aes128" \ + "sha256 hmac-sha256 kw-aes128" \ + "hmac aes" \ + "--keys-file $topfolder/merlin-xmlenc-five/keys.xml $url_map_xml_stylesheet_2005" + +execDSigTest $res_success \ + "" \ + "merlin-xmlenc-five/encsig-sha384-hmac-sha384-kw-aes192" \ + "sha384 hmac-sha384 kw-aes192" \ + "hmac aes" \ + "--keys-file $topfolder/merlin-xmlenc-five/keys.xml $url_map_xml_stylesheet_2005" + +execDSigTest $res_success \ + "" \ + "merlin-xmlenc-five/encsig-sha512-hmac-sha512-kw-aes256" \ + "sha512 hmac-sha512 kw-aes256" \ + "hmac aes" \ + "--keys-file $topfolder/merlin-xmlenc-five/keys.xml $url_map_xml_stylesheet_2005" + +execDSigTest $res_success \ + "" \ + "merlin-xmlenc-five/encsig-hmac-sha256-rsa-1_5" \ + "sha1 hmac-sha256 rsa-1_5" \ + "hmac rsa" \ + "--lax-key-search $priv_key_option $topfolder/merlin-xmlenc-five/rsapriv.$priv_key_format --pwd secret --verification-gmt-time 2005-01-01+10:00:00 $url_map_xml_stylesheet_2005" + +if [ "z$xmlsec_feature_rsa_oaep_sha1" = "zyes" -a "z$xmlsec_feature_rsa_oaep_different_digest_and_mgf1" = "zyes" ] ; then + execDSigTest $res_success \ + "" \ + "merlin-xmlenc-five/encsig-hmac-sha256-rsa-oaep-mgf1p" \ + "sha1 hmac-sha256 rsa-oaep-mgf1p sha1 sha1" \ + "hmac rsa" \ + "--lax-key-search $priv_key_option $topfolder/merlin-xmlenc-five/rsapriv.$priv_key_format --pwd secret $url_map_xml_stylesheet_2005" +fi + + +########################################################################## +# +# merlin-exc-c14n-one +# +########################################################################## +execDSigTest $res_success \ + "" \ + "merlin-exc-c14n-one/exc-signature" \ + "exc-c14n sha1 dsa-sha1" \ + "dsa" \ + "--enabled-key-data key-value,key-name,dsa" \ + "--enabled-key-data key-value,key-name,dsa $priv_key_option:TestKeyName-dsa-1024 $topfolder/keys/dsa/dsa-1024-key.$priv_key_format --pwd secret123" \ + "--enabled-key-data key-value,key-name,dsa" + + +########################################################################## +# +# merlin-c14n-three +# +########################################################################## + +execDSigTest $res_success \ + "" \ + "merlin-c14n-three/signature" \ + "c14n c14n-with-comments exc-c14n exc-c14n-with-comments xpath sha1 dsa-sha1" \ + "dsa" \ + "--enabled-key-data key-value,dsa" + +########################################################################## +# +# merlin-xpath-filter2-three +# +########################################################################## + +execDSigTest $res_success \ + "" \ + "merlin-xpath-filter2-three/sign-xfdl" \ + "enveloped-signature xpath2 sha1 dsa-sha1" \ + "dsa" \ + "--enabled-key-data key-value,dsa" + +execDSigTest $res_success \ + "" \ + "merlin-xpath-filter2-three/sign-spec" \ + "enveloped-signature xpath2 sha1 dsa-sha1" \ + "dsa" \ + "--enabled-key-data key-value,dsa" +########################################################################## +# +# phaos-xmldsig-three +# +########################################################################## + +execDSigTest $res_success \ + "phaos-xmldsig-three" \ + "signature-big" \ + "base64 xslt xpath sha1 rsa-sha1" \ + "rsa x509" \ + "--lax-key-search --pubkey-cert-$cert_format certs/rsa-cert.$cert_format $url_map_rfc3161" + +execDSigTest $res_success \ + "phaos-xmldsig-three" \ + "signature-dsa-detached" \ + "sha1 dsa-sha1" \ + "dsa x509" \ + "--trusted-$cert_format certs/dsa-ca-cert.$cert_format --verification-gmt-time 2009-01-01+10:00:00 $url_map_rfc3161" + +execDSigTest $res_success \ + "phaos-xmldsig-three" \ + "signature-dsa-enveloped" \ + "enveloped-signature sha1 dsa-sha1" \ + "dsa x509" \ + "--trusted-$cert_format certs/dsa-ca-cert.$cert_format --verification-gmt-time 2009-01-01+10:00:00" + +execDSigTest $res_success \ + "phaos-xmldsig-three" \ + "signature-dsa-enveloping" \ + "sha1 dsa-sha1" \ + "dsa x509" \ + "--trusted-$cert_format certs/dsa-ca-cert.$cert_format --verification-gmt-time 2009-01-01+10:00:00" + +execDSigTest $res_success \ + "phaos-xmldsig-three" \ + "signature-dsa-manifest" \ + "sha1 dsa-sha1" \ + "dsa x509" \ + "--enabled-key-data key-value,dsa,x509 --trusted-$cert_format certs/dsa-ca-cert.$cert_format --verification-gmt-time 2009-01-01+10:00:00 $url_map_rfc3161" + +execDSigTest $res_success \ + "phaos-xmldsig-three" \ + "signature-hmac-md5-c14n-enveloping" \ + "md5 hmac-md5" \ + "hmac" \ + "--lax-key-search --hmackey certs/hmackey.bin" + +execDSigTest $res_success \ + "phaos-xmldsig-three" \ + "signature-hmac-sha1-40-c14n-comments-detached" \ + "c14n-with-comments sha1 hmac-sha1" \ + "hmac" \ + "--lax-key-search --hmackey certs/hmackey.bin $url_map_rfc3161" + +execDSigTest $res_success \ + "phaos-xmldsig-three" \ + "signature-hmac-sha1-40-exclusive-c14n-comments-detached" \ + "exc-c14n-with-comments sha1 hmac-sha1" \ + "hmac" \ + "--lax-key-search --hmackey certs/hmackey.bin $url_map_rfc3161" + +execDSigTest $res_success \ + "phaos-xmldsig-three" \ + "signature-hmac-sha1-exclusive-c14n-comments-detached" \ + "exc-c14n-with-comments sha1 hmac-sha1" \ + "hmac" \ + "--lax-key-search --hmackey certs/hmackey.bin $url_map_rfc3161" + +execDSigTest $res_success \ + "phaos-xmldsig-three" \ + "signature-hmac-sha1-exclusive-c14n-enveloped" \ + "enveloped-signature exc-c14n sha1 hmac-sha1" \ + "hmac" \ + "--lax-key-search --hmackey certs/hmackey.bin" + +execDSigTest $res_success \ + "phaos-xmldsig-three" \ + "signature-rsa-detached-b64-transform" \ + "base64 sha1 rsa-sha1" \ + "rsa x509" \ + "--enabled-key-data key-value,rsa,x509 --trusted-$cert_format certs/rsa-ca-cert.$cert_format --verification-gmt-time 2009-01-01+10:00:00 $url_map_rfc3161" + +execDSigTest $res_success \ + "phaos-xmldsig-three" \ + "signature-rsa-detached-xpath-transform" \ + "xpath sha1 rsa-sha1" \ + "rsa x509" \ + "--enabled-key-data key-value,rsa,x509 --trusted-$cert_format certs/rsa-ca-cert.$cert_format --verification-gmt-time 2009-01-01+10:00:00 $url_map_rfc3161" + + +execDSigTest $res_success \ + "phaos-xmldsig-three" \ + "signature-rsa-detached-xslt-transform" \ + "xslt sha1 rsa-sha1" \ + "rsa x509" \ + "--enabled-key-data key-value,rsa,x509 --trusted-$cert_format certs/rsa-ca-cert.$cert_format --verification-gmt-time 2009-01-01+10:00:00 $url_map_rfc3161" + +execDSigTest $res_success \ + "phaos-xmldsig-three" \ + "signature-rsa-manifest" \ + "sha1 rsa-sha1" \ + "rsa x509" \ + "--enabled-key-data key-value,rsa,x509 --trusted-$cert_format certs/rsa-ca-cert.$cert_format --verification-gmt-time 2009-01-01+10:00:00 $url_map_rfc3161" + +if [ "z$xmlsec_feature_md5_certs" = "zyes" ] ; then + execDSigTest $res_success \ + "phaos-xmldsig-three" \ + "signature-rsa-detached" \ + "sha1 rsa-sha1" \ + "rsa x509" \ + "--trusted-$cert_format certs/rsa-ca-cert.$cert_format --verification-gmt-time 2009-01-01+10:00:00 $url_map_rfc3161" + + execDSigTest $res_success \ + "phaos-xmldsig-three" \ + "signature-rsa-detached-xslt-transform-retrieval-method" \ + "xslt sha1 rsa-sha1" \ + "rsa x509" \ + "--trusted-$cert_format certs/rsa-ca-cert.$cert_format --verification-gmt-time 2009-01-01+10:00:00 $url_map_rfc3161" + + execDSigTest $res_success \ + "phaos-xmldsig-three" \ + "signature-rsa-enveloped" \ + "enveloped-signature sha1 rsa-sha1" \ + "rsa x509" \ + "--trusted-$cert_format certs/rsa-ca-cert.$cert_format --verification-gmt-time 2009-01-01+10:00:00" + + execDSigTest $res_success \ + "phaos-xmldsig-three" \ + "signature-rsa-enveloping" \ + "sha1 rsa-sha1" \ + "rsa x509" \ + "--trusted-$cert_format certs/rsa-ca-cert.$cert_format --verification-gmt-time 2009-01-01+10:00:00" + + execDSigTest $res_success \ + "phaos-xmldsig-three" \ + "signature-rsa-manifest-x509-data-cert-chain" \ + "sha1 rsa-sha1" \ + "rsa x509" \ + "--trusted-$cert_format certs/rsa-ca-cert.$cert_format --verification-gmt-time 2009-01-01+10:00:00 $url_map_rfc3161" + + execDSigTest $res_success \ + "phaos-xmldsig-three" \ + "signature-rsa-manifest-x509-data-cert" \ + "sha1 rsa-sha1" \ + "rsa x509" \ + "--trusted-$cert_format certs/rsa-ca-cert.$cert_format --verification-gmt-time 2009-01-01+10:00:00 $url_map_rfc3161" + + execDSigTest $res_success \ + "phaos-xmldsig-three" \ + "signature-rsa-manifest-x509-data-issuer-serial" \ + "sha1 rsa-sha1" \ + "rsa x509" \ + "--trusted-$cert_format certs/rsa-ca-cert.$cert_format --untrusted-$cert_format certs/rsa-cert.$cert_format --verification-gmt-time 2009-01-01+10:00:00 $url_map_rfc3161" + + execDSigTest $res_success \ + "phaos-xmldsig-three" \ + "signature-rsa-manifest-x509-data-ski" \ + "sha1 rsa-sha1" \ + "rsa x509" \ + "--trusted-$cert_format certs/rsa-ca-cert.$cert_format --untrusted-$cert_format certs/rsa-cert.$cert_format --verification-gmt-time 2009-01-01+10:00:00 $url_map_rfc3161" + + execDSigTest $res_success \ + "phaos-xmldsig-three" \ + "signature-rsa-manifest-x509-data-subject-name" \ + "sha1 rsa-sha1" \ + "rsa x509" \ + "--trusted-$cert_format certs/rsa-ca-cert.$cert_format --untrusted-$cert_format certs/rsa-cert.$cert_format --verification-gmt-time 2009-01-01+10:00:00 $url_map_rfc3161" + + execDSigTest $res_success \ + "phaos-xmldsig-three" \ + "signature-rsa-xpath-transform-enveloped" \ + "enveloped-signature xpath sha1 rsa-sha1" \ + "rsa x509" \ + "--enabled-key-data key-value,rsa,x509 --trusted-$cert_format certs/rsa-ca-cert.$cert_format --verification-gmt-time 2009-01-01+10:00:00" +fi + +extra_message="Negative test: bad retrieval method" +execDSigTest $res_fail \ + "phaos-xmldsig-three" \ + "signature-rsa-detached-xslt-transform-bad-retrieval-method" \ + "xslt sha1 rsa-sha1" \ + "rsa x509" \ + "--enabled-key-data key-value,rsa,x509 --trusted-$cert_format certs/rsa-ca-cert.$cert_format $url_map_rfc3161" + +extra_message="Negative test: bad digest" +execDSigTest $res_fail \ + "phaos-xmldsig-three" \ + "signature-rsa-enveloped-bad-digest-val" \ + "enveloped-signature sha1 rsa-sha1" \ + "rsa x509" \ + "--trusted-$cert_format certs/rsa-ca-cert.$cert_format" + +extra_message="Negative test: bad sig" +execDSigTest $res_fail \ + "phaos-xmldsig-three" \ + "signature-rsa-enveloped-bad-sig" \ + "enveloped-signature sha1 rsa-sha1" \ + "rsa x509" \ + "--trusted-$cert_format certs/rsa-ca-cert.$cert_format" + +extra_message="Negative test: CRL present" +execDSigTest $res_fail \ + "phaos-xmldsig-three" \ + "signature-rsa-manifest-x509-data-crl" \ + "sha1 rsa-sha1" \ + "rsa x509" \ + "--trusted-$cert_format certs/rsa-ca-cert.$cert_format" + + +########################################################################## +########################################################################## +########################################################################## +echo "--------- These tests CAN FAIL (extra OS config required) ----------" +execDSigTest $res_success \ + "" \ + "aleksey-xmldsig-01/enveloped-gost2001" \ + "enveloped-signature gostr34102001-gostr3411" \ + "gost2001 x509" \ + "--trusted-$cert_format $topfolder/keys/cacert.$cert_format --enabled-key-data x509" \ + "$priv_key_option $topfolder/keys/gost/gost-2001-key$priv_key_suffix.$priv_key_format --pwd secret123" \ + "--trusted-$cert_format $topfolder/keys/cacert.$cert_format --enabled-key-data x509" + +execDSigTest $res_success \ + "" \ + "aleksey-xmldsig-01/enveloped-gost2012-256" \ + "enveloped-signature gostr34112012-256 gostr34102012-gostr34112012-256" \ + "gostr34102012-256 x509" \ + "--trusted-$cert_format $topfolder/keys/cacert.$cert_format --enabled-key-data x509" \ + "$priv_key_option $topfolder/keys/gost/gost-2012-256-key$priv_key_suffix.$priv_key_format --pwd secret123" \ + "--trusted-$cert_format $topfolder/keys/cacert.$cert_format --enabled-key-data x509" + +execDSigTest $res_success \ + "" \ + "aleksey-xmldsig-01/enveloped-gost2012-512" \ + "enveloped-signature gostr34112012-512 gostr34102012-gostr34112012-512" \ + "gostr34102012-512 x509" \ + "--trusted-$cert_format $topfolder/keys/cacert.$cert_format --enabled-key-data x509" \ + "$priv_key_option $topfolder/keys/gost/gost-2012-512-key$priv_key_suffix.$priv_key_format --pwd secret123" \ + "--trusted-$cert_format $topfolder/keys/cacert.$cert_format --enabled-key-data x509" + + +########################################################################## +########################################################################## +########################################################################## +echo "--- testDSig finished" >> $logfile +echo "--- testDSig finished" +if [ -z "$XMLSEC_TEST_REPRODUCIBLE" ]; then + echo "--- detailed log is written to $logfile" +fi diff --git a/tools/xmlsec1/tests/fixtures/upstream/testEnc.sh b/tools/xmlsec1/tests/fixtures/upstream/testEnc.sh new file mode 100755 index 0000000..ad750dd --- /dev/null +++ b/tools/xmlsec1/tests/fixtures/upstream/testEnc.sh @@ -0,0 +1,2027 @@ +#!/bin/sh +# +# This script needs to be called from testrun.sh script +# + + +# ensure this script is called from testrun.sh +if [ -z "$xmlsec_app" -o -z "$xmlsec_params" ]; then + echo "This script needs to be called from testrun.sh script" + exit 1 +fi + +########################################################################## +########################################################################## +########################################################################## +if [ -z "$XMLSEC_TEST_REPRODUCIBLE" ]; then + echo "--- testEnc started for xmlsec-$crypto library ($timestamp)" +fi +echo "--- LD_LIBRARY_PATH=$LD_LIBRARY_PATH" +echo "--- LTDL_LIBRARY_PATH=$LTDL_LIBRARY_PATH" +if [ -z "$XMLSEC_TEST_REPRODUCIBLE" ]; then + echo "--- log file is $logfile" +fi +echo "--- testEnc started for xmlsec-$crypto library ($timestamp)" >> $logfile +echo "--- LD_LIBRARY_PATH=$LD_LIBRARY_PATH" >> $logfile +echo "--- LTDL_LIBRARY_PATH=$LTDL_LIBRARY_PATH" >> $logfile + + +########################################################################## +########################################################################## +########################################################################## +# +# Enc test function +# +execEncTest() { + execEncTestWithCryptoConfig "$1" "$2" "$3" "$4" "$5" "$6" "$7" "$8" "$9" "" +} + +execEncTestWithCryptoConfig() { + expected_res="$1" + folder="$2" + filename="$3" + req_transforms="$4" + req_key_data="$5" + params1="$6" + params2="$7" + params3="$8" + outputTransform="$9" + crypto_config="${10}" + failures=0 + + if [ -n "$XMLSEC_TEST_NAME" -a "$XMLSEC_TEST_NAME" != "$filename" ]; then + return + fi + + # prepare + setupTest + + # check params + if [ "z$expected_res" != "z$res_success" -a "z$expected_res" != "z$res_fail" ] ; then + echo " Bad parameter: expected_res=$expected_res" + tearDownTest + return + fi + if [ "z$crypto_config" = "z" ] ; then + crypto_config="$default_crypto_config" + fi + + # starting test + if [ -n "$folder" ] ; then + cd $topfolder/$folder + full_file=$filename + echo "Test: $folder/$filename $extra_message" + echo "Test: $folder/$filename in folder " `pwd` " $extra_message -- $expected_res" > $curlogfile + else + full_file=$topfolder/$filename + echo "Test: $filename $extra_message" + echo "Test: $folder/$filename $extra_message -- $expected_res" > $curlogfile + fi + extra_message="" + + # check transforms + if [ -n "$req_transforms" ] ; then + printf " Checking required transforms " + echo "$extra_vars $xmlsec_app check-transforms $xmlsec_params --crypto-config $crypto_config $req_transforms" >> $curlogfile + $xmlsec_app check-transforms $xmlsec_params --crypto-config $crypto_config $req_transforms >> $curlogfile 2>> $curlogfile + printCheckStatus $? + res=$? + if [ $res -ne 0 ]; then + cat $curlogfile >> $logfile + tearDownTest + return + fi + fi + + # check key data + if [ -n "$req_key_data" ] ; then + printf " Checking required key data " + echo "$extra_vars $xmlsec_app check-key-data $xmlsec_params --crypto-config $crypto_config $req_key_data" >> $curlogfile + $xmlsec_app check-key-data $xmlsec_params --crypto-config $crypto_config $req_key_data >> $curlogfile 2>> $curlogfile + printCheckStatus $? + res=$? + if [ $res -ne 0 ]; then + cat $curlogfile >> $logfile + tearDownTest + return + fi + fi + + # run tests + xml_verification_failed="no" + if [ -n "$params1" ] ; then + rm -f $tmpfile + printf " Decrypt existing document " + echo "$extra_vars $VALGRIND $xmlsec_app decrypt $xmlsec_params --crypto-config $crypto_config $params1 $full_file.xml" >> $curlogfile + $VALGRIND $xmlsec_app decrypt $xmlsec_params --crypto-config $crypto_config $params1 --output $tmpfile $full_file.xml >> $curlogfile 2>> $curlogfile + res=$? + echo "=== TEST RESULT: $res; expected: $expected_res" >> $curlogfile + if [ $res -eq 0 -a "$expected_res" = "$res_success" ]; then + if [ "z$outputTransform" != "z" ] ; then + cat $tmpfile | $outputTransform > $tmpfile.2 + mv $tmpfile.2 $tmpfile + fi + diff $diff_param $full_file.data $tmpfile >> $curlogfile 2>> $curlogfile + printRes $expected_res $? + else + printRes $expected_res $res + fi + if [ $? -ne 0 ]; then + xml_verification_failed="yes" + failures=`expr $failures + 1` + fi + fi + + if [ -n "$params2" -a -z "$PERF_TEST" ] ; then + rm -f $tmpfile + printf " Encrypt document " + echo "$extra_vars $VALGRIND $xmlsec_app encrypt $xmlsec_params --crypto-config $crypto_config $params2 --output $tmpfile $full_file.tmpl" >> $curlogfile + $VALGRIND $xmlsec_app encrypt $xmlsec_params --crypto-config $crypto_config $params2 --output $tmpfile $full_file.tmpl >> $curlogfile 2>> $curlogfile + printRes $res_success $? + if [ $? -ne 0 ]; then + failures=`expr $failures + 1` + fi + fi + + # update existing decryption failed + if [ "z$XMLSEC_TEST_UPDATE_XML_ON_FAILURE" = "zyes" -a "z$xml_verification_failed" = "zyes" ] ; then + printf " Update existing enc document " + echo "cp $tmpfile $full_file.xml" >> $curlogfile 2>> $curlogfile + cp $tmpfile $full_file.xml + printRes $res_success $? + if [ $? -ne 0 ]; then + failures=`expr $failures + 1` + fi + fi + + if [ -n "$params3" -a -z "$PERF_TEST" ] ; then + rm -f $tmpfile.2 + printf " Decrypt new document " + echo "$extra_vars $VALGRIND $xmlsec_app decrypt $xmlsec_params --crypto-config $crypto_config $params3 --output $tmpfile.2 $tmpfile" >> $curlogfile + $VALGRIND $xmlsec_app decrypt $xmlsec_params --crypto-config $crypto_config $params3 --output $tmpfile.2 $tmpfile >> $curlogfile 2>> $curlogfile + res=$? + if [ $res -eq 0 ]; then + if [ "z$outputTransform" != "z" ] ; then + cat $tmpfile.2 | $outputTransform > $tmpfile + mv $tmpfile $tmpfile.2 + fi + diff $diff_param $full_file.data $tmpfile.2 >> $curlogfile 2>> $curlogfile + printRes $res_success $? + else + printRes $res_success $res + fi + if [ $? -ne 0 ]; then + failures=`expr $failures + 1` + fi + fi + + # save logs + cat $curlogfile >> $logfile + if [ $failures -ne 0 ] ; then + cat $curlogfile >> $failedlogfile + fi + + # cleanup + tearDownTest +} + + +execEncPrintXmlDebugTest() { + folder="$1" + filename="$2" + req_transforms="$3" + req_key_data="$4" + params1="$5" + outputTransform="$6" + crypto_config="$7" + failures=0 + test_name="$filename (with --print-xml-debug)" + + if [ -n "$XMLSEC_TEST_NAME" -a "$XMLSEC_TEST_NAME" != "$test_name" ]; then + return + fi + + # prepare + setupTest + + if [ "z$crypto_config" = "z" ] ; then + crypto_config="$default_crypto_config" + fi + + # starting test + if [ -n "$folder" ] ; then + cd $topfolder/$folder + full_file=$filename + echo "Test: $folder/$test_name $extra_message" + echo "Test: $folder/$test_name in folder " `pwd` " $extra_message -- $res_success" > $curlogfile + else + full_file=$topfolder/$filename + echo "Test: $test_name $extra_message" + echo "Test: $test_name $extra_message -- $res_success" > $curlogfile + fi + extra_message="" + + # check transforms + if [ -n "$req_transforms" ] ; then + printf " Checking required transforms " + echo "$extra_vars $xmlsec_app check-transforms $xmlsec_params --crypto-config $crypto_config $req_transforms" >> $curlogfile + $xmlsec_app check-transforms $xmlsec_params --crypto-config $crypto_config $req_transforms >> $curlogfile 2>> $curlogfile + res=$? + + printCheckStatus $? + if [ $res -ne 0 ]; then + cat $curlogfile >> $logfile + tearDownTest + return + fi + fi + + # check key data + if [ -n "$req_key_data" ] ; then + printf " Checking required key data " + echo "$extra_vars $xmlsec_app check-key-data $xmlsec_params --crypto-config $crypto_config $req_key_data" >> $curlogfile + $xmlsec_app check-key-data $xmlsec_params --crypto-config $crypto_config $req_key_data >> $curlogfile 2>> $curlogfile + res=$? + printCheckStatus $? + if [ $res -ne 0 ]; then + cat $curlogfile >> $logfile + tearDownTest + return + fi + fi + + # run test + rm -f $tmpfile $tmpfile.3 + if [ -n "$params1" ] ; then + printf " Decrypt with --print-xml-debug " + echo "$extra_vars $VALGRIND $xmlsec_app decrypt $xmlsec_params --print-xml-debug --crypto-config $crypto_config $params1 --output $tmpfile $full_file.xml > $tmpfile.3" >> $curlogfile + $VALGRIND $xmlsec_app decrypt $xmlsec_params --print-xml-debug --crypto-config $crypto_config $params1 --output $tmpfile $full_file.xml > $tmpfile.3 2>> $curlogfile + res=$? + + printCheckStatus $? + if [ $? -ne 0 ]; then + failures=`expr $failures + 1` + cat $curlogfile >> $logfile + cat $curlogfile >> $failedlogfile + tearDownTest + return + fi + fi + + # check xmllint availability for --print-xml-debug test + if command -v xmllint >/dev/null 2>&1 ; then + printf " Verify --print-xml-debug output with xmllint " + echo "xmllint --noout $tmpfile.3" >> $curlogfile + xmllint --noout $tmpfile.3 >> $curlogfile 2>> $curlogfile + + res=$? + + printCheckStatus $? + if [ $res -ne 0 ]; then + failures=`expr $failures + 1` + cat $curlogfile >> $logfile + cat $curlogfile >> $failedlogfile + tearDownTest + return + fi + else + printf " Checking for xmllint availability " + echo "Skipping test: xmllint is not available" >> $curlogfile + printCheckStatus 1 + cat $curlogfile >> $logfile + cat $curlogfile >> $failedlogfile + tearDownTest + return + fi + + # save logs + cat $curlogfile >> $logfile + + # cleanup + tearDownTest +} + +########################################################################## +########################################################################## +########################################################################## +echo "--------- Positive Testing ----------" + + +########################################################################## +# +# xmlenc11-interop-2012: +# https://www.w3.org/TR/2012/NOTE-xmlenc-core1-interop-20121113/ +# +########################################################################## + +# AES GCM +execEncTest $res_success \ + "" \ + "xmlenc11-interop-2012/xenc11-example-AES128-GCM" \ + "aes128-gcm" \ + "" \ + "--lax-key-search --aeskey $topfolder/xmlenc11-interop-2012/xenc11-example-AES128-GCM.key" \ + "--aeskey:TestKeyName_GCM $topfolder/xmlenc11-interop-2012/xenc11-example-AES128-GCM.key --binary-data $topfolder/xmlenc11-interop-2012/xenc11-example-AES128-GCM.data" \ + "--aeskey:TestKeyName_GCM $topfolder/xmlenc11-interop-2012/xenc11-example-AES128-GCM.key" + +if [ "z$xmlsec_feature_rsa_oaep_sha1" = "zyes" -a "z$xmlsec_feature_rsa_oaep_different_digest_and_mgf1" = "zyes" ] ; then + execEncTest $res_success \ + "" \ + "xmlenc11-interop-2012/cipherText__RSA-2048__aes128-gcm__rsa-oaep-mgf1p" \ + "aes128-gcm rsa-oaep-mgf1p sha256 sha1" \ + "" \ + "$priv_key_option:TestRsa2048Key $topfolder/xmlenc11-interop-2012/RSA-2048_SHA256WithRSA.$priv_key_format --pwd passwd" \ + "$priv_key_option:TestRsa2048Key $topfolder/xmlenc11-interop-2012/RSA-2048_SHA256WithRSA.$priv_key_format --pwd passwd --session-key aes-128 --xml-data $topfolder/xmlenc11-interop-2012/cipherText__RSA-2048__aes128-gcm__rsa-oaep-mgf1p.data" \ + "$priv_key_option:TestRsa2048Key $topfolder/xmlenc11-interop-2012/RSA-2048_SHA256WithRSA.$priv_key_format --pwd passwd" + + execEncTest $res_success \ + "" \ + "xmlenc11-interop-2012/cipherText__RSA-3072__aes192-gcm__rsa-oaep-mgf1p__Sha256" \ + "aes192-gcm rsa-oaep-mgf1p sha256 sha1" \ + "" \ + "$priv_key_option:TestRsa3072Key $topfolder/xmlenc11-interop-2012/RSA-3072_SHA256WithRSA.$priv_key_format --pwd passwd" \ + "$priv_key_option:TestRsa3072Key $topfolder/xmlenc11-interop-2012/RSA-3072_SHA256WithRSA.$priv_key_format --pwd passwd --session-key aes-192 --xml-data $topfolder/xmlenc11-interop-2012/cipherText__RSA-3072__aes192-gcm__rsa-oaep-mgf1p__Sha256.data" \ + "$priv_key_option:TestRsa3072Key $topfolder/xmlenc11-interop-2012/RSA-3072_SHA256WithRSA.$priv_key_format --pwd passwd" + + execEncTest $res_success \ + "" \ + "xmlenc11-interop-2012/cipherText__RSA-3072__aes256-gcm__rsa-oaep__Sha384-MGF_Sha1" \ + "aes256-gcm rsa-oaep-mgf1p sha384 sha1" \ + "" \ + "$priv_key_option:TestRsa3072Key $topfolder/xmlenc11-interop-2012/RSA-3072_SHA256WithRSA.$priv_key_format --pwd passwd" \ + "$priv_key_option:TestRsa3072Key $topfolder/xmlenc11-interop-2012/RSA-3072_SHA256WithRSA.$priv_key_format --pwd passwd --session-key aes-256 --xml-data $topfolder/xmlenc11-interop-2012/cipherText__RSA-3072__aes256-gcm__rsa-oaep__Sha384-MGF_Sha1.data" \ + "$priv_key_option:TestRsa3072Key $topfolder/xmlenc11-interop-2012/RSA-3072_SHA256WithRSA.$priv_key_format --pwd passwd" + + execEncTest $res_success \ + "" \ + "xmlenc11-interop-2012/cipherText__RSA-4096__aes256-gcm__rsa-oaep__Sha512-MGF_Sha1_PSource" \ + "aes256-gcm rsa-oaep-mgf1p sha512 sha1" \ + "" \ + "$priv_key_option:TestRsa3072Key $topfolder/xmlenc11-interop-2012/RSA-4096_SHA256WithRSA.$priv_key_format --pwd passwd" \ + "$priv_key_option:TestRsa3072Key $topfolder/xmlenc11-interop-2012/RSA-4096_SHA256WithRSA.$priv_key_format --pwd passwd --session-key aes-256 --xml-data $topfolder/xmlenc11-interop-2012/cipherText__RSA-4096__aes256-gcm__rsa-oaep__Sha512-MGF_Sha1_PSource.data" \ + "$priv_key_option:TestRsa3072Key $topfolder/xmlenc11-interop-2012/RSA-4096_SHA256WithRSA.$priv_key_format --pwd passwd" +fi + +# ConcatCDF +execEncTest $res_success \ + "" \ + "xmlenc11-interop-2012/dkey-example-ConcatKDF-crypto" \ + "aes256-cbc concatkdf sha256" \ + "derived-key" \ + "--concatkdf-key:Secret1 $topfolder/xmlenc11-interop-2012/dkey-concatkdf.bin" \ + "--concatkdf-key:dkey $topfolder/xmlenc11-interop-2012/dkey-concatkdf.bin --binary $topfolder/xmlenc11-interop-2012/dkey-example-ConcatKDF-crypto.data" \ + "--concatkdf-key:dkey $topfolder/xmlenc11-interop-2012/dkey-concatkdf.bin" + +execEncTest $res_success \ + "" \ + "xmlenc11-interop-2012/dkey3-example-ConcatKDF-crypto" \ + "aes256-cbc concatkdf sha256" \ + "derived-key" \ + "--concatkdf-key $topfolder/xmlenc11-interop-2012/dkey3-concatkdf.bin" \ + "--concatkdf-key:dkey3 $topfolder/xmlenc11-interop-2012/dkey3-concatkdf.bin --binary $topfolder/xmlenc11-interop-2012/dkey3-example-ConcatKDF-crypto.data" \ + "--concatkdf-key:dkey3 $topfolder/xmlenc11-interop-2012/dkey3-concatkdf.bin" + +# PBKDF2 +execEncTest $res_success \ + "" \ + "xmlenc11-interop-2012/dkey-example-PBKDF2-crypto" \ + "aes256-cbc pbkdf2 sha256" \ + "derived-key" \ + "--pbkdf2-key:dkey-pbkdf2 $topfolder/xmlenc11-interop-2012/dkey-pbkdf2.bin" \ + "--pbkdf2-key:dkey-pbkdf2 $topfolder/xmlenc11-interop-2012/dkey-pbkdf2.bin --binary $topfolder/xmlenc11-interop-2012/dkey-example-PBKDF2-crypto.data" \ + "--pbkdf2-key:dkey-pbkdf2 $topfolder/xmlenc11-interop-2012/dkey-pbkdf2.bin" + +execEncTest $res_success \ + "" \ + "xmlenc11-interop-2012/dkey3-example-PBKDF2-crypto" \ + "aes256-cbc pbkdf2 sha256" \ + "derived-key" \ + "--pbkdf2-key:dkey3-pbkdf2 $topfolder/xmlenc11-interop-2012/dkey3-pbkdf2.bin" \ + "--pbkdf2-key:dkey3-pbkdf2 $topfolder/xmlenc11-interop-2012/dkey3-pbkdf2.bin --binary $topfolder/xmlenc11-interop-2012/dkey3-example-PBKDF2-crypto.data" \ + "--pbkdf2-key:dkey3-pbkdf2 $topfolder/xmlenc11-interop-2012/dkey3-pbkdf2.bin" + +# PBKDF2 + HMAC-SHA1 + AES-256-GCM +execEncTest $res_success \ + "" \ + "aleksey-xmlenc-01/enc_pbkdf2_hmac_sha1_aes256gcm" \ + "aes256-gcm pbkdf2 hmac-sha1" \ + "derived-key" \ + "--pbkdf2-key:pbkdf2-ikm $topfolder/aleksey-xmlenc-01/pbkdf2-ikm.bin" \ + "--pbkdf2-key:pbkdf2-ikm $topfolder/aleksey-xmlenc-01/pbkdf2-ikm.bin --binary $topfolder/aleksey-xmlenc-01/enc_pbkdf2_hmac_sha1_aes256gcm.data" \ + "--pbkdf2-key:pbkdf2-ikm $topfolder/aleksey-xmlenc-01/pbkdf2-ikm.bin" + +# PBKDF2 + HMAC-SHA224 + AES-256-GCM +execEncTest $res_success \ + "" \ + "aleksey-xmlenc-01/enc_pbkdf2_hmac_sha224_aes256gcm" \ + "aes256-gcm pbkdf2 hmac-sha224" \ + "derived-key" \ + "--pbkdf2-key:pbkdf2-ikm $topfolder/aleksey-xmlenc-01/pbkdf2-ikm.bin" \ + "--pbkdf2-key:pbkdf2-ikm $topfolder/aleksey-xmlenc-01/pbkdf2-ikm.bin --binary $topfolder/aleksey-xmlenc-01/enc_pbkdf2_hmac_sha224_aes256gcm.data" \ + "--pbkdf2-key:pbkdf2-ikm $topfolder/aleksey-xmlenc-01/pbkdf2-ikm.bin" + +# PBKDF2 + HMAC-SHA256 + AES-256-GCM +execEncTest $res_success \ + "" \ + "aleksey-xmlenc-01/enc_pbkdf2_hmac_sha256_aes256gcm" \ + "aes256-gcm pbkdf2 hmac-sha256" \ + "derived-key" \ + "--pbkdf2-key:pbkdf2-ikm $topfolder/aleksey-xmlenc-01/pbkdf2-ikm.bin" \ + "--pbkdf2-key:pbkdf2-ikm $topfolder/aleksey-xmlenc-01/pbkdf2-ikm.bin --binary $topfolder/aleksey-xmlenc-01/enc_pbkdf2_hmac_sha256_aes256gcm.data" \ + "--pbkdf2-key:pbkdf2-ikm $topfolder/aleksey-xmlenc-01/pbkdf2-ikm.bin" + +# PBKDF2 + HMAC-SHA384 + AES-256-GCM +execEncTest $res_success \ + "" \ + "aleksey-xmlenc-01/enc_pbkdf2_hmac_sha384_aes256gcm" \ + "aes256-gcm pbkdf2 hmac-sha384" \ + "derived-key" \ + "--pbkdf2-key:pbkdf2-ikm $topfolder/aleksey-xmlenc-01/pbkdf2-ikm.bin" \ + "--pbkdf2-key:pbkdf2-ikm $topfolder/aleksey-xmlenc-01/pbkdf2-ikm.bin --binary $topfolder/aleksey-xmlenc-01/enc_pbkdf2_hmac_sha384_aes256gcm.data" \ + "--pbkdf2-key:pbkdf2-ikm $topfolder/aleksey-xmlenc-01/pbkdf2-ikm.bin" + +# PBKDF2 + HMAC-SHA512 + AES-256-GCM +execEncTest $res_success \ + "" \ + "aleksey-xmlenc-01/enc_pbkdf2_hmac_sha512_aes256gcm" \ + "aes256-gcm pbkdf2 hmac-sha512" \ + "derived-key" \ + "--pbkdf2-key:pbkdf2-ikm $topfolder/aleksey-xmlenc-01/pbkdf2-ikm.bin" \ + "--pbkdf2-key:pbkdf2-ikm $topfolder/aleksey-xmlenc-01/pbkdf2-ikm.bin --binary $topfolder/aleksey-xmlenc-01/enc_pbkdf2_hmac_sha512_aes256gcm.data" \ + "--pbkdf2-key:pbkdf2-ikm $topfolder/aleksey-xmlenc-01/pbkdf2-ikm.bin" + +# HKDF + HMAC-SHA1 + AES-256-GCM +execEncTest $res_success \ + "" \ + "aleksey-xmlenc-01/enc_hkdf_hmac_sha1_aes256gcm" \ + "aes256-gcm hkdf hmac-sha1" \ + "derived-key" \ + "--hkdf-key:hkdf-ikm $topfolder/aleksey-xmlenc-01/hkdf-ikm.bin" \ + "--hkdf-key:hkdf-ikm $topfolder/aleksey-xmlenc-01/hkdf-ikm.bin --binary $topfolder/aleksey-xmlenc-01/enc_hkdf_hmac_sha1_aes256gcm.data" \ + "--hkdf-key:hkdf-ikm $topfolder/aleksey-xmlenc-01/hkdf-ikm.bin" + +# HKDF + HMAC-SHA224 + AES-256-GCM +execEncTest $res_success \ + "" \ + "aleksey-xmlenc-01/enc_hkdf_hmac_sha224_aes256gcm" \ + "aes256-gcm hkdf hmac-sha224" \ + "derived-key" \ + "--hkdf-key:hkdf-ikm $topfolder/aleksey-xmlenc-01/hkdf-ikm.bin" \ + "--hkdf-key:hkdf-ikm $topfolder/aleksey-xmlenc-01/hkdf-ikm.bin --binary $topfolder/aleksey-xmlenc-01/enc_hkdf_hmac_sha224_aes256gcm.data" \ + "--hkdf-key:hkdf-ikm $topfolder/aleksey-xmlenc-01/hkdf-ikm.bin" + +# HKDF + HMAC-SHA256 + AES-256-GCM +execEncTest $res_success \ + "" \ + "aleksey-xmlenc-01/enc_hkdf_hmac_sha256_aes256gcm" \ + "aes256-gcm hkdf hmac-sha256" \ + "derived-key" \ + "--hkdf-key:hkdf-ikm $topfolder/aleksey-xmlenc-01/hkdf-ikm.bin" \ + "--hkdf-key:hkdf-ikm $topfolder/aleksey-xmlenc-01/hkdf-ikm.bin --binary $topfolder/aleksey-xmlenc-01/enc_hkdf_hmac_sha256_aes256gcm.data" \ + "--hkdf-key:hkdf-ikm $topfolder/aleksey-xmlenc-01/hkdf-ikm.bin" + +# HKDF + HMAC-SHA384 + AES-256-GCM +execEncTest $res_success \ + "" \ + "aleksey-xmlenc-01/enc_hkdf_hmac_sha384_aes256gcm" \ + "aes256-gcm hkdf hmac-sha384" \ + "derived-key" \ + "--hkdf-key:hkdf-ikm $topfolder/aleksey-xmlenc-01/hkdf-ikm.bin" \ + "--hkdf-key:hkdf-ikm $topfolder/aleksey-xmlenc-01/hkdf-ikm.bin --binary $topfolder/aleksey-xmlenc-01/enc_hkdf_hmac_sha384_aes256gcm.data" \ + "--hkdf-key:hkdf-ikm $topfolder/aleksey-xmlenc-01/hkdf-ikm.bin" + +# HKDF + HMAC-SHA512 + AES-256-GCM +execEncTest $res_success \ + "" \ + "aleksey-xmlenc-01/enc_hkdf_hmac_sha512_aes256gcm" \ + "aes256-gcm hkdf hmac-sha512" \ + "derived-key" \ + "--hkdf-key:hkdf-ikm $topfolder/aleksey-xmlenc-01/hkdf-ikm.bin" \ + "--hkdf-key:hkdf-ikm $topfolder/aleksey-xmlenc-01/hkdf-ikm.bin --binary $topfolder/aleksey-xmlenc-01/enc_hkdf_hmac_sha512_aes256gcm.data" \ + "--hkdf-key:hkdf-ikm $topfolder/aleksey-xmlenc-01/hkdf-ikm.bin" + +# HKDF + PRF only (no Salt, no Info, no KeyLength) + AES-256-GCM +execEncTest $res_success \ + "" \ + "aleksey-xmlenc-01/enc_hkdf_prf_only_aes256gcm" \ + "aes256-gcm hkdf hmac-sha256" \ + "derived-key" \ + "--hkdf-key:hkdf-ikm $topfolder/aleksey-xmlenc-01/hkdf-ikm.bin" \ + "--hkdf-key:hkdf-ikm $topfolder/aleksey-xmlenc-01/hkdf-ikm.bin --binary $topfolder/aleksey-xmlenc-01/enc_hkdf_prf_only_aes256gcm.data" \ + "--hkdf-key:hkdf-ikm $topfolder/aleksey-xmlenc-01/hkdf-ikm.bin" + + +# ECDH-ES +execEncTest $res_success \ + "" \ + "xmlenc11-interop-2012/cipherText__EC-P256__aes128-gcm__kw-aes128__ECDH-ES__ConcatKDF" \ + "aes128-gcm kw-aes128 concatkdf ecdh-es sha256" \ + "agreement-method enc-key ec" \ + "--enabled-key-data agreement-method,enc-key,key-value,key-name,ec $ec_interop_priv_key_option:EC-P256 $topfolder/xmlenc11-interop-2012/EC-P256_SHA256WithECDSA-orig.$ec_interop_priv_key_format --pwd passwd" \ + "--enabled-key-data agreement-method,enc-key,key-value,key-name,ec --session-key aes-128 $ec_interop_priv_key_option:EC-P256 $topfolder/xmlenc11-interop-2012/EC-P256_SHA256WithECDSA.$ec_interop_priv_key_format $pub_key_option:TestKeyName-ec-prime256v1 $topfolder/keys/ec/ec-prime256v1-pubkey.$pub_key_format --pwd secret123 --xml-data $topfolder/xmlenc11-interop-2012/cipherText__EC-P256__aes128-gcm__kw-aes128__ECDH-ES__ConcatKDF.data" \ + "--enabled-key-data agreement-method,enc-key,key-value,key-name,ec $priv_key_option:TestKeyName-ec-prime256v1 $topfolder/keys/ec/ec-prime256v1-key.$priv_key_format --pwd secret123" + +execEncTest $res_success \ + "" \ + "xmlenc11-interop-2012/cipherText__EC-P384__aes192-gcm__kw-aes192__ECDH-ES__ConcatKDF" \ + "aes192-gcm kw-aes192 concatkdf ecdh-es sha256" \ + "agreement-method enc-key ec" \ + "--enabled-key-data agreement-method,enc-key,key-value,key-name,ec $ec_interop_priv_key_option:EC-P384 $topfolder/xmlenc11-interop-2012/EC-P384_SHA256WithECDSA-orig.$ec_interop_priv_key_format --pwd passwd" \ + "--enabled-key-data agreement-method,enc-key,key-value,key-name,ec --session-key aes-192 $ec_interop_priv_key_option:EC-P384 $topfolder/xmlenc11-interop-2012/EC-P384_SHA256WithECDSA.$ec_interop_priv_key_format $pub_key_option:TestKeyName-ec-prime384v1 $topfolder/keys/ec/ec-prime384v1-pubkey.$pub_key_format --pwd secret123 --xml-data $topfolder/xmlenc11-interop-2012/cipherText__EC-P384__aes192-gcm__kw-aes192__ECDH-ES__ConcatKDF.data" \ + "--enabled-key-data agreement-method,enc-key,key-value,key-name,ec $priv_key_option:TestKeyName-ec-prime384v1 $topfolder/keys/ec/ec-prime384v1-key.$priv_key_format --pwd secret123" + +execEncTest $res_success \ + "" \ + "xmlenc11-interop-2012/cipherText__EC-P521__aes256-gcm__kw-aes256__ECDH-ES__ConcatKDF" \ + "aes256-gcm kw-aes256 concatkdf ecdh-es sha256" \ + "agreement-method enc-key ec" \ + "--enabled-key-data agreement-method,enc-key,key-value,key-name,ec $ec_interop_priv_key_option:EC-P521 $topfolder/xmlenc11-interop-2012/EC-P521_SHA256WithECDSA-orig.$ec_interop_priv_key_format --pwd passwd" \ + "--enabled-key-data agreement-method,enc-key,key-value,key-name,ec --session-key aes-256 $ec_interop_priv_key_option:EC-P521 $topfolder/xmlenc11-interop-2012/EC-P521_SHA256WithECDSA.$ec_interop_priv_key_format $pub_key_option:TestKeyName-ec-prime521v1 $topfolder/keys/ec/ec-prime521v1-pubkey.$pub_key_format --pwd secret123 --xml-data $topfolder/xmlenc11-interop-2012/cipherText__EC-P521__aes256-gcm__kw-aes256__ECDH-ES__ConcatKDF.data" \ + "--enabled-key-data agreement-method,enc-key,key-value,key-name,ec $priv_key_option:TestKeyName-ec-prime521v1 $topfolder/keys/ec/ec-prime521v1-key.$priv_key_format --pwd secret123" + +# DH-ES +execEncTest $res_success \ + "" \ + "xmlenc11-interop-2012/cipherText__DH-1024__aes128-gcm__kw-aes128__dh-es__ConcatKDF" \ + "aes128-gcm kw-aes128 concatkdf dh-es sha256" \ + "agreement-method enc-key dh" \ + "--enabled-key-data agreement-method,enc-key,key-value,key-name,dh $dh_interop_priv_key_option:DH-1024 $topfolder/xmlenc11-interop-2012/DH-1024_SHA256WithDSA.$dh_interop_priv_key_format --pwd passwd" \ + "--enabled-key-data agreement-method,enc-key,key-value,key-name,dh --session-key aes-128 --privkey-der:dhx-rfc5114-3-first $topfolder/keys/dhx/dhx-rfc5114-3-first-key.der --pubkey-der:dhx-rfc5114-3-second $topfolder/keys/dhx/dhx-rfc5114-3-second-pubkey.der --pwd secret123 --xml-data $topfolder/xmlenc11-interop-2012/cipherText__DH-1024__aes128-gcm__kw-aes128__dh-es__ConcatKDF.data" \ + "--enabled-key-data agreement-method,enc-key,key-value,key-name,dh --privkey-der:dhx-rfc5114-3-second $topfolder/keys/dhx/dhx-rfc5114-3-second-key.der --pwd secret123" + + + +########################################################################## +# +# aleksey-xmlenc-01 +# +######################################################################### + +# ECDH + ConcatKDF + SHA1 +execEncTest $res_success \ + "" \ + "aleksey-xmlenc-01/enc_ecdh_p256_concatkdf_sha1_kw_aes256_aes128gcm" \ + "aes256-gcm kw-aes256 ecdh-es concatkdf sha1" \ + "agreement-method enc-key ec" \ + "--enabled-key-data agreement-method,enc-key,key-name,key-value,ec $priv_key_option:recipient-key-name $topfolder/keys/ec/ec-prime256v1-second-key.$priv_key_format --pwd secret123 $pub_key_option:originator-key-name $topfolder/keys/ec/ec-prime256v1-pubkey.$pub_key_format" \ + "--enabled-key-data agreement-method,enc-key,key-name,key-value,ec --session-key aes-256 $priv_key_option:originator-key-name $topfolder/keys/ec/ec-prime256v1-key.$priv_key_format --pwd secret123 $pub_key_option:recipient-key-name $topfolder/keys/ec/ec-prime256v1-second-pubkey.$pub_key_format --xml-data $topfolder/aleksey-xmlenc-01/enc_ecdh_p256_concatkdf_sha1_kw_aes256_aes128gcm.data" \ + "--enabled-key-data agreement-method,enc-key,key-name,key-value,ec $priv_key_option:recipient-key-name $topfolder/keys/ec/ec-prime256v1-second-key.$priv_key_format --pwd secret123 $pub_key_option:originator-key-name $topfolder/keys/ec/ec-prime256v1-pubkey.$pub_key_format" +# ECDH + ConcatKDF + SHA2 +execEncTest $res_success \ + "" \ + "aleksey-xmlenc-01/enc_ecdh_p256_concatkdf_sha224_kw_aes256_aes128gcm" \ + "aes256-gcm kw-aes256 ecdh-es concatkdf sha224" \ + "agreement-method enc-key ec" \ + "--enabled-key-data agreement-method,enc-key,key-name,key-value,ec $priv_key_option:recipient-key-name $topfolder/keys/ec/ec-prime256v1-second-key.$priv_key_format --pwd secret123 $pub_key_option:originator-key-name $topfolder/keys/ec/ec-prime256v1-pubkey.$pub_key_format" \ + "--enabled-key-data agreement-method,enc-key,key-name,key-value,ec --session-key aes-256 $priv_key_option:originator-key-name $topfolder/keys/ec/ec-prime256v1-key.$priv_key_format --pwd secret123 $pub_key_option:recipient-key-name $topfolder/keys/ec/ec-prime256v1-second-pubkey.$pub_key_format --xml-data $topfolder/aleksey-xmlenc-01/enc_ecdh_p256_concatkdf_sha224_kw_aes256_aes128gcm.data" \ + "--enabled-key-data agreement-method,enc-key,key-name,key-value,ec $priv_key_option:recipient-key-name $topfolder/keys/ec/ec-prime256v1-second-key.$priv_key_format --pwd secret123 $pub_key_option:originator-key-name $topfolder/keys/ec/ec-prime256v1-pubkey.$pub_key_format" + +execEncTest $res_success \ + "" \ + "aleksey-xmlenc-01/enc_ecdh_p256_concatkdf_sha256_kw_aes256_aes128gcm" \ + "aes256-gcm kw-aes256 ecdh-es concatkdf sha256" \ + "agreement-method enc-key ec" \ + "--enabled-key-data agreement-method,enc-key,key-name,key-value,ec $priv_key_option:recipient-key-name $topfolder/keys/ec/ec-prime256v1-second-key.$priv_key_format --pwd secret123 $pub_key_option:originator-key-name $topfolder/keys/ec/ec-prime256v1-pubkey.$pub_key_format" \ + "--enabled-key-data agreement-method,enc-key,key-name,key-value,ec --session-key aes-256 $priv_key_option:originator-key-name $topfolder/keys/ec/ec-prime256v1-key.$priv_key_format --pwd secret123 $pub_key_option:recipient-key-name $topfolder/keys/ec/ec-prime256v1-second-pubkey.$pub_key_format --xml-data $topfolder/aleksey-xmlenc-01/enc_ecdh_p256_concatkdf_sha256_kw_aes256_aes128gcm.data" \ + "--enabled-key-data agreement-method,enc-key,key-name,key-value,ec $priv_key_option:recipient-key-name $topfolder/keys/ec/ec-prime256v1-second-key.$priv_key_format --pwd secret123 $pub_key_option:originator-key-name $topfolder/keys/ec/ec-prime256v1-pubkey.$pub_key_format" + +execEncTest $res_success \ + "" \ + "aleksey-xmlenc-01/enc_ecdh_p256_concatkdf_sha384_kw_aes256_aes128gcm" \ + "aes256-gcm kw-aes256 ecdh-es concatkdf sha384" \ + "agreement-method enc-key ec" \ + "--enabled-key-data agreement-method,enc-key,key-name,key-value,ec $priv_key_option:recipient-key-name $topfolder/keys/ec/ec-prime256v1-second-key.$priv_key_format --pwd secret123 $pub_key_option:originator-key-name $topfolder/keys/ec/ec-prime256v1-pubkey.$pub_key_format" \ + "--enabled-key-data agreement-method,enc-key,key-name,key-value,ec --session-key aes-256 $priv_key_option:originator-key-name $topfolder/keys/ec/ec-prime256v1-key.$priv_key_format --pwd secret123 $pub_key_option:recipient-key-name $topfolder/keys/ec/ec-prime256v1-second-pubkey.$pub_key_format --xml-data $topfolder/aleksey-xmlenc-01/enc_ecdh_p256_concatkdf_sha384_kw_aes256_aes128gcm.data" \ + "--enabled-key-data agreement-method,enc-key,key-name,key-value,ec $priv_key_option:recipient-key-name $topfolder/keys/ec/ec-prime256v1-second-key.$priv_key_format --pwd secret123 $pub_key_option:originator-key-name $topfolder/keys/ec/ec-prime256v1-pubkey.$pub_key_format" + +execEncTest $res_success \ + "" \ + "aleksey-xmlenc-01/enc_ecdh_p256_concatkdf_sha512_kw_aes256_aes128gcm" \ + "aes256-gcm kw-aes256 ecdh-es concatkdf sha512" \ + "agreement-method enc-key ec" \ + "--enabled-key-data agreement-method,enc-key,key-name,key-value,ec $priv_key_option:recipient-key-name $topfolder/keys/ec/ec-prime256v1-second-key.$priv_key_format --pwd secret123 $pub_key_option:originator-key-name $topfolder/keys/ec/ec-prime256v1-pubkey.$pub_key_format" \ + "--enabled-key-data agreement-method,enc-key,key-name,key-value,ec --session-key aes-256 $priv_key_option:originator-key-name $topfolder/keys/ec/ec-prime256v1-key.$priv_key_format --pwd secret123 $pub_key_option:recipient-key-name $topfolder/keys/ec/ec-prime256v1-second-pubkey.$pub_key_format --xml-data $topfolder/aleksey-xmlenc-01/enc_ecdh_p256_concatkdf_sha512_kw_aes256_aes128gcm.data" \ + "--enabled-key-data agreement-method,enc-key,key-name,key-value,ec $priv_key_option:recipient-key-name $topfolder/keys/ec/ec-prime256v1-second-key.$priv_key_format --pwd secret123 $pub_key_option:originator-key-name $topfolder/keys/ec/ec-prime256v1-pubkey.$pub_key_format" + +# ECDH + ConcatKDF + SHA3 +execEncTest $res_success \ + "" \ + "aleksey-xmlenc-01/enc_ecdh_p256_concatkdf_sha3_224_kw_aes256_aes128gcm" \ + "aes256-gcm kw-aes256 ecdh-es concatkdf sha3-224" \ + "agreement-method enc-key ec" \ + "--enabled-key-data agreement-method,enc-key,key-name,key-value,ec $priv_key_option:recipient-key-name $topfolder/keys/ec/ec-prime256v1-second-key.$priv_key_format --pwd secret123 $pub_key_option:originator-key-name $topfolder/keys/ec/ec-prime256v1-pubkey.$pub_key_format" \ + "--enabled-key-data agreement-method,enc-key,key-name,key-value,ec --session-key aes-256 $priv_key_option:originator-key-name $topfolder/keys/ec/ec-prime256v1-key.$priv_key_format --pwd secret123 $pub_key_option:recipient-key-name $topfolder/keys/ec/ec-prime256v1-second-pubkey.$pub_key_format --xml-data $topfolder/aleksey-xmlenc-01/enc_ecdh_p256_concatkdf_sha3_224_kw_aes256_aes128gcm.data" \ + "--enabled-key-data agreement-method,enc-key,key-name,key-value,ec $priv_key_option:recipient-key-name $topfolder/keys/ec/ec-prime256v1-second-key.$priv_key_format --pwd secret123 $pub_key_option:originator-key-name $topfolder/keys/ec/ec-prime256v1-pubkey.$pub_key_format" + +execEncTest $res_success \ + "" \ + "aleksey-xmlenc-01/enc_ecdh_p256_concatkdf_sha3_256_kw_aes256_aes128gcm" \ + "aes256-gcm kw-aes256 ecdh-es concatkdf sha3-256" \ + "agreement-method enc-key ec" \ + "--enabled-key-data agreement-method,enc-key,key-name,key-value,ec $priv_key_option:recipient-key-name $topfolder/keys/ec/ec-prime256v1-second-key.$priv_key_format --pwd secret123 $pub_key_option:originator-key-name $topfolder/keys/ec/ec-prime256v1-pubkey.$pub_key_format" \ + "--enabled-key-data agreement-method,enc-key,key-name,key-value,ec --session-key aes-256 $priv_key_option:originator-key-name $topfolder/keys/ec/ec-prime256v1-key.$priv_key_format --pwd secret123 $pub_key_option:recipient-key-name $topfolder/keys/ec/ec-prime256v1-second-pubkey.$pub_key_format --xml-data $topfolder/aleksey-xmlenc-01/enc_ecdh_p256_concatkdf_sha3_256_kw_aes256_aes128gcm.data" \ + "--enabled-key-data agreement-method,enc-key,key-name,key-value,ec $priv_key_option:recipient-key-name $topfolder/keys/ec/ec-prime256v1-second-key.$priv_key_format --pwd secret123 $pub_key_option:originator-key-name $topfolder/keys/ec/ec-prime256v1-pubkey.$pub_key_format" + +execEncTest $res_success \ + "" \ + "aleksey-xmlenc-01/enc_ecdh_p256_concatkdf_sha3_384_kw_aes256_aes128gcm" \ + "aes256-gcm kw-aes256 ecdh-es concatkdf sha3-384" \ + "agreement-method enc-key ec" \ + "--enabled-key-data agreement-method,enc-key,key-name,key-value,ec $priv_key_option:recipient-key-name $topfolder/keys/ec/ec-prime256v1-second-key.$priv_key_format --pwd secret123 $pub_key_option:originator-key-name $topfolder/keys/ec/ec-prime256v1-pubkey.$pub_key_format" \ + "--enabled-key-data agreement-method,enc-key,key-name,key-value,ec --session-key aes-256 $priv_key_option:originator-key-name $topfolder/keys/ec/ec-prime256v1-key.$priv_key_format --pwd secret123 $pub_key_option:recipient-key-name $topfolder/keys/ec/ec-prime256v1-second-pubkey.$pub_key_format --xml-data $topfolder/aleksey-xmlenc-01/enc_ecdh_p256_concatkdf_sha3_384_kw_aes256_aes128gcm.data" \ + "--enabled-key-data agreement-method,enc-key,key-name,key-value,ec $priv_key_option:recipient-key-name $topfolder/keys/ec/ec-prime256v1-second-key.$priv_key_format --pwd secret123 $pub_key_option:originator-key-name $topfolder/keys/ec/ec-prime256v1-pubkey.$pub_key_format" + +execEncTest $res_success \ + "" \ + "aleksey-xmlenc-01/enc_ecdh_p256_concatkdf_sha3_512_kw_aes256_aes128gcm" \ + "aes256-gcm kw-aes256 ecdh-es concatkdf sha3-512" \ + "agreement-method enc-key ec" \ + "--enabled-key-data agreement-method,enc-key,key-name,key-value,ec $priv_key_option:recipient-key-name $topfolder/keys/ec/ec-prime256v1-second-key.$priv_key_format --pwd secret123 $pub_key_option:originator-key-name $topfolder/keys/ec/ec-prime256v1-pubkey.$pub_key_format" \ + "--enabled-key-data agreement-method,enc-key,key-name,key-value,ec --session-key aes-256 $priv_key_option:originator-key-name $topfolder/keys/ec/ec-prime256v1-key.$priv_key_format --pwd secret123 $pub_key_option:recipient-key-name $topfolder/keys/ec/ec-prime256v1-second-pubkey.$pub_key_format --xml-data $topfolder/aleksey-xmlenc-01/enc_ecdh_p256_concatkdf_sha3_512_kw_aes256_aes128gcm.data" \ + "--enabled-key-data agreement-method,enc-key,key-name,key-value,ec $priv_key_option:recipient-key-name $topfolder/keys/ec/ec-prime256v1-second-key.$priv_key_format --pwd secret123 $pub_key_option:originator-key-name $topfolder/keys/ec/ec-prime256v1-pubkey.$pub_key_format" + +# ECDH-P384 + ConcatKDF + SHA384 + KW-AES192 + AES192-GCM +execEncTest $res_success \ + "" \ + "aleksey-xmlenc-01/enc_ecdh_p384_concatkdf_sha384_kw_aes192_aes192gcm" \ + "aes192-gcm kw-aes192 ecdh-es concatkdf sha384" \ + "agreement-method enc-key ec" \ + "--enabled-key-data agreement-method,enc-key,key-name,key-value,ec $priv_key_option:recipient-key-name $topfolder/keys/ec/ec-prime384v1-second-key.$priv_key_format --pwd secret123 $pub_key_option:originator-key-name $topfolder/keys/ec/ec-prime384v1-pubkey.$pub_key_format" \ + "--enabled-key-data agreement-method,enc-key,key-name,key-value,ec --session-key aes-192 $priv_key_option:originator-key-name $topfolder/keys/ec/ec-prime384v1-key.$priv_key_format --pwd secret123 $pub_key_option:recipient-key-name $topfolder/keys/ec/ec-prime384v1-second-pubkey.$pub_key_format --xml-data $topfolder/aleksey-xmlenc-01/enc_ecdh_p384_concatkdf_sha384_kw_aes192_aes192gcm.data" \ + "--enabled-key-data agreement-method,enc-key,key-name,key-value,ec $priv_key_option:recipient-key-name $topfolder/keys/ec/ec-prime384v1-second-key.$priv_key_format --pwd secret123 $pub_key_option:originator-key-name $topfolder/keys/ec/ec-prime384v1-pubkey.$pub_key_format" + +# ECDH-P521 + ConcatKDF + SHA512 + KW-AES256 + AES256-GCM +execEncTest $res_success \ + "" \ + "aleksey-xmlenc-01/enc_ecdh_p521_concatkdf_sha512_kw_aes256_aes256gcm" \ + "aes256-gcm kw-aes256 ecdh-es concatkdf sha512" \ + "agreement-method enc-key ec" \ + "--enabled-key-data agreement-method,enc-key,key-name,key-value,ec $priv_key_option:recipient-key-name $topfolder/keys/ec/ec-prime521v1-second-key.$priv_key_format --pwd secret123 $pub_key_option:originator-key-name $topfolder/keys/ec/ec-prime521v1-pubkey.$pub_key_format" \ + "--enabled-key-data agreement-method,enc-key,key-name,key-value,ec --session-key aes-256 $priv_key_option:originator-key-name $topfolder/keys/ec/ec-prime521v1-key.$priv_key_format --pwd secret123 $pub_key_option:recipient-key-name $topfolder/keys/ec/ec-prime521v1-second-pubkey.$pub_key_format --xml-data $topfolder/aleksey-xmlenc-01/enc_ecdh_p521_concatkdf_sha512_kw_aes256_aes256gcm.data" \ + "--enabled-key-data agreement-method,enc-key,key-name,key-value,ec $priv_key_option:recipient-key-name $topfolder/keys/ec/ec-prime521v1-second-key.$priv_key_format --pwd secret123 $pub_key_option:originator-key-name $topfolder/keys/ec/ec-prime521v1-pubkey.$pub_key_format" + +# DH-ES + ConcatKDF + SHA256 + KW-AES128 + AES128-GCM +execEncTest $res_success \ + "" \ + "aleksey-xmlenc-01/enc_dh_concatkdf_sha256_kw_aes128_aes128gcm" \ + "aes128-gcm kw-aes128 concatkdf dh-es sha256" \ + "agreement-method enc-key dh" \ + "--enabled-key-data agreement-method,enc-key,key-value,key-name,dh $dhx_priv_key_option:dhx-rfc5114-3-second $topfolder/keys/dhx/dhx-rfc5114-3-second-key.$dhx_priv_key_format" \ + "--enabled-key-data agreement-method,enc-key,key-value,key-name,dh --session-key aes-128 $dhx_priv_key_option:dhx-rfc5114-3-first $topfolder/keys/dhx/dhx-rfc5114-3-first-key.$dhx_priv_key_format $dhx_pub_key_option:dhx-rfc5114-3-second $topfolder/keys/dhx/dhx-rfc5114-3-second-pubkey.$dhx_pub_key_format --xml-data $topfolder/aleksey-xmlenc-01/enc_dh_concatkdf_sha256_kw_aes128_aes128gcm.data" \ + "--enabled-key-data agreement-method,enc-key,key-value,key-name,dh $dhx_priv_key_option:dhx-rfc5114-3-second $topfolder/keys/dhx/dhx-rfc5114-3-second-key.$dhx_priv_key_format" + +# ECDH + PBKDF2+SHA1 +execEncTest $res_success \ + "" \ + "aleksey-xmlenc-01/enc_ecdh_p256_pbkdf2_1000_hmac_sha1_kw_aes256_aes128gcm" \ + "aes256-gcm kw-aes256 ecdh-es pbkdf2 hmac-sha1" \ + "agreement-method enc-key ec" \ + "--enabled-key-data agreement-method,enc-key,key-name,key-value,ec $priv_key_option:recipient-key-name $topfolder/keys/ec/ec-prime256v1-second-key.$priv_key_format --pwd secret123 $pub_key_option:originator-key-name $topfolder/keys/ec/ec-prime256v1-pubkey.$pub_key_format" \ + "--enabled-key-data agreement-method,enc-key,key-name,key-value,ec --session-key aes-256 $priv_key_option:originator-key-name $topfolder/keys/ec/ec-prime256v1-key.$priv_key_format --pwd secret123 $pub_key_option:recipient-key-name $topfolder/keys/ec/ec-prime256v1-second-pubkey.$pub_key_format --xml-data $topfolder/aleksey-xmlenc-01/enc_ecdh_p256_pbkdf2_1000_hmac_sha1_kw_aes256_aes128gcm.data" \ + "--enabled-key-data agreement-method,enc-key,key-name,key-value,ec $priv_key_option:recipient-key-name $topfolder/keys/ec/ec-prime256v1-second-key.$priv_key_format --pwd secret123 $pub_key_option:originator-key-name $topfolder/keys/ec/ec-prime256v1-pubkey.$pub_key_format" + +# ECDH + PBKDF2+SHA2 +execEncTest $res_success \ + "" \ + "aleksey-xmlenc-01/enc_ecdh_p256_pbkdf2_1000_hmac_sha224_kw_aes256_aes128gcm" \ + "aes256-gcm kw-aes256 ecdh-es pbkdf2 hmac-sha224" \ + "agreement-method enc-key ec" \ + "--enabled-key-data agreement-method,enc-key,key-name,key-value,ec $priv_key_option:recipient-key-name $topfolder/keys/ec/ec-prime256v1-second-key.$priv_key_format --pwd secret123 $pub_key_option:originator-key-name $topfolder/keys/ec/ec-prime256v1-pubkey.$pub_key_format" \ + "--enabled-key-data agreement-method,enc-key,key-name,key-value,ec --session-key aes-256 $priv_key_option:originator-key-name $topfolder/keys/ec/ec-prime256v1-key.$priv_key_format --pwd secret123 $pub_key_option:recipient-key-name $topfolder/keys/ec/ec-prime256v1-second-pubkey.$pub_key_format --xml-data $topfolder/aleksey-xmlenc-01/enc_ecdh_p256_pbkdf2_1000_hmac_sha224_kw_aes256_aes128gcm.data" \ + "--enabled-key-data agreement-method,enc-key,key-name,key-value,ec $priv_key_option:recipient-key-name $topfolder/keys/ec/ec-prime256v1-second-key.$priv_key_format --pwd secret123 $pub_key_option:originator-key-name $topfolder/keys/ec/ec-prime256v1-pubkey.$pub_key_format" + +execEncTest $res_success \ + "" \ + "aleksey-xmlenc-01/enc_ecdh_p256_pbkdf2_1000_hmac_sha256_kw_aes256_aes128gcm" \ + "aes256-gcm kw-aes256 ecdh-es pbkdf2 hmac-sha256" \ + "agreement-method enc-key ec" \ + "--enabled-key-data agreement-method,enc-key,key-name,key-value,ec $priv_key_option:recipient-key-name $topfolder/keys/ec/ec-prime256v1-second-key.$priv_key_format --pwd secret123 $pub_key_option:originator-key-name $topfolder/keys/ec/ec-prime256v1-pubkey.$pub_key_format" \ + "--enabled-key-data agreement-method,enc-key,key-name,key-value,ec --session-key aes-256 $priv_key_option:originator-key-name $topfolder/keys/ec/ec-prime256v1-key.$priv_key_format --pwd secret123 $pub_key_option:recipient-key-name $topfolder/keys/ec/ec-prime256v1-second-pubkey.$pub_key_format --xml-data $topfolder/aleksey-xmlenc-01/enc_ecdh_p256_pbkdf2_1000_hmac_sha256_kw_aes256_aes128gcm.data" \ + "--enabled-key-data agreement-method,enc-key,key-name,key-value,ec $priv_key_option:recipient-key-name $topfolder/keys/ec/ec-prime256v1-second-key.$priv_key_format --pwd secret123 $pub_key_option:originator-key-name $topfolder/keys/ec/ec-prime256v1-pubkey.$pub_key_format" + +execEncTest $res_success \ + "" \ + "aleksey-xmlenc-01/enc_ecdh_p256_pbkdf2_1000_hmac_sha384_kw_aes256_aes128gcm" \ + "aes256-gcm kw-aes256 ecdh-es pbkdf2 hmac-sha384" \ + "agreement-method enc-key ec" \ + "--enabled-key-data agreement-method,enc-key,key-name,key-value,ec $priv_key_option:recipient-key-name $topfolder/keys/ec/ec-prime256v1-second-key.$priv_key_format --pwd secret123 $pub_key_option:originator-key-name $topfolder/keys/ec/ec-prime256v1-pubkey.$pub_key_format" \ + "--enabled-key-data agreement-method,enc-key,key-name,key-value,ec --session-key aes-256 $priv_key_option:originator-key-name $topfolder/keys/ec/ec-prime256v1-key.$priv_key_format --pwd secret123 $pub_key_option:recipient-key-name $topfolder/keys/ec/ec-prime256v1-second-pubkey.$pub_key_format --xml-data $topfolder/aleksey-xmlenc-01/enc_ecdh_p256_pbkdf2_1000_hmac_sha384_kw_aes256_aes128gcm.data" \ + "--enabled-key-data agreement-method,enc-key,key-name,key-value,ec $priv_key_option:recipient-key-name $topfolder/keys/ec/ec-prime256v1-second-key.$priv_key_format --pwd secret123 $pub_key_option:originator-key-name $topfolder/keys/ec/ec-prime256v1-pubkey.$pub_key_format" + +execEncTest $res_success \ + "" \ + "aleksey-xmlenc-01/enc_ecdh_p256_pbkdf2_1000_hmac_sha512_kw_aes256_aes128gcm" \ + "aes256-gcm kw-aes256 ecdh-es pbkdf2 hmac-sha512" \ + "agreement-method enc-key ec" \ + "--enabled-key-data agreement-method,enc-key,key-name,key-value,ec $priv_key_option:recipient-key-name $topfolder/keys/ec/ec-prime256v1-second-key.$priv_key_format --pwd secret123 $pub_key_option:originator-key-name $topfolder/keys/ec/ec-prime256v1-pubkey.$pub_key_format" \ + "--enabled-key-data agreement-method,enc-key,key-name,key-value,ec --session-key aes-256 $priv_key_option:originator-key-name $topfolder/keys/ec/ec-prime256v1-key.$priv_key_format --pwd secret123 $pub_key_option:recipient-key-name $topfolder/keys/ec/ec-prime256v1-second-pubkey.$pub_key_format --xml-data $topfolder/aleksey-xmlenc-01/enc_ecdh_p256_pbkdf2_1000_hmac_sha512_kw_aes256_aes128gcm.data" \ + "--enabled-key-data agreement-method,enc-key,key-name,key-value,ec $priv_key_option:recipient-key-name $topfolder/keys/ec/ec-prime256v1-second-key.$priv_key_format --pwd secret123 $pub_key_option:originator-key-name $topfolder/keys/ec/ec-prime256v1-pubkey.$pub_key_format" + +# ECDH + HKDF + SHA256 +execEncTest $res_success \ + "" \ + "aleksey-xmlenc-01/enc_ecdh_p256_hkdf_sha256_kw_aes256_aes128gcm" \ + "aes128-gcm kw-aes256 ecdh-es hkdf hmac-sha256" \ + "agreement-method enc-key ec" \ + "--enabled-key-data agreement-method,enc-key,key-name,key-value,ec $priv_key_option:recipient-key-name $topfolder/keys/ec/ec-prime256v1-second-key.$priv_key_format --pwd secret123 $pub_key_option:originator-key-name $topfolder/keys/ec/ec-prime256v1-pubkey.$pub_key_format" \ + "--enabled-key-data agreement-method,enc-key,key-name,key-value,ec --session-key aes-128 $priv_key_option:originator-key-name $topfolder/keys/ec/ec-prime256v1-key.$priv_key_format --pwd secret123 $pub_key_option:recipient-key-name $topfolder/keys/ec/ec-prime256v1-second-pubkey.$pub_key_format --xml-data $topfolder/aleksey-xmlenc-01/enc_ecdh_p256_hkdf_sha256_kw_aes256_aes128gcm.data" \ + "--enabled-key-data agreement-method,enc-key,key-name,key-value,ec $priv_key_option:recipient-key-name $topfolder/keys/ec/ec-prime256v1-second-key.$priv_key_format --pwd secret123 $pub_key_option:originator-key-name $topfolder/keys/ec/ec-prime256v1-pubkey.$pub_key_format" + +# ECDH + HKDF + SHA384 +execEncTest $res_success \ + "" \ + "aleksey-xmlenc-01/enc_ecdh_p256_hkdf_sha384_kw_aes256_aes128gcm" \ + "aes128-gcm kw-aes256 ecdh-es hkdf hmac-sha384" \ + "agreement-method enc-key ec" \ + "--enabled-key-data agreement-method,enc-key,key-name,key-value,ec $priv_key_option:recipient-key-name $topfolder/keys/ec/ec-prime256v1-second-key.$priv_key_format --pwd secret123 $pub_key_option:originator-key-name $topfolder/keys/ec/ec-prime256v1-pubkey.$pub_key_format" \ + "--enabled-key-data agreement-method,enc-key,key-name,key-value,ec --session-key aes-128 $priv_key_option:originator-key-name $topfolder/keys/ec/ec-prime256v1-key.$priv_key_format --pwd secret123 $pub_key_option:recipient-key-name $topfolder/keys/ec/ec-prime256v1-second-pubkey.$pub_key_format --xml-data $topfolder/aleksey-xmlenc-01/enc_ecdh_p256_hkdf_sha384_kw_aes256_aes128gcm.data" \ + "--enabled-key-data agreement-method,enc-key,key-name,key-value,ec $priv_key_option:recipient-key-name $topfolder/keys/ec/ec-prime256v1-second-key.$priv_key_format --pwd secret123 $pub_key_option:originator-key-name $topfolder/keys/ec/ec-prime256v1-pubkey.$pub_key_format" + +# ECDH + HKDF + SHA512 +execEncTest $res_success \ + "" \ + "aleksey-xmlenc-01/enc_ecdh_p256_hkdf_sha512_kw_aes256_aes128gcm" \ + "aes128-gcm kw-aes256 ecdh-es hkdf hmac-sha512" \ + "agreement-method enc-key ec" \ + "--enabled-key-data agreement-method,enc-key,key-name,key-value,ec $priv_key_option:recipient-key-name $topfolder/keys/ec/ec-prime256v1-second-key.$priv_key_format --pwd secret123 $pub_key_option:originator-key-name $topfolder/keys/ec/ec-prime256v1-pubkey.$pub_key_format" \ + "--enabled-key-data agreement-method,enc-key,key-name,key-value,ec --session-key aes-128 $priv_key_option:originator-key-name $topfolder/keys/ec/ec-prime256v1-key.$priv_key_format --pwd secret123 $pub_key_option:recipient-key-name $topfolder/keys/ec/ec-prime256v1-second-pubkey.$pub_key_format --xml-data $topfolder/aleksey-xmlenc-01/enc_ecdh_p256_hkdf_sha512_kw_aes256_aes128gcm.data" \ + "--enabled-key-data agreement-method,enc-key,key-name,key-value,ec $priv_key_option:recipient-key-name $topfolder/keys/ec/ec-prime256v1-second-key.$priv_key_format --pwd secret123 $pub_key_option:originator-key-name $topfolder/keys/ec/ec-prime256v1-pubkey.$pub_key_format" + +# X25519 + ConcatKDF + SHA2 +execEncTest $res_success \ + "" \ + "aleksey-xmlenc-01/enc_xdh_x25519_concatkdf_sha256_kw_aes256_aes128gcm" \ + "aes256-gcm kw-aes256 x25519 concatkdf sha256" \ + "agreement-method enc-key xdh" \ + "--enabled-key-data agreement-method,enc-key,key-name,key-value,xdh $xdh_priv_key_option:recipient-key-name $topfolder/keys/xdh/xdh-x25519-second-key.$xdh_priv_key_format --pwd secret123 $pub_key_option:originator-key-name $topfolder/keys/xdh/xdh-x25519-first-pubkey.$pub_key_format" \ + "--enabled-key-data agreement-method,enc-key,key-name,key-value,xdh --session-key aes-256 $xdh_priv_key_option:originator-key-name $topfolder/keys/xdh/xdh-x25519-first-key.$xdh_priv_key_format --pwd secret123 $pub_key_option:recipient-key-name $topfolder/keys/xdh/xdh-x25519-second-pubkey.$pub_key_format --xml-data $topfolder/aleksey-xmlenc-01/enc_xdh_x25519_concatkdf_sha256_kw_aes256_aes128gcm.data" \ + "--enabled-key-data agreement-method,enc-key,key-name,key-value,xdh $xdh_priv_key_option:recipient-key-name $topfolder/keys/xdh/xdh-x25519-second-key.$xdh_priv_key_format --pwd secret123 $pub_key_option:originator-key-name $topfolder/keys/xdh/xdh-x25519-first-pubkey.$pub_key_format" + +# X448 + ConcatKDF + SHA2 +execEncTest $res_success \ + "" \ + "aleksey-xmlenc-01/enc_xdh_x448_concatkdf_sha256_kw_aes256_aes128gcm" \ + "aes256-gcm kw-aes256 x448 concatkdf sha256" \ + "agreement-method enc-key xdh" \ + "--enabled-key-data agreement-method,enc-key,key-name,key-value,xdh $xdh_priv_key_option:recipient-key-name $topfolder/keys/xdh/xdh-x448-second-key.$xdh_priv_key_format --pwd secret123 $pub_key_option:originator-key-name $topfolder/keys/xdh/xdh-x448-first-pubkey.$pub_key_format" \ + "--enabled-key-data agreement-method,enc-key,key-name,key-value,xdh --session-key aes-256 $xdh_priv_key_option:originator-key-name $topfolder/keys/xdh/xdh-x448-first-key.$xdh_priv_key_format --pwd secret123 $pub_key_option:recipient-key-name $topfolder/keys/xdh/xdh-x448-second-pubkey.$pub_key_format --xml-data $topfolder/aleksey-xmlenc-01/enc_xdh_x448_concatkdf_sha256_kw_aes256_aes128gcm.data" \ + "--enabled-key-data agreement-method,enc-key,key-name,key-value,xdh $xdh_priv_key_option:recipient-key-name $topfolder/keys/xdh/xdh-x448-second-key.$xdh_priv_key_format --pwd secret123 $pub_key_option:originator-key-name $topfolder/keys/xdh/xdh-x448-first-pubkey.$pub_key_format" + +# X25519 + ConcatKDF + SHA384 + KW-AES256 + AES128-GCM +execEncTest $res_success \ + "" \ + "aleksey-xmlenc-01/enc_xdh_x25519_concatkdf_sha384_kw_aes256_aes128gcm" \ + "aes128-gcm kw-aes256 x25519 concatkdf sha384" \ + "agreement-method enc-key xdh" \ + "--enabled-key-data agreement-method,enc-key,key-name,key-value,xdh $xdh_priv_key_option:recipient-key-name $topfolder/keys/xdh/xdh-x25519-second-key.$xdh_priv_key_format --pwd secret123 $pub_key_option:originator-key-name $topfolder/keys/xdh/xdh-x25519-first-pubkey.$pub_key_format" \ + "--enabled-key-data agreement-method,enc-key,key-name,key-value,xdh --session-key aes-256 $xdh_priv_key_option:originator-key-name $topfolder/keys/xdh/xdh-x25519-first-key.$xdh_priv_key_format --pwd secret123 $pub_key_option:recipient-key-name $topfolder/keys/xdh/xdh-x25519-second-pubkey.$pub_key_format --xml-data $topfolder/aleksey-xmlenc-01/enc_xdh_x25519_concatkdf_sha384_kw_aes256_aes128gcm.data" \ + "--enabled-key-data agreement-method,enc-key,key-name,key-value,xdh $xdh_priv_key_option:recipient-key-name $topfolder/keys/xdh/xdh-x25519-second-key.$xdh_priv_key_format --pwd secret123 $pub_key_option:originator-key-name $topfolder/keys/xdh/xdh-x25519-first-pubkey.$pub_key_format" + +# X448 + ConcatKDF + SHA384 + KW-AES256 + AES128-GCM +execEncTest $res_success \ + "" \ + "aleksey-xmlenc-01/enc_xdh_x448_concatkdf_sha384_kw_aes256_aes128gcm" \ + "aes128-gcm kw-aes256 x448 concatkdf sha384" \ + "agreement-method enc-key xdh" \ + "--enabled-key-data agreement-method,enc-key,key-name,key-value,xdh $xdh_priv_key_option:recipient-key-name $topfolder/keys/xdh/xdh-x448-second-key.$xdh_priv_key_format --pwd secret123 $pub_key_option:originator-key-name $topfolder/keys/xdh/xdh-x448-first-pubkey.$pub_key_format" \ + "--enabled-key-data agreement-method,enc-key,key-name,key-value,xdh --session-key aes-256 $xdh_priv_key_option:originator-key-name $topfolder/keys/xdh/xdh-x448-first-key.$xdh_priv_key_format --pwd secret123 $pub_key_option:recipient-key-name $topfolder/keys/xdh/xdh-x448-second-pubkey.$pub_key_format --xml-data $topfolder/aleksey-xmlenc-01/enc_xdh_x448_concatkdf_sha384_kw_aes256_aes128gcm.data" \ + "--enabled-key-data agreement-method,enc-key,key-name,key-value,xdh $xdh_priv_key_option:recipient-key-name $topfolder/keys/xdh/xdh-x448-second-key.$xdh_priv_key_format --pwd secret123 $pub_key_option:originator-key-name $topfolder/keys/xdh/xdh-x448-first-pubkey.$pub_key_format" + +# X25519 + HKDF + SHA256 + KW-AES256 + AES128-GCM +execEncTest $res_success \ + "" \ + "aleksey-xmlenc-01/enc_xdh_x25519_hkdf_sha256_kw_aes256_aes128gcm" \ + "aes128-gcm kw-aes256 x25519 hkdf hmac-sha256" \ + "agreement-method enc-key xdh" \ + "--enabled-key-data agreement-method,enc-key,key-name,key-value,xdh $xdh_priv_key_option:recipient-key-name $topfolder/keys/xdh/xdh-x25519-second-key.$xdh_priv_key_format --pwd secret123 $pub_key_option:originator-key-name $topfolder/keys/xdh/xdh-x25519-first-pubkey.$pub_key_format" \ + "--enabled-key-data agreement-method,enc-key,key-name,key-value,xdh --session-key aes-128 $xdh_priv_key_option:originator-key-name $topfolder/keys/xdh/xdh-x25519-first-key.$xdh_priv_key_format --pwd secret123 $pub_key_option:recipient-key-name $topfolder/keys/xdh/xdh-x25519-second-pubkey.$pub_key_format --xml-data $topfolder/aleksey-xmlenc-01/enc_xdh_x25519_hkdf_sha256_kw_aes256_aes128gcm.data" \ + "--enabled-key-data agreement-method,enc-key,key-name,key-value,xdh $xdh_priv_key_option:recipient-key-name $topfolder/keys/xdh/xdh-x25519-second-key.$xdh_priv_key_format --pwd secret123 $pub_key_option:originator-key-name $topfolder/keys/xdh/xdh-x25519-first-pubkey.$pub_key_format" + +# X448 + HKDF + SHA256 + KW-AES256 + AES128-GCM +execEncTest $res_success \ + "" \ + "aleksey-xmlenc-01/enc_xdh_x448_hkdf_sha256_kw_aes256_aes128gcm" \ + "aes128-gcm kw-aes256 x448 hkdf hmac-sha256" \ + "agreement-method enc-key xdh" \ + "--enabled-key-data agreement-method,enc-key,key-name,key-value,xdh $xdh_priv_key_option:recipient-key-name $topfolder/keys/xdh/xdh-x448-second-key.$xdh_priv_key_format --pwd secret123 $pub_key_option:originator-key-name $topfolder/keys/xdh/xdh-x448-first-pubkey.$pub_key_format" \ + "--enabled-key-data agreement-method,enc-key,key-name,key-value,xdh --session-key aes-128 $xdh_priv_key_option:originator-key-name $topfolder/keys/xdh/xdh-x448-first-key.$xdh_priv_key_format --pwd secret123 $pub_key_option:recipient-key-name $topfolder/keys/xdh/xdh-x448-second-pubkey.$pub_key_format --xml-data $topfolder/aleksey-xmlenc-01/enc_xdh_x448_hkdf_sha256_kw_aes256_aes128gcm.data" \ + "--enabled-key-data agreement-method,enc-key,key-name,key-value,xdh $xdh_priv_key_option:recipient-key-name $topfolder/keys/xdh/xdh-x448-second-key.$xdh_priv_key_format --pwd secret123 $pub_key_option:originator-key-name $topfolder/keys/xdh/xdh-x448-first-pubkey.$pub_key_format" + +# X25519 + HKDF + SHA384 + KW-AES256 + AES128-GCM +execEncTest $res_success \ + "" \ + "aleksey-xmlenc-01/enc_xdh_x25519_hkdf_sha384_kw_aes256_aes128gcm" \ + "aes128-gcm kw-aes256 x25519 hkdf hmac-sha384" \ + "agreement-method enc-key xdh" \ + "--enabled-key-data agreement-method,enc-key,key-name,key-value,xdh $xdh_priv_key_option:recipient-key-name $topfolder/keys/xdh/xdh-x25519-second-key.$xdh_priv_key_format --pwd secret123 $pub_key_option:originator-key-name $topfolder/keys/xdh/xdh-x25519-first-pubkey.$pub_key_format" \ + "--enabled-key-data agreement-method,enc-key,key-name,key-value,xdh --session-key aes-128 $xdh_priv_key_option:originator-key-name $topfolder/keys/xdh/xdh-x25519-first-key.$xdh_priv_key_format --pwd secret123 $pub_key_option:recipient-key-name $topfolder/keys/xdh/xdh-x25519-second-pubkey.$pub_key_format --xml-data $topfolder/aleksey-xmlenc-01/enc_xdh_x25519_hkdf_sha384_kw_aes256_aes128gcm.data" \ + "--enabled-key-data agreement-method,enc-key,key-name,key-value,xdh $xdh_priv_key_option:recipient-key-name $topfolder/keys/xdh/xdh-x25519-second-key.$xdh_priv_key_format --pwd secret123 $pub_key_option:originator-key-name $topfolder/keys/xdh/xdh-x25519-first-pubkey.$pub_key_format" + +# X448 + HKDF + SHA384 + KW-AES256 + AES128-GCM +execEncTest $res_success \ + "" \ + "aleksey-xmlenc-01/enc_xdh_x448_hkdf_sha384_kw_aes256_aes128gcm" \ + "aes128-gcm kw-aes256 x448 hkdf hmac-sha384" \ + "agreement-method enc-key xdh" \ + "--enabled-key-data agreement-method,enc-key,key-name,key-value,xdh $xdh_priv_key_option:recipient-key-name $topfolder/keys/xdh/xdh-x448-second-key.$xdh_priv_key_format --pwd secret123 $pub_key_option:originator-key-name $topfolder/keys/xdh/xdh-x448-first-pubkey.$pub_key_format" \ + "--enabled-key-data agreement-method,enc-key,key-name,key-value,xdh --session-key aes-128 $xdh_priv_key_option:originator-key-name $topfolder/keys/xdh/xdh-x448-first-key.$xdh_priv_key_format --pwd secret123 $pub_key_option:recipient-key-name $topfolder/keys/xdh/xdh-x448-second-pubkey.$pub_key_format --xml-data $topfolder/aleksey-xmlenc-01/enc_xdh_x448_hkdf_sha384_kw_aes256_aes128gcm.data" \ + "--enabled-key-data agreement-method,enc-key,key-name,key-value,xdh $xdh_priv_key_option:recipient-key-name $topfolder/keys/xdh/xdh-x448-second-key.$xdh_priv_key_format --pwd secret123 $pub_key_option:originator-key-name $topfolder/keys/xdh/xdh-x448-first-pubkey.$pub_key_format" + +# X25519 + HKDF + SHA512 + KW-AES256 + AES128-GCM +execEncTest $res_success \ + "" \ + "aleksey-xmlenc-01/enc_xdh_x25519_hkdf_sha512_kw_aes256_aes128gcm" \ + "aes128-gcm kw-aes256 x25519 hkdf hmac-sha512" \ + "agreement-method enc-key xdh" \ + "--enabled-key-data agreement-method,enc-key,key-name,key-value,xdh $xdh_priv_key_option:recipient-key-name $topfolder/keys/xdh/xdh-x25519-second-key.$xdh_priv_key_format --pwd secret123 $pub_key_option:originator-key-name $topfolder/keys/xdh/xdh-x25519-first-pubkey.$pub_key_format" \ + "--enabled-key-data agreement-method,enc-key,key-name,key-value,xdh --session-key aes-128 $xdh_priv_key_option:originator-key-name $topfolder/keys/xdh/xdh-x25519-first-key.$xdh_priv_key_format --pwd secret123 $pub_key_option:recipient-key-name $topfolder/keys/xdh/xdh-x25519-second-pubkey.$pub_key_format --xml-data $topfolder/aleksey-xmlenc-01/enc_xdh_x25519_hkdf_sha512_kw_aes256_aes128gcm.data" \ + "--enabled-key-data agreement-method,enc-key,key-name,key-value,xdh $xdh_priv_key_option:recipient-key-name $topfolder/keys/xdh/xdh-x25519-second-key.$xdh_priv_key_format --pwd secret123 $pub_key_option:originator-key-name $topfolder/keys/xdh/xdh-x25519-first-pubkey.$pub_key_format" + +# X448 + HKDF + SHA512 + KW-AES256 + AES128-GCM +execEncTest $res_success \ + "" \ + "aleksey-xmlenc-01/enc_xdh_x448_hkdf_sha512_kw_aes256_aes128gcm" \ + "aes128-gcm kw-aes256 x448 hkdf hmac-sha512" \ + "agreement-method enc-key xdh" \ + "--enabled-key-data agreement-method,enc-key,key-name,key-value,xdh $xdh_priv_key_option:recipient-key-name $topfolder/keys/xdh/xdh-x448-second-key.$xdh_priv_key_format --pwd secret123 $pub_key_option:originator-key-name $topfolder/keys/xdh/xdh-x448-first-pubkey.$pub_key_format" \ + "--enabled-key-data agreement-method,enc-key,key-name,key-value,xdh --session-key aes-128 $xdh_priv_key_option:originator-key-name $topfolder/keys/xdh/xdh-x448-first-key.$xdh_priv_key_format --pwd secret123 $pub_key_option:recipient-key-name $topfolder/keys/xdh/xdh-x448-second-pubkey.$pub_key_format --xml-data $topfolder/aleksey-xmlenc-01/enc_xdh_x448_hkdf_sha512_kw_aes256_aes128gcm.data" \ + "--enabled-key-data agreement-method,enc-key,key-name,key-value,xdh $xdh_priv_key_option:recipient-key-name $topfolder/keys/xdh/xdh-x448-second-key.$xdh_priv_key_format --pwd secret123 $pub_key_option:originator-key-name $topfolder/keys/xdh/xdh-x448-first-pubkey.$pub_key_format" + +if [ "z$xmlsec_feature_x509_data_lookup" = "zyes" ] ; then + execEncTest $res_success \ + "" \ + "aleksey-xmlenc-01/enc_rsa_1_5_x509_subject_name" \ + "aes256-cbc rsa-1_5" \ + "x509" \ + "$priv_key_option $topfolder/keys/rsa/rsa-4096-key.$priv_key_format --pwd secret123" \ + "--session-key aes-256 --pubkey-cert-$cert_format $topfolder/keys/rsa/rsa-4096-cert.$cert_format --xml-data $topfolder/aleksey-xmlenc-01/enc_rsa_1_5_x509_subject_name.data --node-name http://example.org/paymentv2:CreditCard" \ + "$priv_key_option $topfolder/keys/rsa/rsa-4096-key.$priv_key_format --pwd secret123" + + execEncTest $res_success \ + "" \ + "aleksey-xmlenc-01/enc_rsa_1_5_x509_issuer_name_serial" \ + "aes256-cbc rsa-1_5" \ + "x509" \ + "$priv_key_option $topfolder/keys/rsa/rsa-4096-key.$priv_key_format --pwd secret123" \ + "--session-key aes-256 --pubkey-cert-$cert_format $topfolder/keys/rsa/rsa-4096-cert.$cert_format --xml-data $topfolder/aleksey-xmlenc-01/enc_rsa_1_5_x509_issuer_name_serial.data --node-name http://example.org/paymentv2:CreditCard" \ + "$priv_key_option $topfolder/keys/rsa/rsa-4096-key.$priv_key_format --pwd secret123" + + execEncTest $res_success \ + "" \ + "aleksey-xmlenc-01/enc_rsa_1_5_x509_ski" \ + "aes256-cbc rsa-1_5" \ + "x509" \ + "$priv_key_option $topfolder/keys/rsa/rsa-4096-key.$priv_key_format --pwd secret123" \ + "--session-key aes-256 --pubkey-cert-$cert_format $topfolder/keys/rsa/rsa-4096-cert.$cert_format --xml-data $topfolder/aleksey-xmlenc-01/enc_rsa_1_5_x509_ski.data --node-name http://example.org/paymentv2:CreditCard" \ + "$priv_key_option $topfolder/keys/rsa/rsa-4096-key.$priv_key_format --pwd secret123" + + execEncTest $res_success \ + "" \ + "aleksey-xmlenc-01/enc_rsa_1_5_x509_digest_sha1" \ + "aes256-cbc rsa-1_5 sha1" \ + "x509" \ + "$priv_key_option $topfolder/keys/rsa/rsa-4096-key.$priv_key_format --pwd secret123" \ + "--session-key aes-256 --pubkey-cert-$cert_format $topfolder/keys/rsa/rsa-4096-cert.$cert_format --xml-data $topfolder/aleksey-xmlenc-01/enc_rsa_1_5_x509_digest_sha1.data --node-name http://example.org/paymentv2:CreditCard" \ + "$priv_key_option $topfolder/keys/rsa/rsa-4096-key.$priv_key_format --pwd secret123" + + execEncTest $res_success \ + "" \ + "aleksey-xmlenc-01/enc_rsa_1_5_x509_digest_sha224" \ + "aes256-cbc rsa-1_5 sha224" \ + "x509" \ + "$priv_key_option $topfolder/keys/rsa/rsa-4096-key.$priv_key_format --pwd secret123" \ + "--session-key aes-256 --pubkey-cert-$cert_format $topfolder/keys/rsa/rsa-4096-cert.$cert_format --xml-data $topfolder/aleksey-xmlenc-01/enc_rsa_1_5_x509_digest_sha224.data --node-name http://example.org/paymentv2:CreditCard" \ + "$priv_key_option $topfolder/keys/rsa/rsa-4096-key.$priv_key_format --pwd secret123" + + execEncTest $res_success \ + "" \ + "aleksey-xmlenc-01/enc_rsa_1_5_x509_digest_sha256" \ + "aes256-cbc rsa-1_5 sha256" \ + "x509" \ + "$priv_key_option $topfolder/keys/rsa/rsa-4096-key.$priv_key_format --pwd secret123" \ + "--session-key aes-256 --pubkey-cert-$cert_format $topfolder/keys/rsa/rsa-4096-cert.$cert_format --xml-data $topfolder/aleksey-xmlenc-01/enc_rsa_1_5_x509_digest_sha256.data --node-name http://example.org/paymentv2:CreditCard" \ + "$priv_key_option $topfolder/keys/rsa/rsa-4096-key.$priv_key_format --pwd secret123" + + execEncTest $res_success \ + "" \ + "aleksey-xmlenc-01/enc_rsa_1_5_x509_digest_sha384" \ + "aes256-cbc rsa-1_5 sha384" \ + "x509" \ + "$priv_key_option $topfolder/keys/rsa/rsa-4096-key.$priv_key_format --pwd secret123" \ + "--session-key aes-256 --pubkey-cert-$cert_format $topfolder/keys/rsa/rsa-4096-cert.$cert_format --xml-data $topfolder/aleksey-xmlenc-01/enc_rsa_1_5_x509_digest_sha384.data --node-name http://example.org/paymentv2:CreditCard" \ + "$priv_key_option $topfolder/keys/rsa/rsa-4096-key.$priv_key_format --pwd secret123" + + execEncTest $res_success \ + "" \ + "aleksey-xmlenc-01/enc_rsa_1_5_x509_digest_sha512" \ + "aes256-cbc rsa-1_5 sha512" \ + "x509" \ + "$priv_key_option $topfolder/keys/rsa/rsa-4096-key.$priv_key_format --pwd secret123" \ + "--session-key aes-256 --pubkey-cert-$cert_format $topfolder/keys/rsa/rsa-4096-cert.$cert_format --xml-data $topfolder/aleksey-xmlenc-01/enc_rsa_1_5_x509_digest_sha512.data --node-name http://example.org/paymentv2:CreditCard" \ + "$priv_key_option $topfolder/keys/rsa/rsa-4096-key.$priv_key_format --pwd secret123" + + execEncTest $res_success \ + "" \ + "aleksey-xmlenc-01/enc_rsa_1_5_x509_digest_sha3_224" \ + "aes256-cbc rsa-1_5 sha3-224" \ + "x509" \ + "$priv_key_option $topfolder/keys/rsa/rsa-4096-key.$priv_key_format --pwd secret123" \ + "--session-key aes-256 --pubkey-cert-$cert_format $topfolder/keys/rsa/rsa-4096-cert.$cert_format --xml-data $topfolder/aleksey-xmlenc-01/enc_rsa_1_5_x509_digest_sha3_224.data --node-name http://example.org/paymentv2:CreditCard" \ + "$priv_key_option $topfolder/keys/rsa/rsa-4096-key.$priv_key_format --pwd secret123" + + execEncTest $res_success \ + "" \ + "aleksey-xmlenc-01/enc_rsa_1_5_x509_digest_sha3_256" \ + "aes256-cbc rsa-1_5 sha3-256" \ + "x509" \ + "$priv_key_option $topfolder/keys/rsa/rsa-4096-key.$priv_key_format --pwd secret123" \ + "--session-key aes-256 --pubkey-cert-$cert_format $topfolder/keys/rsa/rsa-4096-cert.$cert_format --xml-data $topfolder/aleksey-xmlenc-01/enc_rsa_1_5_x509_digest_sha3_256.data --node-name http://example.org/paymentv2:CreditCard" \ + "$priv_key_option $topfolder/keys/rsa/rsa-4096-key.$priv_key_format --pwd secret123" + + execEncTest $res_success \ + "" \ + "aleksey-xmlenc-01/enc_rsa_1_5_x509_digest_sha3_384" \ + "aes256-cbc rsa-1_5 sha3-384" \ + "x509" \ + "$priv_key_option $topfolder/keys/rsa/rsa-4096-key.$priv_key_format --pwd secret123" \ + "--session-key aes-256 --pubkey-cert-$cert_format $topfolder/keys/rsa/rsa-4096-cert.$cert_format --xml-data $topfolder/aleksey-xmlenc-01/enc_rsa_1_5_x509_digest_sha3_384.data --node-name http://example.org/paymentv2:CreditCard" \ + "$priv_key_option $topfolder/keys/rsa/rsa-4096-key.$priv_key_format --pwd secret123" + + execEncTest $res_success \ + "" \ + "aleksey-xmlenc-01/enc_rsa_1_5_x509_digest_sha3_512" \ + "aes256-cbc rsa-1_5 sha3-512" \ + "x509" \ + "$priv_key_option $topfolder/keys/rsa/rsa-4096-key.$priv_key_format --pwd secret123" \ + "--session-key aes-256 --pubkey-cert-$cert_format $topfolder/keys/rsa/rsa-4096-cert.$cert_format --xml-data $topfolder/aleksey-xmlenc-01/enc_rsa_1_5_x509_digest_sha3_512.data --node-name http://example.org/paymentv2:CreditCard" \ + "$priv_key_option $topfolder/keys/rsa/rsa-4096-key.$priv_key_format --pwd secret123" +fi + +# same file is encrypted with two keys, test both +execEncTest $res_success \ + "" \ + "aleksey-xmlenc-01/enc-two-enc-keys" \ + "aes256-cbc rsa-1_5" \ + "x509" \ + "$priv_key_option:TestKeyName-rsa-2048 $topfolder/keys/rsa/rsa-2048-key.$priv_key_format --pwd secret123" \ + "--session-key aes-256 --xml-data $topfolder/aleksey-xmlenc-01/enc-two-enc-keys.data --pubkey-cert-$cert_format:TestKeyName-rsa-2048 $topfolder/keys/rsa/rsa-2048-cert.$cert_format --pubkey-cert-$cert_format:TestKeyName-rsa-4096 $topfolder/keys/rsa/rsa-4096-cert.$cert_format" \ + "$priv_key_option:TestKeyName-rsa-2048 $topfolder/keys/rsa/rsa-2048-key.$priv_key_format --pwd secret123" + +execEncTest $res_success \ + "" \ + "aleksey-xmlenc-01/enc-two-enc-keys" \ + "aes256-cbc rsa-1_5" \ + "x509" \ + "$priv_key_option:TestKeyName-rsa-4096 $topfolder/keys/rsa/rsa-4096-key.$priv_key_format --pwd secret123" \ + "--session-key aes-256 --xml-data $topfolder/aleksey-xmlenc-01/enc-two-enc-keys.data --pubkey-cert-$cert_format:TestKeyName-rsa-2048 $topfolder/keys/rsa/rsa-2048-cert.$cert_format --pubkey-cert-$cert_format:TestKeyName-rsa-4096 $topfolder/keys/rsa/rsa-4096-cert.$cert_format" \ + "$priv_key_option:TestKeyName-rsa-4096 $topfolder/keys/rsa/rsa-4096-key.$priv_key_format --pwd secret123" + + +execEncTest $res_success \ + "" \ + "aleksey-xmlenc-01/large_input" \ + "aes256-cbc rsa-1_5" \ + "x509" \ + "$priv_key_option:TestKeyName-rsa-4096 $topfolder/keys/rsa/rsa-4096-key.$priv_key_format --pwd secret123" \ + "--session-key aes-256 --xml-data $topfolder/aleksey-xmlenc-01/large_input.data --pubkey-cert-$cert_format:TestKeyName-rsa-4096 $topfolder/keys/rsa/rsa-4096-cert.$cert_format" \ + "$priv_key_option:TestKeyName-rsa-4096 $topfolder/keys/rsa/rsa-4096-key.$priv_key_format --pwd secret123" + +execEncTest $res_success \ + "" \ + "aleksey-xmlenc-01/enc-element-isolatin1" \ + "aes256-cbc rsa-1_5" \ + "x509" \ + "$priv_key_option:TestKeyName-rsa-4096 $topfolder/keys/rsa/rsa-4096-key.$priv_key_format --pwd secret123" \ + "--session-key aes-256 --xml-data $topfolder/aleksey-xmlenc-01/enc-element-isolatin1.data --pubkey-cert-$cert_format:TestKeyName-rsa-4096 $topfolder/keys/rsa/rsa-4096-cert.$cert_format" \ + "$priv_key_option:TestKeyName-rsa-4096 $topfolder/keys/rsa/rsa-4096-key.$priv_key_format --pwd secret123" + +execEncTest $res_success \ + "" \ + "aleksey-xmlenc-01/enc-content-isolatin1" \ + "aes256-cbc rsa-1_5" \ + "x509" \ + "$priv_key_option:TestKeyName-rsa-4096 $topfolder/keys/rsa/rsa-4096-key.$priv_key_format --pwd secret123" \ + "--session-key aes-256 --xml-data $topfolder/aleksey-xmlenc-01/enc-content-isolatin1.data --node-name http://example.org/paymentv2:CreditCard --pubkey-cert-$cert_format:TestKeyName-rsa-4096 $topfolder/keys/rsa/rsa-4096-cert.$cert_format" \ + "$priv_key_option:TestKeyName-rsa-4096 $topfolder/keys/rsa/rsa-4096-key.$priv_key_format --pwd secret123" + +execEncTest $res_success \ + "" \ + "aleksey-xmlenc-01/enc-des3cbc-keyname" \ + "tripledes-cbc" \ + "" \ + "--keys-file $topfolder/keys/keys.xml" \ + "--keys-file $topfolder/keys/keys.xml --binary-data $topfolder/aleksey-xmlenc-01/enc-des3cbc-keyname.data" \ + "--keys-file $topfolder/keys/keys.xml" + +extra_message="Test '--des-key' option" +execEncTest $res_success \ + "" \ + "aleksey-xmlenc-01/enc-des3cbc-keyname" \ + "tripledes-cbc" \ + "" \ + "--des-key:test-des $topfolder/aleksey-xmlenc-01/test-des.bin" \ + "--des-key:test-des $topfolder/aleksey-xmlenc-01/test-des.bin --binary-data $topfolder/aleksey-xmlenc-01/enc-des3cbc-keyname.data" \ + "--des-key:test-des $topfolder/aleksey-xmlenc-01/test-des.bin" + +execEncTest $res_success \ + "" \ + "aleksey-xmlenc-01/enc-des3cbc-keyname2" \ + "tripledes-cbc" \ + "" \ + "--keys-file $topfolder/keys/keys.xml" \ + "--keys-file $topfolder/keys/keys.xml --binary-data $topfolder/aleksey-xmlenc-01/enc-des3cbc-keyname2.data" \ + "--keys-file $topfolder/keys/keys.xml" + + +execEncTest $res_success \ + "" \ + "aleksey-xmlenc-01/enc-aes128cbc-keyname" \ + "aes128-cbc" \ + "" \ + "--keys-file $topfolder/keys/keys.xml" \ + "--keys-file $topfolder/keys/keys.xml --binary-data $topfolder/aleksey-xmlenc-01/enc-aes128cbc-keyname.data" \ + "--keys-file $topfolder/keys/keys.xml" + +extra_message="Test '--aes-key' option" +execEncTest $res_success \ + "" \ + "aleksey-xmlenc-01/enc-aes128cbc-keyname" \ + "aes128-cbc" \ + "" \ + "--aes-key:test-aes128 $topfolder/aleksey-xmlenc-01/test-aes128.bin" \ + "--aes-key:test-aes128 $topfolder/aleksey-xmlenc-01/test-aes128.bin --binary-data $topfolder/aleksey-xmlenc-01/enc-aes128cbc-keyname.data" \ + "--aes-key:test-aes128 $topfolder/aleksey-xmlenc-01/test-aes128.bin" + +execEncTest $res_success \ + "" \ + "aleksey-xmlenc-01/enc-aes192cbc-keyname" \ + "aes192-cbc" \ + "" \ + "--keys-file $topfolder/keys/keys.xml" \ + "--keys-file $topfolder/keys/keys.xml --binary-data $topfolder/aleksey-xmlenc-01/enc-aes192cbc-keyname.data" \ + "--keys-file $topfolder/keys/keys.xml" + +execEncTest $res_success \ + "" \ + "aleksey-xmlenc-01/enc-aes192cbc-keyname-ref" \ + "aes192-cbc" \ + "" \ + "--keys-file $topfolder/keys/keys.xml" + + +extra_message="Negative test: all cipher references are disabled" +execEncTest $res_fail \ + "" \ + "aleksey-xmlenc-01/enc-aes192cbc-keyname-ref" \ + "" \ + "" \ + "--keys-file $topfolder/keys/keys.xml --enabled-cipher-reference-uris empty" + +execEncTest $res_success \ + "" \ + "aleksey-xmlenc-01/enc-aes256cbc-keyname" \ + "aes256-cbc" \ + "" \ + "--keys-file $topfolder/keys/keys.xml" \ + "--keys-file $topfolder/keys/keys.xml --binary-data $topfolder/aleksey-xmlenc-01/enc-aes256cbc-keyname.data" \ + "--keys-file $topfolder/keys/keys.xml" + +execEncTest $res_success \ + "" \ + "aleksey-xmlenc-01/enc-aes128gcm-keyname" \ + "aes128-gcm" \ + "" \ + "--keys-file $topfolder/keys/keys.xml" \ + "--keys-file $topfolder/keys/keys.xml --binary-data $topfolder/aleksey-xmlenc-01/enc-aes128gcm-keyname.data" \ + "--keys-file $topfolder/keys/keys.xml" + +execEncTest $res_success \ + "" \ + "aleksey-xmlenc-01/enc-aes192gcm-keyname" \ + "aes192-gcm" \ + "" \ + "--keys-file $topfolder/keys/keys.xml" \ + "--keys-file $topfolder/keys/keys.xml --binary-data $topfolder/aleksey-xmlenc-01/enc-aes192gcm-keyname.data" \ + "--keys-file $topfolder/keys/keys.xml" + +execEncTest $res_success \ + "" \ + "aleksey-xmlenc-01/enc-aes256gcm-keyname" \ + "aes256-gcm" \ + "" \ + "--keys-file $topfolder/keys/keys.xml" \ + "--keys-file $topfolder/keys/keys.xml --binary-data $topfolder/aleksey-xmlenc-01/enc-aes256gcm-keyname.data" \ + "--keys-file $topfolder/keys/keys.xml" + +execEncTest $res_success \ + "" \ + "aleksey-xmlenc-01/enc-camellia128cbc-keyname" \ + "camellia128-cbc" \ + "" \ + "--keys-file $topfolder/keys/keys.xml" \ + "--keys-file $topfolder/keys/keys.xml --binary-data $topfolder/aleksey-xmlenc-01/enc-camellia128cbc-keyname.data" \ + "--keys-file $topfolder/keys/keys.xml" + +execEncTest $res_success \ + "" \ + "aleksey-xmlenc-01/enc-camellia192cbc-keyname" \ + "camellia192-cbc" \ + "" \ + "--keys-file $topfolder/keys/keys.xml" \ + "--keys-file $topfolder/keys/keys.xml --binary-data $topfolder/aleksey-xmlenc-01/enc-camellia192cbc-keyname.data" \ + "--keys-file $topfolder/keys/keys.xml" + +execEncTest $res_success \ + "" \ + "aleksey-xmlenc-01/enc-camellia256cbc-keyname" \ + "camellia256-cbc" \ + "" \ + "--keys-file $topfolder/keys/keys.xml" \ + "--keys-file $topfolder/keys/keys.xml --binary-data $topfolder/aleksey-xmlenc-01/enc-camellia256cbc-keyname.data" \ + "--keys-file $topfolder/keys/keys.xml" + + +extra_message="Test '--camellia-key' option" +execEncTest $res_success \ + "" \ + "aleksey-xmlenc-01/enc-camellia256cbc-keyname" \ + "camellia256-cbc" \ + "" \ + "--camellia-key:test-camellia256 $topfolder/aleksey-xmlenc-01/test-camellia256.bin" \ + "--camellia-key:test-camellia256 $topfolder/aleksey-xmlenc-01/test-camellia256.bin --binary-data $topfolder/aleksey-xmlenc-01/enc-camellia256cbc-keyname.data" \ + "--camellia-key:test-camellia256 $topfolder/aleksey-xmlenc-01/test-camellia256.bin" + +execEncTest $res_success \ + "" \ + "aleksey-xmlenc-01/enc-camellia128cbc-kw-camellia256-keyname" \ + "camellia128-cbc kw-camellia256" \ + "enc-key camellia" \ + "--keys-file $topfolder/keys/keys.xml" \ + "--keys-file $topfolder/keys/keys.xml --session-key camellia-128 --binary-data $topfolder/aleksey-xmlenc-01/enc-camellia128cbc-kw-camellia256-keyname.data" \ + "--keys-file $topfolder/keys/keys.xml" + +execEncTest $res_success \ + "" \ + "aleksey-xmlenc-01/enc-camellia128cbc-kw-camellia128-keyname" \ + "camellia128-cbc kw-camellia128" \ + "enc-key camellia" \ + "--keys-file $topfolder/keys/keys.xml" \ + "--keys-file $topfolder/keys/keys.xml --session-key camellia-128 --binary-data $topfolder/aleksey-xmlenc-01/enc-camellia128cbc-kw-camellia128-keyname.data" \ + "--keys-file $topfolder/keys/keys.xml" + +execEncTest $res_success \ + "" \ + "aleksey-xmlenc-01/enc-camellia128cbc-kw-camellia192-keyname" \ + "camellia128-cbc kw-camellia192" \ + "enc-key camellia" \ + "--keys-file $topfolder/keys/keys.xml" \ + "--keys-file $topfolder/keys/keys.xml --session-key camellia-128 --binary-data $topfolder/aleksey-xmlenc-01/enc-camellia128cbc-kw-camellia192-keyname.data" \ + "--keys-file $topfolder/keys/keys.xml" + +execEncTest $res_success \ + "" \ + "aleksey-xmlenc-01/enc-chacha20-keyname" \ + "chacha20" \ + "" \ + "--keys-file $topfolder/keys/keys.xml" \ + "--keys-file $topfolder/keys/keys.xml --binary-data $topfolder/aleksey-xmlenc-01/enc-chacha20-keyname.data" \ + "--keys-file $topfolder/keys/keys.xml" + +extra_message="Test '--chacha20-key' option" +execEncTest $res_success \ + "" \ + "aleksey-xmlenc-01/enc-chacha20-keyname" \ + "chacha20" \ + "" \ + "--chacha20-key:test-chacha20 $topfolder/aleksey-xmlenc-01/test-chacha20.bin" \ + "--chacha20-key:test-chacha20 $topfolder/aleksey-xmlenc-01/test-chacha20.bin --binary-data $topfolder/aleksey-xmlenc-01/enc-chacha20-keyname.data" \ + "--chacha20-key:test-chacha20 $topfolder/aleksey-xmlenc-01/test-chacha20.bin" + +execEncTest $res_success \ + "" \ + "aleksey-xmlenc-01/enc-chacha20-keyname-missing-nonce" \ + "chacha20" \ + "" \ + "" \ + "--keys-file $topfolder/keys/keys.xml --binary-data $topfolder/aleksey-xmlenc-01/enc-chacha20-keyname-missing-nonce.data" \ + "--keys-file $topfolder/keys/keys.xml" + +execEncTest $res_success \ + "" \ + "aleksey-xmlenc-01/enc-chacha20poly1305-keyname" \ + "chacha20-poly1305" \ + "" \ + "--keys-file $topfolder/keys/keys.xml" \ + "--keys-file $topfolder/keys/keys.xml --binary-data $topfolder/aleksey-xmlenc-01/enc-chacha20poly1305-keyname.data" \ + "--keys-file $topfolder/keys/keys.xml" + +execEncTest $res_success \ + "" \ + "aleksey-xmlenc-01/enc-chacha20poly1305-keyname-missing-nonce" \ + "chacha20-poly1305" \ + "" \ + "" \ + "--keys-file $topfolder/keys/keys.xml --binary-data $topfolder/aleksey-xmlenc-01/enc-chacha20poly1305-keyname-missing-nonce.data" \ + "--keys-file $topfolder/keys/keys.xml" + +execEncTest $res_success \ + "" \ + "aleksey-xmlenc-01/enc-chacha20poly1305-aad-keyname" \ + "chacha20-poly1305" \ + "" \ + "--keys-file $topfolder/keys/keys.xml" \ + "--keys-file $topfolder/keys/keys.xml --binary-data $topfolder/aleksey-xmlenc-01/enc-chacha20poly1305-aad-keyname.data" \ + "--keys-file $topfolder/keys/keys.xml" + +execEncTest $res_success \ + "" \ + "aleksey-xmlenc-01/enc-des3cbc-keyname-content" \ + "tripledes-cbc" \ + " " \ + "--keys-file $topfolder/keys/keys.xml" \ + "--keys-file $topfolder/keys/keys.xml --xml-data $topfolder/aleksey-xmlenc-01/enc-des3cbc-keyname-content.data --node-id Test" \ + "--keys-file $topfolder/keys/keys.xml" + +execEncTest $res_success \ + "" \ + "aleksey-xmlenc-01/enc-des3cbc-keyname-element" \ + "tripledes-cbc" \ + " " \ + "--keys-file $topfolder/keys/keys.xml" \ + "--keys-file $topfolder/keys/keys.xml --xml-data $topfolder/aleksey-xmlenc-01/enc-des3cbc-keyname-element.data --node-id Test" \ + "--keys-file $topfolder/keys/keys.xml" + +execEncTest $res_success \ + "" \ + "aleksey-xmlenc-01/enc-des3cbc-keyname-element-root" \ + "tripledes-cbc" \ + " " \ + "--keys-file $topfolder/keys/keys.xml" \ + "--keys-file $topfolder/keys/keys.xml --xml-data $topfolder/aleksey-xmlenc-01/enc-des3cbc-keyname-element-root.data --node-id Test" \ + "--keys-file $topfolder/keys/keys.xml" + +execEncTest $res_success \ + "" \ + "aleksey-xmlenc-01/enc-des3cbc-aes192-keyname" \ + "tripledes-cbc kw-aes192" \ + "enc-key aes des" \ + "--keys-file $topfolder/keys/keys.xml" \ + "--keys-file $topfolder/keys/keys.xml --session-key des-192 --binary-data $topfolder/aleksey-xmlenc-01/enc-des3cbc-aes192-keyname.data" \ + "--keys-file $topfolder/keys/keys.xml" + +if [ "z$xmlsec_feature_rsa_oaep_sha1" = "zyes" ] ; then + execEncTest $res_success \ + "" \ + "aleksey-xmlenc-01/enc-aes256-kt-rsa_oaep_sha1-params" \ + "aes256-cbc rsa-oaep-mgf1p sha1" \ + "" \ + "$priv_key_option:TestKeyName-rsa-4096 $topfolder/keys/rsa/rsa-4096-key.$priv_key_format --pwd secret123" \ + "$pub_key_option:TestKeyName-rsa-4096 $topfolder/keys/rsa/rsa-4096-pubkey$rsa_pub_key_suffix.$pub_key_format --session-key aes-256 --enabled-key-data key-name,enc-key --xml-data $topfolder/aleksey-xmlenc-01/enc-aes256-kt-rsa_oaep_sha1-params.data --node-name http://example.org/paymentv2:CreditCard" \ + "$priv_key_option:TestKeyName-rsa-4096 $topfolder/keys/rsa/rsa-4096-key.$priv_key_format --pwd secret123" + + execEncTest $res_success \ + "" \ + "aleksey-xmlenc-01/enc-aes256-kt-rsa_oaep_sha1" \ + "aes256-cbc rsa-oaep-mgf1p sha1 sha1" \ + " " \ + "$priv_key_option:TestKeyName-rsa-4096 $topfolder/keys/rsa/rsa-4096-key.$priv_key_format --pwd secret123" \ + "$pub_key_option:TestKeyName-rsa-4096 $topfolder/keys/rsa/rsa-4096-pubkey$rsa_pub_key_suffix.$pub_key_format --session-key aes-256 --enabled-key-data key-name,enc-key --xml-data $topfolder/aleksey-xmlenc-01/enc-aes256-kt-rsa_oaep_sha1.data --node-name http://example.org/paymentv2:CreditCard" \ + "$priv_key_option:TestKeyName-rsa-4096 $topfolder/keys/rsa/rsa-4096-key.$priv_key_format --pwd secret123" + + # verify that XML debug output is correct and contains the expected elements and values + execEncPrintXmlDebugTest \ + "" \ + "aleksey-xmlenc-01/enc-aes256-kt-rsa_oaep_sha1" \ + "aes256-cbc rsa-oaep-mgf1p sha1 sha1" \ + " " \ + "$priv_key_option:TestKeyName-rsa-4096 $topfolder/keys/rsa/rsa-4096-key.$priv_key_format --pwd secret123" +fi + +if [ "z$xmlsec_feature_rsa_oaep_sha1" = "zyes" -a "z$xmlsec_feature_rsa_oaep_different_digest_and_mgf1" = "zyes" ] ; then + # various digest and default mgf1 (sha1) + execEncTest $res_success \ + "" \ + "aleksey-xmlenc-01/enc-aes256-kt-rsa_oaep_md5" \ + "aes256-cbc rsa-oaep-mgf1p md5 sha1" \ + "" \ + "$priv_key_option:TestKeyName-rsa-4096 $topfolder/keys/rsa/rsa-4096-key.$priv_key_format --pwd secret123" \ + "$pub_key_option:TestKeyName-rsa-4096 $topfolder/keys/rsa/rsa-4096-pubkey$rsa_pub_key_suffix.$pub_key_format --session-key aes-256 --enabled-key-data key-name,enc-key --xml-data $topfolder/aleksey-xmlenc-01/enc-aes256-kt-rsa_oaep_md5.data --node-name http://example.org/paymentv2:CreditCard" \ + "$priv_key_option:TestKeyName-rsa-4096 $topfolder/keys/rsa/rsa-4096-key.$priv_key_format --pwd secret123" + + execEncTest $res_success \ + "" \ + "aleksey-xmlenc-01/enc-aes256-kt-rsa_oaep_ripemd160" \ + "aes256-cbc rsa-oaep-mgf1p ripemd160 sha1" \ + "" \ + "$priv_key_option:TestKeyName-rsa-4096 $topfolder/keys/rsa/rsa-4096-key.$priv_key_format --pwd secret123" \ + "$pub_key_option:TestKeyName-rsa-4096 $topfolder/keys/rsa/rsa-4096-pubkey$rsa_pub_key_suffix.$pub_key_format --session-key aes-256 --enabled-key-data key-name,enc-key --xml-data $topfolder/aleksey-xmlenc-01/enc-aes256-kt-rsa_oaep_ripemd160.data --node-name http://example.org/paymentv2:CreditCard" \ + "$priv_key_option:TestKeyName-rsa-4096 $topfolder/keys/rsa/rsa-4096-key.$priv_key_format --pwd secret123" + + execEncTest $res_success \ + "" \ + "aleksey-xmlenc-01/enc-aes256-kt-rsa_oaep_sha224" \ + "aes256-cbc rsa-oaep-mgf1p sha224 sha1" \ + "" \ + "$priv_key_option:TestKeyName-rsa-4096 $topfolder/keys/rsa/rsa-4096-key.$priv_key_format --pwd secret123" \ + "$pub_key_option:TestKeyName-rsa-4096 $topfolder/keys/rsa/rsa-4096-pubkey$rsa_pub_key_suffix.$pub_key_format --session-key aes-256 --enabled-key-data key-name,enc-key --xml-data $topfolder/aleksey-xmlenc-01/enc-aes256-kt-rsa_oaep_sha224.data --node-name http://example.org/paymentv2:CreditCard" \ + "$priv_key_option:TestKeyName-rsa-4096 $topfolder/keys/rsa/rsa-4096-key.$priv_key_format --pwd secret123" + + execEncTest $res_success \ + "" \ + "aleksey-xmlenc-01/enc-aes256-kt-rsa_oaep_sha256" \ + "aes256-cbc rsa-oaep-mgf1p sha256 sha1" \ + "" \ + "$priv_key_option:TestKeyName-rsa-4096 $topfolder/keys/rsa/rsa-4096-key.$priv_key_format --pwd secret123" \ + "$pub_key_option:TestKeyName-rsa-4096 $topfolder/keys/rsa/rsa-4096-pubkey$rsa_pub_key_suffix.$pub_key_format --session-key aes-256 --enabled-key-data key-name,enc-key --xml-data $topfolder/aleksey-xmlenc-01/enc-aes256-kt-rsa_oaep_sha256.data --node-name http://example.org/paymentv2:CreditCard" \ + "$priv_key_option:TestKeyName-rsa-4096 $topfolder/keys/rsa/rsa-4096-key.$priv_key_format --pwd secret123" + + execEncTest $res_success \ + "" \ + "aleksey-xmlenc-01/enc-aes256-kt-rsa_oaep_sha384" \ + "aes256-cbc rsa-oaep-mgf1p sha384 sha1" \ + "" \ + "$priv_key_option:TestKeyName-rsa-4096 $topfolder/keys/rsa/rsa-4096-key.$priv_key_format --pwd secret123" \ + "$pub_key_option:TestKeyName-rsa-4096 $topfolder/keys/rsa/rsa-4096-pubkey$rsa_pub_key_suffix.$pub_key_format --session-key aes-256 --enabled-key-data key-name,enc-key --xml-data $topfolder/aleksey-xmlenc-01/enc-aes256-kt-rsa_oaep_sha384.data --node-name http://example.org/paymentv2:CreditCard" \ + "$priv_key_option:TestKeyName-rsa-4096 $topfolder/keys/rsa/rsa-4096-key.$priv_key_format --pwd secret123" + + execEncTest $res_success \ + "" \ + "aleksey-xmlenc-01/enc-aes256-kt-rsa_oaep_sha512" \ + "aes256-cbc rsa-oaep-mgf1p sha512 sha1" \ + "" \ + "$priv_key_option:TestKeyName-rsa-4096 $topfolder/keys/rsa/rsa-4096-key.$priv_key_format --pwd secret123" \ + "$pub_key_option:TestKeyName-rsa-4096 $topfolder/keys/rsa/rsa-4096-pubkey$rsa_pub_key_suffix.$pub_key_format --session-key aes-256 --enabled-key-data key-name,enc-key --xml-data $topfolder/aleksey-xmlenc-01/enc-aes256-kt-rsa_oaep_sha512.data --node-name http://example.org/paymentv2:CreditCard" \ + "$priv_key_option:TestKeyName-rsa-4096 $topfolder/keys/rsa/rsa-4096-key.$priv_key_format --pwd secret123" + + # various digest and mgf1=sha512 + execEncTest $res_success \ + "" \ + "aleksey-xmlenc-01/enc-aes256-kt-rsa_oaep_md5_mgf1_sha512" \ + "aes256-cbc rsa-oaep-mgf1p md5 sha512" \ + "" \ + "$priv_key_option:TestKeyName-rsa-4096 $topfolder/keys/rsa/rsa-4096-key.$priv_key_format --pwd secret123" \ + "$pub_key_option:TestKeyName-rsa-4096 $topfolder/keys/rsa/rsa-4096-pubkey$rsa_pub_key_suffix.$pub_key_format --session-key aes-256 --enabled-key-data key-name,enc-key --xml-data $topfolder/aleksey-xmlenc-01/enc-aes256-kt-rsa_oaep_md5_mgf1_sha512.data --node-name http://example.org/paymentv2:CreditCard" \ + "$priv_key_option:TestKeyName-rsa-4096 $topfolder/keys/rsa/rsa-4096-key.$priv_key_format --pwd secret123" + + execEncTest $res_success \ + "" \ + "aleksey-xmlenc-01/enc-aes256-kt-rsa_oaep_ripemd160_mgf1_sha512" \ + "aes256-cbc rsa-oaep-mgf1p ripemd160 sha512" \ + "" \ + "$priv_key_option:TestKeyName-rsa-4096 $topfolder/keys/rsa/rsa-4096-key.$priv_key_format --pwd secret123" \ + "$pub_key_option:TestKeyName-rsa-4096 $topfolder/keys/rsa/rsa-4096-pubkey$rsa_pub_key_suffix.$pub_key_format --session-key aes-256 --enabled-key-data key-name,enc-key --xml-data $topfolder/aleksey-xmlenc-01/enc-aes256-kt-rsa_oaep_ripemd160_mgf1_sha512.data --node-name http://example.org/paymentv2:CreditCard" \ + "$priv_key_option:TestKeyName-rsa-4096 $topfolder/keys/rsa/rsa-4096-key.$priv_key_format --pwd secret123" + + execEncTest $res_success \ + "" \ + "aleksey-xmlenc-01/enc-aes256-kt-rsa_oaep_sha1_mgf1_sha512" \ + "aes256-cbc rsa-oaep-mgf1p sha1 sha512" \ + "" \ + "$priv_key_option:TestKeyName-rsa-4096 $topfolder/keys/rsa/rsa-4096-key.$priv_key_format --pwd secret123" \ + "$pub_key_option:TestKeyName-rsa-4096 $topfolder/keys/rsa/rsa-4096-pubkey$rsa_pub_key_suffix.$pub_key_format --session-key aes-256 --enabled-key-data key-name,enc-key --xml-data $topfolder/aleksey-xmlenc-01/enc-aes256-kt-rsa_oaep_sha1_mgf1_sha512.data --node-name http://example.org/paymentv2:CreditCard" \ + "$priv_key_option:TestKeyName-rsa-4096 $topfolder/keys/rsa/rsa-4096-key.$priv_key_format --pwd secret123" + + execEncTest $res_success \ + "" \ + "aleksey-xmlenc-01/enc-aes256-kt-rsa_oaep_sha224_mgf1_sha512" \ + "aes256-cbc rsa-oaep-mgf1p sha224 sha512" \ + "" \ + "$priv_key_option:TestKeyName-rsa-4096 $topfolder/keys/rsa/rsa-4096-key.$priv_key_format --pwd secret123" \ + "$pub_key_option:TestKeyName-rsa-4096 $topfolder/keys/rsa/rsa-4096-pubkey$rsa_pub_key_suffix.$pub_key_format --session-key aes-256 --enabled-key-data key-name,enc-key --xml-data $topfolder/aleksey-xmlenc-01/enc-aes256-kt-rsa_oaep_sha224_mgf1_sha512.data --node-name http://example.org/paymentv2:CreditCard" \ + "$priv_key_option:TestKeyName-rsa-4096 $topfolder/keys/rsa/rsa-4096-key.$priv_key_format --pwd secret123" + + execEncTest $res_success \ + "" \ + "aleksey-xmlenc-01/enc-aes256-kt-rsa_oaep_sha256_mgf1_sha512" \ + "aes256-cbc rsa-oaep-mgf1p sha256 sha512" \ + "" \ + "$priv_key_option:TestKeyName-rsa-4096 $topfolder/keys/rsa/rsa-4096-key.$priv_key_format --pwd secret123" \ + "$pub_key_option:TestKeyName-rsa-4096 $topfolder/keys/rsa/rsa-4096-pubkey$rsa_pub_key_suffix.$pub_key_format --session-key aes-256 --enabled-key-data key-name,enc-key --xml-data $topfolder/aleksey-xmlenc-01/enc-aes256-kt-rsa_oaep_sha256_mgf1_sha512.data --node-name http://example.org/paymentv2:CreditCard" \ + "$priv_key_option:TestKeyName-rsa-4096 $topfolder/keys/rsa/rsa-4096-key.$priv_key_format --pwd secret123" + + execEncTest $res_success \ + "" \ + "aleksey-xmlenc-01/enc-aes256-kt-rsa_oaep_sha384_mgf1_sha512" \ + "aes256-cbc rsa-oaep-mgf1p sha384 sha512" \ + "" \ + "$priv_key_option:TestKeyName-rsa-4096 $topfolder/keys/rsa/rsa-4096-key.$priv_key_format --pwd secret123" \ + "$pub_key_option:TestKeyName-rsa-4096 $topfolder/keys/rsa/rsa-4096-pubkey$rsa_pub_key_suffix.$pub_key_format --session-key aes-256 --enabled-key-data key-name,enc-key --xml-data $topfolder/aleksey-xmlenc-01/enc-aes256-kt-rsa_oaep_sha384_mgf1_sha512.data --node-name http://example.org/paymentv2:CreditCard" \ + "$priv_key_option:TestKeyName-rsa-4096 $topfolder/keys/rsa/rsa-4096-key.$priv_key_format --pwd secret123" + + # digest=sha512 and various mgf1 + execEncTest $res_success \ + "" \ + "aleksey-xmlenc-01/enc-aes256-kt-rsa_oaep_sha512_mgf1_sha1" \ + "aes256-cbc rsa-oaep-mgf1p sha512 sha1" \ + "" \ + "$priv_key_option:TestKeyName-rsa-4096 $topfolder/keys/rsa/rsa-4096-key.$priv_key_format --pwd secret123" \ + "$pub_key_option:TestKeyName-rsa-4096 $topfolder/keys/rsa/rsa-4096-pubkey$rsa_pub_key_suffix.$pub_key_format --session-key aes-256 --enabled-key-data key-name,enc-key --xml-data $topfolder/aleksey-xmlenc-01/enc-aes256-kt-rsa_oaep_sha512_mgf1_sha1.data --node-name http://example.org/paymentv2:CreditCard" \ + "$priv_key_option:TestKeyName-rsa-4096 $topfolder/keys/rsa/rsa-4096-key.$priv_key_format --pwd secret123" + + execEncTest $res_success \ + "" \ + "aleksey-xmlenc-01/enc-aes256-kt-rsa_oaep_sha512_mgf1_sha224" \ + "aes256-cbc rsa-oaep-mgf1p sha512 sha224" \ + "" \ + "$priv_key_option:TestKeyName-rsa-4096 $topfolder/keys/rsa/rsa-4096-key.$priv_key_format --pwd secret123" \ + "$pub_key_option:TestKeyName-rsa-4096 $topfolder/keys/rsa/rsa-4096-pubkey$rsa_pub_key_suffix.$pub_key_format --session-key aes-256 --enabled-key-data key-name,enc-key --xml-data $topfolder/aleksey-xmlenc-01/enc-aes256-kt-rsa_oaep_sha512_mgf1_sha224.data --node-name http://example.org/paymentv2:CreditCard" \ + "$priv_key_option:TestKeyName-rsa-4096 $topfolder/keys/rsa/rsa-4096-key.$priv_key_format --pwd secret123" + + execEncTest $res_success \ + "" \ + "aleksey-xmlenc-01/enc-aes256-kt-rsa_oaep_sha512_mgf1_sha256" \ + "aes256-cbc rsa-oaep-mgf1p sha512 sha256" \ + "" \ + "$priv_key_option:TestKeyName-rsa-4096 $topfolder/keys/rsa/rsa-4096-key.$priv_key_format --pwd secret123" \ + "$pub_key_option:TestKeyName-rsa-4096 $topfolder/keys/rsa/rsa-4096-pubkey$rsa_pub_key_suffix.$pub_key_format --session-key aes-256 --enabled-key-data key-name,enc-key --xml-data $topfolder/aleksey-xmlenc-01/enc-aes256-kt-rsa_oaep_sha512_mgf1_sha256.data --node-name http://example.org/paymentv2:CreditCard" \ + "$priv_key_option:TestKeyName-rsa-4096 $topfolder/keys/rsa/rsa-4096-key.$priv_key_format --pwd secret123" + + execEncTest $res_success \ + "" \ + "aleksey-xmlenc-01/enc-aes256-kt-rsa_oaep_sha512_mgf1_sha384" \ + "aes256-cbc rsa-oaep-mgf1p sha512 sha384" \ + "" \ + "$priv_key_option:TestKeyName-rsa-4096 $topfolder/keys/rsa/rsa-4096-key.$priv_key_format --pwd secret123" \ + "$pub_key_option:TestKeyName-rsa-4096 $topfolder/keys/rsa/rsa-4096-pubkey$rsa_pub_key_suffix.$pub_key_format --session-key aes-256 --enabled-key-data key-name,enc-key --xml-data $topfolder/aleksey-xmlenc-01/enc-aes256-kt-rsa_oaep_sha512_mgf1_sha384.data --node-name http://example.org/paymentv2:CreditCard" \ + "$priv_key_option:TestKeyName-rsa-4096 $topfolder/keys/rsa/rsa-4096-key.$priv_key_format --pwd secret123" +fi + +if [ "z$xmlsec_feature_rsa_oaep_sha1" = "zyes" -a "z$xmlsec_feature_rsa_oaep_sha3" = "zyes" -a "z$xmlsec_feature_rsa_oaep_different_digest_and_mgf1" = "zyes" ] ; then + # SHA3 digest variants with default mgf1 (sha1) + execEncTest $res_success \ + "" \ + "aleksey-xmlenc-01/enc-aes256-kt-rsa_oaep_sha3_224" \ + "aes256-cbc rsa-oaep-mgf1p sha3-224 sha1" \ + "" \ + "$priv_key_option:TestKeyName-rsa-4096 $topfolder/keys/rsa/rsa-4096-key.$priv_key_format --pwd secret123" \ + "$pub_key_option:TestKeyName-rsa-4096 $topfolder/keys/rsa/rsa-4096-pubkey$rsa_pub_key_suffix.$pub_key_format --session-key aes-256 --enabled-key-data key-name,enc-key --xml-data $topfolder/aleksey-xmlenc-01/enc-aes256-kt-rsa_oaep_sha3_224.data --node-name http://example.org/paymentv2:CreditCard" \ + "$priv_key_option:TestKeyName-rsa-4096 $topfolder/keys/rsa/rsa-4096-key.$priv_key_format --pwd secret123" + + execEncTest $res_success \ + "" \ + "aleksey-xmlenc-01/enc-aes256-kt-rsa_oaep_sha3_256" \ + "aes256-cbc rsa-oaep-mgf1p sha3-256 sha1" \ + "" \ + "$priv_key_option:TestKeyName-rsa-4096 $topfolder/keys/rsa/rsa-4096-key.$priv_key_format --pwd secret123" \ + "$pub_key_option:TestKeyName-rsa-4096 $topfolder/keys/rsa/rsa-4096-pubkey$rsa_pub_key_suffix.$pub_key_format --session-key aes-256 --enabled-key-data key-name,enc-key --xml-data $topfolder/aleksey-xmlenc-01/enc-aes256-kt-rsa_oaep_sha3_256.data --node-name http://example.org/paymentv2:CreditCard" \ + "$priv_key_option:TestKeyName-rsa-4096 $topfolder/keys/rsa/rsa-4096-key.$priv_key_format --pwd secret123" + + execEncTest $res_success \ + "" \ + "aleksey-xmlenc-01/enc-aes256-kt-rsa_oaep_sha3_384" \ + "aes256-cbc rsa-oaep-mgf1p sha3-384 sha1" \ + "" \ + "$priv_key_option:TestKeyName-rsa-4096 $topfolder/keys/rsa/rsa-4096-key.$priv_key_format --pwd secret123" \ + "$pub_key_option:TestKeyName-rsa-4096 $topfolder/keys/rsa/rsa-4096-pubkey$rsa_pub_key_suffix.$pub_key_format --session-key aes-256 --enabled-key-data key-name,enc-key --xml-data $topfolder/aleksey-xmlenc-01/enc-aes256-kt-rsa_oaep_sha3_384.data --node-name http://example.org/paymentv2:CreditCard" \ + "$priv_key_option:TestKeyName-rsa-4096 $topfolder/keys/rsa/rsa-4096-key.$priv_key_format --pwd secret123" + + execEncTest $res_success \ + "" \ + "aleksey-xmlenc-01/enc-aes256-kt-rsa_oaep_sha3_512" \ + "aes256-cbc rsa-oaep-mgf1p sha3-512 sha1" \ + "" \ + "$priv_key_option:TestKeyName-rsa-4096 $topfolder/keys/rsa/rsa-4096-key.$priv_key_format --pwd secret123" \ + "$pub_key_option:TestKeyName-rsa-4096 $topfolder/keys/rsa/rsa-4096-pubkey$rsa_pub_key_suffix.$pub_key_format --session-key aes-256 --enabled-key-data key-name,enc-key --xml-data $topfolder/aleksey-xmlenc-01/enc-aes256-kt-rsa_oaep_sha3_512.data --node-name http://example.org/paymentv2:CreditCard" \ + "$priv_key_option:TestKeyName-rsa-4096 $topfolder/keys/rsa/rsa-4096-key.$priv_key_format --pwd secret123" +fi + + +# same algo for both digest and MGF1 +if [ "z$xmlsec_feature_rsa_oaep_sha1" = "zyes" ] ; then + execEncTest $res_success \ + "" \ + "aleksey-xmlenc-01/enc-aes256-kt-rsa_oaep_sha1_mgf1_sha1" \ + "aes256-cbc rsa-oaep-mgf1p sha1 sha1" \ + "" \ + "$priv_key_option:TestKeyName-rsa-4096 $topfolder/keys/rsa/rsa-4096-key.$priv_key_format --pwd secret123" \ + "$pub_key_option:TestKeyName-rsa-4096 $topfolder/keys/rsa/rsa-4096-pubkey$rsa_pub_key_suffix.$pub_key_format --session-key aes-256 --enabled-key-data key-name,enc-key --xml-data $topfolder/aleksey-xmlenc-01/enc-aes256-kt-rsa_oaep_sha1_mgf1_sha1.data --node-name http://example.org/paymentv2:CreditCard" \ + "$priv_key_option:TestKeyName-rsa-4096 $topfolder/keys/rsa/rsa-4096-key.$priv_key_format --pwd secret123" +fi + +if [ "z$xmlsec_feature_rsa_oaep_sha224" = "zyes" ] ; then + execEncTest $res_success \ + "" \ + "aleksey-xmlenc-01/enc-aes256-kt-rsa_oaep_sha224_mgf1_sha224" \ + "aes256-cbc rsa-oaep-mgf1p sha224 sha224" \ + "" \ + "$priv_key_option:TestKeyName-rsa-4096 $topfolder/keys/rsa/rsa-4096-key$priv_key_suffix.$priv_key_format --pwd secret123" \ + "$pub_key_option:TestKeyName-rsa-4096 $topfolder/keys/rsa/rsa-4096-pubkey$rsa_pub_key_suffix.$pub_key_format --session-key aes-256 --enabled-key-data key-name,enc-key --xml-data $topfolder/aleksey-xmlenc-01/enc-aes256-kt-rsa_oaep_sha224_mgf1_sha224.data --node-name http://example.org/paymentv2:CreditCard" \ + "$priv_key_option:TestKeyName-rsa-4096 $topfolder/keys/rsa/rsa-4096-key$priv_key_suffix.$priv_key_format --pwd secret123" +fi + +if [ "z$xmlsec_feature_rsa_oaep_sha256" = "zyes" ] ; then + execEncTest $res_success \ + "" \ + "aleksey-xmlenc-01/enc-aes256-kt-rsa_oaep_sha256_mgf1_sha256" \ + "aes256-cbc rsa-oaep-mgf1p sha256 sha256" \ + "" \ + "$priv_key_option:TestKeyName-rsa-4096 $topfolder/keys/rsa/rsa-4096-key$priv_key_suffix.$priv_key_format --pwd secret123" \ + "$pub_key_option:TestKeyName-rsa-4096 $topfolder/keys/rsa/rsa-4096-pubkey$rsa_pub_key_suffix.$pub_key_format --session-key aes-256 --enabled-key-data key-name,enc-key --xml-data $topfolder/aleksey-xmlenc-01/enc-aes256-kt-rsa_oaep_sha256_mgf1_sha256.data --node-name http://example.org/paymentv2:CreditCard" \ + "$priv_key_option:TestKeyName-rsa-4096 $topfolder/keys/rsa/rsa-4096-key$priv_key_suffix.$priv_key_format --pwd secret123" +fi + +if [ "z$xmlsec_feature_rsa_oaep_sha384" = "zyes" ] ; then + execEncTest $res_success \ + "" \ + "aleksey-xmlenc-01/enc-aes256-kt-rsa_oaep_sha384_mgf1_sha384" \ + "aes256-cbc rsa-oaep-mgf1p sha384 sha384" \ + "" \ + "$priv_key_option:TestKeyName-rsa-4096 $topfolder/keys/rsa/rsa-4096-key$priv_key_suffix.$priv_key_format --pwd secret123" \ + "$pub_key_option:TestKeyName-rsa-4096 $topfolder/keys/rsa/rsa-4096-pubkey$rsa_pub_key_suffix.$pub_key_format --session-key aes-256 --enabled-key-data key-name,enc-key --xml-data $topfolder/aleksey-xmlenc-01/enc-aes256-kt-rsa_oaep_sha384_mgf1_sha384.data --node-name http://example.org/paymentv2:CreditCard" \ + "$priv_key_option:TestKeyName-rsa-4096 $topfolder/keys/rsa/rsa-4096-key$priv_key_suffix.$priv_key_format --pwd secret123" +fi + +if [ "z$xmlsec_feature_rsa_oaep_sha512" = "zyes" ] ; then + execEncTest $res_success \ + "" \ + "aleksey-xmlenc-01/enc-aes256-kt-rsa_oaep_sha512_mgf1_sha512" \ + "aes256-cbc rsa-oaep-mgf1p sha512 sha512" \ + "" \ + "$priv_key_option:TestKeyName-rsa-4096 $topfolder/keys/rsa/rsa-4096-key$priv_key_suffix.$priv_key_format --pwd secret123" \ + "$pub_key_option:TestKeyName-rsa-4096 $topfolder/keys/rsa/rsa-4096-pubkey$rsa_pub_key_suffix.$pub_key_format --session-key aes-256 --enabled-key-data key-name,enc-key --xml-data $topfolder/aleksey-xmlenc-01/enc-aes256-kt-rsa_oaep_sha512_mgf1_sha512.data --node-name http://example.org/paymentv2:CreditCard" \ + "$priv_key_option:TestKeyName-rsa-4096 $topfolder/keys/rsa/rsa-4096-key$priv_key_suffix.$priv_key_format --pwd secret123" + + # RSA OAEP XMLEnc 1.1 transform (exactly same as 1.0 but different URL) + execEncTest $res_success \ + "" \ + "aleksey-xmlenc-01/enc-aes256-kt-rsa_oaep_enc11_sha512_mgf1_sha512" \ + "aes256-cbc rsa-oaep-enc11 sha512 sha512" \ + "" \ + "$priv_key_option:TestKeyName-rsa-4096 $topfolder/keys/rsa/rsa-4096-key$priv_key_suffix.$priv_key_format --pwd secret123" \ + "$pub_key_option:TestKeyName-rsa-4096 $topfolder/keys/rsa/rsa-4096-pubkey$rsa_pub_key_suffix.$pub_key_format --session-key aes-256 --enabled-key-data key-name,enc-key --xml-data $topfolder/aleksey-xmlenc-01/enc-aes256-kt-rsa_oaep_enc11_sha512_mgf1_sha512.data --node-name http://example.org/paymentv2:CreditCard" \ + "$priv_key_option:TestKeyName-rsa-4096 $topfolder/keys/rsa/rsa-4096-key$priv_key_suffix.$priv_key_format --pwd secret123" +fi + +# same test but decrypt using two different keys +execEncTest $res_success \ + "" \ + "aleksey-xmlenc-01/enc-two-recipients" \ + "tripledes-cbc rsa-1_5" \ + "x509" \ + "--lax-key-search $priv_key_option:pub1 $topfolder/keys/rsa/rsa-2048-key.$priv_key_format --pwd secret123" \ + "--pubkey-cert-$cert_format:pub1 $topfolder/keys/rsa/rsa-2048-cert.$cert_format --pubkey-cert-$cert_format:pub2 $topfolder/keys/rsa/rsa-4096-cert.$cert_format --session-key des-192 --xml-data $topfolder/aleksey-xmlenc-01/enc-two-recipients.data" \ + "--lax-key-search $priv_key_option:pub1 $topfolder/keys/rsa/rsa-2048-key.$priv_key_format --pwd secret123" +execEncTest $res_success \ + "" \ + "aleksey-xmlenc-01/enc-two-recipients" \ + "tripledes-cbc rsa-1_5" \ + "x509" \ + "--lax-key-search $priv_key_option:pub1 $topfolder/keys/rsa/rsa-4096-key.$priv_key_format --pwd secret123" \ + "--pubkey-cert-$cert_format:pub1 $topfolder/keys/rsa/rsa-2048-cert.$cert_format --pubkey-cert-$cert_format:pub2 $topfolder/keys/rsa/rsa-4096-cert.$cert_format --session-key des-192 --xml-data $topfolder/aleksey-xmlenc-01/enc-two-recipients.data" \ + "--lax-key-search $priv_key_option:pub1 $topfolder/keys/rsa/rsa-4096-key.$priv_key_format --pwd secret123" + +########################################################################## +# +# ML-KEM (Key Encapsulation Mechanism, EncapsulationMechanism) +# +########################################################################## +if [ "z$xmlsec_feature_ml_kem" = "zyes" ] ; then +execEncTest $res_success \ + "" \ + "aleksey-xmlenc-01/enc-aes256-em-ml-kem-512" \ + "aes256-cbc ml-kem-512" \ + "ml-kem" \ + "$mlkem_priv_key_option:TestKeyName-ml-kem-512 $topfolder/keys/ml-kem/ml-kem-512-key.$mlkem_priv_key_format --pwd secret123 --enabled-key-data key-name,encapsulation-mechanism" \ + "$mlkem_pub_key_option:TestKeyName-ml-kem-512 $topfolder/keys/ml-kem/ml-kem-512-pubkey.$mlkem_pub_key_format --enabled-key-data key-name,encapsulation-mechanism --xml-data $topfolder/aleksey-xmlenc-01/enc-aes256-em-ml-kem-512.data --node-name http://example.org/paymentv2:CreditCard" \ + "$mlkem_priv_key_option:TestKeyName-ml-kem-512 $topfolder/keys/ml-kem/ml-kem-512-key.$mlkem_priv_key_format --pwd secret123 --enabled-key-data key-name,encapsulation-mechanism" +execEncTest $res_success \ + "" \ + "aleksey-xmlenc-01/enc-aes256-em-ml-kem-768" \ + "aes256-cbc ml-kem-768" \ + "ml-kem" \ + "$mlkem_priv_key_option:TestKeyName-ml-kem-768 $topfolder/keys/ml-kem/ml-kem-768-key.$mlkem_priv_key_format --pwd secret123 --enabled-key-data key-name,encapsulation-mechanism" \ + "$mlkem_pub_key_option:TestKeyName-ml-kem-768 $topfolder/keys/ml-kem/ml-kem-768-pubkey.$mlkem_pub_key_format --enabled-key-data key-name,encapsulation-mechanism --xml-data $topfolder/aleksey-xmlenc-01/enc-aes256-em-ml-kem-768.data --node-name http://example.org/paymentv2:CreditCard" \ + "$mlkem_priv_key_option:TestKeyName-ml-kem-768 $topfolder/keys/ml-kem/ml-kem-768-key.$mlkem_priv_key_format --pwd secret123 --enabled-key-data key-name,encapsulation-mechanism" +execEncTest $res_success \ + "" \ + "aleksey-xmlenc-01/enc-aes256-em-ml-kem-1024" \ + "aes256-cbc ml-kem-1024" \ + "ml-kem" \ + "$mlkem_priv_key_option:TestKeyName-ml-kem-1024 $topfolder/keys/ml-kem/ml-kem-1024-key.$mlkem_priv_key_format --pwd secret123 --enabled-key-data key-name,encapsulation-mechanism" \ + "$mlkem_pub_key_option:TestKeyName-ml-kem-1024 $topfolder/keys/ml-kem/ml-kem-1024-pubkey.$mlkem_pub_key_format --enabled-key-data key-name,encapsulation-mechanism --xml-data $topfolder/aleksey-xmlenc-01/enc-aes256-em-ml-kem-1024.data --node-name http://example.org/paymentv2:CreditCard" \ + "$mlkem_priv_key_option:TestKeyName-ml-kem-1024 $topfolder/keys/ml-kem/ml-kem-1024-key.$mlkem_priv_key_format --pwd secret123 --enabled-key-data key-name,encapsulation-mechanism" +execEncTest $res_success \ + "" \ + "aleksey-xmlenc-01/enc-aes128gcm-em-ml-kem-512" \ + "aes128-gcm ml-kem-512" \ + "ml-kem" \ + "$mlkem_priv_key_option:TestKeyName-ml-kem-512 $topfolder/keys/ml-kem/ml-kem-512-key.$mlkem_priv_key_format --pwd secret123 --enabled-key-data key-name,encapsulation-mechanism" \ + "$mlkem_pub_key_option:TestKeyName-ml-kem-512 $topfolder/keys/ml-kem/ml-kem-512-pubkey.$mlkem_pub_key_format --enabled-key-data key-name,encapsulation-mechanism --xml-data $topfolder/aleksey-xmlenc-01/enc-aes128gcm-em-ml-kem-512.data --node-name http://example.org/paymentv2:CreditCard" \ + "$mlkem_priv_key_option:TestKeyName-ml-kem-512 $topfolder/keys/ml-kem/ml-kem-512-key.$mlkem_priv_key_format --pwd secret123 --enabled-key-data key-name,encapsulation-mechanism" +execEncTest $res_success \ + "" \ + "aleksey-xmlenc-01/enc-aes192gcm-em-ml-kem-768" \ + "aes192-gcm ml-kem-768" \ + "ml-kem" \ + "$mlkem_priv_key_option:TestKeyName-ml-kem-768 $topfolder/keys/ml-kem/ml-kem-768-key.$mlkem_priv_key_format --pwd secret123 --enabled-key-data key-name,encapsulation-mechanism" \ + "$mlkem_pub_key_option:TestKeyName-ml-kem-768 $topfolder/keys/ml-kem/ml-kem-768-pubkey.$mlkem_pub_key_format --enabled-key-data key-name,encapsulation-mechanism --xml-data $topfolder/aleksey-xmlenc-01/enc-aes192gcm-em-ml-kem-768.data --node-name http://example.org/paymentv2:CreditCard" \ + "$mlkem_priv_key_option:TestKeyName-ml-kem-768 $topfolder/keys/ml-kem/ml-kem-768-key.$mlkem_priv_key_format --pwd secret123 --enabled-key-data key-name,encapsulation-mechanism" +execEncTest $res_success \ + "" \ + "aleksey-xmlenc-01/enc-aes256gcm-em-ml-kem-1024" \ + "aes256-gcm ml-kem-1024" \ + "ml-kem" \ + "$mlkem_priv_key_option:TestKeyName-ml-kem-1024 $topfolder/keys/ml-kem/ml-kem-1024-key.$mlkem_priv_key_format --pwd secret123 --enabled-key-data key-name,encapsulation-mechanism" \ + "$mlkem_pub_key_option:TestKeyName-ml-kem-1024 $topfolder/keys/ml-kem/ml-kem-1024-pubkey.$mlkem_pub_key_format --enabled-key-data key-name,encapsulation-mechanism --xml-data $topfolder/aleksey-xmlenc-01/enc-aes256gcm-em-ml-kem-1024.data --node-name http://example.org/paymentv2:CreditCard" \ + "$mlkem_priv_key_option:TestKeyName-ml-kem-1024 $topfolder/keys/ml-kem/ml-kem-1024-key.$mlkem_priv_key_format --pwd secret123 --enabled-key-data key-name,encapsulation-mechanism" +fi # xmlsec_feature_ml_kem + + +########################################################################## +# +# merlin-xmlenc-five +# +########################################################################## + +execEncTest $res_success \ + "" \ + "merlin-xmlenc-five/encrypt-data-aes128-cbc" \ + "aes128-cbc" \ + "" \ + "--keys-file $topfolder/merlin-xmlenc-five/keys.xml" \ + "--keys-file $topfolder/merlin-xmlenc-five/keys.xml --binary-data $topfolder/merlin-xmlenc-five/encrypt-data-aes128-cbc.data" \ + "--keys-file $topfolder/merlin-xmlenc-five/keys.xml" + +execEncTest $res_success \ + "" \ + "merlin-xmlenc-five/encrypt-content-tripledes-cbc" \ + "tripledes-cbc" \ + "" \ + "--keys-file $topfolder/merlin-xmlenc-five/keys.xml" \ + "--keys-file $topfolder/merlin-xmlenc-five/keys.xml --enabled-key-data key-name,enc-key --xml-data $topfolder/merlin-xmlenc-five/encrypt-content-tripledes-cbc.data --node-id Payment" \ + "--keys-file $topfolder/merlin-xmlenc-five/keys.xml" + +execEncTest $res_success \ + "" \ + "merlin-xmlenc-five/encrypt-content-aes256-cbc-prop" \ + "aes256-cbc" \ + "" \ + "--keys-file $topfolder/merlin-xmlenc-five/keys.xml" \ + "--keys-file $topfolder/merlin-xmlenc-five/keys.xml --enabled-key-data key-name,enc-key --xml-data $topfolder/merlin-xmlenc-five/encrypt-content-aes256-cbc-prop.data --node-id Payment" \ + "--keys-file $topfolder/merlin-xmlenc-five/keys.xml" + +execEncTest $res_success \ + "" \ + "merlin-xmlenc-five/encrypt-element-aes192-cbc-ref" \ + "aes192-cbc" \ + "" \ + "--keys-file $topfolder/merlin-xmlenc-five/keys.xml" + +execEncTest $res_success \ + "" \ + "merlin-xmlenc-five/encrypt-element-aes128-cbc-rsa-1_5" \ + "aes128-cbc rsa-1_5" \ + "" \ + "--lax-key-search $priv_key_option $topfolder/merlin-xmlenc-five/rsapriv.$priv_key_format --pwd secret --verification-gmt-time 2003-01-01+10:00:00" \ + "--keys-file $topfolder/merlin-xmlenc-five/keys.xml --session-key aes-128 $priv_key_option:merlin-rsa-key $topfolder/merlin-xmlenc-five/rsapriv.$priv_key_format --xml-data $topfolder/merlin-xmlenc-five/encrypt-element-aes128-cbc-rsa-1_5.data --node-id Purchase --pwd secret" \ + "$priv_key_option:merlin-rsa-key $topfolder/merlin-xmlenc-five/rsapriv.$priv_key_format --pwd secret" + +if [ "z$xmlsec_feature_rsa_oaep_sha1" = "zyes" ] ; then + execEncTest $res_success \ + "" \ + "merlin-xmlenc-five/encrypt-data-tripledes-cbc-rsa-oaep-mgf1p" \ + "tripledes-cbc rsa-oaep-mgf1p sha1" \ + "" \ + "--lax-key-search $priv_key_option $topfolder/merlin-xmlenc-five/rsapriv.$priv_key_format --pwd secret" \ + "--keys-file $topfolder/merlin-xmlenc-five/keys.xml --session-key des-192 $priv_key_option:merlin-rsa-key $topfolder/merlin-xmlenc-five/rsapriv.$priv_key_format --binary-data $topfolder/merlin-xmlenc-five/encrypt-data-tripledes-cbc-rsa-oaep-mgf1p.data --pwd secret" \ + "$priv_key_option:merlin-rsa-key $topfolder/merlin-xmlenc-five/rsapriv.$priv_key_format --pwd secret" +fi + +if [ "z$xmlsec_feature_rsa_oaep_sha1" = "zyes" -a "z$xmlsec_feature_rsa_oaep_different_digest_and_mgf1" = "zyes" ] ; then + execEncTest $res_success \ + "" \ + "merlin-xmlenc-five/encrypt-data-tripledes-cbc-rsa-oaep-mgf1p-sha256" \ + "tripledes-cbc rsa-oaep-mgf1p sha256 sha1" \ + "" \ + "--lax-key-search $priv_key_option $topfolder/merlin-xmlenc-five/rsapriv.$priv_key_format --pwd secret" \ + "--keys-file $topfolder/merlin-xmlenc-five/keys.xml --session-key des-192 $priv_key_option:merlin-rsa-key $topfolder/merlin-xmlenc-five/rsapriv.$priv_key_format --binary-data $topfolder/merlin-xmlenc-five/encrypt-data-tripledes-cbc-rsa-oaep-mgf1p-sha256.data --pwd secret" \ + "$priv_key_option:merlin-rsa-key $topfolder/merlin-xmlenc-five/rsapriv.$priv_key_format --pwd secret" +fi + +execEncTest $res_success \ + "" \ + "merlin-xmlenc-five/encrypt-data-aes256-cbc-kw-tripledes" \ + "aes256-cbc kw-tripledes" \ + "" \ + "--keys-file $topfolder/merlin-xmlenc-five/keys.xml" \ + "--keys-file $topfolder/merlin-xmlenc-five/keys.xml --session-key aes-256 --binary-data $topfolder/merlin-xmlenc-five/encrypt-data-aes256-cbc-kw-tripledes.data" \ + "--keys-file $topfolder/merlin-xmlenc-five/keys.xml" + +execEncTest $res_success \ + "" \ + "merlin-xmlenc-five/encrypt-content-aes128-cbc-kw-aes192" \ + "aes128-cbc kw-aes192" \ + "" \ + "--keys-file $topfolder/merlin-xmlenc-five/keys.xml" \ + "--keys-file $topfolder/merlin-xmlenc-five/keys.xml --session-key aes-128 --node-name urn:example:po:PaymentInfo --xml-data $topfolder/merlin-xmlenc-five/encrypt-content-aes128-cbc-kw-aes192.data" \ + "--keys-file $topfolder/merlin-xmlenc-five/keys.xml" + +execEncTest $res_success \ + "" \ + "merlin-xmlenc-five/encrypt-data-aes192-cbc-kw-aes256" \ + "aes192-cbc kw-aes256" \ + "" \ + "--keys-file $topfolder/merlin-xmlenc-five/keys.xml" \ + "--keys-file $topfolder/merlin-xmlenc-five/keys.xml --session-key aes-192 --binary-data $topfolder/merlin-xmlenc-five/encrypt-data-aes192-cbc-kw-aes256.data" \ + "--keys-file $topfolder/merlin-xmlenc-five/keys.xml" + +execEncTest $res_success \ + "" \ + "merlin-xmlenc-five/encrypt-element-tripledes-cbc-kw-aes128" \ + "tripledes-cbc kw-aes128" \ + "" \ + "--keys-file $topfolder/merlin-xmlenc-five/keys.xml" \ + "--keys-file $topfolder/merlin-xmlenc-five/keys.xml --session-key des-192 --node-name urn:example:po:PaymentInfo --xml-data $topfolder/merlin-xmlenc-five/encrypt-element-tripledes-cbc-kw-aes128.data" \ + "--keys-file $topfolder/merlin-xmlenc-five/keys.xml" + +execEncTest $res_success \ + "" \ + "merlin-xmlenc-five/encrypt-element-aes256-cbc-retrieved-kw-aes256" \ + "aes256-cbc kw-aes256" \ + "" \ + "--keys-file $topfolder/merlin-xmlenc-five/keys.xml" + + +#merlin-xmlenc-five/encrypt-element-aes256-cbc-carried-kw-aes256.xml +#merlin-xmlenc-five/decryption-transform-except.xml +#merlin-xmlenc-five/decryption-transform.xml + +#merlin-xmlenc-five/encrypt-element-aes256-cbc-kw-aes256-dh-ripemd160.xml +#merlin-xmlenc-five/encrypt-content-aes192-cbc-dh-sha512.xml +#merlin-xmlenc-five/encsig-hmac-sha256-dh.xml +#merlin-xmlenc-five/encsig-hmac-sha256-kw-tripledes-dh.xml + +########################################################################## +# +# 01-phaos-xmlenc-3 +# +########################################################################## + +execEncTest $res_success \ + "" \ + "01-phaos-xmlenc-3/enc-element-3des-kt-rsa1_5" \ + "tripledes-cbc rsa-1_5" \ + "" \ + "$priv_key_option:my-rsa-key $topfolder/01-phaos-xmlenc-3/rsa-priv-key.$priv_key_format --pwd secret" \ + "--session-key des-192 --keys-file $topfolder/01-phaos-xmlenc-3/keys.xml --enabled-key-data key-name,enc-key --xml-data $topfolder/01-phaos-xmlenc-3/enc-element-3des-kt-rsa1_5.data --node-name http://example.org/paymentv2:CreditCard" \ + "$priv_key_option:my-rsa-key $topfolder/01-phaos-xmlenc-3/rsa-priv-key.$priv_key_format --pwd secret" + +if [ "z$xmlsec_feature_rsa_oaep_sha1" = "zyes" ] ; then + execEncTest $res_success \ + "" \ + "01-phaos-xmlenc-3/enc-element-3des-kt-rsa_oaep_sha1" \ + "tripledes-cbc rsa-oaep-mgf1p sha1 sha1" \ + "" \ + "$priv_key_option:my-rsa-key $topfolder/01-phaos-xmlenc-3/rsa-priv-key.$priv_key_format --pwd secret" \ + "--session-key des-192 --keys-file $topfolder/01-phaos-xmlenc-3/keys.xml --enabled-key-data key-name,enc-key --xml-data $topfolder/01-phaos-xmlenc-3/enc-element-3des-kt-rsa_oaep_sha1.data --node-name http://example.org/paymentv2:CreditCard" \ + "$priv_key_option:my-rsa-key $topfolder/01-phaos-xmlenc-3/rsa-priv-key.$priv_key_format --pwd secret" +fi + +if [ "z$xmlsec_feature_rsa_oaep_sha1" = "zyes" -a "z$xmlsec_feature_rsa_oaep_different_digest_and_mgf1" = "zyes" ] ; then + execEncTest $res_success \ + "" \ + "01-phaos-xmlenc-3/enc-element-3des-kt-rsa_oaep_sha256" \ + "tripledes-cbc rsa-oaep-mgf1p sha256 sha1" \ + "" \ + "$priv_key_option:my-rsa-key $topfolder/01-phaos-xmlenc-3/rsa-priv-key.$priv_key_format --pwd secret" \ + "--session-key des-192 --keys-file $topfolder/01-phaos-xmlenc-3/keys.xml --enabled-key-data key-name,enc-key --xml-data $topfolder/01-phaos-xmlenc-3/enc-element-3des-kt-rsa_oaep_sha256.data --node-name http://example.org/paymentv2:CreditCard" \ + "$priv_key_option:my-rsa-key $topfolder/01-phaos-xmlenc-3/rsa-priv-key.$priv_key_format --pwd secret" + + execEncTest $res_success \ + "" \ + "01-phaos-xmlenc-3/enc-element-3des-kt-rsa_oaep_sha512" \ + "tripledes-cbc rsa-oaep-mgf1p sha512 sha1" \ + "" \ + "$priv_key_option:my-rsa-key $topfolder/01-phaos-xmlenc-3/rsa-priv-key.$priv_key_format --pwd secret" \ + "--session-key des-192 --keys-file $topfolder/01-phaos-xmlenc-3/keys.xml --enabled-key-data key-name,enc-key --xml-data $topfolder/01-phaos-xmlenc-3/enc-element-3des-kt-rsa_oaep_sha512.data --node-name http://example.org/paymentv2:CreditCard" \ + "$priv_key_option:my-rsa-key $topfolder/01-phaos-xmlenc-3/rsa-priv-key.$priv_key_format --pwd secret" +fi + +execEncTest $res_success \ + "" \ + "01-phaos-xmlenc-3/enc-element-aes128-kt-rsa1_5" \ + "aes128-cbc rsa-1_5" \ + "" \ + "$priv_key_option:my-rsa-key $topfolder/01-phaos-xmlenc-3/rsa-priv-key.$priv_key_format --pwd secret" \ + "--session-key aes-128 --keys-file $topfolder/01-phaos-xmlenc-3/keys.xml --enabled-key-data key-name,enc-key --xml-data $topfolder/01-phaos-xmlenc-3/enc-element-aes128-kt-rsa1_5.data --node-name http://example.org/paymentv2:CreditCard" \ + "$priv_key_option:my-rsa-key $topfolder/01-phaos-xmlenc-3/rsa-priv-key.$priv_key_format --pwd secret" + +if [ "z$xmlsec_feature_rsa_oaep_sha1" = "zyes" ] ; then + execEncTest $res_success \ + "" \ + "01-phaos-xmlenc-3/enc-element-aes128-kt-rsa_oaep_sha1" \ + "aes128-cbc rsa-oaep-mgf1p sha1 sha1" \ + "" \ + "$priv_key_option:my-rsa-key $topfolder/01-phaos-xmlenc-3/rsa-priv-key.$priv_key_format --pwd secret" \ + "--session-key aes-128 --keys-file $topfolder/01-phaos-xmlenc-3/keys.xml --enabled-key-data key-name,enc-key --xml-data $topfolder/01-phaos-xmlenc-3/enc-element-aes128-kt-rsa_oaep_sha1.data --node-name http://example.org/paymentv2:CreditCard" \ + "$priv_key_option:my-rsa-key $topfolder/01-phaos-xmlenc-3/rsa-priv-key.$priv_key_format --pwd secret" + + execEncTest $res_success \ + "" \ + "01-phaos-xmlenc-3/enc-element-aes192-kt-rsa_oaep_sha1" \ + "aes192-cbc rsa-oaep-mgf1p sha1 sha1" \ + "" \ + "$priv_key_option:my-rsa-key $topfolder/01-phaos-xmlenc-3/rsa-priv-key.$priv_key_format --pwd secret" \ + "--session-key aes-192 --keys-file $topfolder/01-phaos-xmlenc-3/keys.xml --enabled-key-data key-name,enc-key --xml-data $topfolder/01-phaos-xmlenc-3/enc-element-aes192-kt-rsa_oaep_sha1.data --node-name http://example.org/paymentv2:CreditCard" \ + "$priv_key_option:my-rsa-key $topfolder/01-phaos-xmlenc-3/rsa-priv-key.$priv_key_format --pwd secret" +fi + +execEncTest $res_success \ + "" \ + "01-phaos-xmlenc-3/enc-text-aes192-kt-rsa1_5" \ + "aes192-cbc rsa-1_5" \ + "" \ + "$priv_key_option:my-rsa-key $topfolder/01-phaos-xmlenc-3/rsa-priv-key.$priv_key_format --pwd secret" \ + "--session-key aes-192 --keys-file $topfolder/01-phaos-xmlenc-3/keys.xml --enabled-key-data key-name,enc-key --xml-data $topfolder/01-phaos-xmlenc-3/enc-text-aes192-kt-rsa1_5.data --node-name http://example.org/paymentv2:CreditCard" \ + "$priv_key_option:my-rsa-key $topfolder/01-phaos-xmlenc-3/rsa-priv-key.$priv_key_format --pwd secret" + +execEncTest $res_success \ + "" \ + "01-phaos-xmlenc-3/enc-content-aes256-kt-rsa1_5" \ + "aes256-cbc rsa-1_5" \ + "" \ + "$priv_key_option:my-rsa-key $topfolder/01-phaos-xmlenc-3/rsa-priv-key.$priv_key_format --pwd secret" \ + "--session-key aes-256 --keys-file $topfolder/01-phaos-xmlenc-3/keys.xml --enabled-key-data key-name,enc-key --xml-data $topfolder/01-phaos-xmlenc-3/enc-content-aes256-kt-rsa1_5.data --node-name http://example.org/paymentv2:CreditCard" \ + "$priv_key_option:my-rsa-key $topfolder/01-phaos-xmlenc-3/rsa-priv-key.$priv_key_format --pwd secret" + + +extra_message="Negative test: missing key" +execEncTest $res_fail \ + "" \ + "01-phaos-xmlenc-3/enc-content-aes256-kt-rsa1_5" \ + "" \ + "" \ + "--keys-file $topfolder/01-phaos-xmlenc-3/keys.xml --enabled-retrieval-method-uris empty" + +if [ "z$xmlsec_feature_rsa_oaep_sha1" = "zyes" ] ; then + execEncTest $res_success \ + "" \ + "01-phaos-xmlenc-3/enc-text-aes256-kt-rsa_oaep_sha1" \ + "aes256-cbc rsa-oaep-mgf1p sha1 sha1" \ + "" \ + "$priv_key_option:my-rsa-key $topfolder/01-phaos-xmlenc-3/rsa-priv-key.$priv_key_format --pwd secret" \ + "--session-key aes-256 --keys-file $topfolder/01-phaos-xmlenc-3/keys.xml --enabled-key-data key-name,enc-key --xml-data $topfolder/01-phaos-xmlenc-3/enc-text-aes256-kt-rsa_oaep_sha1.data --node-name http://example.org/paymentv2:CreditCard" \ + "$priv_key_option:my-rsa-key $topfolder/01-phaos-xmlenc-3/rsa-priv-key.$priv_key_format --pwd secret" +fi + +execEncTest $res_success \ + "" \ + "01-phaos-xmlenc-3/enc-element-3des-kw-3des" \ + "tripledes-cbc kw-tripledes" \ + "" \ + "--keys-file $topfolder/01-phaos-xmlenc-3/keys.xml" \ + "--session-key des-192 --keys-file $topfolder/01-phaos-xmlenc-3/keys.xml --enabled-key-data key-name,enc-key --xml-data $topfolder/01-phaos-xmlenc-3/enc-element-3des-kw-3des.data --node-name http://example.org/paymentv2:CreditCard" \ + "--keys-file $topfolder/01-phaos-xmlenc-3/keys.xml" + +execEncTest $res_success \ + "" \ + "01-phaos-xmlenc-3/enc-content-aes128-kw-3des" \ + "aes128-cbc kw-tripledes" \ + "" \ + "--keys-file $topfolder/01-phaos-xmlenc-3/keys.xml" \ + "--session-key aes-128 --keys-file $topfolder/01-phaos-xmlenc-3/keys.xml --enabled-key-data key-name,enc-key --xml-data $topfolder/01-phaos-xmlenc-3/enc-content-aes128-kw-3des.data --node-name http://example.org/paymentv2:CreditCard" \ + "--keys-file $topfolder/01-phaos-xmlenc-3/keys.xml" + +execEncTest $res_success \ + "" \ + "01-phaos-xmlenc-3/enc-element-aes128-kw-aes128" \ + "aes128-cbc kw-aes128" \ + "" \ + "--keys-file $topfolder/01-phaos-xmlenc-3/keys.xml" \ + "--session-key aes-128 --keys-file $topfolder/01-phaos-xmlenc-3/keys.xml --enabled-key-data key-name,enc-key --xml-data $topfolder/01-phaos-xmlenc-3/enc-element-aes128-kw-aes128.data --node-name http://example.org/paymentv2:CreditCard" \ + "--keys-file $topfolder/01-phaos-xmlenc-3/keys.xml" + +execEncTest $res_success \ + "" \ + "01-phaos-xmlenc-3/enc-element-aes128-kw-aes256" \ + "aes128-cbc kw-aes256" \ + "" \ + "--keys-file $topfolder/01-phaos-xmlenc-3/keys.xml" \ + "--session-key aes-128 --keys-file $topfolder/01-phaos-xmlenc-3/keys.xml --enabled-key-data key-name,enc-key --xml-data $topfolder/01-phaos-xmlenc-3/enc-element-aes128-kw-aes256.data --node-name http://example.org/paymentv2:CreditCard" \ + "--keys-file $topfolder/01-phaos-xmlenc-3/keys.xml" + +execEncTest $res_success \ + "" \ + "01-phaos-xmlenc-3/enc-content-3des-kw-aes192" \ + "tripledes-cbc kw-aes192" \ + "" \ + "--keys-file $topfolder/01-phaos-xmlenc-3/keys.xml" \ + "--session-key des-192 --keys-file $topfolder/01-phaos-xmlenc-3/keys.xml --enabled-key-data key-name,enc-key --xml-data $topfolder/01-phaos-xmlenc-3/enc-content-3des-kw-aes192.data --node-name http://example.org/paymentv2:CreditCard" \ + "--keys-file $topfolder/01-phaos-xmlenc-3/keys.xml" + +execEncTest $res_success \ + "" \ + "01-phaos-xmlenc-3/enc-content-aes192-kw-aes256" \ + "aes192-cbc kw-aes256" \ + "" \ + "--keys-file $topfolder/01-phaos-xmlenc-3/keys.xml" \ + "--session-key aes-192 --keys-file $topfolder/01-phaos-xmlenc-3/keys.xml --enabled-key-data key-name,enc-key --xml-data $topfolder/01-phaos-xmlenc-3/enc-content-aes192-kw-aes256.data --node-name http://example.org/paymentv2:CreditCard" \ + "--keys-file $topfolder/01-phaos-xmlenc-3/keys.xml" + +execEncTest $res_success \ + "" \ + "01-phaos-xmlenc-3/enc-element-aes192-kw-aes192" \ + "aes192-cbc kw-aes192" \ + "" \ + "--keys-file $topfolder/01-phaos-xmlenc-3/keys.xml" \ + "--session-key aes-192 --keys-file $topfolder/01-phaos-xmlenc-3/keys.xml --enabled-key-data key-name,enc-key --xml-data $topfolder/01-phaos-xmlenc-3/enc-element-aes192-kw-aes192.data --node-name http://example.org/paymentv2:CreditCard" \ + "--keys-file $topfolder/01-phaos-xmlenc-3/keys.xml" + +execEncTest $res_success \ + "" \ + "01-phaos-xmlenc-3/enc-element-aes256-kw-aes256" \ + "aes256-cbc kw-aes256" \ + "" \ + "--keys-file $topfolder/01-phaos-xmlenc-3/keys.xml" \ + "--session-key aes-256 --keys-file $topfolder/01-phaos-xmlenc-3/keys.xml --enabled-key-data key-name,enc-key --xml-data $topfolder/01-phaos-xmlenc-3/enc-element-aes256-kw-aes256.data --node-name http://example.org/paymentv2:CreditCard" \ + "--keys-file $topfolder/01-phaos-xmlenc-3/keys.xml" + +execEncTest $res_success \ + "" \ + "01-phaos-xmlenc-3/enc-text-3des-kw-aes256" \ + "tripledes-cbc kw-aes256" \ + "" \ + "--keys-file $topfolder/01-phaos-xmlenc-3/keys.xml" \ + "--session-key des-192 --keys-file $topfolder/01-phaos-xmlenc-3/keys.xml --enabled-key-data key-name,enc-key --xml-data $topfolder/01-phaos-xmlenc-3/enc-text-3des-kw-aes256.data --node-name http://example.org/paymentv2:CreditCard" \ + "--keys-file $topfolder/01-phaos-xmlenc-3/keys.xml" + +execEncTest $res_success \ + "" \ + "01-phaos-xmlenc-3/enc-text-aes128-kw-aes192" \ + "aes128-cbc kw-aes192" \ + "" \ + "--keys-file $topfolder/01-phaos-xmlenc-3/keys.xml" \ + "--session-key aes-128 --keys-file $topfolder/01-phaos-xmlenc-3/keys.xml --enabled-key-data key-name,enc-key --xml-data $topfolder/01-phaos-xmlenc-3/enc-text-aes128-kw-aes192.data --node-name http://example.org/paymentv2:CreditCard" \ + "--keys-file $topfolder/01-phaos-xmlenc-3/keys.xml" + +extra_message="Negative test: bad alg enc element" +execEncTest $res_fail \ + "" \ + "01-phaos-xmlenc-3/bad-alg-enc-element-aes128-kw-3des" \ + "" \ + "" \ + "--keys-file $topfolder/01-phaos-xmlenc-3/keys.xml" + + +#01-phaos-xmlenc-3/enc-element-3des-ka-dh.xml +#01-phaos-xmlenc-3/enc-element-aes128-ka-dh.xml +#01-phaos-xmlenc-3/enc-element-aes192-ka-dh.xml +#01-phaos-xmlenc-3/enc-element-aes256-ka-dh.xml + + +echo "--------- AES-GCM tests include both positive and negative tests ----------" +if [ -z "$XMLSEC_TEST_REPRODUCIBLE" ]; then + echo "--- detailed log is written to $logfile" +fi +########################################################################## +# +# AES-GCM +# +# IV length=96, AAD length=0 and tag length=128 +########################################################################## +aesgcm_key_lengths="128 192 256" +aesgcm_plaintext_lengths="104 128 256 408" +aesgcm_vectors="01 02 03 04 05 06 07 08 09 10 11 12 13 14 15" +for aesgcm_k_l in $aesgcm_key_lengths ; do + for aesgcm_pt_l in $aesgcm_plaintext_lengths ; do + for aesgcm_v in $aesgcm_vectors ; do + base_test_name="nist-aesgcm/aes${aesgcm_k_l}/aes${aesgcm_k_l}-gcm-96-${aesgcm_pt_l}-0-128-${aesgcm_v}" + # If the corresponding *.data file is missing then we expect the test to fail + if [ -f "$topfolder/$base_test_name.xml" -a ! -f "$topfolder/$base_test_name.data" ] ; then + execEncTest "$res_fail" \ + "" \ + "$base_test_name" \ + "aes${aesgcm_k_l}-gcm" \ + "" \ + "--keys-file $topfolder/nist-aesgcm/keys-aes${aesgcm_k_l}-gcm.xml" \ + "" \ + "" + else + # generate binary file out of base64 + DECODE="-d" + if [ "`uname`" = "Darwin" ]; then + DECODE="-D" + fi + cat "$topfolder/$base_test_name.data" | base64 $DECODE > $tmpfile.3 + execEncTest "$res_success" \ + "" \ + "$base_test_name" \ + "aes${aesgcm_k_l}-gcm" \ + "" \ + "--keys-file $topfolder/nist-aesgcm/keys-aes${aesgcm_k_l}-gcm.xml" \ + "--keys-file $topfolder/nist-aesgcm/keys-aes${aesgcm_k_l}-gcm.xml --binary-data $tmpfile.3" \ + "--keys-file $topfolder/nist-aesgcm/keys-aes${aesgcm_k_l}-gcm.xml" \ + "base64" + fi + done + done +done + +########################################################################## +########################################################################## +########################################################################## +echo "--- testEnc finished" >> $logfile +echo "--- testEnc finished" +if [ -z "$XMLSEC_TEST_REPRODUCIBLE" ]; then + echo "--- detailed log is written to $logfile" +fi diff --git a/tools/xmlsec1/tests/fixtures/upstream/testKeys.sh b/tools/xmlsec1/tests/fixtures/upstream/testKeys.sh new file mode 100755 index 0000000..d82e7c5 --- /dev/null +++ b/tools/xmlsec1/tests/fixtures/upstream/testKeys.sh @@ -0,0 +1,613 @@ +#!/bin/sh +# +# This script needs to be called from testrun.sh script +# + +# ensure this script is called from testrun.sh +if [ -z "$xmlsec_app" -o -z "$xmlsec_params" ]; then + echo "This script needs to be called from testrun.sh script" + exit 1 +fi + +########################################################################## +########################################################################## +########################################################################## +if [ -z "$XMLSEC_TEST_REPRODUCIBLE" ]; then + echo "--- testKeys started for xmlsec-$crypto library ($timestamp) ---" +fi +echo "--- LD_LIBRARY_PATH=$LD_LIBRARY_PATH" +echo "--- LTDL_LIBRARY_PATH=$LTDL_LIBRARY_PATH" +if [ -z "$XMLSEC_TEST_REPRODUCIBLE" ]; then + echo "--- log file is $logfile" +fi +echo "--- testKeys started for xmlsec-$crypto library ($timestamp) ---" >> $logfile +echo "--- LD_LIBRARY_PATH=$LD_LIBRARY_PATH" >> $logfile +echo "--- LTDL_LIBRARY_PATH=$LTDL_LIBRARY_PATH" >> $logfile + + +########################################################################## +########################################################################## +########################################################################## +# +# Keys test function +# +execKeysTest() { + execKeysTestWithCryptoConfig "$1" "$2" "$3" "$4" "$5" "$6" "$7" "$8" "$9" "" +} + +execKeysTestWithCryptoConfig() { + expected_res="$1" + req_key_data="$2" + key_name="$3" + alg_name="$4" + privkey_file="$5" + pubkey_file="$6" + certkey_file="$7" + asym_key_test="$8" + key_test_options="$9" + crypto_config="${10}" + failures=0 + + if [ -n "$XMLSEC_TEST_NAME" -a "$XMLSEC_TEST_NAME" != "$key_name" ]; then + return + fi + + # prepare + setupTest + + xmlsec_feature_pkcs12="yes" + xmlsec_feature_pkcs12_persist="no" + xmlsec_feature_pkcs12_keyname="yes" + xmlsec_feature_pkcs8="yes" + xmlsec_feature_privkey_pem="yes" + xmlsec_feature_privkey_der="yes" + xmlsec_feature_pubkey_pem="yes" + xmlsec_feature_pubkey_der="yes" + xmlsec_feature_cert_pem="yes" + xmlsec_feature_cert_der="yes" + xmlsec_feature_gen_key="yes" + + # NSS limitations + if [ "z$crypto" = "znss" ] ; then + xmlsec_feature_pkcs8="no" + xmlsec_feature_privkey_pem="no" + xmlsec_feature_privkey_der="no" + xmlsec_feature_pubkey_pem="no" + + case "$alg_name" in + eddsa-ed25519) + xmlsec_feature_pkcs12="no" + xmlsec_feature_pkcs12_keyname="no" + ;; + eddsa-ed448) + xmlsec_feature_pkcs12="no" + xmlsec_feature_pkcs12_keyname="no" + xmlsec_feature_pubkey_der="no" + xmlsec_feature_cert_pem="no" + xmlsec_feature_cert_der="no" + ;; + esac + fi + + # MSCNG limitations + if [ "z$crypto" = "zmscng" ] ; then + xmlsec_feature_pkcs12_persist="yes" + xmlsec_feature_pkcs8="no" + xmlsec_feature_privkey_pem="no" + xmlsec_feature_privkey_der="no" + xmlsec_feature_pubkey_pem="no" + xmlsec_feature_pubkey_der="no" + xmlsec_feature_cert_pem="no" + fi + + # MSCRYPTO limitations + if [ "z$crypto" = "zmscrypto" ] ; then + xmlsec_feature_pkcs12_keyname="no" + xmlsec_feature_pkcs8="no" + xmlsec_feature_privkey_pem="no" + xmlsec_feature_privkey_der="no" + xmlsec_feature_pubkey_pem="no" + xmlsec_feature_pubkey_der="no" + xmlsec_feature_cert_pem="no" + fi + + # Gcrypt limitations + if [ "z$crypto" = "zgcrypt" ] ; then + xmlsec_feature_pkcs12="no" + xmlsec_feature_pkcs12_keyname="no" + xmlsec_feature_pkcs8="no" + xmlsec_feature_privkey_pem="no" + xmlsec_feature_pubkey_pem="no" + xmlsec_feature_cert_pem="no" + xmlsec_feature_cert_der="no" + fi + + # Keys file path + if test "z$OS_ARCH" = "zCygwin" || test "z$OS_ARCH" = "zMsys" ; then + keysfile=`cygpath -wa $crypto_config_folder/keys.xml` + else + keysfile=$crypto_config_folder/keys.xml + fi + + # check params + if [ "z$expected_res" != "z$res_success" -a "z$expected_res" != "z$res_fail" ] ; then + echo " Bad parameter: expected_res=$expected_res" + tearDownTest + return + fi + if [ "z$crypto_config" = "z" ] ; then + crypto_config="$default_crypto_config" + fi + + # starting test + echo "Test: $alg_name $extra_message" + echo "Test: $alg_name $extra_message -- expected $expected_res" > $curlogfile + extra_message="" + + # check key data + if [ -n "$req_key_data" ] ; then + printf " Checking required key data " + echo "$extra_vars $xmlsec_app check-key-data $xmlsec_params --crypto-config $crypto_config $req_key_data" >> $curlogfile + $xmlsec_app check-key-data $xmlsec_params --crypto-config $crypto_config $req_key_data >> $curlogfile 2>> $curlogfile + printCheckStatus $? + res=$? + if [ $res -ne 0 ]; then + cat $curlogfile >> $logfile + tearDownTest + return + fi + fi + + # run tests + + # generate key + if [ -n "$alg_name" -a -n "$key_name" -a "z$xmlsec_feature_gen_key" = "zyes" ]; then + printf " Creating new key " + params="--gen-key:$key_name $alg_name" + if [ -f $keysfile ] ; then + params="$params --keys-file $keysfile" + fi + echo "$extra_vars $VALGRIND $xmlsec_app keys $params $xmlsec_params --crypto-config $crypto_config $keysfile" >> $curlogfile + $VALGRIND $xmlsec_app keys $params $xmlsec_params --crypto-config $crypto_config $keysfile >> $curlogfile 2>> $curlogfile + printRes $expected_res $? + if [ $? -ne 0 ]; then + failures=`expr $failures + 1` + fi + fi + + # test reading private keys + if [ -n "$privkey_file" -a -n "$asym_key_test" ]; then + if [ "z$xmlsec_feature_pkcs12" = "zyes" ] ; then + printf " Reading private key from pkcs12 file " + rm -f $tmpfile + params="--lax-key-search --pkcs12 $privkey_file.p12 $pkcs12_key_extra_options $key_test_options --output $tmpfile $asym_key_test.tmpl" + echo "$extra_vars $VALGRIND $xmlsec_app sign $xmlsec_params --crypto-config $crypto_config $params" >> $curlogfile + $VALGRIND $xmlsec_app sign $xmlsec_params --crypto-config $crypto_config $params >> $curlogfile 2>> $curlogfile + printRes $expected_res $? + if [ $? -ne 0 ]; then + failures=`expr $failures + 1` + fi + fi + if [ "z$xmlsec_feature_pkcs12_persist" = "zyes" ] ; then + printf " Reading private key from pkcs12 file (persist) " + rm -f $tmpfile + params="--lax-key-search --pkcs12 $privkey_file.p12 $pkcs12_key_extra_options $key_test_options --output $tmpfile $asym_key_test.tmpl" + echo "$extra_vars $VALGRIND $xmlsec_app sign $xmlsec_params --crypto-config $crypto_config $params" >> $curlogfile + $VALGRIND $xmlsec_app sign $xmlsec_params --crypto-config $crypto_config $params >> $curlogfile 2>> $curlogfile + printRes $expected_res $? + if [ $? -ne 0 ]; then + failures=`expr $failures + 1` + fi + fi + if [ "z$xmlsec_feature_pkcs12_keyname" = "zyes" ] ; then + printf " Reading private key name from pkcs12 file " + rm -f $tmpfile + params="--pkcs12 $privkey_file.p12 $pkcs12_key_extra_options $key_test_options --output $tmpfile $asym_key_test.tmpl" + echo "$extra_vars $VALGRIND $xmlsec_app sign $xmlsec_params --crypto-config $crypto_config $params" >> $curlogfile + $VALGRIND $xmlsec_app sign $xmlsec_params --crypto-config $crypto_config $params >> $curlogfile 2>> $curlogfile + printRes $expected_res $? + if [ $? -ne 0 ]; then + failures=`expr $failures + 1` + fi + fi + if [ "z$xmlsec_feature_pkcs12_keyname" = "zyes" -a "z$xmlsec_feature_pkcs12_persist" = "zyes" ] ; then + printf " Reading private key name from pkcs12 file (persist) " + rm -f $tmpfile + params="--pkcs12-persist --pkcs12 $privkey_file.p12 $pkcs12_key_extra_options $key_test_options --output $tmpfile $asym_key_test.tmpl" + echo "$extra_vars $VALGRIND $xmlsec_app sign $xmlsec_params --crypto-config $crypto_config $params" >> $curlogfile + $VALGRIND $xmlsec_app sign $xmlsec_params --crypto-config $crypto_config $params >> $curlogfile 2>> $curlogfile + printRes $expected_res $? + if [ $? -ne 0 ]; then + failures=`expr $failures + 1` + fi + fi + + if [ "z$xmlsec_feature_openssl_store" = "zyes" ] ; then + printf " Reading private key from pkcs12 file using ossl-store " + rm -f $tmpfile + params="--lax-key-search --privkey-openssl-store $privkey_file.p12 $pkcs12_key_extra_options $key_test_options --output $tmpfile $asym_key_test.tmpl" + echo "$extra_vars $VALGRIND $xmlsec_app sign $xmlsec_params --crypto-config $crypto_config $params" >> $curlogfile + $VALGRIND $xmlsec_app sign $xmlsec_params --crypto-config $crypto_config $params >> $curlogfile 2>> $curlogfile + printRes $expected_res $? + if [ $? -ne 0 ]; then + failures=`expr $failures + 1` + fi + fi + + if [ "z$xmlsec_feature_pkcs8" = "zyes" ] ; then + printf " Reading private key from pkcs8 pem file " + rm -f $tmpfile + params="--lax-key-search --pkcs8-pem $privkey_file.p8-pem $key_test_options --output $tmpfile $asym_key_test.tmpl" + echo "$extra_vars $VALGRIND $xmlsec_app sign $xmlsec_params --crypto-config $v $params" >> $curlogfile + $VALGRIND $xmlsec_app sign $xmlsec_params --crypto-config $crypto_config $params >> $curlogfile 2>> $curlogfile + printRes $expected_res $? + if [ $? -ne 0 ]; then + failures=`expr $failures + 1` + fi + fi + + if [ "z$xmlsec_feature_pkcs8" = "zyes" ] ; then + printf " Reading private key from pkcs8 der file " + rm -f $tmpfile + params="--lax-key-search --pkcs8-der $privkey_file.p8-der $key_test_options --output $tmpfile $asym_key_test.tmpl" + echo "$extra_vars $VALGRIND $xmlsec_app sign $xmlsec_params --crypto-config $crypto_config $params" >> $curlogfile + $VALGRIND $xmlsec_app sign $xmlsec_params --crypto-config $crypto_config $params >> $curlogfile 2>> $curlogfile + printRes $expected_res $? + if [ $? -ne 0 ]; then + failures=`expr $failures + 1` + fi + fi + + if [ "z$xmlsec_feature_privkey_pem" = "zyes" ] ; then + printf " Reading private key from pem file " + rm -f $tmpfile + params="--lax-key-search --privkey-pem $privkey_file.pem $key_test_options --output $tmpfile $asym_key_test.tmpl" + echo "$extra_vars $VALGRIND $xmlsec_app sign $xmlsec_params --crypto-config $crypto_config $params" >> $curlogfile + $VALGRIND $xmlsec_app sign $xmlsec_params --crypto-config $crypto_config $params >> $curlogfile 2>> $curlogfile + printRes $expected_res $? + if [ $? -ne 0 ]; then + failures=`expr $failures + 1` + fi + fi + + if [ "z$xmlsec_feature_privkey_der" = "zyes" ] ; then + printf " Reading private key from der file " + rm -f $tmpfile + params="--lax-key-search --privkey-der $privkey_file.der $key_test_options --output $tmpfile $asym_key_test.tmpl" + echo "$extra_vars $VALGRIND $xmlsec_app sign $xmlsec_params --crypto-config $crypto_config $params" >> $curlogfile + $VALGRIND $xmlsec_app sign $xmlsec_params --crypto-config $crypto_config $params >> $curlogfile 2>> $curlogfile + printRes $expected_res $? + if [ $? -ne 0 ]; then + failures=`expr $failures + 1` + fi + fi + fi + + # test reading public keys + if [ -n "$pubkey_file" -a -n "$asym_key_test" ]; then + if [ "z$xmlsec_feature_openssl_store" = "zyes" ] ; then + printf " Reading public key from pem file using ossl-store " + rm -f $tmpfile + params="--lax-key-search --pubkey-openssl-store $pubkey_file.pem $key_test_options $asym_key_test.xml" + echo "$extra_vars $VALGRIND $xmlsec_app verify $xmlsec_params --crypto-config $crypto_config $params" >> $curlogfile + $VALGRIND $xmlsec_app verify $xmlsec_params --crypto-config $crypto_config $params >> $curlogfile 2>> $curlogfile + printRes $expected_res $? + if [ $? -ne 0 ]; then + failures=`expr $failures + 1` + fi + + fi + + if [ "z$xmlsec_feature_pubkey_pem" = "zyes" ] ; then + printf " Reading public key from pem file " + rm -f $tmpfile + params="--lax-key-search --pubkey-pem $pubkey_file.pem $key_test_options $asym_key_test.xml" + echo "$extra_vars $VALGRIND $xmlsec_app verify $xmlsec_params --crypto-config $crypto_config $params" >> $curlogfile + $VALGRIND $xmlsec_app verify $xmlsec_params --crypto-config $crypto_config $params >> $curlogfile 2>> $curlogfile + printRes $expected_res $? + if [ $? -ne 0 ]; then + failures=`expr $failures + 1` + fi + fi + + # gcrypt DER format is very basic + if [ "z$crypto" = "zgcrypt" -a "z$req_key_data" = "zrsa" ] ; then + pubkey_file="$pubkey_file-gcrypt" + fi + if [ "z$xmlsec_feature_pubkey_der" = "zyes" ] ; then + printf " Reading public key from der file " + rm -f $tmpfile + params="--lax-key-search --pubkey-der $pubkey_file.der $key_test_options $asym_key_test.xml" + echo "$extra_vars $VALGRIND $xmlsec_app verify $xmlsec_params --crypto-config $crypto_config $params" >> $curlogfile + $VALGRIND $xmlsec_app verify $xmlsec_params --crypto-config $crypto_config $params >> $curlogfile 2>> $curlogfile + printRes $expected_res $? + if [ $? -ne 0 ]; then + failures=`expr $failures + 1` + fi + fi + fi + + if [ -n "$certkey_file" -a -n "$asym_key_test" ]; then + if [ "z$xmlsec_feature_cert_pem" = "zyes" ] ; then + printf " Reading public key from pem cert file " + rm -f $tmpfile + params="--lax-key-search --pubkey-cert-pem $certkey_file.pem $key_test_options $asym_key_test.xml" + echo "$extra_vars $VALGRIND $xmlsec_app verify $xmlsec_params --crypto-config $crypto_config $params" >> $curlogfile + $VALGRIND $xmlsec_app verify $xmlsec_params --crypto-config $crypto_config $params >> $curlogfile 2>> $curlogfile + printRes $expected_res $? + if [ $? -ne 0 ]; then + failures=`expr $failures + 1` + fi + fi + + if [ "z$xmlsec_feature_cert_der" = "zyes" ] ; then + printf " Reading public key from der cert file " + rm -f $tmpfile + params="--lax-key-search --pubkey-cert-der $certkey_file.der $key_test_options $asym_key_test.xml" + echo "$extra_vars $VALGRIND $xmlsec_app verify $xmlsec_params --crypto-config $crypto_config $params" >> $curlogfile + $VALGRIND $xmlsec_app verify $xmlsec_params --crypto-config $crypto_config $params >> $curlogfile 2>> $curlogfile + printRes $expected_res $? + if [ $? -ne 0 ]; then + failures=`expr $failures + 1` + fi + fi + fi + + # save logs + cat $curlogfile >> $logfile + if [ $failures -ne 0 ] ; then + cat $curlogfile >> $failedlogfile + fi + + # cleanup + tearDownTest +} + +########################################################################## +########################################################################## +########################################################################## +echo "--------- Positive Testing ----------" +execKeysTest $res_success \ + "aes" \ + "test-aes128" \ + "aes-128" + +execKeysTest $res_success \ + "aes" \ + "test-aes192" \ + "aes-192" + +execKeysTest $res_success \ + "aes" \ + "test-aes256" \ + "aes-256" + +execKeysTest $res_success \ + "camellia" \ + "test-camellia128" \ + "camellia-128" + +execKeysTest $res_success \ + "camellia" \ + "test-camellia192" \ + "camellia-192" + +execKeysTest $res_success \ + "camellia" \ + "test-camellia256" \ + "camellia-256" + +execKeysTest $res_success \ + "chacha20" \ + "test-chacha20" \ + "chacha20-256" + +execKeysTest $res_success \ + "concatkdf" \ + "test-concatkdf" \ + "concatkdf-256" + +execKeysTest $res_success \ + "der-encoded-key-value" \ + "" \ + "der-encoded-key-value" + +execKeysTest $res_success \ + "des" \ + "test-des" \ + "des-192" + +# generating large dh keys takes forever +execKeysTest $res_success \ + "dh" \ + "" \ + "dh" + +execKeysTest $res_success \ + "dsa" \ + "test-dsa" \ + "dsa-1024" \ + "$topfolder/keys/dsa/dsa-1024-key" \ + "$topfolder/keys/dsa/dsa-1024-pubkey" \ + "$topfolder/keys/dsa/dsa-1024-cert" \ + "$topfolder/aleksey-xmldsig-01/enveloped-sha1-dsa-sha1" \ + "--pwd secret123 --enabled-key-data key-name" + +execKeysTest $res_success \ + "ec" \ + "" \ + "ec" \ + "$topfolder/keys/ec/ec-prime256v1-key" \ + "$topfolder/keys/ec/ec-prime256v1-pubkey" \ + "$topfolder/keys/ec/ec-prime256v1-cert" \ + "$topfolder/aleksey-xmldsig-01/enveloped-sha1-ecdsa-sha1" \ + "--pwd secret123 --enabled-key-data key-name" + +execKeysTest $res_success \ + "eddsa" \ + "" \ + "eddsa-ed25519" \ + "$topfolder/keys/eddsa/eddsa-ed25519-key" \ + "$topfolder/keys/eddsa/eddsa-ed25519-pubkey" \ + "$topfolder/keys/eddsa/eddsa-ed25519-cert" \ + "$topfolder/aleksey-xmldsig-01/enveloped-sha256-eddsa-ed25519" \ + "--pwd secret123 --enabled-key-data key-name" + +execKeysTest $res_success \ + "eddsa" \ + "" \ + "eddsa-ed448" \ + "$topfolder/keys/eddsa/eddsa-ed448-key" \ + "$topfolder/keys/eddsa/eddsa-ed448-pubkey" \ + "$topfolder/keys/eddsa/eddsa-ed448-cert" \ + "$topfolder/aleksey-xmldsig-01/enveloped-sha256-eddsa-ed448" \ + "--pwd secret123 --enabled-key-data key-name" + +execKeysTest $res_success \ + "gost2001" \ + "" \ + "gost-2001" \ + "$topfolder/keys/gost/gost-2001-key" \ + "$topfolder/keys/gost/gost-2001-pubkey" \ + "$topfolder/keys/gost/gost-2001-cert" \ + "$topfolder/aleksey-xmldsig-01/enveloped-gost2001" \ + "--pwd secret123 --enabled-key-data key-name" + +execKeysTest $res_success \ + "gostr34102012-256" \ + "" \ + "gost-2012-256" \ + "$topfolder/keys/gost/gost-2012-256-key" \ + "$topfolder/keys/gost/gost-2012-256-pubkey" \ + "$topfolder/keys/gost/gost-2012-256-cert" \ + "$topfolder/aleksey-xmldsig-01/enveloped-gost2012-256" \ + "--pwd secret123 --enabled-key-data key-name" + +execKeysTest $res_success \ + "gostr34102012-512" \ + "" \ + "gost-2012-512" \ + "$topfolder/keys/gost/gost-2012-512-key" \ + "$topfolder/keys/gost/gost-2012-512-pubkey" \ + "$topfolder/keys/gost/gost-2012-512-cert" \ + "$topfolder/aleksey-xmldsig-01/enveloped-gost2012-512" \ + "--pwd secret123 --enabled-key-data key-name" + +execKeysTest $res_success \ + "hkdf" \ + "test-hkdf" \ + "hkdf-256" + +execKeysTest $res_success \ + "hmac" \ + "test-hmac-sha1" \ + "hmac-192" + +execKeysTest $res_success \ + "ml-dsa" \ + "" \ + "ml-dsa-44" \ + "$topfolder/keys/ml-dsa/ml-dsa-44-key" \ + "$topfolder/keys/ml-dsa/ml-dsa-44-pubkey" \ + "" \ + "$topfolder/aleksey-xmldsig-01/enveloped-sha512-mldsa44" \ + "--pwd secret123 --enabled-key-data key-name" + +execKeysTest $res_success \ + "ml-dsa" \ + "" \ + "ml-dsa-65" \ + "$topfolder/keys/ml-dsa/ml-dsa-65-key" \ + "$topfolder/keys/ml-dsa/ml-dsa-65-pubkey" \ + "" \ + "$topfolder/aleksey-xmldsig-01/enveloped-sha512-mldsa65" \ + "--pwd secret123 --enabled-key-data key-name" + +execKeysTest $res_success \ + "ml-dsa" \ + "" \ + "ml-dsa-87" \ + "$topfolder/keys/ml-dsa/ml-dsa-87-key" \ + "$topfolder/keys/ml-dsa/ml-dsa-87-pubkey" \ + "" \ + "$topfolder/aleksey-xmldsig-01/enveloped-sha512-mldsa87" \ + "--pwd secret123 --enabled-key-data key-name" + +execKeysTest $res_success \ + "ml-kem" \ + "" \ + "ml-kem-512" \ + "$topfolder/keys/ml-kem/ml-kem-512-key" \ + "$topfolder/keys/ml-kem/ml-kem-512-pubkey" \ + "" \ + "" \ + "--pwd secret123" + +execKeysTest $res_success \ + "ml-kem" \ + "" \ + "ml-kem-768" \ + "$topfolder/keys/ml-kem/ml-kem-768-key" \ + "$topfolder/keys/ml-kem/ml-kem-768-pubkey" \ + "" \ + "" \ + "--pwd secret123" + +execKeysTest $res_success \ + "ml-kem" \ + "" \ + "ml-kem-1024" \ + "$topfolder/keys/ml-kem/ml-kem-1024-key" \ + "$topfolder/keys/ml-kem/ml-kem-1024-pubkey" \ + "" \ + "" \ + "--pwd secret123" + +execKeysTest $res_success \ + "pbkdf2" \ + "test-pbkdf2" \ + "pbkdf2-256" + +execKeysTest $res_success \ + "raw-x509-cert" \ + "" \ + "raw-x509-cert" + +execKeysTest $res_success \ + "rsa" \ + "test-rsa" \ + "rsa-1024" \ + "$topfolder/keys/rsa/rsa-4096-key" \ + "$topfolder/keys/rsa/rsa-4096-pubkey" \ + "$topfolder/keys/rsa/rsa-4096-cert" \ + "$topfolder/aleksey-xmldsig-01/enveloped-sha1-rsa-sha1" \ + "--pwd secret123 --enabled-key-data key-name" + +execKeysTest $res_success \ + "slh-dsa" \ + "" \ + "slh-dsa-sha2-128f" \ + "$topfolder/keys/slh-dsa/slh-dsa-sha2-128f-key" \ + "$topfolder/keys/slh-dsa/slh-dsa-sha2-128f-pubkey" \ + "" \ + "$topfolder/aleksey-xmldsig-01/enveloped-sha512-slhdsa-sha2-128f" \ + "--pwd secret123 --enabled-key-data key-name" + +execKeysTest $res_success \ + "x509" \ + "" \ + "x509" + +execKeysTest $res_success \ + "xdh" \ + "" \ + "xdh" + +########################################################################## +########################################################################## +########################################################################## +echo "--- testKeys finished ---" >> $logfile +echo "--- testKeys finished ---" +if [ -z "$XMLSEC_TEST_REPRODUCIBLE" ]; then + echo "--- detailed log is written to $logfile ---" +fi diff --git a/tools/xmlsec1/tests/fixtures/upstream/testrun.sh b/tools/xmlsec1/tests/fixtures/upstream/testrun.sh new file mode 100755 index 0000000..ef23a4a --- /dev/null +++ b/tools/xmlsec1/tests/fixtures/upstream/testrun.sh @@ -0,0 +1,590 @@ +#!/bin/sh -x + +OS_ARCH=`uname -o 2>/dev/null || echo ""` +OS_KERNEL=`uname -s` + +# +# Get command line params +# +testfile="$1" +crypto="$2" +topfolder="$3" +xmlsec_app="$4" +file_format="$5" +timestamp=`date +%Y%m%d_%H%M%S` +exit_code=0 + +if test "z$OS_ARCH" = "zCygwin" || test "z$OS_ARCH" = "zMsys" ; then + topfolder=`cygpath -wa "$topfolder"` + xmlsec_app=`cygpath -a "$xmlsec_app"` +fi + +# Ensure we get detailed errors +xmlsec_params="--verbose --print-crypto-library-errors" + +# +# Prepare folders +# +if [ "z$TMPFOLDER" = "z" ] ; then + TMPFOLDER=/tmp +fi +testname=`basename $testfile` +testfolder=$TMPFOLDER/xmlsec-$testname-$crypto-$timestamp +mkdir -p $testfolder + +if test "z$OS_ARCH" = "zCygwin" || test "z$OS_ARCH" = "zMsys" ; then + tmpfile=`cygpath -wa $testfolder/tmp.tmp` + logfile=`cygpath -wa $testfolder/full.log` + curlogfile=`cygpath -wa $testfolder/cur.log` + failedlogfile=`cygpath -wa $testfolder/failed.log` +else + tmpfile=$testfolder/tmp.tmp + logfile=$testfolder/full.log + curlogfile=$testfolder/cur.log + failedlogfile=$testfolder/failed.log +fi + +# +# Valgrind +# +if [ "z$crypto" = "zopenssl" ] ; then + valgrind_suppression="--suppressions=$topfolder/valgrind-openssl.supp" +elif [ "z$crypto" = "znss" ] ; then + valgrind_suppression="--suppressions=$topfolder/valgrind-nss.supp" +elif [ "z$crypto" = "zgcrypt" ] ; then + valgrind_suppression="--suppressions=$topfolder/valgrind-gcrypt.supp" +elif [ "z$crypto" = "zgnutls" ] ; then + valgrind_suppression="--suppressions=$topfolder/valgrind-gcrypt.supp" +else + valgrind_suppression="" +fi + +valgrind_options="--leak-check=full --show-reachable=yes --num-callers=32 --track-origins=yes -s" +if [ -n "$DEBUG_MEMORY" ] ; then + export VALGRIND="valgrind $valgrind_options $valgrind_suppression" + export REPEAT=3 + xmlsec_params="$xmlsec_params --repeat $REPEAT" +fi + + +# +# Setup crypto engine +# +if [ "z$XMLSEC_DEFAULT_CRYPTO" != "z" ] ; then + xmlsec_params="$xmlsec_params --crypto $XMLSEC_DEFAULT_CRYPTO" +elif [ "z$crypto" != "z" ] ; then + xmlsec_params="$xmlsec_params --crypto $crypto" +fi + +# +# Setup extra vars +# +extra_vars= +if [ "z$crypto" = "zopenssl" -a "z$XMLSEC_OPENSSL_TEST_CONFIG" != "z" ] ; then + if test "z$OS_ARCH" = "zCygwin" || test "z$OS_ARCH" = "zMsys" ; then + opensslconf=`cygpath -wa $topfolder/$XMLSEC_OPENSSL_TEST_CONFIG` + else + opensslconf=$topfolder/$XMLSEC_OPENSSL_TEST_CONFIG + fi + extra_vars="$extra_vars OPENSSL_CONF=$opensslconf" + export OPENSSL_CONF="$opensslconf" +fi + +# +# Configure supported features +# +case $XMLSEC_OPENSSL_VERSION in +*LibreSSL*) + xmlsec_openssl_flavor="libressl" + ;; +*BoringSSL*) + xmlsec_openssl_flavor="boringssl" + ;; +*AWSLC*) + xmlsec_openssl_flavor="aws-lc" + ;; +*) + xmlsec_openssl_flavor="openssl" + ;; +esac + +# only original openssl supports --privkey-openssl-store +if [ "z$crypto" = "zopenssl" -a "z$xmlsec_openssl_flavor" = "zopenssl" ] ; then + xmlsec_feature_openssl_store="yes" +else + xmlsec_feature_openssl_store="no" +fi + +# phaos certs use RSA-MD5 which might be disabled +if [ "z$crypto" = "zopenssl" -a "z$xmlsec_openssl_flavor" != "zaws-lc" -a "z$xmlsec_openssl_flavor" != "zboringssl" ] ; then + extra_vars="$extra_vars OPENSSL_ENABLE_MD5_VERIFY=1" + export OPENSSL_ENABLE_MD5_VERIFY=1 + + xmlsec_feature_md5_certs="yes" +else + xmlsec_feature_md5_certs="no" +fi + + +# Only OpenSSL / NSS / GnuTLS currently has capability to lookup the certs/keys using X509 data +if [ "z$crypto" = "zopenssl" -o "z$crypto" = "znss" -o "z$crypto" = "zgnutls" ] ; then + xmlsec_feature_x509_data_lookup="yes" +else + xmlsec_feature_x509_data_lookup="no" +fi + +# MSCng only supports SHA1 as cert digests and cannot lookup the key +if [ "z$crypto" = "zopenssl" -o "z$crypto" = "znss" -o "z$crypto" = "zgnutls" -o "z$crypto" = "zmscng" ] ; then + xmlsec_feature_x509_data_lookup_digest_sha1="yes" +else + xmlsec_feature_x509_data_lookup_digest_sha1="no" +fi +if [ "z$crypto" = "zopenssl" -o "z$crypto" = "znss" -o "z$crypto" = "zgnutls" ] ; then + xmlsec_feature_x509_data_lookup_digest_sha224="yes" +else + xmlsec_feature_x509_data_lookup_digest_sha224="no" +fi +if [ "z$crypto" = "zopenssl" -o "z$crypto" = "znss" -o "z$crypto" = "zgnutls" -o "z$crypto" = "zmscng" ] ; then + xmlsec_feature_x509_data_lookup_digest_sha256="yes" +else + xmlsec_feature_x509_data_lookup_digest_sha256="no" +fi +if [ "z$crypto" = "zopenssl" -o "z$crypto" = "znss" -o "z$crypto" = "zgnutls" ] ; then + xmlsec_feature_x509_data_lookup_digest_sha384="yes" +else + xmlsec_feature_x509_data_lookup_digest_sha384="no" +fi +if [ "z$crypto" = "zopenssl" -o "z$crypto" = "znss" -o "z$crypto" = "zgnutls" ] ; then + xmlsec_feature_x509_data_lookup_digest_sha512="yes" +else + xmlsec_feature_x509_data_lookup_digest_sha512="no" +fi +if [ "z$crypto" = "zopenssl" -o "z$crypto" = "znss" -o "z$crypto" = "zgnutls" ] ; then + xmlsec_feature_x509_data_lookup_digest_sha3="yes" +else + xmlsec_feature_x509_data_lookup_digest_sha3="no" +fi + + +# Only NSS can lookup certs in NSS DB, skip certs verification for signatures +if [ "z$crypto" = "znss" ] ; then + xmlsec_feature_nssdb_lookup="yes" +else + xmlsec_feature_nssdb_lookup="no" +fi + +# currently only openssl and gnutls support skipping time checks +# https://github.com/lsh123/xmlsec/issues/852 +if [ "z$crypto" = "zopenssl" -o "z$crypto" = "zgnutls" -o "z$crypto" = "zmscng" ] ; then + xmlsec_feature_cert_check_skip_time="yes" +else + xmlsec_feature_cert_check_skip_time="no" +fi + +# currently only openssl/gnutls/nss/mscng support loading CRL from the command line +# https://github.com/lsh123/xmlsec/issues/583 +if [ "z$crypto" = "zopenssl" -o "z$crypto" = "zgnutls" -o "z$crypto" = "znss" -o "z$crypto" = "zmscng" ] ; then + xmlsec_feature_crl_load="yes" +else + xmlsec_feature_crl_load="no" +fi + +# only openssl/gnutls/nss/mscng support crl verification +# https://github.com/lsh123/xmlsec/issues/585 +if [ "z$crypto" = "zopenssl" -o "z$crypto" = "zgnutls" -o "z$crypto" = "znss" -o "z$crypto" = "zmscng" ] ; then + xmlsec_feature_crl_verification="yes" +else + xmlsec_feature_crl_verification="no" +fi + +# currently only openssl/mscng support CRL verification by time +# https://github.com/lsh123/xmlsec/issues/579 +if [ "z$crypto" = "zopenssl" -o "z$crypto" = "zmscng" ] ; then + xmlsec_feature_crl_check_skip_time="yes" +else + xmlsec_feature_crl_check_skip_time="no" +fi + +# only openssl, gnutls, nss, and mcng support key verification +# https://github.com/lsh123/xmlsec/issues/587 +if [ "z$crypto" = "zopenssl" -o "z$crypto" = "zgnutls" -o "z$crypto" = "znss" -o "z$crypto" = "zmscng" ] ; then + xmlsec_feature_key_check="yes" +else + xmlsec_feature_key_check="no" +fi + + +# Advanced RSA OAEP modes: +# - GnuTLS: digest/MFG1 must be same, only supports SHA-256, SHA-384, SHA-512 for RSA-OAEP hash (not SHA-1) +# - MSCng: digest/MFG1 must be same +# - MSCrypto: digest/MFG1 must be same, only supports SHA1 for digest and mgf1 +# - GCrypt: digest/MFG1 must be same +if [ "z$crypto" != "zgnutls" -a "z$crypto" != "zmscng" -a "z$crypto" != "zmscrypto" -a "z$crypto" != "zgcrypt" ] ; then + xmlsec_feature_rsa_oaep_different_digest_and_mgf1="yes" +else + xmlsec_feature_rsa_oaep_different_digest_and_mgf1="no" +fi +if [ "z$crypto" != "zgnutls" ] ; then + xmlsec_feature_rsa_oaep_sha1="yes" +else + xmlsec_feature_rsa_oaep_sha1="no" +fi +if [ "z$crypto" != "zgnutls" -a "z$crypto" != "zmscrypto" ] ; then + xmlsec_feature_rsa_oaep_sha224="yes" +else + xmlsec_feature_rsa_oaep_sha224="no" +fi +if [ "z$crypto" != "zmscrypto" ] ; then + xmlsec_feature_rsa_oaep_sha256="yes" +else + xmlsec_feature_rsa_oaep_sha256="no" +fi +if [ "z$crypto" != "zmscrypto" ] ; then + xmlsec_feature_rsa_oaep_sha384="yes" +else + xmlsec_feature_rsa_oaep_sha384="no" +fi +if [ "z$crypto" != "zmscrypto" ] ; then + xmlsec_feature_rsa_oaep_sha512="yes" +else + xmlsec_feature_rsa_oaep_sha512="no" +fi +if [ "z$crypto" != "znss" ] ; then + xmlsec_feature_rsa_oaep_sha3="yes" +else + xmlsec_feature_rsa_oaep_sha3="no" +fi + + +# Support for ASN1 signatures +if [ "z$crypto" = "zopenssl" -o "z$crypto" = "zgnutls" -o "z$crypto" = "znss" -o "z$crypto" = "zmscng" ] ; then + xmlsec_feature_asn1_signatures="yes" +else + xmlsec_feature_asn1_signatures="no" +fi + +# Support for context string in ML-DSA or SLH-DSA signatures +if [ "z$crypto" = "zopenssl" ] ; then + xmlsec_feature_context_string="yes" +else + xmlsec_feature_context_string="no" +fi + +# Support for ML-KEM key transport (OpenSSL 3.5+) +if [ "z$crypto" = "zopenssl" ] ; then + xmlsec_feature_ml_kem="yes" +else + xmlsec_feature_ml_kem="no" +fi + +# +# Setup keys config +# +cert_format=$file_format + +# +# MSCrypto needs persistent keys for pkcs12 +# +pkcs12_key_extra_options="" +if [ "z$crypto" = "zmscrypto" ] ; then + pkcs12_key_extra_options="--pkcs12-persist $pkcs12_key_extra_options" +fi + +# +# GCrypt only supports DER format for now, others are good to go with PKCS12 for private keys +# +if [ "z$crypto" != "zgcrypt" ] ; then + priv_key_option="$pkcs12_key_extra_options --pkcs12" + priv_key_format="p12" +else + priv_key_option="--privkey-der" + priv_key_format="der" +fi + +# +# NSS cannot import XDH (X25519/X448) private keys from OpenSSL-3.x-generated +# PKCS12 files (SEC_ERROR_PKCS12_UNABLE_TO_IMPORT_KEY). MSCng cannot import +# them either because PFXImportCertStore does not support Curve25519/Curve448. +# Use unencrypted DER (PrivateKeyInfo) format for XDH keys in both cases. +# +if [ "z$crypto" = "znss" -o "z$crypto" = "zmscng" ] ; then + xdh_priv_key_option="--privkey-der" + xdh_priv_key_format="der" +else + xdh_priv_key_option="$priv_key_option" + xdh_priv_key_format="$priv_key_format" +fi + +# +# NSS cannot import EdDSA (ED25519/ED448) private keys from OpenSSL-3.x-generated +# PKCS12 files (SEC_ERROR_PKCS12_UNABLE_TO_IMPORT_KEY). Use unencrypted DER +# (PrivateKeyInfo) format for EdDSA keys when running under NSS. +# +if [ "z$crypto" = "znss" ] ; then + eddsa_priv_key_option="--privkey-der" + eddsa_priv_key_format="der" +else + eddsa_priv_key_option="$priv_key_option" + eddsa_priv_key_format="$priv_key_format" +fi + +# +# GnuTLS cannot import EC keys from OpenSSL-3.x-generated xmlenc11-interop-2012 +# PKCS12 files (PBES2/PBKDF2/AES-256-CBC encryption not supported). +# Use unencrypted DER (PrivateKeyInfo) format for those specific interop tests. +# +if [ "z$crypto" = "zgnutls" ] ; then + ec_interop_priv_key_option="--privkey-der" + ec_interop_priv_key_format="der" +else + ec_interop_priv_key_option="$priv_key_option" + ec_interop_priv_key_format="$priv_key_format" +fi + +# +# Windows MSCng cannot load X9.42 DH keys from OpenSSL-generated PKCS12 files. +# Use unencrypted DER (PrivateKeyInfo/SubjectPublicKeyInfo) format instead. +# +if [ "z$crypto" = "zmscng" ] ; then + dh_interop_priv_key_option="--privkey-der" + dh_interop_priv_key_format="der" +else + dh_interop_priv_key_option="$priv_key_option" + dh_interop_priv_key_format="$priv_key_format" +fi + +# +# Windows MSCng cannot load DHX private/public keys from PEM files. +# Use unencrypted DER (PrivateKeyInfo/SubjectPublicKeyInfo) format instead. +# +if [ "z$crypto" = "zmscng" ] ; then + dhx_priv_key_option="--privkey-der" + dhx_priv_key_format="der" + dhx_pub_key_option="--pubkey-der" + dhx_pub_key_format="der" +else + dhx_priv_key_option="--privkey-pem" + dhx_priv_key_format="pem" + dhx_pub_key_option="--pubkey-pem" + dhx_pub_key_format="pem" +fi + +# +# GCrypt only supports DER format for now, others are good to go with certs for public keys +# +if [ "z$crypto" != "zgcrypt" ] ; then + pub_key_option="--pubkey-cert-der" + pub_key_format="crt" +else + pub_key_option="--pubkey-der" + pub_key_format="der" +fi +# GCrypt has problems reading public RSA keys and needs special handling +if [ "z$crypto" = "zgcrypt" ] ; then + rsa_pub_key_suffix="-gcrypt" +else + rsa_pub_key_suffix="" +fi + +# ML-KEM keys have no certificates and cannot be stored in PKCS12. +# Use PKCS8-PEM format for private keys and PEM for public keys on all platforms. +mlkem_priv_key_option="--pkcs8-pem" +mlkem_priv_key_format="p8-pem" +mlkem_pub_key_option="--pubkey-pem" +mlkem_pub_key_format="pem" + +# On Windows, we needs to specify Crypto Service Provider (CSP) +# in the pkcs12 file to ensure it is loaded correctly to be used +# with SHA2 algorithms +if [ "z$crypto" = "zmscrypto" -o "z$crypto" = "zmscng" ] ; then + priv_key_suffix="-win" +else + priv_key_suffix="" +fi + + +# +# Misc +# +if [ -n "$PERF_TEST" ] ; then + xmlsec_params="$xmlsec_params --repeat $PERF_TEST" +fi + +if test "z$OS_ARCH" = "zCygwin" || test "z$OS_ARCH" = "zMsys" ; then + diff_param=-uw +else + diff_param=-u +fi + + +# +# Setup crypto config folder +# +config_number=0 +setupCryptoConfig() { + config_number=$((config_number + 1)) + if test "z$OS_ARCH" = "zCygwin" || test "z$OS_ARCH" = "zMsys" ; then + crypto_config_folder=`cygpath -wa $testfolder/crypto-config-$config_number` + else + crypto_config_folder=$testfolder/crypto-config-$config_number + fi + mkdir $crypto_config_folder + + # see https://learn.microsoft.com/en-us/windows/win32/api/wincrypt/nf-wincrypt-certopensystemstorea + if [ "z$crypto" = "zmscng" ] ; then + default_crypto_config="MY" + else + default_crypto_config="$crypto_config_folder" + fi +} + +tearDownCryptoConfig() { + if [ -n "$crypto_config_folder" ]; then + rm -rf $crypto_config_folder + fi + unset crypto_config_folder + unset default_crypto_config +} + +setupTest() { + # prepare + old_pwd=`pwd` + setupCryptoConfig +} + +tearDownTest() { + # cleanup + tearDownCryptoConfig + rm -f $tmpfile $tmpfile.2 $tmpfile.3 + if [ -n "$old_pwd" ]; then + cd $old_pwd + fi + unset old_pwd +} + +# +# Check the command result and print it to stdout +# +res_success="success" +res_fail="fail" +count_success=0 +count_fail=0 +count_skip=0 +printRes() { + expected_res="$1" + actual_res="$2" + + # convert status to string + if [ $actual_res -eq 0 ]; then + actual_res_str=$res_success + else + actual_res_str=$res_fail + fi + + # check + if [ "z$expected_res" = "z$actual_res_str" ] ; then + count_success=`expr $count_success + 1` + actual_res="0" + echo " OK" + else + count_fail=`expr $count_fail + 1` + actual_res="1" + echo " Fail" + fi + + # memlog + if [ -f .memdump ] ; then + cat .memdump >> $curlogfile + fi + + return "$actual_res" +} + +printCheckStatus() { + check_res="$1" + if [ $check_res -eq 0 ]; then + echo " OK" + else + count_skip=`expr $count_skip + 1` + echo " Skip" + fi + return "$check_res" +} + +extra_message="" + +# prepare +rm -rf $tmpfile $tmpfile.2 $tmpfile.3 + +# run tests +source "$testfile" + +# calculate success +percent_success=0 +count_total=`expr $count_success + $count_fail + $count_skip` +if [ $count_total -gt 0 ] ; then + percent_success=`expr 100 \* $count_success / $count_total` +fi + +if [ "z$crypto" = "zopenssl" -a "z$xmlsec_openssl_flavor" = "zaws-lc" ] ; then + # bunch of tests with MD5 certificates are disabled + echo "--- OPENSSL FLAVOR: $xmlsec_openssl_flavor" >> $logfile + echo "--- OPENSSL FLAVOR: $xmlsec_openssl_flavor" + min_percent_success=75 +elif [ "z$crypto" = "zopenssl" -a "z$xmlsec_openssl_flavor" = "zboringssl" ] ; then + # bunch of tests with MD5 certificates are disabled + echo "--- OPENSSL FLAVOR: $xmlsec_openssl_flavor" >> $logfile + echo "--- OPENSSL FLAVOR: $xmlsec_openssl_flavor" + min_percent_success=75 +elif [ "z$crypto" = "zopenssl" ] ; then + echo "--- OPENSSL FLAVOR: $xmlsec_openssl_flavor" >> $logfile + echo "--- OPENSSL FLAVOR: $xmlsec_openssl_flavor" + min_percent_success=90 +elif [ "z$crypto" = "znss" ] ; then + min_percent_success=75 +elif [ "z$crypto" = "zgnutls" ] ; then + min_percent_success=75 +elif [ "z$crypto" = "zmscng" ] ; then + min_percent_success=75 +elif [ "z$crypto" = "zmscrypto" ] ; then + min_percent_success=30 +elif [ "z$crypto" = "zgcrypt" ] ; then + min_percent_success=30 +else + min_percent_success=50 +fi + + +# print results +echo "--- TOTAL OK: $count_success; OK (percent): $percent_success; TOTAL FAILED: $count_fail; TOTAL SKIPPED: $count_skip" >> $logfile +echo "--- TOTAL OK: $count_success; OK (percent): $percent_success; TOTAL FAILED: $count_fail; TOTAL SKIPPED: $count_skip" + +# disable this check for test jeys since the number of tests is very small and the success percent is not representative +if [[ "$testfile" =~ 'testKeys' ]]; then + XMLSEC_TEST_IGNORE_PERCENT_SUCCESS=1 + echo "--- SUCCESS PERCENT check is disabled for testKeys tests since the number of tests is very small and the success percent is not representative" >> $logfile + echo "--- SUCCESS PERCENT check is disabled for testKeys tests since the number of tests is very small and the success percent is not representative" +fi + +# print log file if failed (we have to have at least some good tests) +if [ $count_fail -ne 0 ] ; then + cat $failedlogfile + exit_code=$count_fail +elif [ $count_success -eq 0 ] ; then + cat $logfile + exit_code=1 +elif [ -z "$XMLSEC_TEST_IGNORE_PERCENT_SUCCESS" -a $min_percent_success -gt $percent_success ]; then + echo "--- SUCCESS PERCENT $percent_success IS LOWER THAN THE EXPECTED $min_percent_success PERCENT, FAILING TESTS" >> $logfile + echo "--- If you disabled some features and expect lower success percent then set environment variable 'XMLSEC_TEST_IGNORE_PERCENT_SUCCESS' before running the test" >> $logfile + + echo "--- SUCCESS PERCENT $percent_success IS LOWER THAN THE EXPECTED $min_percent_success PERCENT, FAILING TESTS" + echo "--- If you disabled some features and expect lower success percent then set environment variable 'XMLSEC_TEST_IGNORE_PERCENT_SUCCESS' before running the test" + + cat $logfile + exit_code=1 +fi + +# cleanup +rm -rf $tmpfile $tmpfile.2 tmpfile.3 $curlogfile + +exit $exit_code diff --git a/tools/xmlsec1/tests/fixtures/upstream/xmlenc11-interop-2012/xenc11-example-AES128-GCM.data b/tools/xmlsec1/tests/fixtures/upstream/xmlenc11-interop-2012/xenc11-example-AES128-GCM.data new file mode 100644 index 0000000..f1e3193 --- /dev/null +++ b/tools/xmlsec1/tests/fixtures/upstream/xmlenc11-interop-2012/xenc11-example-AES128-GCM.data @@ -0,0 +1 @@ +12%Y ů& \ No newline at end of file diff --git a/tools/xmlsec1/tests/fixtures/upstream/xmlenc11-interop-2012/xenc11-example-AES128-GCM.key b/tools/xmlsec1/tests/fixtures/upstream/xmlenc11-interop-2012/xenc11-example-AES128-GCM.key new file mode 100644 index 0000000..767ebda --- /dev/null +++ b/tools/xmlsec1/tests/fixtures/upstream/xmlenc11-interop-2012/xenc11-example-AES128-GCM.key @@ -0,0 +1 @@ +钆esmjg0 \ No newline at end of file diff --git a/tools/xmlsec1/tests/fixtures/upstream/xmlenc11-interop-2012/xenc11-example-AES128-GCM.tmpl b/tools/xmlsec1/tests/fixtures/upstream/xmlenc11-interop-2012/xenc11-example-AES128-GCM.tmpl new file mode 100644 index 0000000..db76bee --- /dev/null +++ b/tools/xmlsec1/tests/fixtures/upstream/xmlenc11-interop-2012/xenc11-example-AES128-GCM.tmpl @@ -0,0 +1,14 @@ + + + + + TestKeyName_GCM + + + + + diff --git a/tools/xmlsec1/tests/fixtures/upstream/xmlenc11-interop-2012/xenc11-example-AES128-GCM.xml b/tools/xmlsec1/tests/fixtures/upstream/xmlenc11-interop-2012/xenc11-example-AES128-GCM.xml new file mode 100644 index 0000000..ccd26ab --- /dev/null +++ b/tools/xmlsec1/tests/fixtures/upstream/xmlenc11-interop-2012/xenc11-example-AES128-GCM.xml @@ -0,0 +1,16 @@ + + + + + Test Key 1 + + + + yv66vvrO263eyviIQoMewiF3dCRLciG3hNDUnFeSbd6SpcAe6FTcmzPryFY= + + + diff --git a/tools/xmlsec1/tests/process_contract.rs b/tools/xmlsec1/tests/process_contract.rs index 8f30633..06f5b16 100644 --- a/tools/xmlsec1/tests/process_contract.rs +++ b/tools/xmlsec1/tests/process_contract.rs @@ -1,5 +1,10 @@ use std::{fs, path::Path, process::Command}; +use rsa::{ + RsaPrivateKey, + pkcs8::{DecodePrivateKey as _, EncodePrivateKey as _}, +}; + fn binary() -> &'static str { env!("CARGO_BIN_EXE_xmlsec1") } @@ -125,6 +130,66 @@ fn encrypts_decrypts_and_rejects_wrong_symmetric_key() { assert!(!rejected.status.success()); } +#[test] +fn encryption_preserves_template_metadata_and_supports_id_selection() { + // Encryption templates are output contracts. Only CipherValue is mutable; + // caller-owned identifiers, KeyInfo, properties, and extension attributes remain. + let temp = tempfile::tempdir().unwrap(); + let template = temp.path().join("template.xml"); + let plaintext = temp.path().join("plaintext.bin"); + let key = temp.path().join("key.bin"); + let encrypted = temp.path().join("encrypted.xml"); + fs::write( + &template, + r#" + +content + +kept +"#, + ) + .unwrap(); + fs::write(&plaintext, b"template metadata payload").unwrap(); + fs::write(&key, b"0123456789abcdef").unwrap(); + let result = Command::new(binary()) + .args(["encrypt", "--aeskey:content"]) + .arg(&key) + .args(["--xml-data"]) + .arg(&plaintext) + .args(["--output"]) + .arg(&encrypted) + .arg(&template) + .output() + .unwrap(); + assert!( + result.status.success(), + "{}", + String::from_utf8_lossy(&result.stderr) + ); + let output = fs::read_to_string(&encrypted).unwrap(); + assert!(output.contains("Id=\"payload\"")); + assert!(output.contains("MimeType=\"application/xml\"")); + assert!(output.contains("content")); + assert!(output.contains("kept")); + + let decrypted = Command::new(binary()) + .args(["decrypt", "--aeskey"]) + .arg(&key) + .args(["--node-id", "payload"]) + .arg(&encrypted) + .output() + .unwrap(); + assert!( + decrypted.status.success(), + "{}", + String::from_utf8_lossy(&decrypted.stderr) + ); + assert_eq!( + decrypted.stdout, + b"template metadata payload" + ); +} + #[test] fn encrypts_and_decrypts_with_an_rsa_oaep_recipient() { // The advertised RSA path must emit XML Encryption 1.1 OAEP and unwrap its @@ -179,6 +244,60 @@ fn encrypts_and_decrypts_with_an_rsa_oaep_recipient() { assert_eq!(decrypt.stdout, fs::read(&plaintext).unwrap()); } +#[test] +fn honors_legacy_rsa_oaep_parameters_from_the_template() { + // Advertising rsa-oaep-mgf1p requires an actual process round trip, and + // template parameters must drive key wrapping rather than only survive as XML. + let temp = tempfile::tempdir().unwrap(); + let template = temp.path().join("legacy-oaep.xml"); + let plaintext = temp.path().join("plaintext.bin"); + let encrypted = temp.path().join("encrypted.xml"); + let private_key = project_root().join("tests/fixtures/keys/rsa/rsa-4096-key.pem"); + let public_key = project_root().join("tests/fixtures/keys/rsa/rsa-4096-pubkey.pem"); + fs::write( + &template, + r#" + + + + +"#, + ) + .unwrap(); + fs::write(&plaintext, b"legacy OAEP payload").unwrap(); + let encrypt = Command::new(binary()) + .args(["encrypt", "--pubkey-pem"]) + .arg(&public_key) + .args(["--binary-data"]) + .arg(&plaintext) + .args(["--output"]) + .arg(&encrypted) + .arg(&template) + .output() + .unwrap(); + assert!( + encrypt.status.success(), + "{}", + String::from_utf8_lossy(&encrypt.stderr) + ); + let xml = fs::read_to_string(&encrypted).unwrap(); + assert!(xml.contains("rsa-oaep-mgf1p")); + assert!(xml.contains("Recipient=\"legacy\"")); + + let decrypt = Command::new(binary()) + .args(["decrypt", "--privkey-pem"]) + .arg(&private_key) + .arg(&encrypted) + .output() + .unwrap(); + assert!( + decrypt.status.success(), + "{}", + String::from_utf8_lossy(&decrypt.stderr) + ); + assert_eq!(decrypt.stdout, b"legacy OAEP payload"); +} + #[test] fn decrypts_encrypted_data_embedded_in_a_document() { // libxmlsec1 decrypt replaces EncryptedData in its containing document; a @@ -285,18 +404,199 @@ fn generated_key_store_uses_the_libxmlsec1_xml_shape() { ); } +#[cfg(unix)] +#[test] +fn generated_key_store_is_private_on_create_and_overwrite() { + use std::os::unix::fs::PermissionsExt as _; + + // Key stores contain raw symmetric keys; both a new file and an existing + // permissive file must end with owner-only permissions. + let temp = tempfile::tempdir().unwrap(); + let key_store = temp.path().join("keys.xml"); + fs::write(&key_store, b"old").unwrap(); + fs::set_permissions(&key_store, fs::Permissions::from_mode(0o666)).unwrap(); + let generated = Command::new(binary()) + .args(["keys", "--gen-key:private", "aes-128"]) + .arg(&key_store) + .output() + .unwrap(); + assert!(generated.status.success()); + assert_eq!( + fs::metadata(&key_store).unwrap().permissions().mode() & 0o777, + 0o600 + ); +} + +#[test] +fn explicit_certificate_pins_the_verification_identity() { + // An embedded certificate must not override an explicit certificate passed + // by the caller, even when both certificates are structurally valid. + let temp = tempfile::tempdir().unwrap(); + let template = project_root() + .join("tests/fixtures/xmldsig/aleksey-xmldsig-01/enveloping-sha256-rsa-sha256.tmpl"); + let private_key = project_root().join("tests/fixtures/keys/rsa/rsa-4096-key.pem"); + let certificate = project_root().join("tests/fixtures/keys/rsa/rsa-4096-cert.pem"); + let wrong_certificate = project_root().join("tests/fixtures/keys/rsa/rsa-2048-cert.pem"); + let signed = temp.path().join("signed.xml"); + let compound = format!("{},{}", private_key.display(), certificate.display()); + let sign = Command::new(binary()) + .args(["sign", "--privkey-pem"]) + .arg(compound) + .args(["--output"]) + .arg(&signed) + .arg(&template) + .output() + .unwrap(); + assert!( + sign.status.success(), + "{}", + String::from_utf8_lossy(&sign.stderr) + ); + + assert!( + Command::new(binary()) + .args(["verify", "--pubkey-cert-pem"]) + .arg(&certificate) + .arg(&signed) + .status() + .unwrap() + .success() + ); + assert!( + !Command::new(binary()) + .args(["verify", "--pubkey-cert-pem"]) + .arg(&wrong_certificate) + .arg(&signed) + .status() + .unwrap() + .success() + ); +} + +#[test] +fn der_private_key_option_decodes_its_der_companion_certificate() { + // The comma-separated companion uses the same encoding family as the key + // option; treating DER certificate bytes as UTF-8 would reject valid input. + let temp = tempfile::tempdir().unwrap(); + let template = project_root() + .join("tests/fixtures/xmldsig/aleksey-xmldsig-01/enveloping-sha256-rsa-sha256.tmpl"); + let private_pem = + fs::read_to_string(project_root().join("tests/fixtures/keys/rsa/rsa-4096-key.pem")) + .unwrap(); + let private_der = temp.path().join("private.der"); + fs::write( + &private_der, + RsaPrivateKey::from_pkcs8_pem(&private_pem) + .unwrap() + .to_pkcs8_der() + .unwrap() + .as_bytes(), + ) + .unwrap(); + let certificate_pem = + fs::read_to_string(project_root().join("tests/fixtures/keys/rsa/rsa-4096-cert.pem")) + .unwrap(); + let (_, certificate) = x509_parser::pem::parse_x509_pem(certificate_pem.as_bytes()).unwrap(); + let certificate_der = temp.path().join("certificate.der"); + fs::write(&certificate_der, certificate.contents).unwrap(); + let compound = format!("{},{}", private_der.display(), certificate_der.display()); + + let output = Command::new(binary()) + .args(["sign", "--privkey-p8-der"]) + .arg(compound) + .arg(&template) + .output() + .unwrap(); + assert!( + output.status.success(), + "{}", + String::from_utf8_lossy(&output.stderr) + ); + assert!( + String::from_utf8(output.stdout) + .unwrap() + .contains("X509Certificate") + ); +} + +#[test] +fn multiple_signing_keys_require_the_matching_template_name() { + // Repeated key options are a key set, not first-one-wins. A template name + // selects exactly one named key and an unknown name fails deterministically. + let template = project_root() + .join("tests/fixtures/xmldsig/aleksey-xmldsig-01/enveloping-sha256-rsa-sha256.tmpl"); + let private_key = project_root().join("tests/fixtures/keys/rsa/rsa-4096-key.pem"); + let wrong_key = project_root().join("tests/fixtures/keys/rsa/rsa-2048-key.pem"); + let selected = Command::new(binary()) + .args(["sign", "--privkey-pem:wrong"]) + .arg(&wrong_key) + .args(["--privkey-pem:TestKeyName-rsa-2048"]) + .arg(&private_key) + .arg(&template) + .output() + .unwrap(); + assert!( + selected.status.success(), + "{}", + String::from_utf8_lossy(&selected.stderr) + ); + + let missing = Command::new(binary()) + .args(["sign", "--privkey-pem:first"]) + .arg(&private_key) + .args(["--privkey-pem:second"]) + .arg(&wrong_key) + .arg(&template) + .output() + .unwrap(); + assert!(!missing.status.success()); + assert!(String::from_utf8_lossy(&missing.stderr).contains("unknown KeyName")); +} + +#[cfg(target_os = "linux")] +#[test] +fn signs_through_non_utf8_filesystem_paths() { + use std::os::unix::ffi::OsStringExt as _; + + // Unix paths are byte strings; CLI parsing must not reject a valid file + // solely because its name cannot be represented as UTF-8. + let temp = tempfile::tempdir().unwrap(); + let template = temp + .path() + .join(std::ffi::OsString::from_vec(b"template-\xff.xml".to_vec())); + let key = temp + .path() + .join(std::ffi::OsString::from_vec(b"key-\xfe.pem".to_vec())); + fs::copy( + project_root() + .join("tests/fixtures/xmldsig/aleksey-xmldsig-01/enveloping-sha256-rsa-sha256.tmpl"), + &template, + ) + .unwrap(); + fs::copy( + project_root().join("tests/fixtures/keys/rsa/rsa-4096-key.pem"), + &key, + ) + .unwrap(); + let output = Command::new(binary()) + .args(["sign", "--privkey-pem"]) + .arg(&key) + .arg(&template) + .output() + .unwrap(); + assert!( + output.status.success(), + "{}", + String::from_utf8_lossy(&output.stderr) + ); +} + #[test] fn generated_key_store_contains_every_requested_key() { // Repeated --gen-key options are independent requests and must never be // silently collapsed to the first parsed value. let generated = Command::new(binary()) - .args([ - "keys", - "--gen-key:first", - "aes-128", - "--gen-key:second", - "aes-256", - ]) + .args(["keys", "-g:first", "aes-128", "--gen-key:second", "aes-256"]) .output() .unwrap(); assert!(generated.status.success()); @@ -317,6 +617,12 @@ fn reports_capabilities_and_process_failures_deterministically() { let temp = tempfile::tempdir().unwrap(); let malformed = temp.path().join("malformed.xml"); fs::write(&malformed, "").unwrap(); + let foreign_template = temp.path().join("foreign-template.xml"); + fs::write( + &foreign_template, + "", + ) + .unwrap(); assert!( Command::new(binary()) @@ -325,6 +631,55 @@ fn reports_capabilities_and_process_failures_deterministically() { .unwrap() .success() ); + let conflicting_keys = Command::new(binary()) + .args([ + "verify", + "--pubkey-pem", + "first.pem", + "--pubkey-pem", + "second.pem", + ]) + .arg(&malformed) + .output() + .unwrap(); + assert!(!conflicting_keys.status.success()); + assert!( + String::from_utf8_lossy(&conflicting_keys.stderr) + .contains("exactly one explicit public key") + ); + + let foreign = Command::new(binary()) + .args(["encrypt", "--aeskey", "missing.key", "--binary-data"]) + .arg(&malformed) + .arg(&foreign_template) + .output() + .unwrap(); + assert!(!foreign.status.success()); + assert!(String::from_utf8_lossy(&foreign.stderr).contains("no EncryptedData")); + assert_eq!( + Command::new(binary()) + .arg("check-transforms") + .status() + .unwrap() + .code(), + Some(0) + ); + assert_eq!( + Command::new(binary()) + .args(["check-transforms", "rsa-oaep-mgf1p"]) + .status() + .unwrap() + .code(), + Some(0) + ); + assert_eq!( + Command::new(binary()) + .arg("unknown-command") + .status() + .unwrap() + .code(), + Some(1) + ); assert!( !Command::new(binary()) .args(["check-transforms", "xslt"]) @@ -358,4 +713,17 @@ fn reports_capabilities_and_process_failures_deterministically() { .unwrap() .success() ); + + let config = temp.path().join("crypto-config"); + fs::create_dir(&config).unwrap(); + fs::write(config.join("backend.conf"), "unsupported").unwrap(); + assert!( + !Command::new(binary()) + .args(["check-transforms", "--crypto-config"]) + .arg(&config) + .arg("c14n") + .status() + .unwrap() + .success() + ); } diff --git a/tools/xmlsec1/tests/upstream_runner.rs b/tools/xmlsec1/tests/upstream_runner.rs index 48759c1..64ad118 100644 --- a/tools/xmlsec1/tests/upstream_runner.rs +++ b/tools/xmlsec1/tests/upstream_runner.rs @@ -1,14 +1,7 @@ use std::{path::Path, process::Command}; -fn root() -> &'static Path { - Path::new(env!("CARGO_MANIFEST_DIR")) - .parent() - .and_then(Path::parent) - .unwrap() -} - fn run_upstream(script: &str, selected_test: &str) { - let tests = root().join("donors/xmlsec/tests"); + let tests = Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures/upstream"); let output = Command::new(tests.join("testrun.sh")) .arg(tests.join(script)) .arg("rustcrypto") From a79bdd34baf6f121a2844db53605d724833ae605 Mon Sep 17 00:00:00 2001 From: Dmitry Prudnikov Date: Fri, 14 Aug 2026 00:17:28 +0300 Subject: [PATCH 03/27] fix(ci): isolate oracle source checkout --- .github/workflows/ci.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index def5158..4bf6d34 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -13,7 +13,6 @@ env: RUSTFLAGS: -Dwarnings XMLSEC1_PREFIX: ${{ github.workspace }}/.tools/xmlsec1-1.3.13 XMLSEC1_BIN: ${{ github.workspace }}/.tools/xmlsec1-1.3.13/bin/xmlsec1 - XMLSEC1_SOURCE_DIR: ${{ github.workspace }}/donors/xmlsec LD_LIBRARY_PATH: ${{ github.workspace }}/.tools/xmlsec1-1.3.13/lib jobs: @@ -101,6 +100,8 @@ jobs: - name: Refresh apt package index run: sudo apt-get update - name: Build pinned xmlsec1 for external-oracle tests + env: + XMLSEC1_SOURCE_DIR: ${{ github.workspace }}/donors/xmlsec run: | sudo apt-get install --yes autoconf automake build-essential libltdl-dev libssl-dev libtool libxml2-dev pkg-config scripts/install-xmlsec1.sh From 0e8e501ce31399f04be1004419c01ba44bbe0610 Mon Sep 17 00:00:00 2001 From: Dmitry Prudnikov Date: Fri, 14 Aug 2026 00:58:31 +0300 Subject: [PATCH 04/27] fix(cli): enforce compatibility contracts - validate pinned certificates through the configured trust policy - bound plaintext reads and restore donor output and key semantics - make fixture checks, packaged tests, and workspace CI effective --- .github/workflows/ci.yml | 8 +- docs/cli.md | 13 +- scripts/import-xmlsec1-cli-fixtures.sh | 14 +- tools/xmlsec1/Cargo.toml | 1 + tools/xmlsec1/README.md | 4 +- tools/xmlsec1/src/commands.rs | 162 ++++++++++++++++++++---- tools/xmlsec1/src/key_material.rs | 38 +----- tools/xmlsec1/tests/import_snapshot.rs | 36 ++++++ tools/xmlsec1/tests/process_contract.rs | 122 ++++++++++++++++++ 9 files changed, 328 insertions(+), 70 deletions(-) create mode 100644 tools/xmlsec1/tests/import_snapshot.rs diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4bf6d34..fcea66e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -61,7 +61,7 @@ jobs: with: toolchain: ${{ matrix.rust }} - uses: Swatinem/rust-cache@v2 - - run: cargo build --all-features + - run: cargo build --workspace --all-features build: runs-on: ubuntu-latest @@ -107,8 +107,8 @@ jobs: scripts/install-xmlsec1.sh "$XMLSEC1_BIN" --version - uses: Swatinem/rust-cache@v2 - - run: cargo nextest run --all-features - - run: cargo test --doc --all-features + - run: cargo nextest run --workspace --all-features + - run: cargo test --doc --workspace --all-features test: runs-on: ubuntu-latest @@ -127,7 +127,7 @@ jobs: with: components: clippy - uses: Swatinem/rust-cache@v2 - - run: cargo clippy --all-features --all-targets -- -D warnings + - run: cargo clippy --workspace --all-features --all-targets -- -D warnings fmt: runs-on: ubuntu-latest diff --git a/docs/cli.md b/docs/cli.md index d33dc72..8e160aa 100644 --- a/docs/cli.md +++ b/docs/cli.md @@ -39,6 +39,11 @@ xmlsec1 sign --privkey-pem signing-key.pem --output signed.xml template.xml xmlsec1 verify --pubkey-pem signing-key.pub.pem signed.xml ``` +`--output` follows the upstream filename-template contract. The first +`{inputfile}` token is replaced with the input file's basename after removing +its final extension, for example `--output 'signed-{inputfile}.xml'` with +`templates/order.tmpl` writes `signed-order.xml`. + Encrypt and decrypt binary data with a direct AES key: ```sh @@ -62,6 +67,9 @@ Generate an AES key store using the upstream command shape: xmlsec1 keys --gen-key:content aes-256 keys.xml ``` +The key name is optional. `--gen-key aes-128` writes an unnamed key without a +`KeyName` element, while `--gen-key:content aes-128` writes the supplied name. + ## Compatibility boundary The command and status surface is available now, while individual key formats, @@ -71,7 +79,10 @@ PKCS#1 RSA in PEM or DER; `--privkey-p8-pem` and `--privkey-p8-der` are accepted as upstream PKCS#8 aliases. Public verification accepts SubjectPublicKeyInfo, PKCS#1 RSA public keys, and X.509 certificates. Explicit certificate options pin verification to that certificate's public key instead of permitting an -embedded `KeyInfo` to select another identity. Direct XMLEnc keys accept +embedded `KeyInfo` to select another identity. When `--trusted-pem` or +`--trusted-der` is also supplied, the explicit certificate must build a valid +path through any `--untrusted-*` intermediates to a supplied anchor; `--insecure` +is the explicit opt-out. Direct XMLEnc keys accept AES-128/256; RSA-OAEP supports both the XMLEnc 1.0 `rsa-oaep-mgf1p` and XMLEnc 1.1 parameter contracts. Encrypted PKCS#8, PKCS#12, platform crypto stores, external DTDs, implicit network access, diff --git a/scripts/import-xmlsec1-cli-fixtures.sh b/scripts/import-xmlsec1-cli-fixtures.sh index cdd13cd..6d98143 100755 --- a/scripts/import-xmlsec1-cli-fixtures.sh +++ b/scripts/import-xmlsec1-cli-fixtures.sh @@ -3,9 +3,9 @@ set -euo pipefail repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" donor_tests="${XMLSEC_DONOR_ROOT:-$repo_root/donors/xmlsec/tests}" -target="$repo_root/tools/xmlsec1/tests/fixtures/upstream" -mode="${1:-import}" -if [[ "$mode" != "import" && "$mode" != "--check" ]]; then +target="${XMLSEC_FIXTURE_TARGET:-$repo_root/tools/xmlsec1/tests/fixtures/upstream}" +operation="${1:-import}" +if [[ "$operation" != "import" && "$operation" != "--check" ]]; then printf 'usage: %s [--check]\n' "$0" >&2 exit 2 fi @@ -37,18 +37,18 @@ for asset in "${assets[@]}"; do exit 1 fi mkdir -p "$staging/$(dirname "$asset")" - mode=0644 + file_mode=0644 if [[ "$asset" == *.sh ]]; then - mode=0755 + file_mode=0755 fi - install -m "$mode" "$source" "$staging/$asset" + install -m "$file_mode" "$source" "$staging/$asset" done printf '%s\n' "$(<"$repo_root/compatibility/libxmlsec1-1.3.13-donor-commit.txt")" \ > "$staging/DONOR_COMMIT" backup="${target}.backup.$$" -if [[ "$mode" == "--check" ]]; then +if [[ "$operation" == "--check" ]]; then diff --recursive --brief "$target" "$staging" exit fi diff --git a/tools/xmlsec1/Cargo.toml b/tools/xmlsec1/Cargo.toml index c27f875..6ebf672 100644 --- a/tools/xmlsec1/Cargo.toml +++ b/tools/xmlsec1/Cargo.toml @@ -27,4 +27,5 @@ x509-parser = "0.18" xml-sec = { version = "0.1.11", path = "../..", features = ["xmldsig", "xmlenc", "c14n"] } [dev-dependencies] +rand_chacha = "0.10" tempfile = "3" diff --git a/tools/xmlsec1/README.md b/tools/xmlsec1/README.md index cb9b18d..0866c64 100644 --- a/tools/xmlsec1/README.md +++ b/tools/xmlsec1/README.md @@ -18,4 +18,6 @@ coverage. `EncryptedData` and performs in-document replacement, optionally selected by `--node-id`. Encryption retains template metadata and RSA-OAEP parameters; PKCS#1 RSA and PKCS#8/SPKI/X.509 PEM or DER key material is normalized into the -same core signing, verification, and encryption pipelines. +same core signing, verification, and encryption pipelines. Output paths support +the upstream `{inputfile}` basename template, and `--gen-key[:name]` emits both +named and unnamed AES key-store entries. diff --git a/tools/xmlsec1/src/commands.rs b/tools/xmlsec1/src/commands.rs index 1539b9f..e5ef9ec 100644 --- a/tools/xmlsec1/src/commands.rs +++ b/tools/xmlsec1/src/commands.rs @@ -1,6 +1,6 @@ use std::{ collections::HashSet, - ffi::OsStr, + ffi::{OsStr, OsString}, fs::{self, File, OpenOptions}, io::{Read, Write}, path::{Path, PathBuf}, @@ -11,8 +11,9 @@ use xml_sec::{ policy::{DecryptionPolicy, EncryptionPolicy, SigningPolicy, VerificationPolicy}, provider::default_provider, xmldsig::{ - DefaultKeyResolver, DsigStatus, KeyResolverConfig, SignContext, SignatureAlgorithm, - UriTypeSet, VerifyContext, X509CertificateKeyInfoWriter, + DefaultKeyResolver, DsigStatus, KeyResolver, KeyResolverConfig, SignContext, + SignatureAlgorithm, UriTypeSet, VerifyContext, X509CertificateKeyInfoWriter, + parse_key_info, }, xmlenc::{ DataEncryptionAlgorithm, DecryptContext, DecryptedContent, DecryptionKeyResolver, @@ -59,6 +60,8 @@ pub enum CommandError { InputTooLarge { maximum: usize }, #[error("input XML is not valid UTF-8")] InvalidUtf8Input, + #[error("encryption plaintext exceeds policy limit of {maximum} bytes")] + PlaintextTooLarge { maximum: usize }, #[error(transparent)] Key(#[from] key_material::KeyMaterialError), #[error("XML signature operation failed: {0}")] @@ -220,16 +223,62 @@ fn write_output( bytes: &[u8], stdout: &mut dyn Write, ) -> Result<(), CommandError> { - if let Some(path) = invocation.last_value("output") { - fs::write(path, bytes).map_err(|source| CommandError::Io { - path: PathBuf::from(path), - source, - }) + if let Some(template) = invocation.last_value("output") { + let path = expand_output_path(invocation, template)?; + fs::write(&path, bytes).map_err(|source| CommandError::Io { path, source }) } else { stdout.write_all(bytes).map_err(stdout_error) } } +fn expand_output_path(invocation: &Invocation, template: &OsStr) -> Result { + const PLACEHOLDER: &[u8] = b"{inputfile}"; + let template_bytes = template.as_encoded_bytes(); + let Some(start) = template_bytes + .windows(PLACEHOLDER.len()) + .position(|candidate| candidate == PLACEHOLDER) + else { + return Ok(PathBuf::from(template)); + }; + let input = input_path(invocation)?; + let basename = Path::new(input) + .file_name() + .unwrap_or(input) + .as_encoded_bytes(); + let stem = basename + .iter() + .rposition(|byte| *byte == b'.') + .map_or(basename, |dot| &basename[..dot]); + let mut expanded = Vec::with_capacity(template_bytes.len() - PLACEHOLDER.len() + stem.len()); + expanded.extend_from_slice(&template_bytes[..start]); + expanded.extend_from_slice(stem); + expanded.extend_from_slice(&template_bytes[start + PLACEHOLDER.len()..]); + // The placeholder is ASCII and every other boundary comes from a complete + // OsStr, so concatenation preserves the platform's encoded-byte contract. + Ok(PathBuf::from(unsafe { + OsString::from_encoded_bytes_unchecked(expanded) + })) +} + +fn read_plaintext(path: &OsStr, maximum: usize) -> Result, CommandError> { + let mut bytes = Vec::with_capacity(maximum.min(64 * 1024)); + File::open(path) + .map_err(|source| CommandError::Io { + path: PathBuf::from(path), + source, + })? + .take(maximum.saturating_add(1) as u64) + .read_to_end(&mut bytes) + .map_err(|source| CommandError::Io { + path: PathBuf::from(path), + source, + })?; + if bytes.len() > maximum { + return Err(CommandError::PlaintextTooLarge { maximum }); + } + Ok(bytes) +} + fn option_text<'a>( invocation: &'a Invocation, name: &str, @@ -461,13 +510,13 @@ fn verify(invocation: &Invocation, stdout: &mut dyn Write) -> Result<(), Command let algorithm = key_material::signature_algorithm(&xml)?; let result = if let Some(path) = direct_path { let key = key_material::load_verification_key(path, algorithm)?; - VerifyContext::new().policy(policy).key(&key).verify(&xml) + VerifyContext::new() + .policy(policy) + .key(&key) + .verify(&xml) + .map_err(|error| CommandError::Signature(error.to_string()))? } else if let [certificate] = explicit_certificates.as_slice() { - let key = key_material::load_certificate_verification_key( - certificate.value.as_deref().unwrap_or_default(), - algorithm, - )?; - VerifyContext::new().policy(policy).key(&key).verify(&xml) + verify_with_explicit_certificate(invocation, certificate, algorithm, policy, &xml)? } else { let mut config = KeyResolverConfig::default(); for name in [ @@ -494,8 +543,8 @@ fn verify(invocation: &Invocation, stdout: &mut dyn Write) -> Result<(), Command .policy(policy) .key_resolver(&resolver) .verify(&xml) - } - .map_err(|error| CommandError::Signature(error.to_string()))?; + .map_err(|error| CommandError::Signature(error.to_string()))? + }; if result.status != DsigStatus::Valid || result .manifest_references @@ -510,6 +559,55 @@ fn verify(invocation: &Invocation, stdout: &mut dyn Write) -> Result<(), Command Ok(()) } +fn verify_with_explicit_certificate( + invocation: &Invocation, + certificate: &crate::OptionValue, + algorithm: SignatureAlgorithm, + policy: VerificationPolicy, + xml: &str, +) -> Result { + let certificate_der = + key_material::load_certificate(certificate.value.as_deref().unwrap_or_default())?; + // Model the caller-pinned leaf as the sole document key source. The core + // resolver can then build its path through caller-supplied intermediates + // and anchors without allowing the document's embedded KeyInfo to replace + // the explicitly selected identity. + let encoded = + base64::Engine::encode(&base64::engine::general_purpose::STANDARD, certificate_der); + let key_info_xml = format!( + "{encoded}" + ); + let document = Document::parse(&key_info_xml) + .map_err(|error| CommandError::Signature(error.to_string()))?; + let key_info = parse_key_info(document.root_element()) + .map_err(|error| CommandError::Signature(error.to_string()))?; + let mut config = KeyResolverConfig::default(); + for name in ["untrusted-pem", "untrusted-der"] { + for option in invocation.values(name) { + config.lookup_certs.push(key_material::load_certificate( + option.value.as_deref().unwrap_or_default(), + )?); + } + } + for name in ["trusted-pem", "trusted-der"] { + for option in invocation.values(name) { + config.trusted_certs.push(key_material::load_certificate( + option.value.as_deref().unwrap_or_default(), + )?); + } + } + let resolver = DefaultKeyResolver::new(config); + let key = resolver + .resolve_with_policy(Some(&key_info), algorithm, &policy) + .map_err(|error| CommandError::Signature(error.to_string()))? + .ok_or_else(|| CommandError::Signature("explicit certificate was not resolved".into()))?; + VerifyContext::new() + .policy(policy) + .key(key.as_ref()) + .verify(xml) + .map_err(|error| CommandError::Signature(error.to_string())) +} + fn encrypt(invocation: &Invocation, stdout: &mut dyn Write) -> Result<(), CommandError> { validate_options( invocation, @@ -531,6 +629,7 @@ fn encrypt(invocation: &Invocation, stdout: &mut dyn Write) -> Result<(), Comman reject_unimplemented_selectors(invocation, &[])?; let policy = EncryptionPolicy::default(); let maximum_document_bytes = policy.resources.max_xml_document_bytes; + let maximum_plaintext_bytes = policy.resources.max_encryption_plaintext_bytes; let template = read_input(invocation, policy.resources.max_xml_document_bytes)?; let (algorithm, encrypted_type) = encryption_template(&template)?; let mut builder = EncryptedDataBuilder::new(algorithm).policy(policy); @@ -567,10 +666,11 @@ fn encrypt(invocation: &Invocation, stdout: &mut dyn Write) -> Result<(), Comman } builder = builder.encryption_type(encrypted_type); let result = if let Some(path) = invocation.last_value("binary-data") { - let data = key_material::read(path)?; + let data = read_plaintext(path, maximum_plaintext_bytes)?; builder.encrypt_binary(&data) } else if let Some(path) = invocation.last_value("xml-data") { - let data = key_material::read_text(path)?; + let data = String::from_utf8(read_plaintext(path, maximum_plaintext_bytes)?) + .map_err(|_| CommandError::InvalidUtf8Input)?; builder.encrypt_xml(&data) } else { return Err(CommandError::Usage( @@ -878,10 +978,6 @@ fn keys(invocation: &Invocation, stdout: &mut dyn Write) -> Result<(), CommandEr } let mut entries = String::new(); for generated in generated { - let name = generated - .parameter - .as_deref() - .ok_or_else(|| CommandError::Usage("--gen-key requires a key name".into()))?; let algorithm = option_value_text(generated)?; let size = match algorithm { "aes-128" => 16, @@ -894,10 +990,15 @@ fn keys(invocation: &Invocation, stdout: &mut dyn Write) -> Result<(), CommandEr .fill_random(&mut key) .map_err(|error| CommandError::Encryption(error.to_string()))?; let encoded = base64::Engine::encode(&base64::engine::general_purpose::STANDARD, key); - let name = quick_xml::escape::escape(name); + let key_name = generated + .parameter + .as_deref() + .map_or_else(String::new, |name| { + format!("{}\n", quick_xml::escape::escape(name)) + }); entries.push_str(&format!( "\n\ - {name}\n\ + {key_name}\ \n\ {encoded}\n\ \n\ @@ -1068,4 +1169,17 @@ mod tests { Err(CommandError::InputTooLarge { maximum: 4 }) )); } + + #[test] + fn plaintext_reader_enforces_the_compiled_policy_limit_before_encryption() { + // Payload limits must be enforced by the reader, before the encryption + // builder receives an attacker-controlled allocation. + let temp = tempfile::tempdir().unwrap(); + let path = temp.path().join("oversized.bin"); + fs::write(&path, b"12345").unwrap(); + assert!(matches!( + read_plaintext(path.as_os_str(), 4), + Err(CommandError::PlaintextTooLarge { maximum: 4 }) + )); + } } diff --git a/tools/xmlsec1/src/key_material.rs b/tools/xmlsec1/src/key_material.rs index a782ebd..30bc13b 100644 --- a/tools/xmlsec1/src/key_material.rs +++ b/tools/xmlsec1/src/key_material.rs @@ -162,22 +162,6 @@ pub fn load_certificate(path: impl AsRef) -> Result, KeyMaterialEr Ok(der) } -pub fn load_certificate_verification_key( - path: impl AsRef, - algorithm: SignatureAlgorithm, -) -> Result { - let path = path.as_ref(); - let certificate_der = load_certificate(path)?; - let (_, certificate) = x509_parser::certificate::X509Certificate::from_der(&certificate_der) - .map_err(|_| KeyMaterialError::InvalidCertificate(path.to_owned()))?; - Ok(VerificationKey { - algorithm, - public_key_bytes: certificate.public_key().raw.to_vec(), - certificate_der: Some(certificate_der), - name: None, - }) -} - fn parse_pem(text: &str, expected_label: &str, path: &Path) -> Result, KeyMaterialError> { let (rest, pem) = x509_parser::pem::parse_x509_pem(text.as_bytes()) .map_err(|_| KeyMaterialError::InvalidPem(path.to_owned()))?; @@ -242,29 +226,17 @@ pub fn load_symmetric( #[cfg(test)] mod tests { - use rsa::{ - pkcs1::{EncodeRsaPrivateKey as _, EncodeRsaPublicKey as _}, - pkcs8::DecodePrivateKey as _, - }; + use rand_chacha::{ChaCha20Rng, rand_core::SeedableRng as _}; + use rsa::pkcs1::{EncodeRsaPrivateKey as _, EncodeRsaPublicKey as _}; use super::*; - fn fixture(path: &str) -> PathBuf { - Path::new(env!("CARGO_MANIFEST_DIR")) - .parent() - .and_then(Path::parent) - .unwrap() - .join(path) - } - #[test] fn normalizes_pkcs1_private_and_public_keys() { // PKCS#1 is a donor-supported RSA container. The CLI normalizes it to - // the core's PKCS#8/SPKI contracts instead of duplicating crypto code. - let original = RsaPrivateKey::from_pkcs8_pem( - &fs::read_to_string(fixture("tests/fixtures/keys/rsa/rsa-2048-key.pem")).unwrap(), - ) - .unwrap(); + // the core's PKCS#8/SPKI contracts. Generating the source key keeps this + // unit test runnable from the published crate without repository paths. + let original = RsaPrivateKey::new(&mut ChaCha20Rng::from_seed([7; 32]), 1024).unwrap(); let temp = tempfile::tempdir().unwrap(); let private = temp.path().join("private.pem"); let public = temp.path().join("public.der"); diff --git a/tools/xmlsec1/tests/import_snapshot.rs b/tools/xmlsec1/tests/import_snapshot.rs new file mode 100644 index 0000000..19d34da --- /dev/null +++ b/tools/xmlsec1/tests/import_snapshot.rs @@ -0,0 +1,36 @@ +use std::{fs, path::Path, process::Command}; + +fn project_root() -> &'static Path { + Path::new(env!("CARGO_MANIFEST_DIR")) + .parent() + .and_then(Path::parent) + .unwrap() +} + +#[test] +fn check_mode_detects_drift_without_replacing_the_snapshot() { + // The CI reproducibility gate must be observational: drift fails the check + // and leaves the candidate snapshot untouched for diagnosis. + let temp = tempfile::tempdir().unwrap(); + let target = temp.path().join("snapshot"); + let donor = project_root().join("tools/xmlsec1/tests/fixtures/upstream"); + let script = project_root().join("scripts/import-xmlsec1-cli-fixtures.sh"); + let imported = Command::new(&script) + .env("XMLSEC_DONOR_ROOT", &donor) + .env("XMLSEC_FIXTURE_TARGET", &target) + .status() + .unwrap(); + assert!(imported.success()); + + let changed = target.join("testDSig.sh"); + fs::write(&changed, "local drift\n").unwrap(); + let checked = Command::new(script) + .arg("--check") + .env("XMLSEC_DONOR_ROOT", donor) + .env("XMLSEC_FIXTURE_TARGET", &target) + .status() + .unwrap(); + + assert!(!checked.success()); + assert_eq!(fs::read_to_string(changed).unwrap(), "local drift\n"); +} diff --git a/tools/xmlsec1/tests/process_contract.rs b/tools/xmlsec1/tests/process_contract.rs index 06f5b16..1a504c9 100644 --- a/tools/xmlsec1/tests/process_contract.rs +++ b/tools/xmlsec1/tests/process_contract.rs @@ -66,6 +66,36 @@ fn signs_verifies_and_rejects_tampering_through_process_api() { assert!(String::from_utf8_lossy(&rejected.stderr).contains("invalid")); } +#[test] +fn output_template_expands_the_extensionless_input_basename() { + // libxmlsec1 automation uses one output template across many input files; + // only the first placeholder is replaced and the input extension is removed. + let temp = tempfile::tempdir().unwrap(); + let template = project_root() + .join("tests/fixtures/xmldsig/aleksey-xmldsig-01/enveloping-sha256-rsa-sha256.tmpl"); + let private_key = project_root().join("tests/fixtures/keys/rsa/rsa-4096-key.pem"); + let output_template = temp.path().join("signed-{inputfile}-{inputfile}.xml"); + let expected = temp + .path() + .join("signed-enveloping-sha256-rsa-sha256-{inputfile}.xml"); + + let signed = Command::new(binary()) + .args(["sign", "--privkey-pem"]) + .arg(private_key) + .arg("--output") + .arg(output_template) + .arg(template) + .output() + .unwrap(); + + assert!( + signed.status.success(), + "{}", + String::from_utf8_lossy(&signed.stderr) + ); + assert!(expected.is_file()); +} + #[test] fn encrypts_decrypts_and_rejects_wrong_symmetric_key() { // A reciprocal binary round trip must preserve non-UTF-8 bytes, while an @@ -404,6 +434,39 @@ fn generated_key_store_uses_the_libxmlsec1_xml_shape() { ); } +#[test] +fn generated_key_store_allows_an_unnamed_key() { + // The optional --gen-key parameter controls KeyName presence; omitting it + // must still generate usable AES material rather than rejecting the command. + let generated = Command::new(binary()) + .args(["keys", "--gen-key", "aes-128"]) + .output() + .unwrap(); + assert!( + generated.status.success(), + "{}", + String::from_utf8_lossy(&generated.stderr) + ); + let xml = String::from_utf8(generated.stdout).unwrap(); + let document = roxmltree::Document::parse(&xml).unwrap(); + assert!( + !document + .descendants() + .any(|node| node.has_tag_name(("http://www.w3.org/2000/09/xmldsig#", "KeyName"))) + ); + let value = document + .descendants() + .find(|node| node.has_tag_name(("http://www.aleksey.com/xmlsec/2002", "AESKeyValue"))) + .and_then(|node| node.text()) + .unwrap(); + assert_eq!( + base64::Engine::decode(&base64::engine::general_purpose::STANDARD, value) + .unwrap() + .len(), + 16 + ); +} + #[cfg(unix)] #[test] fn generated_key_store_is_private_on_create_and_overwrite() { @@ -473,6 +536,65 @@ fn explicit_certificate_pins_the_verification_identity() { ); } +#[test] +fn explicit_certificate_obeys_trust_anchor_policy() { + // An explicit leaf pins identity but does not establish trust when callers + // also supply anchors; --insecure is the explicit compatibility opt-out. + let temp = tempfile::tempdir().unwrap(); + let template = project_root() + .join("tests/fixtures/xmldsig/aleksey-xmldsig-01/enveloping-sha256-rsa-sha256.tmpl"); + let private_key = project_root().join("tests/fixtures/keys/rsa/rsa-4096-key.pem"); + let certificate = project_root().join("tests/fixtures/keys/rsa/rsa-4096-cert.pem"); + let wrong_anchor = project_root().join("tests/fixtures/keys/rsa/rsa-2048-cert.pem"); + let signed = temp.path().join("signed.xml"); + let compound = format!("{},{}", private_key.display(), certificate.display()); + assert!( + Command::new(binary()) + .args(["sign", "--privkey-pem"]) + .arg(compound) + .arg("--output") + .arg(&signed) + .arg(&template) + .status() + .unwrap() + .success() + ); + + assert!( + Command::new(binary()) + .args(["verify", "--pubkey-cert-pem"]) + .arg(&certificate) + .arg("--trusted-pem") + .arg(&certificate) + .arg(&signed) + .status() + .unwrap() + .success() + ); + assert!( + !Command::new(binary()) + .args(["verify", "--pubkey-cert-pem"]) + .arg(&certificate) + .arg("--trusted-pem") + .arg(&wrong_anchor) + .arg(&signed) + .status() + .unwrap() + .success() + ); + assert!( + Command::new(binary()) + .args(["verify", "--insecure", "--pubkey-cert-pem"]) + .arg(&certificate) + .arg("--trusted-pem") + .arg(&wrong_anchor) + .arg(&signed) + .status() + .unwrap() + .success() + ); +} + #[test] fn der_private_key_option_decodes_its_der_companion_certificate() { // The comma-separated companion uses the same encoding family as the key From a3974a74ec4c1dd9587f2887ce5bc44a810d615c Mon Sep 17 00:00:00 2001 From: Dmitry Prudnikov Date: Fri, 14 Aug 2026 03:04:01 +0300 Subject: [PATCH 05/27] fix(cli): honor native input contracts - embed complete private-key certificate chains - enforce named-key and selected-signature semantics - verify stdin and donor negative paths end to end - run checked-in donor scripts with their actual shell --- README.md | 3 +- docs/cli.md | 18 ++ src/xmldsig/sign.rs | 93 ++++++--- src/xmldsig/uri.rs | 6 +- src/xmldsig/verify.rs | 65 ++++++- tools/xmlsec1/README.md | 8 +- tools/xmlsec1/src/args.rs | 12 +- tools/xmlsec1/src/commands.rs | 81 +++++--- tools/xmlsec1/src/key_material.rs | 49 ++++- tools/xmlsec1/tests/process_contract.rs | 247 +++++++++++++++++++++++- tools/xmlsec1/tests/upstream_runner.rs | 27 ++- 11 files changed, 534 insertions(+), 75 deletions(-) diff --git a/README.md b/README.md index 17b31b3..ea142f0 100644 --- a/README.md +++ b/README.md @@ -79,7 +79,8 @@ xmlsec1 list-key-data The native binary supports sign/verify, template-preserving encrypt/decrypt, AES key generation, capability checks, libxmlsec1 key aliases and option syntax, -and deterministic process statuses. Its process tests run a minimal checked-in +certificate-chain embedding, stdin input, signature selection by node ID, and +deterministic process statuses. Its process tests run a minimal checked-in snapshot of the unmodified upstream DSig, Enc, and Keys runners without network access or a system `xmlsec1` installation. Unsupported algorithms, key formats, providers, and policy controls fail closed diff --git a/docs/cli.md b/docs/cli.md index 8e160aa..c06b495 100644 --- a/docs/cli.md +++ b/docs/cli.md @@ -39,6 +39,18 @@ xmlsec1 sign --privkey-pem signing-key.pem --output signed.xml template.xml xmlsec1 verify --pubkey-pem signing-key.pub.pem signed.xml ``` +Signing key options accept libxmlsec1's comma-separated certificate form, +`key.pem,leaf.pem,intermediate.pem,...`. Every certificate is validated and +embedded in order under `X509Data`; the first certificate must contain the +signing key's public key. Named keys are matched against the template's +`KeyName` even when only one key is supplied. `--lax-key-search` explicitly +opts out of that name match. + +Verification accepts `-` as the conventional stdin marker. For documents with +multiple signatures, `--node-id ` selects an ID-bearing start node and +verifies the single `Signature` in its subtree; missing and duplicate IDs fail +closed. + `--output` follows the upstream filename-template contract. The first `{inputfile}` token is replaced with the input file's basename after removing its final extension, for example `--output 'signed-{inputfile}.xml'` with @@ -89,6 +101,12 @@ PKCS#8, PKCS#12, platform crypto stores, external DTDs, implicit network access, and unsupported CLI policy knobs fail rather than weakening policy or falling back. +The compatibility CLI accepts `--X509-skip-strict-checks`. libxmlsec1 uses +that switch to lower provider security levels for legacy certificate +signatures; RustCrypto has no corresponding provider strict mode and verifies +every certificate signature algorithm implemented by the selected provider, +so no additional policy relaxation is applied. + Filesystem arguments remain native `OsString` values, so Unix paths are not required to be UTF-8. Values immediately following valued options are consumed verbatim, including names beginning with `-`, matching the upstream parser. diff --git a/src/xmldsig/sign.rs b/src/xmldsig/sign.rs index c760810..40ba3f4 100644 --- a/src/xmldsig/sign.rs +++ b/src/xmldsig/sign.rs @@ -337,6 +337,10 @@ pub enum KeyInfoWriteError { #[error("invalid X.509 certificate DER")] InvalidCertificateDer, + /// A certificate-backed KeyInfo writer requires at least one certificate. + #[error("X.509 certificate chain must not be empty")] + EmptyCertificateChain, + /// The signing key could not expose public-key material for validation. #[error("signing key public-key extraction failed: {0}")] SigningKey(#[from] SigningKeyError), @@ -346,43 +350,78 @@ pub enum KeyInfoWriteError { CertificateKeyMismatch, } -/// `` writer that embeds one DER X.509 certificate. +/// `` writer that embeds an ordered DER X.509 certificate chain. pub struct X509CertificateKeyInfoWriter { - certificate_der: Vec, + certificates_der: Vec>, } impl X509CertificateKeyInfoWriter { /// Parse a PEM `CERTIFICATE` block for XMLDSig `` output. pub fn from_pem(certificate_pem: &str) -> Result { - let (rest, pem) = x509_parser::pem::parse_x509_pem(certificate_pem.as_bytes()) - .map_err(|_| KeyInfoWriteError::InvalidCertificatePem)?; - if !rest.iter().all(|byte| byte.is_ascii_whitespace()) { - return Err(KeyInfoWriteError::InvalidCertificatePem); - } - if pem.label != "CERTIFICATE" { - return Err(KeyInfoWriteError::InvalidCertificateFormat { label: pem.label }); + Self::from_pem_chain([certificate_pem]) + } + + /// Parse an ordered sequence of PEM `CERTIFICATE` blocks for ``. + pub fn from_pem_chain(certificate_pems: I) -> Result + where + I: IntoIterator, + S: AsRef, + { + let mut certificates_der = Vec::new(); + for certificate_pem in certificate_pems { + certificates_der.push(parse_certificate_pem(certificate_pem.as_ref())?); } - Self::from_der(&pem.contents) + Self::from_der_chain(certificates_der) } /// Validate and store DER certificate bytes for XMLDSig `` output. pub fn from_der(certificate_der: &[u8]) -> Result { - let (rest, _) = x509_parser::certificate::X509Certificate::from_der(certificate_der) - .map_err(|_| KeyInfoWriteError::InvalidCertificateDer)?; - if !rest.is_empty() { - return Err(KeyInfoWriteError::InvalidCertificateDer); + Self::from_der_chain([certificate_der]) + } + + /// Validate and store an ordered DER certificate chain for ``. + pub fn from_der_chain(certificates_der: I) -> Result + where + I: IntoIterator, + B: AsRef<[u8]>, + { + let certificates_der = certificates_der + .into_iter() + .map(|certificate_der| { + let certificate_der = certificate_der.as_ref(); + let (rest, _) = + x509_parser::certificate::X509Certificate::from_der(certificate_der) + .map_err(|_| KeyInfoWriteError::InvalidCertificateDer)?; + if !rest.is_empty() { + return Err(KeyInfoWriteError::InvalidCertificateDer); + } + Ok(certificate_der.to_vec()) + }) + .collect::, _>>()?; + if certificates_der.is_empty() { + return Err(KeyInfoWriteError::EmptyCertificateChain); } - Ok(Self { - certificate_der: certificate_der.to_vec(), - }) + Ok(Self { certificates_der }) } } +fn parse_certificate_pem(certificate_pem: &str) -> Result, KeyInfoWriteError> { + let (rest, pem) = x509_parser::pem::parse_x509_pem(certificate_pem.as_bytes()) + .map_err(|_| KeyInfoWriteError::InvalidCertificatePem)?; + if !rest.iter().all(|byte| byte.is_ascii_whitespace()) { + return Err(KeyInfoWriteError::InvalidCertificatePem); + } + if pem.label != "CERTIFICATE" { + return Err(KeyInfoWriteError::InvalidCertificateFormat { label: pem.label }); + } + Ok(pem.contents) +} + impl KeyInfoWriter for X509CertificateKeyInfoWriter { fn write_key_info(&self, signing_key: &dyn SigningKey) -> Result { - let (rest, certificate) = - x509_parser::certificate::X509Certificate::from_der(&self.certificate_der) - .map_err(|_| KeyInfoWriteError::InvalidCertificateDer)?; + let leaf_der = &self.certificates_der[0]; + let (rest, certificate) = x509_parser::certificate::X509Certificate::from_der(leaf_der) + .map_err(|_| KeyInfoWriteError::InvalidCertificateDer)?; if !rest.is_empty() { return Err(KeyInfoWriteError::InvalidCertificateDer); } @@ -391,11 +430,15 @@ impl KeyInfoWriter for X509CertificateKeyInfoWriter { return Err(KeyInfoWriteError::CertificateKeyMismatch); } - let certificate_b64 = - base64::engine::general_purpose::STANDARD.encode(&self.certificate_der); - Ok(format!( - "{certificate_b64}" - )) + let mut xml = format!(""); + for certificate_der in &self.certificates_der { + let certificate_b64 = base64::engine::general_purpose::STANDARD.encode(certificate_der); + xml.push_str(""); + xml.push_str(&certificate_b64); + xml.push_str(""); + } + xml.push_str(""); + Ok(xml) } } diff --git a/src/xmldsig/uri.rs b/src/xmldsig/uri.rs index f92f327..c02b50e 100644 --- a/src/xmldsig/uri.rs +++ b/src/xmldsig/uri.rs @@ -359,7 +359,11 @@ impl<'a> UriReferenceResolver<'a> { self.id_map.get(id).map(|node| node.id()) } - pub(crate) fn node_for_id(&self, id: &str) -> Option> { + /// Resolve an unambiguous XML ID to its element node. + /// + /// Returns `None` when the ID is absent or duplicated, matching fragment + /// dereferencing and operation start-node selection. + pub fn node_for_id(&self, id: &str) -> Option> { self.id_map.get(id).copied() } diff --git a/src/xmldsig/verify.rs b/src/xmldsig/verify.rs index 1ca7dce..facf679 100644 --- a/src/xmldsig/verify.rs +++ b/src/xmldsig/verify.rs @@ -229,6 +229,7 @@ pub struct VerifyContext<'a> { provider: &'a dyn crate::provider::CryptoProvider, store_pre_digest: bool, external_resources: Option<&'a HashMap>>, + start_node_id: Option<&'a str>, } impl<'a> VerifyContext<'a> { @@ -248,6 +249,7 @@ impl<'a> VerifyContext<'a> { provider: crate::provider::default_provider(), store_pre_digest: false, external_resources: None, + start_node_id: None, } } @@ -344,6 +346,16 @@ impl<'a> VerifyContext<'a> { self } + /// Select the operation start node by its XML ID value. + /// + /// Verification searches for exactly one `` in that node's + /// subtree. This is request context, not a policy decision, and mirrors the + /// start-node contract of libxmlsec1's `--node-id` option. + pub fn start_node_id(mut self, id: &'a str) -> Self { + self.start_node_id = Some(id); + self + } + /// Allow bounded internal DTD declarations while keeping external entity /// resolution disabled. This is off by default. pub fn allow_internal_dtd(mut self, enabled: bool) -> Self { @@ -878,6 +890,13 @@ pub enum DsigError { reason: &'static str, }, + /// The requested operation start node is absent or has a duplicate ID. + #[error("selected node ID is missing or ambiguous: {id}")] + SelectedNodeUnavailable { + /// Caller-provided XML ID value. + id: String, + }, + /// `` parsing failed. #[error("failed to parse SignedInfo: {0}")] ParseSignedInfo(#[from] super::parse::ParseError), @@ -995,8 +1014,22 @@ fn verify_signature_with_context( entity_resolver: None, }, )?; + let resolver = UriReferenceResolver::new(&doc).with_external_resource_limits( + ctx.policy.resources.max_external_resource_bytes, + ctx.policy.resources.max_external_resource_total_bytes, + ); + let resolver = match ctx.external_resources { + Some(resources) => resolver.with_external_resources(resources), + None => resolver, + }; let execution_budget = TransformExecutionBudget::from_resources(&ctx.policy.resources); - let mut signatures = doc.descendants().filter(|node| { + let start_node = match ctx.start_node_id { + Some(id) => resolver.node_for_id(id).ok_or_else(|| { + SignatureVerificationPipelineError::SelectedNodeUnavailable { id: id.to_owned() } + })?, + None => doc.root(), + }; + let mut signatures = start_node.descendants().filter(|node| { node.is_element() && node.tag_name().name() == "Signature" && node.tag_name().namespace() == Some(XMLDSIG_NS) @@ -1108,14 +1141,6 @@ fn verify_signature_with_context( .into()); } } - let resolver = UriReferenceResolver::new(&doc).with_external_resource_limits( - ctx.policy.resources.max_external_resource_bytes, - ctx.policy.resources.max_external_resource_total_bytes, - ); - let resolver = match ctx.external_resources { - Some(resources) => resolver.with_external_resources(resources), - None => resolver, - }; let retrieval_materialization = if let Some(info) = key_info.as_mut() { materialize_retrieval_methods( info, @@ -5248,6 +5273,28 @@ mod tests { )); } + #[test] + fn pipeline_start_node_limits_signature_cardinality_to_its_subtree() { + // A start-node selector changes the operation root, not global ID or + // reference resolution; another Signature outside the subtree is irrelevant. + let xml = r#" + + + + +"#; + let err = VerifyContext::new() + .start_node_id("selected") + .verify(xml) + .expect_err("the selected Signature remains structurally incomplete"); + assert!(matches!( + err, + SignatureVerificationPipelineError::MissingElement { + element: "SignedInfo" + } + )); + } + #[test] fn pipeline_reports_keyinfo_parse_error() { let xml = r#" diff --git a/tools/xmlsec1/README.md b/tools/xmlsec1/README.md index 0866c64..f25d2a5 100644 --- a/tools/xmlsec1/README.md +++ b/tools/xmlsec1/README.md @@ -18,6 +18,8 @@ coverage. `EncryptedData` and performs in-document replacement, optionally selected by `--node-id`. Encryption retains template metadata and RSA-OAEP parameters; PKCS#1 RSA and PKCS#8/SPKI/X.509 PEM or DER key material is normalized into the -same core signing, verification, and encryption pipelines. Output paths support -the upstream `{inputfile}` basename template, and `--gen-key[:name]` emits both -named and unnamed AES key-store entries. +same core signing, verification, and encryption pipelines. Signing options +embed every certificate from `key,leaf,intermediate,...`; verification accepts +stdin as `-` and can select one signature subtree with `--node-id`. Output paths +support the upstream `{inputfile}` basename template, and `--gen-key[:name]` +emits both named and unnamed AES key-store entries. diff --git a/tools/xmlsec1/src/args.rs b/tools/xmlsec1/src/args.rs index 3c8e760..5eded24 100644 --- a/tools/xmlsec1/src/args.rs +++ b/tools/xmlsec1/src/args.rs @@ -108,7 +108,10 @@ impl Invocation { continue; } let option_text = argument.to_str(); - if options_finished || !option_text.is_some_and(|value| value.starts_with('-')) { + if options_finished + || argument == OsStr::new("-") + || !option_text.is_some_and(|value| value.starts_with('-')) + { positional.push(argument.clone()); options_finished = true; index += 1; @@ -333,6 +336,13 @@ mod tests { ); } + #[test] + fn parses_the_stdin_marker_as_positional_input() { + let parsed = parse(&["xmlsec1", "verify", "--pubkey-pem", "key.pem", "-"]) + .expect("a lone dash is the donor stdin marker"); + assert_eq!(parsed.positional, [OsString::from("-")]); + } + #[test] fn recognizes_donor_key_generation_and_pkcs8_aliases() { // These spellings are used by unmodified donor automation. diff --git a/tools/xmlsec1/src/commands.rs b/tools/xmlsec1/src/commands.rs index e5ef9ec..96ae3d7 100644 --- a/tools/xmlsec1/src/commands.rs +++ b/tools/xmlsec1/src/commands.rs @@ -327,13 +327,21 @@ fn sign(invocation: &Invocation, stdout: &mut dyn Write) -> Result<(), CommandEr let xml = read_input(invocation, policy.resources.max_xml_document_bytes)?; let (key_option, certificate_is_der) = select_signing_key(invocation, &xml)?; let value = key_option.value.as_deref().unwrap_or_default(); - let (key_path, certificate_path) = split_key_and_certificate(value)?; + let (key_path, certificate_paths) = split_key_and_certificates(value)?; let key = key_material::load_signing_key(key_path)?; - let signed = if let Some(certificate_path) = certificate_path { + let signed = if !certificate_paths.is_empty() { let writer = if certificate_is_der { - X509CertificateKeyInfoWriter::from_der(&key_material::read(certificate_path)?) + let certificates = certificate_paths + .iter() + .map(key_material::read) + .collect::, _>>()?; + X509CertificateKeyInfoWriter::from_der_chain(&certificates) } else { - X509CertificateKeyInfoWriter::from_pem(&key_material::read_text(certificate_path)?) + let certificates = certificate_paths + .iter() + .map(key_material::read_text) + .collect::, _>>()?; + X509CertificateKeyInfoWriter::from_pem_chain(&certificates) } .map_err(|error| CommandError::Signature(error.to_string()))?; SignContext::new(key.as_ref()) @@ -367,7 +375,9 @@ fn select_signing_key<'a>( "sign requires --privkey-pem or --pkcs8-pem/der".into(), )); } - if let [selected] = keys.as_slice() { + if let [selected] = keys.as_slice() + && (selected.0.parameter.is_none() || invocation.flag("lax-key-search")) + { return Ok(*selected); } let requested_name = template_key_name(xml)?; @@ -411,21 +421,21 @@ fn template_key_name(xml: &str) -> Result, CommandError> { })) } -fn split_key_and_certificate(value: &OsStr) -> Result<(&OsStr, Option<&OsStr>), CommandError> { +fn split_key_and_certificates(value: &OsStr) -> Result<(&OsStr, Vec<&OsStr>), CommandError> { let bytes = value.as_encoded_bytes(); - let Some(separator) = bytes.iter().position(|byte| *byte == b',') else { - return Ok((value, None)); - }; - if bytes[separator + 1..].contains(&b',') { + // Splitting at an ASCII byte preserves encoded-byte boundaries on every + // platform covered by OsStr's encoded-byte contract. + let mut components = bytes + .split(|byte| *byte == b',') + .map(|component| unsafe { OsStr::from_encoded_bytes_unchecked(component) }); + let key = components.next().unwrap_or(OsStr::new("")); + let certificates = components.collect::>(); + if key.is_empty() || certificates.iter().any(|path| path.is_empty()) { return Err(CommandError::Usage( - "private key accepts at most one certificate path".into(), + "private key and certificate paths must not be empty".into(), )); } - // Splitting at an ASCII byte preserves encoded-byte boundaries on every - // platform covered by OsStr's encoded-byte contract. - let key = unsafe { OsStr::from_encoded_bytes_unchecked(&bytes[..separator]) }; - let certificate = unsafe { OsStr::from_encoded_bytes_unchecked(&bytes[separator + 1..]) }; - Ok((key, Some(certificate))) + Ok((key, certificates)) } fn xmlsec_compatibility_verification_policy(invocation: &Invocation) -> VerificationPolicy { @@ -468,6 +478,10 @@ fn verify(invocation: &Invocation, stdout: &mut dyn Write) -> Result<(), Command "lax-key-search", "verify-crls", "X509-skip-time-checks", + // libxmlsec uses this to relax provider security levels for legacy + // certificate signatures. RustCrypto has no provider strict mode and + // already verifies every certificate signature algorithm it implements. + "X509-skip-strict-checks", "insecure", "verification-time", "depth", @@ -479,7 +493,7 @@ fn verify(invocation: &Invocation, stdout: &mut dyn Write) -> Result<(), Command "url-map", ], )?; - reject_unimplemented_selectors(invocation, &[])?; + reject_unimplemented_selectors(invocation, &["node-id"])?; reject_unimplemented_verification_policy(invocation)?; let direct_keys = ["pubkey-pem", "pubkey-der"] .into_iter() @@ -507,16 +521,23 @@ fn verify(invocation: &Invocation, stdout: &mut dyn Write) -> Result<(), Command } let policy = xmlsec_compatibility_verification_policy(invocation); let xml = read_input(invocation, policy.resources.max_xml_document_bytes)?; - let algorithm = key_material::signature_algorithm(&xml)?; + let start_node_id = option_text(invocation, "node-id")?; + let algorithm = key_material::signature_algorithm(&xml, start_node_id)?; let result = if let Some(path) = direct_path { let key = key_material::load_verification_key(path, algorithm)?; - VerifyContext::new() - .policy(policy) + verification_context(policy, start_node_id) .key(&key) .verify(&xml) .map_err(|error| CommandError::Signature(error.to_string()))? } else if let [certificate] = explicit_certificates.as_slice() { - verify_with_explicit_certificate(invocation, certificate, algorithm, policy, &xml)? + verify_with_explicit_certificate( + invocation, + certificate, + algorithm, + policy, + start_node_id, + &xml, + )? } else { let mut config = KeyResolverConfig::default(); for name in [ @@ -539,8 +560,7 @@ fn verify(invocation: &Invocation, stdout: &mut dyn Write) -> Result<(), Command } } let resolver = DefaultKeyResolver::new(config); - VerifyContext::new() - .policy(policy) + verification_context(policy, start_node_id) .key_resolver(&resolver) .verify(&xml) .map_err(|error| CommandError::Signature(error.to_string()))? @@ -559,11 +579,23 @@ fn verify(invocation: &Invocation, stdout: &mut dyn Write) -> Result<(), Command Ok(()) } +fn verification_context( + policy: VerificationPolicy, + start_node_id: Option<&str>, +) -> VerifyContext<'_> { + let context = VerifyContext::new().policy(policy); + match start_node_id { + Some(id) => context.start_node_id(id), + None => context, + } +} + fn verify_with_explicit_certificate( invocation: &Invocation, certificate: &crate::OptionValue, algorithm: SignatureAlgorithm, policy: VerificationPolicy, + start_node_id: Option<&str>, xml: &str, ) -> Result { let certificate_der = @@ -601,8 +633,7 @@ fn verify_with_explicit_certificate( .resolve_with_policy(Some(&key_info), algorithm, &policy) .map_err(|error| CommandError::Signature(error.to_string()))? .ok_or_else(|| CommandError::Signature("explicit certificate was not resolved".into()))?; - VerifyContext::new() - .policy(policy) + verification_context(policy, start_node_id) .key(key.as_ref()) .verify(xml) .map_err(|error| CommandError::Signature(error.to_string())) diff --git a/tools/xmlsec1/src/key_material.rs b/tools/xmlsec1/src/key_material.rs index 30bc13b..d7eeee0 100644 --- a/tools/xmlsec1/src/key_material.rs +++ b/tools/xmlsec1/src/key_material.rs @@ -14,7 +14,7 @@ use rsa::{ use x509_parser::prelude::FromDer as _; use xml_sec::xmldsig::{ EcdsaP256SigningKey, EcdsaP384SigningKey, RsaSigningKey, SignatureAlgorithm, SigningKey, - VerificationKey, find_signature_node, parse_signed_info, + VerificationKey, find_signature_node, parse_signed_info, uri::UriReferenceResolver, }; #[derive(Debug, thiserror::Error)] @@ -34,6 +34,8 @@ pub enum KeyMaterialError { InvalidCertificate(PathBuf), #[error("signature template does not contain a valid SignedInfo")] MissingSignedInfo, + #[error("selected node ID is missing or ambiguous: {0}")] + SelectedNodeUnavailable(String), #[error("invalid XML signature: {0}")] Signature(String), #[error("invalid symmetric key length: expected {expected} bytes, got {actual}")] @@ -53,10 +55,24 @@ pub fn read_text(path: impl AsRef) -> Result { String::from_utf8(read(path)?).map_err(|_| KeyMaterialError::InvalidPem(path.to_owned())) } -pub fn signature_algorithm(xml: &str) -> Result { +pub fn signature_algorithm( + xml: &str, + start_node_id: Option<&str>, +) -> Result { let document = Document::parse(xml).map_err(|error| KeyMaterialError::Signature(error.to_string()))?; - let signature = find_signature_node(&document).ok_or(KeyMaterialError::MissingSignedInfo)?; + let signature = match start_node_id { + Some(id) => { + let start = UriReferenceResolver::new(&document) + .node_for_id(id) + .ok_or_else(|| KeyMaterialError::SelectedNodeUnavailable(id.to_owned()))?; + start + .descendants() + .find(|node| node.has_tag_name(("http://www.w3.org/2000/09/xmldsig#", "Signature"))) + } + None => find_signature_node(&document), + } + .ok_or(KeyMaterialError::MissingSignedInfo)?; let signed_info = signature .children() .find(|node| node.is_element() && node.tag_name().name() == "SignedInfo") @@ -268,4 +284,31 @@ mod tests { assert!(error.to_string().contains(path.to_str().unwrap())); assert!(!error.to_string().contains("in PUBLIC KEY")); } + + #[test] + fn selected_signature_controls_verification_key_algorithm() { + // Key decoding must inspect the same selected Signature as verification; + // an unrelated earlier signature may use a different key family. + let digest = base64::Engine::encode(&base64::engine::general_purpose::STANDARD, [0_u8; 32]); + let signature = |id: &str, algorithm: &str| { + format!( + r#" + + + +{digest} +AA=="# + ) + }; + let xml = format!( + "{}{}", + signature("rsa", "http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"), + signature("ec", "http://www.w3.org/2001/04/xmldsig-more#ecdsa-sha256") + ); + + assert_eq!( + signature_algorithm(&xml, Some("ec")).unwrap(), + SignatureAlgorithm::EcdsaSha256 + ); + } } diff --git a/tools/xmlsec1/tests/process_contract.rs b/tools/xmlsec1/tests/process_contract.rs index 1a504c9..49d7ed2 100644 --- a/tools/xmlsec1/tests/process_contract.rs +++ b/tools/xmlsec1/tests/process_contract.rs @@ -1,4 +1,9 @@ -use std::{fs, path::Path, process::Command}; +use std::{ + fs, + io::Write as _, + path::Path, + process::{Command, Stdio}, +}; use rsa::{ RsaPrivateKey, @@ -66,6 +71,144 @@ fn signs_verifies_and_rejects_tampering_through_process_api() { assert!(String::from_utf8_lossy(&rejected.stderr).contains("invalid")); } +#[test] +fn verification_reads_the_conventional_stdin_marker() { + // A lone dash is input data, not an option name; this is the process-level + // contract used by shell pipelines and the donor CLI. + let template = project_root() + .join("tests/fixtures/xmldsig/aleksey-xmldsig-01/enveloping-sha256-rsa-sha256.tmpl"); + let private_key = project_root().join("tests/fixtures/keys/rsa/rsa-4096-key.pem"); + let public_key = project_root().join("tests/fixtures/keys/rsa/rsa-4096-pubkey.pem"); + let signed = Command::new(binary()) + .args(["sign", "--privkey-pem"]) + .arg(&private_key) + .arg(&template) + .output() + .unwrap(); + assert!(signed.status.success()); + + let mut verify = Command::new(binary()) + .args(["verify", "--pubkey-pem"]) + .arg(&public_key) + .arg("-") + .stdin(Stdio::piped()) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()) + .spawn() + .unwrap(); + verify + .stdin + .take() + .unwrap() + .write_all(&signed.stdout) + .unwrap(); + let output = verify.wait_with_output().unwrap(); + assert!( + output.status.success(), + "{}", + String::from_utf8_lossy(&output.stderr) + ); +} + +#[test] +fn verification_node_id_selects_one_signature_subtree() { + // libxmlsec1 resolves --node-id to a start node and finds the Signature + // below it; unrelated signatures elsewhere in the document are ignored. + let temp = tempfile::tempdir().unwrap(); + let original = fs::read_to_string( + project_root() + .join("tests/fixtures/xmldsig/aleksey-xmldsig-01/enveloping-sha256-rsa-sha256.tmpl"), + ) + .unwrap(); + let private_key = project_root().join("tests/fixtures/keys/rsa/rsa-4096-key.pem"); + let public_key = project_root().join("tests/fixtures/keys/rsa/rsa-4096-pubkey.pem"); + let mut signatures = Vec::new(); + for id in ["first", "second"] { + let template = temp.path().join(format!("{id}.xml")); + fs::write( + &template, + original + .replace("#object", &format!("#{id}-object")) + .replace("Id=\"object\"", &format!("Id=\"{id}-object\"")), + ) + .unwrap(); + let output = Command::new(binary()) + .args(["sign", "--privkey-pem"]) + .arg(&private_key) + .arg(&template) + .output() + .unwrap(); + assert!(output.status.success()); + let signed = String::from_utf8(output.stdout).unwrap(); + let signed = signed + .split_once("?>") + .map_or(signed.as_str(), |(_, body)| body) + .replace( + "{}{}", signatures[0], signatures[1]), + ) + .unwrap(); + + let valid = Command::new(binary()) + .args(["verify", "--pubkey-pem"]) + .arg(&public_key) + .args(["--node-id", "first"]) + .arg(&document) + .output() + .unwrap(); + assert!( + valid.status.success(), + "{}", + String::from_utf8_lossy(&valid.stderr) + ); + + let invalid = Command::new(binary()) + .args(["verify", "--pubkey-pem"]) + .arg(&public_key) + .args(["--node-id", "second"]) + .arg(&document) + .output() + .unwrap(); + assert!(!invalid.status.success()); + assert!(String::from_utf8_lossy(&invalid.stderr).contains("signature is invalid")); + + let missing = Command::new(binary()) + .args(["verify", "--pubkey-pem"]) + .arg(&public_key) + .args(["--node-id", "missing"]) + .arg(&document) + .output() + .unwrap(); + assert!(!missing.status.success()); + assert!(String::from_utf8_lossy(&missing.stderr).contains("selected node")); + + let duplicate = temp.path().join("duplicate.xml"); + fs::write( + &duplicate, + fs::read_to_string(&document) + .unwrap() + .replace("Id=\"second\"", "Id=\"first\""), + ) + .unwrap(); + let ambiguous = Command::new(binary()) + .args(["verify", "--pubkey-pem"]) + .arg(&public_key) + .args(["--node-id", "first"]) + .arg(&duplicate) + .output() + .unwrap(); + assert!(!ambiguous.status.success()); + assert!(String::from_utf8_lossy(&ambiguous.stderr).contains("ambiguous")); +} + #[test] fn output_template_expands_the_extensionless_input_basename() { // libxmlsec1 automation uses one output template across many input files; @@ -536,6 +679,78 @@ fn explicit_certificate_pins_the_verification_identity() { ); } +#[test] +fn signing_embeds_every_certificate_from_the_private_key_option() { + // libxmlsec1 treats every comma-separated path after the private key as a + // certificate to embed, so recipients can reconstruct the supplied chain. + let temp = tempfile::tempdir().unwrap(); + let template = project_root() + .join("tests/fixtures/xmldsig/aleksey-xmldsig-01/enveloping-sha256-rsa-sha256.tmpl"); + let private_key = project_root().join("tests/fixtures/keys/rsa/rsa-4096-key.pem"); + let leaf = project_root().join("tests/fixtures/keys/rsa/rsa-4096-cert.pem"); + let issuer = project_root().join("tests/fixtures/keys/rsa/rsa-2048-cert.pem"); + let signed = temp.path().join("signed.xml"); + let compound = format!( + "{},{},{}", + private_key.display(), + leaf.display(), + issuer.display() + ); + + let result = Command::new(binary()) + .args(["sign", "--privkey-pem"]) + .arg(compound) + .args(["--output"]) + .arg(&signed) + .arg(&template) + .output() + .unwrap(); + assert!( + result.status.success(), + "{}", + String::from_utf8_lossy(&result.stderr) + ); + let xml = fs::read_to_string(signed).unwrap(); + let document = roxmltree::Document::parse(&xml).unwrap(); + assert_eq!( + document + .descendants() + .filter( + |node| node.has_tag_name(("http://www.w3.org/2000/09/xmldsig#", "X509Certificate")) + ) + .count(), + 2 + ); +} + +#[test] +fn signing_rejects_a_malformed_secondary_certificate() { + // Every certificate path is parsed before signing; a malformed trailing + // chain member must not be silently omitted from the emitted KeyInfo. + let temp = tempfile::tempdir().unwrap(); + let template = project_root() + .join("tests/fixtures/xmldsig/aleksey-xmldsig-01/enveloping-sha256-rsa-sha256.tmpl"); + let private_key = project_root().join("tests/fixtures/keys/rsa/rsa-4096-key.pem"); + let leaf = project_root().join("tests/fixtures/keys/rsa/rsa-4096-cert.pem"); + let malformed = temp.path().join("malformed.pem"); + fs::write(&malformed, "not a certificate").unwrap(); + let compound = format!( + "{},{},{}", + private_key.display(), + leaf.display(), + malformed.display() + ); + + let result = Command::new(binary()) + .args(["sign", "--privkey-pem"]) + .arg(compound) + .arg(&template) + .output() + .unwrap(); + assert!(!result.status.success()); + assert!(String::from_utf8_lossy(&result.stderr).contains("invalid PEM certificate")); +} + #[test] fn explicit_certificate_obeys_trust_anchor_policy() { // An explicit leaf pins identity but does not establish trust when callers @@ -675,6 +890,36 @@ fn multiple_signing_keys_require_the_matching_template_name() { assert!(String::from_utf8_lossy(&missing.stderr).contains("unknown KeyName")); } +#[test] +fn singleton_named_signing_key_obeys_template_key_name() { + // Naming one key enables strict KeyName lookup even when the key set has a + // single entry; lax lookup is the explicit donor-compatible escape hatch. + let template = project_root() + .join("tests/fixtures/xmldsig/aleksey-xmldsig-01/enveloping-sha256-rsa-sha256.tmpl"); + let private_key = project_root().join("tests/fixtures/keys/rsa/rsa-4096-key.pem"); + + let strict = Command::new(binary()) + .args(["sign", "--privkey-pem:unexpected"]) + .arg(&private_key) + .arg(&template) + .output() + .unwrap(); + assert!(!strict.status.success()); + assert!(String::from_utf8_lossy(&strict.stderr).contains("unknown KeyName")); + + let lax = Command::new(binary()) + .args(["sign", "--lax-key-search", "--privkey-pem:unexpected"]) + .arg(&private_key) + .arg(&template) + .output() + .unwrap(); + assert!( + lax.status.success(), + "{}", + String::from_utf8_lossy(&lax.stderr) + ); +} + #[cfg(target_os = "linux")] #[test] fn signs_through_non_utf8_filesystem_paths() { diff --git a/tools/xmlsec1/tests/upstream_runner.rs b/tools/xmlsec1/tests/upstream_runner.rs index 64ad118..f9e3b85 100644 --- a/tools/xmlsec1/tests/upstream_runner.rs +++ b/tools/xmlsec1/tests/upstream_runner.rs @@ -1,8 +1,12 @@ -use std::{path::Path, process::Command}; +use std::{fs, path::Path, process::Command}; -fn run_upstream(script: &str, selected_test: &str) { +fn run_upstream(script: &str, selected_test: &str) -> String { let tests = Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures/upstream"); - let output = Command::new(tests.join("testrun.sh")) + let run_root = tempfile::tempdir().expect("the upstream runner needs a private log directory"); + // The upstream runner uses Bash-only `source` and `[[` despite its `/bin/sh` + // shebang. Invoke its actual language explicitly on platforms where `sh` is dash. + let output = Command::new("bash") + .arg(tests.join("testrun.sh")) .arg(tests.join(script)) .arg("rustcrypto") .arg(&tests) @@ -10,6 +14,7 @@ fn run_upstream(script: &str, selected_test: &str) { .arg("der") .env("XMLSEC_TEST_NAME", selected_test) .env("XMLSEC_TEST_REPRODUCIBLE", "1") + .env("TMPFOLDER", run_root.path()) .output() .expect("the checked-in upstream runner must execute"); assert!( @@ -24,18 +29,28 @@ fn run_upstream(script: &str, selected_test: &str) { stdout.contains("TOTAL FAILED: 0"), "runner reported a failed operation" ); + let run_directory = fs::read_dir(run_root.path()) + .expect("the runner log root must be readable") + .next() + .expect("the runner must create one log directory") + .expect("the runner log directory entry must be readable") + .path(); + fs::read_to_string(run_directory.join("full.log")) + .expect("the unmodified runner must preserve its full operation log") } #[test] fn unmodified_dsig_runner_observes_failure_status() { // This upstream negative vector proves that digest tampering reaches the // native process and is reported with the status expected by testrun.sh. - run_upstream("testDSig.sh", "signature-rsa-enveloped-bad-digest-val"); + let log = run_upstream("testDSig.sh", "signature-rsa-enveloped-bad-digest-val"); + assert!(log.contains("Error: signature is invalid"), "{log}"); + assert!(!log.contains("unsupported option"), "{log}"); } #[test] fn unmodified_enc_runner_round_trips_aes_gcm() { - run_upstream( + let _ = run_upstream( "testEnc.sh", "xmlenc11-interop-2012/xenc11-example-AES128-GCM", ); @@ -43,5 +58,5 @@ fn unmodified_enc_runner_round_trips_aes_gcm() { #[test] fn unmodified_keys_runner_generates_aes_key_store() { - run_upstream("testKeys.sh", "test-aes128"); + let _ = run_upstream("testKeys.sh", "test-aes128"); } From 07415fb8ad39e904c6282a8b978dd308a0cc4c2b Mon Sep 17 00:00:00 2001 From: Dmitry Prudnikov Date: Fri, 14 Aug 2026 10:32:46 +0300 Subject: [PATCH 06/27] fix(cli): enforce compatibility contracts - synchronize option parsing and help metadata - enforce strict named-key and input-type semantics - harden fixture import and source installation workflows --- README.md | 4 +- docs/cli.md | 12 + scripts/import-xmlsec1-cli-fixtures.sh | 30 +- scripts/install-xmlsec1.sh | 6 +- tests/install_xmlsec1.rs | 44 ++- tools/xmlsec1/README.md | 4 +- tools/xmlsec1/src/args.rs | 411 ++++++++++++++++++++---- tools/xmlsec1/src/commands.rs | 186 ++++++++++- tools/xmlsec1/tests/import_snapshot.rs | 98 ++++++ tools/xmlsec1/tests/process_contract.rs | 241 +++++++++++--- tools/xmlsec1/tests/upstream_runner.rs | 21 +- 11 files changed, 916 insertions(+), 141 deletions(-) diff --git a/README.md b/README.md index ea142f0..6798130 100644 --- a/README.md +++ b/README.md @@ -80,7 +80,9 @@ xmlsec1 list-key-data The native binary supports sign/verify, template-preserving encrypt/decrypt, AES key generation, capability checks, libxmlsec1 key aliases and option syntax, certificate-chain embedding, stdin input, signature selection by node ID, and -deterministic process statuses. Its process tests run a minimal checked-in +deterministic process statuses. `help-all` enumerates the same registered +commands and options accepted by the parser, while named direct keys obey +template `KeyName` unless lax lookup is explicitly requested. Its process tests run a minimal checked-in snapshot of the unmodified upstream DSig, Enc, and Keys runners without network access or a system `xmlsec1` installation. Unsupported algorithms, key formats, providers, and policy controls fail closed diff --git a/docs/cli.md b/docs/cli.md index c06b495..892fe8b 100644 --- a/docs/cli.md +++ b/docs/cli.md @@ -30,6 +30,12 @@ backend. RustCrypto accepts an absent or empty configuration directory because it has no external backend configuration; a non-empty path is rejected rather than ignored. +`help-all` enumerates every registered command and canonical option, including +which options accept `[:name]` and which consume a value. The listing is built +from the parser's option metadata, so it cannot advertise a syntax the parser +does not recognize. A `:` suffix on flags or unrelated valued +options is rejected rather than silently activating the underlying option. + ## Examples Sign an existing XMLDSig template and verify it with an explicit public key: @@ -50,6 +56,8 @@ Verification accepts `-` as the conventional stdin marker. For documents with multiple signatures, `--node-id ` selects an ID-bearing start node and verifies the single `Signature` in its subtree; missing and duplicate IDs fail closed. +Named `--pubkey-pem:name` and `--pubkey-der:name` inputs must match the selected +signature's `KeyName`; `--lax-key-search` is the explicit opt-out. `--output` follows the upstream filename-template contract. The first `{inputfile}` token is replaced with the input file's basename after removing @@ -72,6 +80,10 @@ larger XML document; `--node-id` selects an embedded `EncryptedData` by `Id`. Encryption preserves the template's `Id`, `Type`, `MimeType`, `KeyInfo`, `EncryptionProperties`, and RSA-OAEP parameters while replacing only the cryptographic `CipherValue` payloads. +For direct AES encryption, a named key must match an existing template +`KeyName` unless `--lax-key-search` is supplied. `--binary-data` rejects +templates explicitly typed as XML `Element` or `Content`; use `--xml-data` for +those templates so ciphertext metadata cannot mislabel arbitrary bytes as XML. Generate an AES key store using the upstream command shape: diff --git a/scripts/import-xmlsec1-cli-fixtures.sh b/scripts/import-xmlsec1-cli-fixtures.sh index 6d98143..a075307 100755 --- a/scripts/import-xmlsec1-cli-fixtures.sh +++ b/scripts/import-xmlsec1-cli-fixtures.sh @@ -11,11 +11,26 @@ if [[ "$operation" != "import" && "$operation" != "--check" ]]; then fi mkdir -p "$(dirname "$target")" staging="$(mktemp -d "${target}.import.XXXXXX")" +backup="" cleanup() { + local status="${1:-$?}" + trap - EXIT INT TERM HUP rm -rf "$staging" + if [[ -n "$backup" && -e "$backup" ]]; then + if [[ -e "$target" ]]; then + rm -rf "$backup" + elif ! mv "$backup" "$target"; then + printf 'failed to restore fixture snapshot from %s\n' "$backup" >&2 + status=1 + fi + fi + exit "$status" } -trap cleanup EXIT +trap 'cleanup $?' EXIT +trap 'cleanup 130' INT +trap 'cleanup 143' TERM +trap 'cleanup 129' HUP assets=( "testrun.sh" @@ -44,8 +59,17 @@ for asset in "${assets[@]}"; do install -m "$file_mode" "$source" "$staging/$asset" done -printf '%s\n' "$(<"$repo_root/compatibility/libxmlsec1-1.3.13-donor-commit.txt")" \ - > "$staging/DONOR_COMMIT" +donor_commit_file="$repo_root/compatibility/libxmlsec1-1.3.13-donor-commit.txt" +if [[ ! -s "$donor_commit_file" ]]; then + printf 'donor commit pin is missing or empty: %s\n' "$donor_commit_file" >&2 + exit 1 +fi +donor_commit="$(<"$donor_commit_file")" +if [[ ! "$donor_commit" =~ ^([0-9a-f]{40}|[0-9a-f]{64})$ ]]; then + printf 'donor commit pin is not a Git object ID: %s\n' "$donor_commit_file" >&2 + exit 1 +fi +printf '%s\n' "$donor_commit" > "$staging/DONOR_COMMIT" backup="${target}.backup.$$" if [[ "$operation" == "--check" ]]; then diff --git a/scripts/install-xmlsec1.sh b/scripts/install-xmlsec1.sh index c9d5e46..4d36f35 100755 --- a/scripts/install-xmlsec1.sh +++ b/scripts/install-xmlsec1.sh @@ -85,7 +85,11 @@ stage_dir="$work_dir/stage" if [[ -n "${XMLSEC1_SOURCE_DIR:-}" ]]; then local_source_dir="$XMLSEC1_SOURCE_DIR" - if [[ "$local_source_dir" != /* || ! -d "$local_source_dir/.git" ]]; then + if [[ "$local_source_dir" != /* || ! -d "$local_source_dir" ]]; then + printf 'XMLSEC1_SOURCE_DIR must be an absolute git checkout: %s\n' "$local_source_dir" >&2 + exit 1 + fi + if [[ "$(git -C "$local_source_dir" rev-parse --is-inside-work-tree 2>/dev/null)" != "true" ]]; then printf 'XMLSEC1_SOURCE_DIR must be an absolute git checkout: %s\n' "$local_source_dir" >&2 exit 1 fi diff --git a/tests/install_xmlsec1.rs b/tests/install_xmlsec1.rs index 51dc814..ccd012e 100644 --- a/tests/install_xmlsec1.rs +++ b/tests/install_xmlsec1.rs @@ -75,7 +75,7 @@ impl InstallHarness { root.tool( "git", - "#!/bin/sh\nif [ \"$1\" = \"init\" ]; then mkdir -p \"$2\"; exit 0; fi\n[ \"$1\" = \"-C\" ] || exit 2\nsource=$2\nshift 2\ncommand=$1\nshift\ncase \"$command\" in\n remote) exit 0 ;;\n fetch)\n for argument in \"$@\"; do requested=$argument; done\n printf '%s\\n' \"${GIT_REPORTED_COMMIT:-$requested}\" > \"$GIT_FETCHED_COMMIT_FILE\"\n printf '#!/bin/sh\\nexit 0\\n' > \"$source/autogen.sh\"\n chmod +x \"$source/autogen.sh\"\n ;;\n rev-parse) cat \"$GIT_FETCHED_COMMIT_FILE\" ;;\n checkout) exit 0 ;;\n *) exit 2 ;;\nesac\n", + "#!/bin/sh\nif [ \"$1\" = \"init\" ]; then mkdir -p \"$2\"; exit 0; fi\n[ \"$1\" = \"-C\" ] || exit 2\nsource=$2\nshift 2\ncommand=$1\nshift\ncase \"$command\" in\n remote) exit 0 ;;\n fetch)\n for argument in \"$@\"; do requested=$argument; done\n printf '%s\\n' \"${GIT_REPORTED_COMMIT:-$requested}\" > \"$GIT_FETCHED_COMMIT_FILE\"\n printf '#!/bin/sh\\nexit 0\\n' > \"$source/autogen.sh\"\n chmod +x \"$source/autogen.sh\"\n ;;\n rev-parse)\n if [ \"${1:-}\" = \"--is-inside-work-tree\" ]; then\n [ -e \"$source/.git\" ] || exit 128\n printf 'true\\n'\n elif [ -n \"${XMLSEC1_SOURCE_DIR:-}\" ] && [ \"$source\" = \"$XMLSEC1_SOURCE_DIR\" ]; then\n printf '%s\\n' \"$GIT_REPORTED_COMMIT\"\n else\n cat \"$GIT_FETCHED_COMMIT_FILE\"\n fi\n ;;\n archive) /usr/bin/tar -C \"$source\" -cf - autogen.sh ;;\n checkout) exit 0 ;;\n *) exit 2 ;;\nesac\n", ); root.tool("nproc", "#!/bin/sh\nprintf '1\\n'\n"); root.tool( @@ -130,6 +130,27 @@ impl InstallHarness { } command.status().expect("installation script must run") } + + fn run_from_source(&self, source: &Path) -> std::process::ExitStatus { + let inherited_path = std::env::var_os("PATH").expect("test process must have PATH"); + let path = std::env::join_paths( + std::iter::once(self.tools.clone()).chain(std::env::split_paths(&inherited_path)), + ) + .expect("test PATH must be joinable"); + Command::new("bash") + .arg("scripts/install-xmlsec1.sh") + .env("XMLSEC1_PREFIX", &self.prefix) + .env("XMLSEC1_SOURCE_DIR", source) + .env("GIT_REPORTED_COMMIT", DONOR_COMMIT.trim()) + .env( + "GIT_FETCHED_COMMIT_FILE", + self.root.path().join("fetched-commit"), + ) + .env("MV_COUNT_FILE", self.root.path().join("mv-count")) + .env("PATH", path) + .status() + .expect("installation script must run") + } } #[test] @@ -174,6 +195,27 @@ fn installer_rejects_source_revision_mismatch() { ); } +#[test] +fn installer_accepts_a_linked_worktree_source_checkout() { + // Linked worktrees represent .git as a file. Repository identity must come + // from Git rather than a filesystem-shape assumption. + let harness = InstallHarness::new(); + let source = harness.root.path().join("linked-worktree"); + std::fs::create_dir(&source).unwrap(); + std::fs::write(source.join(".git"), "gitdir: /tmp/fake-worktree\n").unwrap(); + let autogen = source.join("autogen.sh"); + std::fs::write(&autogen, "#!/bin/sh\nexit 0\n").unwrap(); + std::fs::set_permissions(&autogen, std::fs::Permissions::from_mode(0o755)).unwrap(); + + let status = harness.run_from_source(&source); + + assert!( + status.success(), + "linked worktree checkout must be accepted" + ); + assert!(!harness.prefix.join("sentinel").exists()); +} + #[test] fn failed_first_install_removes_promoted_prefix() { // A failed smoke test must not leave an executable plus source marker that diff --git a/tools/xmlsec1/README.md b/tools/xmlsec1/README.md index f25d2a5..eceae45 100644 --- a/tools/xmlsec1/README.md +++ b/tools/xmlsec1/README.md @@ -22,4 +22,6 @@ same core signing, verification, and encryption pipelines. Signing options embed every certificate from `key,leaf,intermediate,...`; verification accepts stdin as `-` and can select one signature subtree with `--node-id`. Output paths support the upstream `{inputfile}` basename template, and `--gen-key[:name]` -emits both named and unnamed AES key-store entries. +emits both named and unnamed AES key-store entries. `help-all` is generated from +the parser registry, named direct keys are checked against template `KeyName`, +and binary payloads cannot be emitted with XML Element/Content type metadata. diff --git a/tools/xmlsec1/src/args.rs b/tools/xmlsec1/src/args.rs index 5eded24..cf7d972 100644 --- a/tools/xmlsec1/src/args.rs +++ b/tools/xmlsec1/src/args.rs @@ -75,16 +75,325 @@ pub enum ParseError { MissingOptionValue(String), #[error("unsupported option: {0}")] UnsupportedOption(String), + #[error("option {0} does not accept a name parameter")] + UnexpectedOptionParameter(String), #[error("arguments are not valid UTF-8")] NonUtf8, } #[derive(Clone, Copy)] -enum Arity { +pub(crate) enum Arity { Flag, Value, } +pub(crate) struct OptionSpec { + pub canonical: &'static str, + aliases: &'static [&'static str], + pub arity: Arity, + pub accepts_parameter: bool, +} + +const FLAG: Arity = Arity::Flag; +const VALUE: Arity = Arity::Value; + +pub(crate) const OPTION_SPECS: &[OptionSpec] = &[ + OptionSpec { + canonical: "output", + aliases: &["o"], + arity: VALUE, + accepts_parameter: false, + }, + OptionSpec { + canonical: "crypto", + aliases: &[], + arity: VALUE, + accepts_parameter: false, + }, + OptionSpec { + canonical: "crypto-config", + aliases: &[], + arity: VALUE, + accepts_parameter: false, + }, + OptionSpec { + canonical: "verbose", + aliases: &[], + arity: FLAG, + accepts_parameter: false, + }, + OptionSpec { + canonical: "print-crypto-library-errors", + aliases: &[], + arity: FLAG, + accepts_parameter: false, + }, + OptionSpec { + canonical: "print-debug", + aliases: &[], + arity: FLAG, + accepts_parameter: false, + }, + OptionSpec { + canonical: "print-xml-debug", + aliases: &[], + arity: FLAG, + accepts_parameter: false, + }, + OptionSpec { + canonical: "repeat", + aliases: &[], + arity: VALUE, + accepts_parameter: false, + }, + OptionSpec { + canonical: "keys-file", + aliases: &[], + arity: VALUE, + accepts_parameter: false, + }, + OptionSpec { + canonical: "gen-key", + aliases: &["g"], + arity: VALUE, + accepts_parameter: true, + }, + OptionSpec { + canonical: "privkey-pem", + aliases: &["privkey"], + arity: VALUE, + accepts_parameter: true, + }, + OptionSpec { + canonical: "privkey-der", + aliases: &[], + arity: VALUE, + accepts_parameter: true, + }, + OptionSpec { + canonical: "pkcs8-pem", + aliases: &["privkey-p8-pem"], + arity: VALUE, + accepts_parameter: true, + }, + OptionSpec { + canonical: "pkcs8-der", + aliases: &["privkey-p8-der"], + arity: VALUE, + accepts_parameter: true, + }, + OptionSpec { + canonical: "pubkey-pem", + aliases: &["pubkey"], + arity: VALUE, + accepts_parameter: true, + }, + OptionSpec { + canonical: "pubkey-der", + aliases: &[], + arity: VALUE, + accepts_parameter: true, + }, + OptionSpec { + canonical: "pubkey-cert-pem", + aliases: &[], + arity: VALUE, + accepts_parameter: true, + }, + OptionSpec { + canonical: "pubkey-cert-der", + aliases: &[], + arity: VALUE, + accepts_parameter: true, + }, + OptionSpec { + canonical: "trusted-pem", + aliases: &["trusted"], + arity: VALUE, + accepts_parameter: false, + }, + OptionSpec { + canonical: "trusted-der", + aliases: &[], + arity: VALUE, + accepts_parameter: false, + }, + OptionSpec { + canonical: "untrusted-pem", + aliases: &["untrusted"], + arity: VALUE, + accepts_parameter: false, + }, + OptionSpec { + canonical: "untrusted-der", + aliases: &[], + arity: VALUE, + accepts_parameter: false, + }, + OptionSpec { + canonical: "aes-key", + aliases: &["aeskey"], + arity: VALUE, + accepts_parameter: true, + }, + OptionSpec { + canonical: "hmac-key", + aliases: &["hmackey"], + arity: VALUE, + accepts_parameter: true, + }, + OptionSpec { + canonical: "pwd", + aliases: &[], + arity: VALUE, + accepts_parameter: false, + }, + OptionSpec { + canonical: "enabled-key-data", + aliases: &[], + arity: VALUE, + accepts_parameter: false, + }, + OptionSpec { + canonical: "enabled-reference-uris", + aliases: &[], + arity: VALUE, + accepts_parameter: false, + }, + OptionSpec { + canonical: "enabled-retrieval-uris", + aliases: &[], + arity: VALUE, + accepts_parameter: false, + }, + OptionSpec { + canonical: "enabled-cipher-reference-uris", + aliases: &[], + arity: VALUE, + accepts_parameter: false, + }, + OptionSpec { + canonical: "ignore-manifests", + aliases: &[], + arity: FLAG, + accepts_parameter: false, + }, + OptionSpec { + canonical: "lax-key-search", + aliases: &[], + arity: FLAG, + accepts_parameter: false, + }, + OptionSpec { + canonical: "verify-keys", + aliases: &[], + arity: FLAG, + accepts_parameter: false, + }, + OptionSpec { + canonical: "verify-crls", + aliases: &[], + arity: FLAG, + accepts_parameter: false, + }, + OptionSpec { + canonical: "X509-skip-time-checks", + aliases: &[], + arity: FLAG, + accepts_parameter: false, + }, + OptionSpec { + canonical: "X509-skip-strict-checks", + aliases: &[], + arity: FLAG, + accepts_parameter: false, + }, + OptionSpec { + canonical: "insecure", + aliases: &[], + arity: FLAG, + accepts_parameter: false, + }, + OptionSpec { + canonical: "verification-time", + aliases: &["verification-gmt-time"], + arity: VALUE, + accepts_parameter: false, + }, + OptionSpec { + canonical: "depth", + aliases: &[], + arity: VALUE, + accepts_parameter: false, + }, + OptionSpec { + canonical: "node-id", + aliases: &[], + arity: VALUE, + accepts_parameter: false, + }, + OptionSpec { + canonical: "node-name", + aliases: &[], + arity: VALUE, + accepts_parameter: false, + }, + OptionSpec { + canonical: "node-xpath", + aliases: &[], + arity: VALUE, + accepts_parameter: false, + }, + OptionSpec { + canonical: "id-attr", + aliases: &[], + arity: VALUE, + accepts_parameter: true, + }, + OptionSpec { + canonical: "add-id-attr", + aliases: &[], + arity: VALUE, + accepts_parameter: true, + }, + OptionSpec { + canonical: "binary-data", + aliases: &[], + arity: VALUE, + accepts_parameter: false, + }, + OptionSpec { + canonical: "xml-data", + aliases: &[], + arity: VALUE, + accepts_parameter: false, + }, + OptionSpec { + canonical: "session-key", + aliases: &[], + arity: VALUE, + accepts_parameter: false, + }, + OptionSpec { + canonical: "url-map", + aliases: &[], + arity: VALUE, + accepts_parameter: true, + }, + OptionSpec { + canonical: "enable-asn1-signatures-hack", + aliases: &[], + arity: FLAG, + accepts_parameter: false, + }, + OptionSpec { + canonical: "help", + aliases: &[], + arity: FLAG, + accepts_parameter: false, + }, +]; + impl Invocation { pub fn parse(args: impl IntoIterator) -> Result { let mut args = args.into_iter(); @@ -126,6 +435,11 @@ impl Invocation { }); let name = canonical_option(raw_name) .ok_or_else(|| ParseError::UnsupportedOption(argument_text.to_owned()))?; + if parameter.is_some() && !option_spec(name).accepts_parameter { + return Err(ParseError::UnexpectedOptionParameter( + argument_text.to_owned(), + )); + } let value = match option_arity(name) { Arity::Flag => None, @@ -170,77 +484,21 @@ impl Invocation { } fn canonical_option(name: &str) -> Option<&'static str> { - Some(match name { - "o" | "output" => "output", - "crypto" => "crypto", - "crypto-config" => "crypto-config", - "verbose" => "verbose", - "print-crypto-library-errors" => "print-crypto-library-errors", - "print-debug" => "print-debug", - "print-xml-debug" => "print-xml-debug", - "repeat" => "repeat", - "keys-file" => "keys-file", - "g" | "gen-key" => "gen-key", - "privkey" | "privkey-pem" => "privkey-pem", - "privkey-der" => "privkey-der", - "pkcs8-pem" | "privkey-p8-pem" => "pkcs8-pem", - "pkcs8-der" | "privkey-p8-der" => "pkcs8-der", - "pubkey" | "pubkey-pem" => "pubkey-pem", - "pubkey-der" => "pubkey-der", - "pubkey-cert-pem" => "pubkey-cert-pem", - "pubkey-cert-der" => "pubkey-cert-der", - "trusted-pem" | "trusted" => "trusted-pem", - "trusted-der" => "trusted-der", - "untrusted-pem" | "untrusted" => "untrusted-pem", - "untrusted-der" => "untrusted-der", - "aes-key" | "aeskey" => "aes-key", - "hmac-key" | "hmackey" => "hmac-key", - "pwd" => "pwd", - "enabled-key-data" => "enabled-key-data", - "enabled-reference-uris" => "enabled-reference-uris", - "enabled-retrieval-uris" => "enabled-retrieval-uris", - "enabled-cipher-reference-uris" => "enabled-cipher-reference-uris", - "ignore-manifests" => "ignore-manifests", - "lax-key-search" => "lax-key-search", - "verify-keys" => "verify-keys", - "verify-crls" => "verify-crls", - "X509-skip-time-checks" => "X509-skip-time-checks", - "X509-skip-strict-checks" => "X509-skip-strict-checks", - "insecure" => "insecure", - "verification-time" | "verification-gmt-time" => "verification-time", - "depth" => "depth", - "node-id" => "node-id", - "node-name" => "node-name", - "node-xpath" => "node-xpath", - "id-attr" => "id-attr", - "add-id-attr" => "add-id-attr", - "binary-data" => "binary-data", - "xml-data" => "xml-data", - "session-key" => "session-key", - "url-map" => "url-map", - "enable-asn1-signatures-hack" => "enable-asn1-signatures-hack", - "help" => "help", - _ => return None, - }) + OPTION_SPECS + .iter() + .find(|spec| spec.canonical == name || spec.aliases.contains(&name)) + .map(|spec| spec.canonical) } fn option_arity(name: &str) -> Arity { - match name { - "verbose" - | "print-crypto-library-errors" - | "print-debug" - | "print-xml-debug" - | "ignore-manifests" - | "lax-key-search" - | "verify-keys" - | "verify-crls" - | "X509-skip-time-checks" - | "X509-skip-strict-checks" - | "insecure" - | "enable-asn1-signatures-hack" - | "help" => Arity::Flag, - _ => Arity::Value, - } + option_spec(name).arity +} + +fn option_spec(name: &str) -> &'static OptionSpec { + OPTION_SPECS + .iter() + .find(|spec| spec.canonical == name) + .expect("canonical options must have metadata") } impl fmt::Display for Command { @@ -323,6 +581,21 @@ mod tests { parse(&["xmlsec1", "verify", "--output"]), Err(ParseError::MissingOptionValue(_)) )); + assert!( + parse(&["xmlsec1", "verify", "--insecure:false", "input.xml"]).is_err(), + "flag parameters must not silently enable the underlying flag" + ); + assert!( + parse(&[ + "xmlsec1", + "verify", + "--trusted-pem:anchor", + "root.pem", + "input.xml" + ]) + .is_err(), + "only key options with named lookup semantics accept parameters" + ); } #[test] diff --git a/tools/xmlsec1/src/commands.rs b/tools/xmlsec1/src/commands.rs index 96ae3d7..cde0c7c 100644 --- a/tools/xmlsec1/src/commands.rs +++ b/tools/xmlsec1/src/commands.rs @@ -13,7 +13,7 @@ use xml_sec::{ xmldsig::{ DefaultKeyResolver, DsigStatus, KeyResolver, KeyResolverConfig, SignContext, SignatureAlgorithm, UriTypeSet, VerifyContext, X509CertificateKeyInfoWriter, - parse_key_info, + parse_key_info, uri::UriReferenceResolver, }, xmlenc::{ DataEncryptionAlgorithm, DecryptContext, DecryptedContent, DecryptionKeyResolver, @@ -24,6 +24,7 @@ use xml_sec::{ use crate::{ Command, Invocation, + args::{Arity, OPTION_SPECS}, capabilities::{self, KEY_DATA, TRANSFORMS}, key_material, }; @@ -85,12 +86,11 @@ pub fn execute( validate_provider(&invocation)?; validate_crypto_config(&invocation)?; match invocation.command { - Command::Help - | Command::HelpAll - | Command::HelpDsig - | Command::HelpEnc - | Command::HelpKeys - | Command::HelpX509 => help(stdout), + Command::Help => help(stdout), + Command::HelpAll => help_all(stdout), + Command::HelpDsig | Command::HelpEnc | Command::HelpKeys | Command::HelpX509 => { + help(stdout) + } Command::Version => writeln!(stdout, "xmlsec1 1.3.13 (rustcrypto)").map_err(stdout_error), Command::ListTransforms => { validate_options(&invocation, &[])?; @@ -134,6 +134,32 @@ fn help(output: &mut dyn Write) -> Result<(), CommandError> { .map_err(stdout_error) } +fn help_all(output: &mut dyn Write) -> Result<(), CommandError> { + writeln!(output, "Usage: xmlsec1 [options] [files]").map_err(stdout_error)?; + writeln!( + output, + "Commands: help help-all help-dsig help-enc help-keys help-x509 version \ + list-key-data check-key-data list-transforms check-transforms keys sign \ + verify sign-tmpl encrypt decrypt" + ) + .map_err(stdout_error)?; + writeln!(output, "Options:").map_err(stdout_error)?; + for spec in OPTION_SPECS { + let parameter = if spec.accepts_parameter { + "[:name]" + } else { + "" + }; + let value = if matches!(spec.arity, Arity::Value) { + " " + } else { + "" + }; + writeln!(output, " --{}{parameter}{value}", spec.canonical).map_err(stdout_error)?; + } + Ok(()) +} + fn validate_provider(invocation: &Invocation) -> Result<(), CommandError> { if let Some(provider) = option_text(invocation, "crypto")? && !matches!(provider, "rustcrypto" | "default") @@ -402,10 +428,19 @@ fn select_signing_key<'a>( } fn template_key_name(xml: &str) -> Result, CommandError> { + signature_key_name(xml, None) +} + +fn signature_key_name( + xml: &str, + start_node_id: Option<&str>, +) -> Result, CommandError> { let document = Document::parse(xml).map_err(|error| CommandError::Signature(error.to_string()))?; - let signature = document - .descendants() + let selected_root = + start_node_id.and_then(|id| UriReferenceResolver::new(&document).node_for_id(id)); + let signature = selected_root + .map_or_else(|| document.descendants(), |node| node.descendants()) .find(|node| node.has_tag_name((XMLDSIG_NS, "Signature"))); Ok(signature.and_then(|signature| { signature @@ -523,6 +558,15 @@ fn verify(invocation: &Invocation, stdout: &mut dyn Write) -> Result<(), Command let xml = read_input(invocation, policy.resources.max_xml_document_bytes)?; let start_node_id = option_text(invocation, "node-id")?; let algorithm = key_material::signature_algorithm(&xml, start_node_id)?; + if let [direct_key] = direct_keys.as_slice() + && let Some(name) = direct_key.parameter.as_deref() + && !invocation.flag("lax-key-search") + && signature_key_name(&xml, start_node_id)?.as_deref() != Some(name) + { + return Err(CommandError::Usage(format!( + "signature KeyName does not match named public key {name}" + ))); + } let result = if let Some(path) = direct_path { let key = key_material::load_verification_key(path, algorithm)?; verification_context(policy, start_node_id) @@ -662,7 +706,7 @@ fn encrypt(invocation: &Invocation, stdout: &mut dyn Write) -> Result<(), Comman let maximum_document_bytes = policy.resources.max_xml_document_bytes; let maximum_plaintext_bytes = policy.resources.max_encryption_plaintext_bytes; let template = read_input(invocation, policy.resources.max_xml_document_bytes)?; - let (algorithm, encrypted_type) = encryption_template(&template)?; + let (algorithm, encrypted_type, explicit_encrypted_type) = encryption_template(&template)?; let mut builder = EncryptedDataBuilder::new(algorithm).policy(policy); let aes_keys = invocation.values("aes-key").collect::>(); let public_keys = ["pubkey-pem", "pubkey-der"] @@ -675,6 +719,15 @@ fn encrypt(invocation: &Invocation, stdout: &mut dyn Write) -> Result<(), Comman )); } if let [option] = aes_keys.as_slice() { + if let Some(name) = option.parameter.as_deref() + && !invocation.flag("lax-key-search") + && let Some(template_name) = encrypted_data_key_name(&template)? + && template_name != name + { + return Err(CommandError::Usage(format!( + "template KeyName {template_name} does not match named AES key {name}" + ))); + } let key = key_material::load_symmetric( option.value.as_deref().unwrap_or_default(), Some(algorithm.key_len()), @@ -697,6 +750,11 @@ fn encrypt(invocation: &Invocation, stdout: &mut dyn Write) -> Result<(), Comman } builder = builder.encryption_type(encrypted_type); let result = if let Some(path) = invocation.last_value("binary-data") { + if explicit_encrypted_type { + return Err(CommandError::Usage( + "--binary-data cannot be used with an XML Element or Content template Type".into(), + )); + } let data = read_plaintext(path, maximum_plaintext_bytes)?; builder.encrypt_binary(&data) } else if let Some(path) = invocation.last_value("xml-data") { @@ -845,11 +903,7 @@ fn apply_encryption_template(template: &str, generated: &str) -> Result { let cipher_data = direct_child_element(template_data, XMLENC_NS, "CipherData") .ok_or_else(|| CommandError::Encryption("template has no CipherData".into()))?; - let key_info = generated[generated_key_info.range()].replacen( - ") -> String { ) } +fn standalone_element(source: &str, node: roxmltree::Node<'_, '_>) -> Result { + let fragment = &source[node.range()]; + let opening_end = fragment + .find('>') + .ok_or_else(|| CommandError::Encryption("generated KeyInfo has no opening tag".into()))?; + let closing_start = fragment + .rfind("'); + output.push_str(&fragment[opening_end + 1..closing_start]); + output.push_str("'); + Ok(output) +} + fn direct_child_element<'a, 'input>( node: roxmltree::Node<'a, 'input>, namespace: &str, @@ -973,7 +1061,7 @@ fn decrypt_input( fn encryption_template( xml: &str, -) -> Result<(DataEncryptionAlgorithm, EncryptedDataType), CommandError> { +) -> Result<(DataEncryptionAlgorithm, EncryptedDataType, bool), CommandError> { let document = Document::parse(xml).map_err(|error| CommandError::Encryption(error.to_string()))?; let encrypted_data = document @@ -987,6 +1075,7 @@ fn encryption_template( .ok_or_else(|| CommandError::Encryption("template has no encryption algorithm".into()))?; let algorithm = DataEncryptionAlgorithm::from_uri(method) .map_err(|error| CommandError::Encryption(error.to_string()))?; + let explicit_encrypted_type = encrypted_data.attribute("Type").is_some(); let encrypted_type = match encrypted_data.attribute("Type") { None | Some("http://www.w3.org/2001/04/xmlenc#Element") => EncryptedDataType::Element, Some("http://www.w3.org/2001/04/xmlenc#Content") => EncryptedDataType::Content, @@ -996,7 +1085,20 @@ fn encryption_template( ))); } }; - Ok((algorithm, encrypted_type)) + Ok((algorithm, encrypted_type, explicit_encrypted_type)) +} + +fn encrypted_data_key_name(xml: &str) -> Result, CommandError> { + let document = + Document::parse(xml).map_err(|error| CommandError::Encryption(error.to_string()))?; + let encrypted_data = document + .descendants() + .find(|node| node.has_tag_name((XMLENC_NS, "EncryptedData"))) + .ok_or_else(|| CommandError::Encryption("template has no EncryptedData".into()))?; + Ok(direct_child_element(encrypted_data, XMLDSIG_NS, "KeyInfo") + .and_then(|key_info| direct_child_element(key_info, XMLDSIG_NS, "KeyName")) + .and_then(|key_name| key_name.text()) + .map(str::to_owned)) } fn keys(invocation: &Invocation, stdout: &mut dyn Write) -> Result<(), CommandError> { @@ -1182,6 +1284,56 @@ mod tests { assert!(matches!(error, CommandError::UnsupportedOption(_))); } + #[test] + fn help_all_enumerates_the_registered_surface() { + let mut output = Vec::new(); + execute( + invocation(&["xmlsec1", "help-all"]), + &mut output, + &mut Vec::new(), + ) + .unwrap(); + let help = String::from_utf8(output).unwrap(); + for command in ["help-dsig", "check-key-data", "sign-tmpl", "decrypt"] { + assert!(help.contains(command), "missing command {command}"); + } + for option in OPTION_SPECS { + assert!( + help.contains(&format!("--{}", option.canonical)), + "missing option --{}", + option.canonical + ); + } + assert!(help.contains("--gen-key[:name] ")); + assert!(help.contains("--insecure\n")); + } + + #[test] + fn injected_key_info_carries_alternate_prefix_bindings() { + // Extracting a subtree must preserve namespace bindings inherited from + // the generated EncryptedData root, regardless of the chosen prefixes. + let template = format!( + "" + ); + let generated = format!( + "a2V5ZGF0YQ==" + ); + + let rendered = apply_encryption_template(&template, &generated).unwrap(); + let document = Document::parse(&rendered) + .expect("injected KeyInfo prefixes must remain namespace-bound"); + assert!( + document + .descendants() + .any(|node| node.has_tag_name((XMLDSIG_NS, "KeyInfo"))) + ); + assert!( + document + .descendants() + .any(|node| node.has_tag_name(("http://www.w3.org/2009/xmlenc11#", "MGF"))) + ); + } + #[test] fn input_reader_enforces_the_compiled_policy_limit_before_parsing() { // The reader must stop at maximum + 1 rather than allocating an entire diff --git a/tools/xmlsec1/tests/import_snapshot.rs b/tools/xmlsec1/tests/import_snapshot.rs index 19d34da..6642ceb 100644 --- a/tools/xmlsec1/tests/import_snapshot.rs +++ b/tools/xmlsec1/tests/import_snapshot.rs @@ -34,3 +34,101 @@ fn check_mode_detects_drift_without_replacing_the_snapshot() { assert!(!checked.success()); assert_eq!(fs::read_to_string(changed).unwrap(), "local drift\n"); } + +#[test] +fn importer_rejects_missing_and_empty_donor_pins() { + // Provenance is part of the snapshot contract; an import without an exact + // donor commit must fail rather than producing an unpinned fixture tree. + let temp = tempfile::tempdir().unwrap(); + let isolated_root = temp.path().join("isolated"); + let scripts = isolated_root.join("scripts"); + let compatibility = isolated_root.join("compatibility"); + fs::create_dir_all(&scripts).unwrap(); + fs::create_dir_all(&compatibility).unwrap(); + let script = scripts.join("import-xmlsec1-cli-fixtures.sh"); + fs::copy( + project_root().join("scripts/import-xmlsec1-cli-fixtures.sh"), + &script, + ) + .unwrap(); + let donor = project_root().join("tools/xmlsec1/tests/fixtures/upstream"); + + for (case, write_empty_pin) in [("missing", false), ("empty", true)] { + let pin = compatibility.join("libxmlsec1-1.3.13-donor-commit.txt"); + if write_empty_pin { + fs::write(&pin, "").unwrap(); + } else if pin.exists() { + fs::remove_file(&pin).unwrap(); + } + let status = Command::new("bash") + .arg(&script) + .env("XMLSEC_DONOR_ROOT", &donor) + .env( + "XMLSEC_FIXTURE_TARGET", + temp.path().join(format!("snapshot-{case}")), + ) + .status() + .unwrap(); + assert!(!status.success(), "{case} donor pin must fail closed"); + } +} + +#[cfg(unix)] +#[test] +fn interrupted_promotion_restores_the_previous_snapshot() { + use std::os::unix::fs::PermissionsExt as _; + + // Simulate TERM after target -> backup but before staging -> target. EXIT + // recovery must restore the tracked snapshot and remove transaction debris. + let temp = tempfile::tempdir().unwrap(); + let target = temp.path().join("snapshot"); + fs::create_dir(&target).unwrap(); + fs::write(target.join("sentinel"), "previous snapshot").unwrap(); + let tools = temp.path().join("tools"); + fs::create_dir(&tools).unwrap(); + let fake_mv = tools.join("mv"); + fs::write( + &fake_mv, + r#"#!/usr/bin/env bash +set -euo pipefail +count=0 +[[ ! -f "$MV_COUNT_FILE" ]] || count="$(<"$MV_COUNT_FILE")" +count=$((count + 1)) +printf '%s\n' "$count" > "$MV_COUNT_FILE" +if (( count == 2 )); then + kill -TERM "$PPID" + exit 143 +fi +exec /bin/mv "$@" +"#, + ) + .unwrap(); + fs::set_permissions(&fake_mv, fs::Permissions::from_mode(0o755)).unwrap(); + let inherited_path = std::env::var_os("PATH").unwrap(); + let path = + std::env::join_paths(std::iter::once(tools).chain(std::env::split_paths(&inherited_path))) + .unwrap(); + let status = Command::new(project_root().join("scripts/import-xmlsec1-cli-fixtures.sh")) + .env( + "XMLSEC_DONOR_ROOT", + project_root().join("tools/xmlsec1/tests/fixtures/upstream"), + ) + .env("XMLSEC_FIXTURE_TARGET", &target) + .env("MV_COUNT_FILE", temp.path().join("mv-count")) + .env("PATH", path) + .status() + .unwrap(); + + assert!(!status.success()); + assert_eq!( + fs::read_to_string(target.join("sentinel")).unwrap(), + "previous snapshot" + ); + let debris = fs::read_dir(temp.path()) + .unwrap() + .filter_map(Result::ok) + .map(|entry| entry.file_name()) + .filter(|name| name.to_string_lossy().starts_with("snapshot.")) + .collect::>(); + assert!(debris.is_empty(), "transaction debris remains: {debris:?}"); +} diff --git a/tools/xmlsec1/tests/process_contract.rs b/tools/xmlsec1/tests/process_contract.rs index 49d7ed2..123f3ca 100644 --- a/tools/xmlsec1/tests/process_contract.rs +++ b/tools/xmlsec1/tests/process_contract.rs @@ -71,6 +71,48 @@ fn signs_verifies_and_rejects_tampering_through_process_api() { assert!(String::from_utf8_lossy(&rejected.stderr).contains("invalid")); } +#[test] +fn named_public_key_obeys_signature_key_name_unless_lax() { + // Named direct keys participate in the same strict KeyName contract as a + // key manager; --lax-key-search is the explicit compatibility escape hatch. + let temp = tempfile::tempdir().unwrap(); + let template = project_root() + .join("tests/fixtures/xmldsig/aleksey-xmldsig-01/enveloping-sha256-rsa-sha256.tmpl"); + let private_key = project_root().join("tests/fixtures/keys/rsa/rsa-4096-key.pem"); + let public_key = project_root().join("tests/fixtures/keys/rsa/rsa-4096-pubkey.pem"); + let signed = temp.path().join("signed.xml"); + let sign = Command::new(binary()) + .args(["sign", "--privkey-pem"]) + .arg(&private_key) + .args(["--output"]) + .arg(&signed) + .arg(&template) + .output() + .unwrap(); + assert!(sign.status.success()); + + let strict = Command::new(binary()) + .args(["verify", "--pubkey-pem:wrong"]) + .arg(&public_key) + .arg(&signed) + .output() + .unwrap(); + assert!(!strict.status.success()); + assert!(String::from_utf8_lossy(&strict.stderr).contains("KeyName")); + + let lax = Command::new(binary()) + .args(["verify", "--lax-key-search", "--pubkey-pem:wrong"]) + .arg(&public_key) + .arg(&signed) + .output() + .unwrap(); + assert!( + lax.status.success(), + "{}", + String::from_utf8_lossy(&lax.stderr) + ); +} + #[test] fn verification_reads_the_conventional_stdin_marker() { // A lone dash is input data, not an option name; this is the process-level @@ -363,6 +405,74 @@ fn encryption_preserves_template_metadata_and_supports_id_selection() { ); } +#[test] +fn binary_encryption_rejects_xml_typed_templates() { + // Binary payloads cannot truthfully carry the XML Element or Content type; + // otherwise decryption routes arbitrary bytes through XML validation. + let temp = tempfile::tempdir().unwrap(); + let template = temp.path().join("typed-template.xml"); + let plaintext = temp.path().join("plaintext.bin"); + let key = temp.path().join("key.bin"); + fs::write( + &template, + r#""#, + ) + .unwrap(); + fs::write(&plaintext, [0xff, 0x00, 0xfe]).unwrap(); + fs::write(&key, b"0123456789abcdef").unwrap(); + + let result = Command::new(binary()) + .args(["encrypt", "--aes-key"]) + .arg(&key) + .args(["--binary-data"]) + .arg(&plaintext) + .arg(&template) + .output() + .unwrap(); + assert!(!result.status.success()); + assert!(String::from_utf8_lossy(&result.stderr).contains("binary-data")); +} + +#[test] +fn direct_aes_key_name_must_match_the_template_unless_lax() { + let temp = tempfile::tempdir().unwrap(); + let template = temp.path().join("named-template.xml"); + let plaintext = temp.path().join("plaintext.xml"); + let key = temp.path().join("key.bin"); + fs::write( + &template, + r#"expected"#, + ) + .unwrap(); + fs::write(&plaintext, "").unwrap(); + fs::write(&key, b"0123456789abcdef").unwrap(); + + let strict = Command::new(binary()) + .args(["encrypt", "--aes-key:wrong"]) + .arg(&key) + .args(["--xml-data"]) + .arg(&plaintext) + .arg(&template) + .output() + .unwrap(); + assert!(!strict.status.success()); + assert!(String::from_utf8_lossy(&strict.stderr).contains("KeyName")); + + let lax = Command::new(binary()) + .args(["encrypt", "--lax-key-search", "--aes-key:wrong"]) + .arg(&key) + .args(["--xml-data"]) + .arg(&plaintext) + .arg(&template) + .output() + .unwrap(); + assert!( + lax.status.success(), + "{}", + String::from_utf8_lossy(&lax.stderr) + ); +} + #[test] fn encrypts_and_decrypts_with_an_rsa_oaep_recipient() { // The advertised RSA path must emit XML Encryption 1.1 OAEP and unwrap its @@ -618,6 +728,18 @@ fn generated_key_store_is_private_on_create_and_overwrite() { // Key stores contain raw symmetric keys; both a new file and an existing // permissive file must end with owner-only permissions. let temp = tempfile::tempdir().unwrap(); + let created = temp.path().join("created.xml"); + let create = Command::new(binary()) + .args(["keys", "--gen-key:private", "aes-128"]) + .arg(&created) + .output() + .unwrap(); + assert!(create.status.success()); + assert_eq!( + fs::metadata(&created).unwrap().permissions().mode() & 0o777, + 0o600 + ); + let key_store = temp.path().join("keys.xml"); fs::write(&key_store, b"old").unwrap(); fs::set_permissions(&key_store, fs::Permissions::from_mode(0o666)).unwrap(); @@ -975,22 +1097,23 @@ fn generated_key_store_contains_every_requested_key() { .filter_map(|node| node.text()) .collect::>(); assert_eq!(names, ["first", "second"]); + let key_lengths = document + .descendants() + .filter(|node| node.has_tag_name(("http://www.aleksey.com/xmlsec/2002", "AESKeyValue"))) + .map(|node| { + base64::Engine::decode( + &base64::engine::general_purpose::STANDARD, + node.text().unwrap(), + ) + .unwrap() + .len() + }) + .collect::>(); + assert_eq!(key_lengths, [16, 32]); } #[test] -fn reports_capabilities_and_process_failures_deterministically() { - // Cover parser, capability, malformed-input, and output-path failures at - // the executable boundary where automation observes only status and stderr. - let temp = tempfile::tempdir().unwrap(); - let malformed = temp.path().join("malformed.xml"); - fs::write(&malformed, "").unwrap(); - let foreign_template = temp.path().join("foreign-template.xml"); - fs::write( - &foreign_template, - "", - ) - .unwrap(); - +fn capability_queries_report_supported_and_unsupported_names() { assert!( Command::new(binary()) .args(["check-transforms", "c14n", "rsa-sha256"]) @@ -998,6 +1121,36 @@ fn reports_capabilities_and_process_failures_deterministically() { .unwrap() .success() ); + assert_eq!( + Command::new(binary()) + .arg("check-transforms") + .status() + .unwrap() + .code(), + Some(0) + ); + assert_eq!( + Command::new(binary()) + .args(["check-transforms", "rsa-oaep-mgf1p"]) + .status() + .unwrap() + .code(), + Some(0) + ); + assert!( + !Command::new(binary()) + .args(["check-transforms", "xslt"]) + .status() + .unwrap() + .success() + ); +} + +#[test] +fn conflicting_verification_keys_fail_before_input_parsing() { + let temp = tempfile::tempdir().unwrap(); + let malformed = temp.path().join("malformed.xml"); + fs::write(&malformed, "").unwrap(); let conflicting_keys = Command::new(binary()) .args([ "verify", @@ -1014,7 +1167,19 @@ fn reports_capabilities_and_process_failures_deterministically() { String::from_utf8_lossy(&conflicting_keys.stderr) .contains("exactly one explicit public key") ); +} +#[test] +fn foreign_encryption_namespace_is_rejected() { + let temp = tempfile::tempdir().unwrap(); + let malformed = temp.path().join("malformed.xml"); + let foreign_template = temp.path().join("foreign-template.xml"); + fs::write(&malformed, "").unwrap(); + fs::write( + &foreign_template, + "", + ) + .unwrap(); let foreign = Command::new(binary()) .args(["encrypt", "--aeskey", "missing.key", "--binary-data"]) .arg(&malformed) @@ -1023,22 +1188,13 @@ fn reports_capabilities_and_process_failures_deterministically() { .unwrap(); assert!(!foreign.status.success()); assert!(String::from_utf8_lossy(&foreign.stderr).contains("no EncryptedData")); - assert_eq!( - Command::new(binary()) - .arg("check-transforms") - .status() - .unwrap() - .code(), - Some(0) - ); - assert_eq!( - Command::new(binary()) - .args(["check-transforms", "rsa-oaep-mgf1p"]) - .status() - .unwrap() - .code(), - Some(0) - ); +} + +#[test] +fn parser_input_and_output_failures_are_nonzero() { + let temp = tempfile::tempdir().unwrap(); + let malformed = temp.path().join("malformed.xml"); + fs::write(&malformed, "").unwrap(); assert_eq!( Command::new(binary()) .arg("unknown-command") @@ -1047,13 +1203,6 @@ fn reports_capabilities_and_process_failures_deterministically() { .code(), Some(1) ); - assert!( - !Command::new(binary()) - .args(["check-transforms", "xslt"]) - .status() - .unwrap() - .success() - ); let invalid_xml = Command::new(binary()) .args(["verify", "--pubkey-pem", "missing.pem"]) .arg(&malformed) @@ -1080,17 +1229,23 @@ fn reports_capabilities_and_process_failures_deterministically() { .unwrap() .success() ); +} +#[test] +fn nonempty_crypto_config_is_rejected_with_the_expected_diagnostic() { + let temp = tempfile::tempdir().unwrap(); let config = temp.path().join("crypto-config"); fs::create_dir(&config).unwrap(); fs::write(config.join("backend.conf"), "unsupported").unwrap(); + let output = Command::new(binary()) + .args(["check-transforms", "--crypto-config"]) + .arg(&config) + .arg("c14n") + .output() + .unwrap(); + assert!(!output.status.success()); assert!( - !Command::new(binary()) - .args(["check-transforms", "--crypto-config"]) - .arg(&config) - .arg("c14n") - .status() - .unwrap() - .success() + String::from_utf8_lossy(&output.stderr) + .contains("unsupported option for this command: --crypto-config") ); } diff --git a/tools/xmlsec1/tests/upstream_runner.rs b/tools/xmlsec1/tests/upstream_runner.rs index f9e3b85..c12453c 100644 --- a/tools/xmlsec1/tests/upstream_runner.rs +++ b/tools/xmlsec1/tests/upstream_runner.rs @@ -25,16 +25,27 @@ fn run_upstream(script: &str, selected_test: &str) -> String { ); let stdout = String::from_utf8_lossy(&output.stdout); assert!(stdout.contains("TOTAL OK:"), "runner summary is missing"); + let total_ok = stdout + .lines() + .find_map(|line| line.split_once("TOTAL OK:").map(|(_, count)| count)) + .and_then(|count| count.split(';').next()) + .and_then(|count| count.trim().parse::().ok()) + .expect("runner TOTAL OK count must be numeric"); + assert!(total_ok > 0, "selected upstream test did not execute"); assert!( stdout.contains("TOTAL FAILED: 0"), "runner reported a failed operation" ); - let run_directory = fs::read_dir(run_root.path()) + let mut run_directories = fs::read_dir(run_root.path()) .expect("the runner log root must be readable") - .next() - .expect("the runner must create one log directory") - .expect("the runner log directory entry must be readable") - .path(); + .map(|entry| entry.expect("the runner log directory entry must be readable")) + .filter(|entry| entry.path().is_dir()) + .map(|entry| entry.path()) + .collect::>(); + run_directories.sort(); + let [run_directory] = run_directories.as_slice() else { + panic!("the runner must create exactly one log directory: {run_directories:?}"); + }; fs::read_to_string(run_directory.join("full.log")) .expect("the unmodified runner must preserve its full operation log") } From beb9af253956eb78cbdfae4d58df6dea395cebac Mon Sep 17 00:00:00 2001 From: Dmitry Prudnikov Date: Fri, 14 Aug 2026 12:41:10 +0300 Subject: [PATCH 07/27] fix(cli): unify explicit key selection - enforce template names across keys and certificates - bound signature metadata discovery by operation policy - accept certificate companions on RSA decryption --- README.md | 4 +- docs/cli.md | 15 ++- tools/xmlsec1/README.md | 6 +- tools/xmlsec1/src/commands.rs | 121 ++++++++++------- tools/xmlsec1/src/key_material.rs | 108 ++++++++++++++-- tools/xmlsec1/tests/process_contract.rs | 165 ++++++++++++++++++++++++ 6 files changed, 352 insertions(+), 67 deletions(-) diff --git a/README.md b/README.md index 6798130..410e90b 100644 --- a/README.md +++ b/README.md @@ -82,7 +82,9 @@ AES key generation, capability checks, libxmlsec1 key aliases and option syntax, certificate-chain embedding, stdin input, signature selection by node ID, and deterministic process statuses. `help-all` enumerates the same registered commands and options accepted by the parser, while named direct keys obey -template `KeyName` unless lax lookup is explicitly requested. Its process tests run a minimal checked-in +an explicit template `KeyName` unless lax lookup is requested; unnamed templates +still use their sole explicit key. This applies to raw keys, explicit certificates, +and RSA recipients. Its process tests run a minimal checked-in snapshot of the unmodified upstream DSig, Enc, and Keys runners without network access or a system `xmlsec1` installation. Unsupported algorithms, key formats, providers, and policy controls fail closed diff --git a/docs/cli.md b/docs/cli.md index 892fe8b..fddb4c7 100644 --- a/docs/cli.md +++ b/docs/cli.md @@ -56,8 +56,10 @@ Verification accepts `-` as the conventional stdin marker. For documents with multiple signatures, `--node-id ` selects an ID-bearing start node and verifies the single `Signature` in its subtree; missing and duplicate IDs fail closed. -Named `--pubkey-pem:name` and `--pubkey-der:name` inputs must match the selected -signature's `KeyName`; `--lax-key-search` is the explicit opt-out. +When the selected signature contains `KeyName`, named raw public-key and +explicit certificate inputs must match it; `--lax-key-search` is the explicit +opt-out. A signature without `KeyName` does not request a different identity, +so its sole explicit key remains usable even when that key has a registry name. `--output` follows the upstream filename-template contract. The first `{inputfile}` token is replaced with the input file's basename after removing @@ -80,8 +82,13 @@ larger XML document; `--node-id` selects an embedded `EncryptedData` by `Id`. Encryption preserves the template's `Id`, `Type`, `MimeType`, `KeyInfo`, `EncryptionProperties`, and RSA-OAEP parameters while replacing only the cryptographic `CipherValue` payloads. -For direct AES encryption, a named key must match an existing template -`KeyName` unless `--lax-key-search` is supplied. `--binary-data` rejects +When an encryption template contains a direct content-key `KeyName`, a named +AES key must match it. Likewise, an RSA wrapping key must match a recipient +`KeyName` inside `EncryptedKey`. An unnamed template does not constrain the sole +explicit key; an explicit mismatch fails unless `--lax-key-search` is supplied. +RSA private-key decryption accepts the upstream +`key.pem,certificate.pem,...` option syntax and consumes the first component as +the decryption key. `--binary-data` rejects templates explicitly typed as XML `Element` or `Content`; use `--xml-data` for those templates so ciphertext metadata cannot mislabel arbitrary bytes as XML. diff --git a/tools/xmlsec1/README.md b/tools/xmlsec1/README.md index eceae45..6da9fb2 100644 --- a/tools/xmlsec1/README.md +++ b/tools/xmlsec1/README.md @@ -23,5 +23,7 @@ embed every certificate from `key,leaf,intermediate,...`; verification accepts stdin as `-` and can select one signature subtree with `--node-id`. Output paths support the upstream `{inputfile}` basename template, and `--gen-key[:name]` emits both named and unnamed AES key-store entries. `help-all` is generated from -the parser registry, named direct keys are checked against template `KeyName`, -and binary payloads cannot be emitted with XML Element/Content type metadata. +the parser registry. When a template requests `KeyName`, named raw keys, +certificates, and RSA recipients require an exact match unless lax lookup is +explicit; unnamed templates leave the sole explicit key unconstrained. Binary +payloads cannot be emitted with XML Element/Content type metadata. diff --git a/tools/xmlsec1/src/commands.rs b/tools/xmlsec1/src/commands.rs index cde0c7c..85ac2ab 100644 --- a/tools/xmlsec1/src/commands.rs +++ b/tools/xmlsec1/src/commands.rs @@ -13,7 +13,7 @@ use xml_sec::{ xmldsig::{ DefaultKeyResolver, DsigStatus, KeyResolver, KeyResolverConfig, SignContext, SignatureAlgorithm, UriTypeSet, VerifyContext, X509CertificateKeyInfoWriter, - parse_key_info, uri::UriReferenceResolver, + parse_key_info, }, xmlenc::{ DataEncryptionAlgorithm, DecryptContext, DecryptedContent, DecryptionKeyResolver, @@ -351,7 +351,7 @@ fn sign(invocation: &Invocation, stdout: &mut dyn Write) -> Result<(), CommandEr } let policy = SigningPolicy::default(); let xml = read_input(invocation, policy.resources.max_xml_document_bytes)?; - let (key_option, certificate_is_der) = select_signing_key(invocation, &xml)?; + let (key_option, certificate_is_der) = select_signing_key(invocation, &xml, &policy)?; let value = key_option.value.as_deref().unwrap_or_default(); let (key_path, certificate_paths) = split_key_and_certificates(value)?; let key = key_material::load_signing_key(key_path)?; @@ -386,6 +386,7 @@ fn sign(invocation: &Invocation, stdout: &mut dyn Write) -> Result<(), CommandEr fn select_signing_key<'a>( invocation: &'a Invocation, xml: &str, + policy: &SigningPolicy, ) -> Result<(&'a crate::OptionValue, bool), CommandError> { let mut keys = Vec::new(); for (name, certificate_is_der) in [ @@ -406,7 +407,7 @@ fn select_signing_key<'a>( { return Ok(*selected); } - let requested_name = template_key_name(xml)?; + let requested_name = key_material::signing_signature_key_name(xml, policy)?; if let Some(requested_name) = requested_name { let matching = keys .into_iter() @@ -427,35 +428,6 @@ fn select_signing_key<'a>( )) } -fn template_key_name(xml: &str) -> Result, CommandError> { - signature_key_name(xml, None) -} - -fn signature_key_name( - xml: &str, - start_node_id: Option<&str>, -) -> Result, CommandError> { - let document = - Document::parse(xml).map_err(|error| CommandError::Signature(error.to_string()))?; - let selected_root = - start_node_id.and_then(|id| UriReferenceResolver::new(&document).node_for_id(id)); - let signature = selected_root - .map_or_else(|| document.descendants(), |node| node.descendants()) - .find(|node| node.has_tag_name((XMLDSIG_NS, "Signature"))); - Ok(signature.and_then(|signature| { - signature - .children() - .find(|node| node.has_tag_name((XMLDSIG_NS, "KeyInfo"))) - .and_then(|key_info| { - key_info - .children() - .find(|node| node.has_tag_name((XMLDSIG_NS, "KeyName"))) - }) - .and_then(|key_name| key_name.text()) - .map(str::to_owned) - })) -} - fn split_key_and_certificates(value: &OsStr) -> Result<(&OsStr, Vec<&OsStr>), CommandError> { let bytes = value.as_encoded_bytes(); // Splitting at an ASCII byte preserves encoded-byte boundaries on every @@ -557,15 +529,18 @@ fn verify(invocation: &Invocation, stdout: &mut dyn Write) -> Result<(), Command let policy = xmlsec_compatibility_verification_policy(invocation); let xml = read_input(invocation, policy.resources.max_xml_document_bytes)?; let start_node_id = option_text(invocation, "node-id")?; - let algorithm = key_material::signature_algorithm(&xml, start_node_id)?; - if let [direct_key] = direct_keys.as_slice() - && let Some(name) = direct_key.parameter.as_deref() - && !invocation.flag("lax-key-search") - && signature_key_name(&xml, start_node_id)?.as_deref() != Some(name) + let signature = key_material::verification_signature_metadata(&xml, start_node_id, &policy)?; + let algorithm = signature.algorithm; + if let Some(identity) = direct_keys + .first() + .or_else(|| explicit_certificates.first()) { - return Err(CommandError::Usage(format!( - "signature KeyName does not match named public key {name}" - ))); + enforce_named_key_match( + identity, + signature.key_name.as_deref(), + invocation.flag("lax-key-search"), + "verification key", + )?; } let result = if let Some(path) = direct_path { let key = key_material::load_verification_key(path, algorithm)?; @@ -719,15 +694,13 @@ fn encrypt(invocation: &Invocation, stdout: &mut dyn Write) -> Result<(), Comman )); } if let [option] = aes_keys.as_slice() { - if let Some(name) = option.parameter.as_deref() - && !invocation.flag("lax-key-search") - && let Some(template_name) = encrypted_data_key_name(&template)? - && template_name != name - { - return Err(CommandError::Usage(format!( - "template KeyName {template_name} does not match named AES key {name}" - ))); - } + let template_name = encrypted_data_key_name(&template)?; + enforce_named_key_match( + option, + template_name.as_deref(), + invocation.flag("lax-key-search"), + "AES key", + )?; let key = key_material::load_symmetric( option.value.as_deref().unwrap_or_default(), Some(algorithm.key_len()), @@ -737,6 +710,13 @@ fn encrypt(invocation: &Invocation, stdout: &mut dyn Write) -> Result<(), Comman builder = builder.direct_key_name(name); } } else if let [option] = public_keys.as_slice() { + let recipient_name = encrypted_key_recipient_name(&template)?; + enforce_named_key_match( + option, + recipient_name.as_deref(), + invocation.flag("lax-key-search"), + "RSA recipient key", + )?; let path = option.value.as_deref().unwrap_or_default(); let mut recipient = EncryptionRecipient::rsa_oaep(key_material::load_rsa_public(path)?); if let Some(parameters) = template_oaep_parameters(&template)? { @@ -1021,7 +1001,7 @@ fn decrypt(invocation: &Invocation, stdout: &mut dyn Write) -> Result<(), Comman policy, )? } else if let [option] = private_keys.as_slice() { - let path = option.value.as_deref().unwrap_or_default(); + let (path, _) = split_key_and_certificates(option.value.as_deref().unwrap_or_default())?; let resolver = PrivateKeyDecryptor::new(key_material::load_rsa_private(path)?); decrypt_input(&resolver, &xml, encrypted_data_id, policy)? } else { @@ -1101,6 +1081,47 @@ fn encrypted_data_key_name(xml: &str) -> Result, CommandError> { .map(str::to_owned)) } +fn encrypted_key_recipient_name(xml: &str) -> Result, CommandError> { + let document = + Document::parse(xml).map_err(|error| CommandError::Encryption(error.to_string()))?; + let encrypted_data = document + .descendants() + .find(|node| node.has_tag_name((XMLENC_NS, "EncryptedData"))) + .ok_or_else(|| CommandError::Encryption("template has no EncryptedData".into()))?; + Ok(direct_child_element(encrypted_data, XMLDSIG_NS, "KeyInfo") + .and_then(|key_info| direct_child_element(key_info, XMLENC_NS, "EncryptedKey")) + .and_then(|encrypted_key| direct_child_element(encrypted_key, XMLDSIG_NS, "KeyInfo")) + .and_then(|key_info| direct_child_element(key_info, XMLDSIG_NS, "KeyName")) + .and_then(|key_name| key_name.text()) + .map(str::to_owned)) +} + +fn enforce_named_key_match( + option: &crate::OptionValue, + template_name: Option<&str>, + lax_key_search: bool, + key_kind: &str, +) -> Result<(), CommandError> { + let Some(option_name) = option.parameter.as_deref() else { + return Ok(()); + }; + // A missing KeyName does not request a different identity: with one + // explicit key, libxmlsec uses that key regardless of its registry name. + // Strict lookup applies when the document actually names an identity. + if lax_key_search { + return Ok(()); + } + let Some(template_name) = template_name else { + return Ok(()); + }; + if template_name == option_name { + return Ok(()); + } + Err(CommandError::Usage(format!( + "template KeyName {template_name} does not match named {key_kind} {option_name}" + ))) +} + fn keys(invocation: &Invocation, stdout: &mut dyn Write) -> Result<(), CommandError> { validate_options(invocation, &["gen-key"])?; let generated = invocation.values("gen-key").collect::>(); diff --git a/tools/xmlsec1/src/key_material.rs b/tools/xmlsec1/src/key_material.rs index d7eeee0..b0006da 100644 --- a/tools/xmlsec1/src/key_material.rs +++ b/tools/xmlsec1/src/key_material.rs @@ -12,6 +12,7 @@ use rsa::{ }, }; use x509_parser::prelude::FromDer as _; +use xml_sec::policy::{PolicyViolation, SigningPolicy, VerificationPolicy}; use xml_sec::xmldsig::{ EcdsaP256SigningKey, EcdsaP384SigningKey, RsaSigningKey, SignatureAlgorithm, SigningKey, VerificationKey, find_signature_node, parse_signed_info, uri::UriReferenceResolver, @@ -40,6 +41,14 @@ pub enum KeyMaterialError { Signature(String), #[error("invalid symmetric key length: expected {expected} bytes, got {actual}")] SymmetricLength { expected: usize, actual: usize }, + #[error("invalid operation policy: {0}")] + Policy(#[from] PolicyViolation), +} + +#[derive(Debug, Eq, PartialEq)] +pub struct SignatureMetadata { + pub algorithm: SignatureAlgorithm, + pub key_name: Option, } pub fn read(path: impl AsRef) -> Result, KeyMaterialError> { @@ -55,12 +64,17 @@ pub fn read_text(path: impl AsRef) -> Result { String::from_utf8(read(path)?).map_err(|_| KeyMaterialError::InvalidPem(path.to_owned())) } -pub fn signature_algorithm( +pub fn verification_signature_metadata( xml: &str, start_node_id: Option<&str>, -) -> Result { - let document = - Document::parse(xml).map_err(|error| KeyMaterialError::Signature(error.to_string()))?; + policy: &VerificationPolicy, +) -> Result { + policy.validate()?; + let document = parse_signature_document( + xml, + policy.xml.allow_internal_dtd, + policy.resources.max_xml_nodes, + )?; let signature = match start_node_id { Some(id) => { let start = UriReferenceResolver::new(&document) @@ -75,11 +89,62 @@ pub fn signature_algorithm( .ok_or(KeyMaterialError::MissingSignedInfo)?; let signed_info = signature .children() - .find(|node| node.is_element() && node.tag_name().name() == "SignedInfo") + .find(|node| node.has_tag_name(("http://www.w3.org/2000/09/xmldsig#", "SignedInfo"))) .ok_or(KeyMaterialError::MissingSignedInfo)?; - parse_signed_info(signed_info) + let algorithm = parse_signed_info(signed_info) .map(|info| info.signature_method) - .map_err(|error| KeyMaterialError::Signature(error.to_string())) + .map_err(|error| KeyMaterialError::Signature(error.to_string()))?; + Ok(SignatureMetadata { + algorithm, + key_name: signature_key_name(signature), + }) +} + +pub fn signing_signature_key_name( + xml: &str, + policy: &SigningPolicy, +) -> Result, KeyMaterialError> { + policy.validate()?; + let document = parse_signature_document( + xml, + policy.xml.allow_internal_dtd, + policy.resources.max_xml_nodes, + )?; + find_signature_node(&document) + .map(signature_key_name) + .ok_or(KeyMaterialError::MissingSignedInfo) +} + +fn parse_signature_document( + xml: &str, + allow_internal_dtd: bool, + max_xml_nodes: usize, +) -> Result, KeyMaterialError> { + let nodes_limit = u32::try_from(max_xml_nodes).map_err(|_| { + KeyMaterialError::Signature("XML node ceiling does not fit the parser limit".into()) + })?; + Document::parse_with_options( + xml, + roxmltree::ParsingOptions { + allow_dtd: allow_internal_dtd, + nodes_limit, + entity_resolver: None, + }, + ) + .map_err(|error| KeyMaterialError::Signature(error.to_string())) +} + +fn signature_key_name(signature: roxmltree::Node<'_, '_>) -> Option { + signature + .children() + .find(|node| node.has_tag_name(("http://www.w3.org/2000/09/xmldsig#", "KeyInfo"))) + .and_then(|key_info| { + key_info + .children() + .find(|node| node.has_tag_name(("http://www.w3.org/2000/09/xmldsig#", "KeyName"))) + }) + .and_then(|key_name| key_name.text()) + .map(str::to_owned) } pub fn load_signing_key(path: impl AsRef) -> Result, KeyMaterialError> { @@ -306,9 +371,32 @@ mod tests { signature("ec", "http://www.w3.org/2001/04/xmldsig-more#ecdsa-sha256") ); - assert_eq!( - signature_algorithm(&xml, Some("ec")).unwrap(), - SignatureAlgorithm::EcdsaSha256 + let metadata = verification_signature_metadata( + &xml, + Some("ec"), + &xml_sec::policy::VerificationPolicy::default(), + ) + .unwrap(); + assert_eq!(metadata.algorithm, SignatureAlgorithm::EcdsaSha256); + } + + #[test] + fn signature_discovery_obeys_the_verification_node_ceiling() { + // Metadata discovery runs before cryptographic verification and must + // not allocate a DOM larger than the operation policy permits. + let digest = base64::Engine::encode(&base64::engine::general_purpose::STANDARD, [0_u8; 32]); + let xml = format!( + r#"{digest}AA=="# ); + let policy = xml_sec::policy::VerificationPolicy { + resources: xml_sec::policy::ResourcePolicy { + max_xml_nodes: 4, + ..xml_sec::policy::ResourcePolicy::default() + }, + ..xml_sec::policy::VerificationPolicy::default() + }; + + let error = verification_signature_metadata(&xml, None, &policy).unwrap_err(); + assert!(error.to_string().contains("nodes limit")); } } diff --git a/tools/xmlsec1/tests/process_contract.rs b/tools/xmlsec1/tests/process_contract.rs index 123f3ca..131a773 100644 --- a/tools/xmlsec1/tests/process_contract.rs +++ b/tools/xmlsec1/tests/process_contract.rs @@ -113,6 +113,69 @@ fn named_public_key_obeys_signature_key_name_unless_lax() { ); } +#[test] +fn named_verification_certificate_obeys_signature_key_name_unless_lax() { + // Explicit certificates are pinned verification identities, so naming one + // must use the same strict lookup contract as naming a raw public key. + let temp = tempfile::tempdir().unwrap(); + let template = project_root() + .join("tests/fixtures/xmldsig/aleksey-xmldsig-01/enveloping-sha256-rsa-sha256.tmpl"); + let private_key = project_root().join("tests/fixtures/keys/rsa/rsa-4096-key.pem"); + let certificate = project_root().join("tests/fixtures/keys/rsa/rsa-4096-cert.pem"); + let certificate_der = temp.path().join("certificate.der"); + let certificate_pem = fs::read(&certificate).unwrap(); + let (_, certificate_contents) = x509_parser::pem::parse_x509_pem(&certificate_pem).unwrap(); + fs::write(&certificate_der, certificate_contents.contents).unwrap(); + let signed = temp.path().join("signed.xml"); + let sign = Command::new(binary()) + .args(["sign", "--privkey-pem"]) + .arg(&private_key) + .arg("--output") + .arg(&signed) + .arg(&template) + .output() + .unwrap(); + assert!(sign.status.success()); + + for (option, path) in [ + ("pubkey-cert-pem", certificate.as_path()), + ("pubkey-cert-der", certificate_der.as_path()), + ] { + let matching = Command::new(binary()) + .args(["verify", &format!("--{option}:TestKeyName-rsa-2048")]) + .arg(path) + .arg(&signed) + .output() + .unwrap(); + assert!( + matching.status.success(), + "{}", + String::from_utf8_lossy(&matching.stderr) + ); + + let strict = Command::new(binary()) + .args(["verify", &format!("--{option}:wrong")]) + .arg(path) + .arg(&signed) + .output() + .unwrap(); + assert!(!strict.status.success()); + assert!(String::from_utf8_lossy(&strict.stderr).contains("KeyName")); + + let lax = Command::new(binary()) + .args(["verify", "--lax-key-search", &format!("--{option}:wrong")]) + .arg(path) + .arg(&signed) + .output() + .unwrap(); + assert!( + lax.status.success(), + "{}", + String::from_utf8_lossy(&lax.stderr) + ); + } +} + #[test] fn verification_reads_the_conventional_stdin_marker() { // A lone dash is input data, not an option name; this is the process-level @@ -473,6 +536,64 @@ fn direct_aes_key_name_must_match_the_template_unless_lax() { ); } +#[test] +fn rsa_recipient_name_must_match_the_template_unless_lax() { + // A named RSA key selects the nested EncryptedKey recipient identity, not + // the direct content-encryption key metadata on EncryptedData. + let temp = tempfile::tempdir().unwrap(); + let template = temp.path().join("named-rsa-template.xml"); + let plaintext = temp.path().join("plaintext.bin"); + let public_key = project_root().join("tests/fixtures/keys/rsa/rsa-4096-pubkey.pem"); + fs::write( + &template, + r#" + +recipient +"#, + ) + .unwrap(); + fs::write(&plaintext, b"named recipient").unwrap(); + + let matching = Command::new(binary()) + .args(["encrypt", "--pubkey-pem:recipient"]) + .arg(&public_key) + .arg("--binary-data") + .arg(&plaintext) + .arg(&template) + .output() + .unwrap(); + assert!( + matching.status.success(), + "{}", + String::from_utf8_lossy(&matching.stderr) + ); + + let strict = Command::new(binary()) + .args(["encrypt", "--pubkey-pem:wrong"]) + .arg(&public_key) + .arg("--binary-data") + .arg(&plaintext) + .arg(&template) + .output() + .unwrap(); + assert!(!strict.status.success()); + assert!(String::from_utf8_lossy(&strict.stderr).contains("KeyName")); + + let lax = Command::new(binary()) + .args(["encrypt", "--lax-key-search", "--pubkey-pem:wrong"]) + .arg(&public_key) + .arg("--binary-data") + .arg(&plaintext) + .arg(&template) + .output() + .unwrap(); + assert!( + lax.status.success(), + "{}", + String::from_utf8_lossy(&lax.stderr) + ); +} + #[test] fn encrypts_and_decrypts_with_an_rsa_oaep_recipient() { // The advertised RSA path must emit XML Encryption 1.1 OAEP and unwrap its @@ -527,6 +648,50 @@ fn encrypts_and_decrypts_with_an_rsa_oaep_recipient() { assert_eq!(decrypt.stdout, fs::read(&plaintext).unwrap()); } +#[test] +fn rsa_decryption_accepts_private_key_certificate_companions() { + // libxmlsec private-key options permit certificate companions after the + // key path; decryption consumes the key while retaining that syntax. + let temp = tempfile::tempdir().unwrap(); + let template = temp.path().join("template.xml"); + let plaintext = temp.path().join("plaintext.bin"); + let encrypted = temp.path().join("encrypted.xml"); + let private_key = project_root().join("tests/fixtures/keys/rsa/rsa-4096-key.pem"); + let certificate = project_root().join("tests/fixtures/keys/rsa/rsa-4096-cert.pem"); + let public_key = project_root().join("tests/fixtures/keys/rsa/rsa-4096-pubkey.pem"); + fs::write( + &template, + r#""#, + ) + .unwrap(); + fs::write(&plaintext, b"certificate companion").unwrap(); + let encrypt = Command::new(binary()) + .args(["encrypt", "--pubkey-pem"]) + .arg(&public_key) + .arg("--binary-data") + .arg(&plaintext) + .arg("--output") + .arg(&encrypted) + .arg(&template) + .output() + .unwrap(); + assert!(encrypt.status.success()); + + let compound = format!("{},{}", private_key.display(), certificate.display()); + let decrypt = Command::new(binary()) + .args(["decrypt", "--privkey-pem"]) + .arg(compound) + .arg(&encrypted) + .output() + .unwrap(); + assert!( + decrypt.status.success(), + "{}", + String::from_utf8_lossy(&decrypt.stderr) + ); + assert_eq!(decrypt.stdout, b"certificate companion"); +} + #[test] fn honors_legacy_rsa_oaep_parameters_from_the_template() { // Advertising rsa-oaep-mgf1p requires an actual process round trip, and From 276351308a62d7cf07db83e43074c47d2e26a5ea Mon Sep 17 00:00:00 2001 From: Dmitry Prudnikov Date: Fri, 14 Aug 2026 14:19:44 +0300 Subject: [PATCH 08/27] fix(cli): harden key and template handling - enforce named decryption identities and bounded XML inspection - validate compound certificates without requiring KeyInfo output - preserve standalone binary decryption for root node selection --- README.md | 10 +- docs/cli.md | 22 +- tools/xmlsec1/README.md | 10 +- tools/xmlsec1/src/commands.rs | 264 ++++++++++++++++-------- tools/xmlsec1/src/key_material.rs | 39 +++- tools/xmlsec1/tests/process_contract.rs | 228 ++++++++++++++++++++ 6 files changed, 460 insertions(+), 113 deletions(-) diff --git a/README.md b/README.md index 410e90b..7185702 100644 --- a/README.md +++ b/README.md @@ -81,10 +81,12 @@ The native binary supports sign/verify, template-preserving encrypt/decrypt, AES key generation, capability checks, libxmlsec1 key aliases and option syntax, certificate-chain embedding, stdin input, signature selection by node ID, and deterministic process statuses. `help-all` enumerates the same registered -commands and options accepted by the parser, while named direct keys obey -an explicit template `KeyName` unless lax lookup is requested; unnamed templates -still use their sole explicit key. This applies to raw keys, explicit certificates, -and RSA recipients. Its process tests run a minimal checked-in +commands and options accepted by the parser. Named signing keys require a +template `KeyName`, while named verification and encryption/decryption keys +obey a selected XML `KeyName` unless lax lookup is requested; unnamed templates +still use their sole explicit verification or encryption key. Certificate +companions are validated even when no output `KeyInfo` placeholder is present. +Its process tests run a minimal checked-in snapshot of the unmodified upstream DSig, Enc, and Keys runners without network access or a system `xmlsec1` installation. Unsupported algorithms, key formats, providers, and policy controls fail closed diff --git a/docs/cli.md b/docs/cli.md index fddb4c7..4743b82 100644 --- a/docs/cli.md +++ b/docs/cli.md @@ -46,11 +46,15 @@ xmlsec1 verify --pubkey-pem signing-key.pub.pem signed.xml ``` Signing key options accept libxmlsec1's comma-separated certificate form, -`key.pem,leaf.pem,intermediate.pem,...`. Every certificate is validated and -embedded in order under `X509Data`; the first certificate must contain the -signing key's public key. Named keys are matched against the template's -`KeyName` even when only one key is supplied. `--lax-key-search` explicitly -opts out of that name match. +`key.pem,leaf.pem,intermediate.pem,...`. Every certificate is structurally +validated, and the first certificate must contain the signing key. When the +template contains the optional direct `KeyInfo` +placeholder, the chain is embedded there in order under `X509Data`; omitting +that placeholder leaves the signed output without `KeyInfo`. Named signing keys +require a matching template `KeyName` even when only one key is supplied. A +named key with no template `KeyName` fails unless `--lax-key-search` explicitly +opts out of lookup. Verification and encryption instead leave a `KeyName`-less +template unconstrained when one explicit key is supplied. Verification accepts `-` as the conventional stdin marker. For documents with multiple signatures, `--node-id ` selects an ID-bearing start node and @@ -87,8 +91,12 @@ AES key must match it. Likewise, an RSA wrapping key must match a recipient `KeyName` inside `EncryptedKey`. An unnamed template does not constrain the sole explicit key; an explicit mismatch fails unless `--lax-key-search` is supplied. RSA private-key decryption accepts the upstream -`key.pem,certificate.pem,...` option syntax and consumes the first component as -the decryption key. `--binary-data` rejects +`key.pem,certificate.pem,...` option syntax, consumes the first component as +the decryption key, and validates every certificate companion before decrypting. +Named AES and RSA decryption keys obey the selected `EncryptedData` or nested +`EncryptedKey` name unless lax lookup is requested. Selecting a standalone +`EncryptedData` by its own `Id` still returns opaque decrypted bytes rather than +routing them through XML document replacement. `--binary-data` rejects templates explicitly typed as XML `Element` or `Content`; use `--xml-data` for those templates so ciphertext metadata cannot mislabel arbitrary bytes as XML. diff --git a/tools/xmlsec1/README.md b/tools/xmlsec1/README.md index 6da9fb2..703d1f6 100644 --- a/tools/xmlsec1/README.md +++ b/tools/xmlsec1/README.md @@ -19,11 +19,13 @@ coverage. `--node-id`. Encryption retains template metadata and RSA-OAEP parameters; PKCS#1 RSA and PKCS#8/SPKI/X.509 PEM or DER key material is normalized into the same core signing, verification, and encryption pipelines. Signing options -embed every certificate from `key,leaf,intermediate,...`; verification accepts +validate every certificate from `key,leaf,intermediate,...` and embed the chain +when the template provides a `KeyInfo` placeholder; verification accepts stdin as `-` and can select one signature subtree with `--node-id`. Output paths support the upstream `{inputfile}` basename template, and `--gen-key[:name]` emits both named and unnamed AES key-store entries. `help-all` is generated from -the parser registry. When a template requests `KeyName`, named raw keys, -certificates, and RSA recipients require an exact match unless lax lookup is -explicit; unnamed templates leave the sole explicit key unconstrained. Binary +the parser registry. Named signing keys require a template `KeyName`; named +verification and encryption/decryption keys require an exact match when the +selected XML names a key, unless lax lookup is explicit. Unnamed verification +and encryption templates leave the sole explicit key unconstrained. Binary payloads cannot be emitted with XML Element/Content type metadata. diff --git a/tools/xmlsec1/src/commands.rs b/tools/xmlsec1/src/commands.rs index 85ac2ab..6f10f21 100644 --- a/tools/xmlsec1/src/commands.rs +++ b/tools/xmlsec1/src/commands.rs @@ -6,12 +6,12 @@ use std::{ path::{Path, PathBuf}, }; -use roxmltree::Document; +use roxmltree::{Document, Node, ParsingOptions}; use xml_sec::{ policy::{DecryptionPolicy, EncryptionPolicy, SigningPolicy, VerificationPolicy}, provider::default_provider, xmldsig::{ - DefaultKeyResolver, DsigStatus, KeyResolver, KeyResolverConfig, SignContext, + DefaultKeyResolver, DsigStatus, KeyInfoWriter, KeyResolver, KeyResolverConfig, SignContext, SignatureAlgorithm, UriTypeSet, VerifyContext, X509CertificateKeyInfoWriter, parse_key_info, }, @@ -351,42 +351,53 @@ fn sign(invocation: &Invocation, stdout: &mut dyn Write) -> Result<(), CommandEr } let policy = SigningPolicy::default(); let xml = read_input(invocation, policy.resources.max_xml_document_bytes)?; - let (key_option, certificate_is_der) = select_signing_key(invocation, &xml, &policy)?; + let signature = key_material::signing_signature_metadata(&xml, &policy)?; + let (key_option, certificate_is_der) = + select_signing_key(invocation, signature.key_name.as_deref())?; let value = key_option.value.as_deref().unwrap_or_default(); let (key_path, certificate_paths) = split_key_and_certificates(value)?; let key = key_material::load_signing_key(key_path)?; - let signed = if !certificate_paths.is_empty() { - let writer = if certificate_is_der { - let certificates = certificate_paths - .iter() - .map(key_material::read) - .collect::, _>>()?; - X509CertificateKeyInfoWriter::from_der_chain(&certificates) + let writer = if certificate_paths.is_empty() { + None + } else { + Some( + if certificate_is_der { + let certificates = certificate_paths + .iter() + .map(key_material::read) + .collect::, _>>()?; + X509CertificateKeyInfoWriter::from_der_chain(&certificates) + } else { + let certificates = certificate_paths + .iter() + .map(key_material::read_text) + .collect::, _>>()?; + X509CertificateKeyInfoWriter::from_pem_chain(&certificates) + } + .map_err(|error| CommandError::Signature(error.to_string()))?, + ) + }; + let mut context = SignContext::new(key.as_ref()).policy(policy); + if let Some(writer) = &writer { + if signature.has_key_info { + context = context.key_info_writer(writer); } else { - let certificates = certificate_paths - .iter() - .map(key_material::read_text) - .collect::, _>>()?; - X509CertificateKeyInfoWriter::from_pem_chain(&certificates) + // Companions remain key-bound inputs even when the optional output + // placeholder is absent; validate the chain without injecting XML. + writer + .write_key_info(key.as_ref()) + .map_err(|error| CommandError::Signature(error.to_string()))?; } - .map_err(|error| CommandError::Signature(error.to_string()))?; - SignContext::new(key.as_ref()) - .policy(policy) - .key_info_writer(&writer) - .sign_template(&xml) - } else { - SignContext::new(key.as_ref()) - .policy(policy) - .sign_template(&xml) } - .map_err(|error| CommandError::Signature(error.to_string()))?; + let signed = context + .sign_template(&xml) + .map_err(|error| CommandError::Signature(error.to_string()))?; write_output(invocation, signed.as_bytes(), stdout) } fn select_signing_key<'a>( invocation: &'a Invocation, - xml: &str, - policy: &SigningPolicy, + requested_name: Option<&str>, ) -> Result<(&'a crate::OptionValue, bool), CommandError> { let mut keys = Vec::new(); for (name, certificate_is_der) in [ @@ -407,11 +418,10 @@ fn select_signing_key<'a>( { return Ok(*selected); } - let requested_name = key_material::signing_signature_key_name(xml, policy)?; if let Some(requested_name) = requested_name { let matching = keys .into_iter() - .filter(|(key, _)| key.parameter.as_deref() == Some(requested_name.as_str())) + .filter(|(key, _)| key.parameter.as_deref() == Some(requested_name)) .collect::>(); return match matching.as_slice() { [selected] => Ok(*selected), @@ -423,9 +433,12 @@ fn select_signing_key<'a>( ))), }; } - Err(CommandError::Usage( - "multiple private keys require a template KeyName and named options".into(), - )) + let message = if keys.len() == 1 { + "a named private key requires a template KeyName; use --lax-key-search to opt out" + } else { + "multiple private keys require a template KeyName and named options" + }; + Err(CommandError::Usage(message.into())) } fn split_key_and_certificates(value: &OsStr) -> Result<(&OsStr, Vec<&OsStr>), CommandError> { @@ -681,8 +694,11 @@ fn encrypt(invocation: &Invocation, stdout: &mut dyn Write) -> Result<(), Comman let maximum_document_bytes = policy.resources.max_xml_document_bytes; let maximum_plaintext_bytes = policy.resources.max_encryption_plaintext_bytes; let template = read_input(invocation, policy.resources.max_xml_document_bytes)?; - let (algorithm, encrypted_type, explicit_encrypted_type) = encryption_template(&template)?; - let mut builder = EncryptedDataBuilder::new(algorithm).policy(policy); + let metadata = encryption_template(&template, &policy)?; + let algorithm = metadata.algorithm; + let encrypted_type = metadata.encrypted_type; + let explicit_encrypted_type = metadata.explicit_encrypted_type; + let mut builder = EncryptedDataBuilder::new(algorithm).policy(policy.clone()); let aes_keys = invocation.values("aes-key").collect::>(); let public_keys = ["pubkey-pem", "pubkey-der"] .into_iter() @@ -694,10 +710,9 @@ fn encrypt(invocation: &Invocation, stdout: &mut dyn Write) -> Result<(), Comman )); } if let [option] = aes_keys.as_slice() { - let template_name = encrypted_data_key_name(&template)?; enforce_named_key_match( option, - template_name.as_deref(), + metadata.content_key_name.as_deref(), invocation.flag("lax-key-search"), "AES key", )?; @@ -710,16 +725,15 @@ fn encrypt(invocation: &Invocation, stdout: &mut dyn Write) -> Result<(), Comman builder = builder.direct_key_name(name); } } else if let [option] = public_keys.as_slice() { - let recipient_name = encrypted_key_recipient_name(&template)?; enforce_named_key_match( option, - recipient_name.as_deref(), + metadata.recipient_key_name.as_deref(), invocation.flag("lax-key-search"), "RSA recipient key", )?; let path = option.value.as_deref().unwrap_or_default(); let mut recipient = EncryptionRecipient::rsa_oaep(key_material::load_rsa_public(path)?); - if let Some(parameters) = template_oaep_parameters(&template)? { + if let Some(parameters) = metadata.oaep_parameters { recipient = recipient.oaep_parameters(parameters); } builder = builder.add_recipient(recipient); @@ -747,7 +761,7 @@ fn encrypt(invocation: &Invocation, stdout: &mut dyn Write) -> Result<(), Comman )); } .map_err(|error| CommandError::Encryption(error.to_string()))?; - let rendered = apply_encryption_template(&template, &result.encrypted_data_xml)?; + let rendered = apply_encryption_template(&template, &result.encrypted_data_xml, &policy)?; if rendered.len() > maximum_document_bytes { return Err(CommandError::Encryption( "encrypted template output exceeds XML document policy".into(), @@ -756,10 +770,10 @@ fn encrypt(invocation: &Invocation, stdout: &mut dyn Write) -> Result<(), Comman write_output(invocation, rendered.as_bytes(), stdout) } -fn template_oaep_parameters(xml: &str) -> Result, CommandError> { - let document = - Document::parse(xml).map_err(|error| CommandError::Encryption(error.to_string()))?; - let Some(method) = document +fn template_oaep_parameters( + encrypted_data: Node<'_, '_>, +) -> Result, CommandError> { + let Some(method) = encrypted_data .descendants() .find(|node| node.has_tag_name((XMLENC_NS, "EncryptedKey"))) .and_then(|key| { @@ -834,15 +848,14 @@ fn oaep_mgf_from_uri(uri: &str) -> Result { .ok_or_else(|| CommandError::Encryption(format!("unsupported OAEP MGF: {uri}"))) } -fn apply_encryption_template(template: &str, generated: &str) -> Result { - let template_document = - Document::parse(template).map_err(|error| CommandError::Encryption(error.to_string()))?; - let generated_document = - Document::parse(generated).map_err(|error| CommandError::Encryption(error.to_string()))?; - let template_data = template_document - .descendants() - .find(|node| node.has_tag_name((XMLENC_NS, "EncryptedData"))) - .ok_or_else(|| CommandError::Encryption("template has no EncryptedData".into()))?; +fn apply_encryption_template( + template: &str, + generated: &str, + policy: &EncryptionPolicy, +) -> Result { + let template_document = parse_encryption_document(template, policy)?; + let generated_document = parse_encryption_document(generated, policy)?; + let template_data = select_encrypted_data(&template_document, None)?; let generated_data = generated_document.root_element(); let template_cipher = encrypted_data_cipher_value(template_data) .ok_or_else(|| CommandError::Encryption("template has no CipherValue".into()))?; @@ -982,6 +995,11 @@ fn decrypt(invocation: &Invocation, stdout: &mut dyn Write) -> Result<(), Comman let policy = DecryptionPolicy::default(); let xml = read_input(invocation, policy.resources.max_xml_document_bytes)?; let encrypted_data_id = option_text(invocation, "node-id")?; + let document = parse_encryption_document(&xml, &policy)?; + let encrypted_data = select_encrypted_data(&document, encrypted_data_id)?; + let standalone = encrypted_data == document.root_element(); + let content_key_name = encrypted_data_key_name(encrypted_data); + let recipient_key_name = encrypted_key_recipient_name(encrypted_data); let aes_keys = invocation.values("aes-key").collect::>(); let private_keys = ["privkey-pem", "privkey-der", "pkcs8-pem", "pkcs8-der"] .into_iter() @@ -993,17 +1011,34 @@ fn decrypt(invocation: &Invocation, stdout: &mut dyn Write) -> Result<(), Comman )); } let bytes = if let [option] = aes_keys.as_slice() { + enforce_named_key_match( + option, + content_key_name.as_deref(), + invocation.flag("lax-key-search"), + "AES key", + )?; let key = key_material::load_symmetric(option.value.as_deref().unwrap_or_default(), None)?; decrypt_input( &SymmetricKeyDecryptor::new(key), &xml, encrypted_data_id, + standalone, policy, )? } else if let [option] = private_keys.as_slice() { - let (path, _) = split_key_and_certificates(option.value.as_deref().unwrap_or_default())?; + enforce_named_key_match( + option, + recipient_key_name.as_deref(), + invocation.flag("lax-key-search"), + "RSA recipient key", + )?; + let (path, certificate_paths) = + split_key_and_certificates(option.value.as_deref().unwrap_or_default())?; + for certificate in certificate_paths { + key_material::load_certificate(certificate)?; + } let resolver = PrivateKeyDecryptor::new(key_material::load_rsa_private(path)?); - decrypt_input(&resolver, &xml, encrypted_data_id, policy)? + decrypt_input(&resolver, &xml, encrypted_data_id, standalone, policy)? } else { return Err(CommandError::Usage( "decrypt requires --aes-key or an RSA private key".into(), @@ -1016,15 +1051,11 @@ fn decrypt_input( resolver: &dyn DecryptionKeyResolver, xml: &str, encrypted_data_id: Option<&str>, + standalone: bool, policy: DecryptionPolicy, ) -> Result, CommandError> { - let document = - Document::parse(xml).map_err(|error| CommandError::Encryption(error.to_string()))?; - let standalone = document - .root_element() - .has_tag_name(("http://www.w3.org/2001/04/xmlenc#", "EncryptedData")); let context = DecryptContext::new(resolver).policy(policy); - if standalone && encrypted_data_id.is_none() { + if standalone { return context .decrypt(xml) .map(|content| match content { @@ -1039,15 +1070,21 @@ fn decrypt_input( .map_err(|error| CommandError::Encryption(error.to_string())) } +struct EncryptionTemplateMetadata { + algorithm: DataEncryptionAlgorithm, + encrypted_type: EncryptedDataType, + explicit_encrypted_type: bool, + content_key_name: Option, + recipient_key_name: Option, + oaep_parameters: Option, +} + fn encryption_template( xml: &str, -) -> Result<(DataEncryptionAlgorithm, EncryptedDataType, bool), CommandError> { - let document = - Document::parse(xml).map_err(|error| CommandError::Encryption(error.to_string()))?; - let encrypted_data = document - .descendants() - .find(|node| node.has_tag_name((XMLENC_NS, "EncryptedData"))) - .ok_or_else(|| CommandError::Encryption("template has no EncryptedData".into()))?; + policy: &EncryptionPolicy, +) -> Result { + let document = parse_encryption_document(xml, policy)?; + let encrypted_data = select_encrypted_data(&document, None)?; let method = encrypted_data .children() .find(|node| node.has_tag_name((XMLENC_NS, "EncryptionMethod"))) @@ -1065,35 +1102,70 @@ fn encryption_template( ))); } }; - Ok((algorithm, encrypted_type, explicit_encrypted_type)) + Ok(EncryptionTemplateMetadata { + algorithm, + encrypted_type, + explicit_encrypted_type, + content_key_name: encrypted_data_key_name(encrypted_data), + recipient_key_name: encrypted_key_recipient_name(encrypted_data), + oaep_parameters: template_oaep_parameters(encrypted_data)?, + }) } -fn encrypted_data_key_name(xml: &str) -> Result, CommandError> { - let document = - Document::parse(xml).map_err(|error| CommandError::Encryption(error.to_string()))?; - let encrypted_data = document - .descendants() - .find(|node| node.has_tag_name((XMLENC_NS, "EncryptedData"))) - .ok_or_else(|| CommandError::Encryption("template has no EncryptedData".into()))?; - Ok(direct_child_element(encrypted_data, XMLDSIG_NS, "KeyInfo") +fn encrypted_data_key_name(encrypted_data: Node<'_, '_>) -> Option { + direct_child_element(encrypted_data, XMLDSIG_NS, "KeyInfo") .and_then(|key_info| direct_child_element(key_info, XMLDSIG_NS, "KeyName")) .and_then(|key_name| key_name.text()) - .map(str::to_owned)) + .map(str::to_owned) } -fn encrypted_key_recipient_name(xml: &str) -> Result, CommandError> { - let document = - Document::parse(xml).map_err(|error| CommandError::Encryption(error.to_string()))?; - let encrypted_data = document - .descendants() - .find(|node| node.has_tag_name((XMLENC_NS, "EncryptedData"))) - .ok_or_else(|| CommandError::Encryption("template has no EncryptedData".into()))?; - Ok(direct_child_element(encrypted_data, XMLDSIG_NS, "KeyInfo") +fn encrypted_key_recipient_name(encrypted_data: Node<'_, '_>) -> Option { + direct_child_element(encrypted_data, XMLDSIG_NS, "KeyInfo") .and_then(|key_info| direct_child_element(key_info, XMLENC_NS, "EncryptedKey")) .and_then(|encrypted_key| direct_child_element(encrypted_key, XMLDSIG_NS, "KeyInfo")) .and_then(|key_info| direct_child_element(key_info, XMLDSIG_NS, "KeyName")) .and_then(|key_name| key_name.text()) - .map(str::to_owned)) + .map(str::to_owned) +} + +fn parse_encryption_document<'a>( + xml: &'a str, + policy: &EncryptionPolicy, +) -> Result, CommandError> { + policy + .validate() + .map_err(|error| CommandError::Encryption(error.to_string()))?; + let nodes_limit = u32::try_from(policy.resources.max_xml_nodes).map_err(|_| { + CommandError::Encryption("XML node ceiling does not fit the parser limit".into()) + })?; + Document::parse_with_options( + xml, + ParsingOptions { + allow_dtd: policy.xml.allow_internal_dtd, + nodes_limit, + entity_resolver: None, + }, + ) + .map_err(|error| CommandError::Encryption(error.to_string())) +} + +fn select_encrypted_data<'a, 'input>( + document: &'a Document<'input>, + id: Option<&str>, +) -> Result, CommandError> { + let mut matches = document.descendants().filter(|node| { + node.has_tag_name((XMLENC_NS, "EncryptedData")) + && id.is_none_or(|id| node.attribute("Id") == Some(id)) + }); + let selected = matches + .next() + .ok_or_else(|| CommandError::Encryption("document has no EncryptedData".into()))?; + if matches.next().is_some() { + return Err(CommandError::Encryption( + "multiple matching EncryptedData elements".into(), + )); + } + Ok(selected) } fn enforce_named_key_match( @@ -1340,7 +1412,8 @@ mod tests { "a2V5ZGF0YQ==" ); - let rendered = apply_encryption_template(&template, &generated).unwrap(); + let rendered = + apply_encryption_template(&template, &generated, &EncryptionPolicy::default()).unwrap(); let document = Document::parse(&rendered) .expect("injected KeyInfo prefixes must remain namespace-bound"); assert!( @@ -1374,6 +1447,21 @@ mod tests { )); } + #[test] + fn encryption_template_inspection_enforces_the_xml_node_ceiling() { + // CLI metadata discovery runs before the core builder, so it must reject + // over-budget templates instead of constructing an unrestricted DOM. + let mut xml = format!( + "" + ); + for _ in 0..100_000 { + xml.push_str(""); + } + xml.push_str(""); + + assert!(encryption_template(&xml, &EncryptionPolicy::default()).is_err()); + } + #[test] fn plaintext_reader_enforces_the_compiled_policy_limit_before_encryption() { // Payload limits must be enforced by the reader, before the encryption diff --git a/tools/xmlsec1/src/key_material.rs b/tools/xmlsec1/src/key_material.rs index b0006da..f5f62a7 100644 --- a/tools/xmlsec1/src/key_material.rs +++ b/tools/xmlsec1/src/key_material.rs @@ -51,6 +51,12 @@ pub struct SignatureMetadata { pub key_name: Option, } +#[derive(Debug, Eq, PartialEq)] +pub struct SigningTemplateMetadata { + pub key_name: Option, + pub has_key_info: bool, +} + pub fn read(path: impl AsRef) -> Result, KeyMaterialError> { let path = path.as_ref(); fs::read(path).map_err(|source| KeyMaterialError::Read { @@ -100,19 +106,21 @@ pub fn verification_signature_metadata( }) } -pub fn signing_signature_key_name( +pub fn signing_signature_metadata( xml: &str, policy: &SigningPolicy, -) -> Result, KeyMaterialError> { +) -> Result { policy.validate()?; let document = parse_signature_document( xml, policy.xml.allow_internal_dtd, policy.resources.max_xml_nodes, )?; - find_signature_node(&document) - .map(signature_key_name) - .ok_or(KeyMaterialError::MissingSignedInfo) + let signature = find_signature_node(&document).ok_or(KeyMaterialError::MissingSignedInfo)?; + Ok(SigningTemplateMetadata { + key_name: signature_key_name(signature), + has_key_info: signature_key_info(signature).is_some(), + }) } fn parse_signature_document( @@ -135,9 +143,7 @@ fn parse_signature_document( } fn signature_key_name(signature: roxmltree::Node<'_, '_>) -> Option { - signature - .children() - .find(|node| node.has_tag_name(("http://www.w3.org/2000/09/xmldsig#", "KeyInfo"))) + signature_key_info(signature) .and_then(|key_info| { key_info .children() @@ -147,6 +153,14 @@ fn signature_key_name(signature: roxmltree::Node<'_, '_>) -> Option { .map(str::to_owned) } +fn signature_key_info<'a, 'input>( + signature: roxmltree::Node<'a, 'input>, +) -> Option> { + signature + .children() + .find(|node| node.has_tag_name(("http://www.w3.org/2000/09/xmldsig#", "KeyInfo"))) +} + pub fn load_signing_key(path: impl AsRef) -> Result, KeyMaterialError> { let path = path.as_ref(); let bytes = read(path)?; @@ -230,8 +244,13 @@ fn valid_spki(bytes: &[u8]) -> bool { pub fn load_certificate(path: impl AsRef) -> Result, KeyMaterialError> { let path = path.as_ref(); let bytes = read(path)?; - let der = if let Ok(text) = std::str::from_utf8(&bytes) { - parse_pem(text, "CERTIFICATE", path)? + let der = if std::str::from_utf8(&bytes).is_ok() { + let (rest, pem) = x509_parser::pem::parse_x509_pem(&bytes) + .map_err(|_| KeyMaterialError::InvalidCertificate(path.to_owned()))?; + if !rest.iter().all(u8::is_ascii_whitespace) || pem.label != "CERTIFICATE" { + return Err(KeyMaterialError::InvalidCertificate(path.to_owned())); + } + pem.contents } else { bytes }; diff --git a/tools/xmlsec1/tests/process_contract.rs b/tools/xmlsec1/tests/process_contract.rs index 131a773..7e03a65 100644 --- a/tools/xmlsec1/tests/process_contract.rs +++ b/tools/xmlsec1/tests/process_contract.rs @@ -21,6 +21,18 @@ fn project_root() -> &'static Path { .unwrap() } +fn signature_template_without_key_info() -> &'static str { + r##" + + + + + + +payload +"## +} + #[test] fn signs_verifies_and_rejects_tampering_through_process_api() { // Exercise the process boundary and prove a post-signature content change @@ -536,6 +548,111 @@ fn direct_aes_key_name_must_match_the_template_unless_lax() { ); } +#[test] +fn named_aes_decryption_obeys_encrypted_data_key_name_unless_lax() { + // Decryption key selection must enforce the same document identity contract + // as encryption rather than discarding the CLI option's registry name. + let temp = tempfile::tempdir().unwrap(); + let template = temp.path().join("named-template.xml"); + let plaintext = temp.path().join("plaintext.bin"); + let encrypted = temp.path().join("encrypted.xml"); + let key = temp.path().join("key.bin"); + fs::write( + &template, + r#"expected"#, + ) + .unwrap(); + fs::write(&plaintext, b"named decrypt").unwrap(); + fs::write(&key, b"0123456789abcdef").unwrap(); + + let encrypt = Command::new(binary()) + .args(["encrypt", "--aes-key:expected"]) + .arg(&key) + .arg("--binary-data") + .arg(&plaintext) + .arg("--output") + .arg(&encrypted) + .arg(&template) + .output() + .unwrap(); + assert!(encrypt.status.success()); + + let strict = Command::new(binary()) + .args(["decrypt", "--aes-key:wrong"]) + .arg(&key) + .arg(&encrypted) + .output() + .unwrap(); + assert!(!strict.status.success()); + assert!(String::from_utf8_lossy(&strict.stderr).contains("KeyName")); + + let lax = Command::new(binary()) + .args(["decrypt", "--lax-key-search", "--aes-key:wrong"]) + .arg(&key) + .arg(&encrypted) + .output() + .unwrap(); + assert!( + lax.status.success(), + "{}", + String::from_utf8_lossy(&lax.stderr) + ); + assert_eq!(lax.stdout, b"named decrypt"); +} + +#[test] +fn standalone_binary_decryption_accepts_its_root_node_id() { + // --node-id selects an operation start point; selecting the standalone root + // must not route opaque bytes through XML document replacement validation. + let temp = tempfile::tempdir().unwrap(); + let template = temp.path().join("template.xml"); + let plaintext = temp.path().join("plaintext.bin"); + let encrypted = temp.path().join("encrypted.xml"); + let key = temp.path().join("key.bin"); + fs::write( + &template, + r#""#, + ) + .unwrap(); + fs::write(&plaintext, [0xff, 0x00, 0xfe]).unwrap(); + fs::write(&key, b"0123456789abcdef").unwrap(); + + let encrypt = Command::new(binary()) + .args(["encrypt", "--aes-key"]) + .arg(&key) + .arg("--binary-data") + .arg(&plaintext) + .arg("--output") + .arg(&encrypted) + .arg(&template) + .output() + .unwrap(); + assert!(encrypt.status.success()); + + let decrypt = Command::new(binary()) + .args(["decrypt", "--aes-key"]) + .arg(&key) + .args(["--node-id", "payload"]) + .arg(&encrypted) + .output() + .unwrap(); + assert!( + decrypt.status.success(), + "{}", + String::from_utf8_lossy(&decrypt.stderr) + ); + assert_eq!(decrypt.stdout, [0xff, 0x00, 0xfe]); + + let missing = Command::new(binary()) + .args(["decrypt", "--aes-key"]) + .arg(&key) + .args(["--node-id", "missing"]) + .arg(&encrypted) + .output() + .unwrap(); + assert!(!missing.status.success()); +} + #[test] fn rsa_recipient_name_must_match_the_template_unless_lax() { // A named RSA key selects the nested EncryptedKey recipient identity, not @@ -543,7 +660,9 @@ fn rsa_recipient_name_must_match_the_template_unless_lax() { let temp = tempfile::tempdir().unwrap(); let template = temp.path().join("named-rsa-template.xml"); let plaintext = temp.path().join("plaintext.bin"); + let encrypted = temp.path().join("encrypted.xml"); let public_key = project_root().join("tests/fixtures/keys/rsa/rsa-4096-pubkey.pem"); + let private_key = project_root().join("tests/fixtures/keys/rsa/rsa-4096-key.pem"); fs::write( &template, r#" @@ -567,6 +686,7 @@ fn rsa_recipient_name_must_match_the_template_unless_lax() { "{}", String::from_utf8_lossy(&matching.stderr) ); + fs::write(&encrypted, &matching.stdout).unwrap(); let strict = Command::new(binary()) .args(["encrypt", "--pubkey-pem:wrong"]) @@ -592,6 +712,28 @@ fn rsa_recipient_name_must_match_the_template_unless_lax() { "{}", String::from_utf8_lossy(&lax.stderr) ); + + let strict_decrypt = Command::new(binary()) + .args(["decrypt", "--privkey-pem:wrong"]) + .arg(&private_key) + .arg(&encrypted) + .output() + .unwrap(); + assert!(!strict_decrypt.status.success()); + assert!(String::from_utf8_lossy(&strict_decrypt.stderr).contains("KeyName")); + + let lax_decrypt = Command::new(binary()) + .args(["decrypt", "--lax-key-search", "--privkey-pem:wrong"]) + .arg(&private_key) + .arg(&encrypted) + .output() + .unwrap(); + assert!( + lax_decrypt.status.success(), + "{}", + String::from_utf8_lossy(&lax_decrypt.stderr) + ); + assert_eq!(lax_decrypt.stdout, b"named recipient"); } #[test] @@ -690,6 +832,32 @@ fn rsa_decryption_accepts_private_key_certificate_companions() { String::from_utf8_lossy(&decrypt.stderr) ); assert_eq!(decrypt.stdout, b"certificate companion"); + + let malformed = temp.path().join("malformed.pem"); + fs::write(&malformed, "not a certificate").unwrap(); + let malformed_compound = format!("{},{}", private_key.display(), malformed.display()); + let rejected = Command::new(binary()) + .args(["decrypt", "--privkey-pem"]) + .arg(malformed_compound) + .arg(&encrypted) + .output() + .unwrap(); + assert!(!rejected.status.success()); + assert!(String::from_utf8_lossy(&rejected.stderr).contains("certificate")); + + let missing_compound = format!( + "{},{}", + private_key.display(), + temp.path().join("missing.pem").display() + ); + let missing = Command::new(binary()) + .args(["decrypt", "--privkey-pem"]) + .arg(missing_compound) + .arg(&encrypted) + .output() + .unwrap(); + assert!(!missing.status.success()); + assert!(String::from_utf8_lossy(&missing.stderr).contains("missing.pem")); } #[test] @@ -1038,6 +1206,45 @@ fn signing_rejects_a_malformed_secondary_certificate() { assert!(String::from_utf8_lossy(&result.stderr).contains("invalid PEM certificate")); } +#[test] +fn signing_with_certificate_companions_does_not_require_key_info() { + // Certificate companions are always validated, but optional output metadata + // must only be written when the signature template provides its placeholder. + let temp = tempfile::tempdir().unwrap(); + let template = temp.path().join("without-key-info.xml"); + let private_key = project_root().join("tests/fixtures/keys/rsa/rsa-4096-key.pem"); + let certificate = project_root().join("tests/fixtures/keys/rsa/rsa-4096-cert.pem"); + fs::write(&template, signature_template_without_key_info()).unwrap(); + let compound = format!("{},{}", private_key.display(), certificate.display()); + + let result = Command::new(binary()) + .args(["sign", "--privkey-pem"]) + .arg(compound) + .arg(&template) + .output() + .unwrap(); + assert!( + result.status.success(), + "{}", + String::from_utf8_lossy(&result.stderr) + ); + let signed = String::from_utf8(result.stdout).unwrap(); + assert!(signed.contains("")); + assert!(!signed.contains(" Date: Fri, 14 Aug 2026 17:27:32 +0300 Subject: [PATCH 09/27] fix(cli): align donor command contracts - reject commands absent from the pinned donor surface - restore native option aliases and legacy XPath semantics - reject ambiguous encryption payload selection --- docs/cli.md | 12 +++- tools/xmlsec1/src/args.rs | 40 +++++++++-- tools/xmlsec1/src/commands.rs | 32 +++++++-- tools/xmlsec1/tests/process_contract.rs | 96 +++++++++++++++++++++++++ 4 files changed, 168 insertions(+), 12 deletions(-) diff --git a/docs/cli.md b/docs/cli.md index 4743b82..4ab6b68 100644 --- a/docs/cli.md +++ b/docs/cli.md @@ -19,7 +19,9 @@ The binary recognizes libxmlsec1's command names and leading-dash aliases for `check-transforms`, `list-key-data`, `check-key-data`, `help`, and `version`. Successful operations exit zero. Invalid arguments, unavailable capabilities, policy violations, invalid signatures, decryption failures, and I/O errors exit -non-zero. +non-zero. Commands absent from the pinned 1.3.13 surface are not advertised; +historical `sign-tmpl` spellings are rejected instead of being routed to `sign` +without template-generation semantics. Capability checks and runtime dispatch use one registry. A transform or key-data class absent from `list-*` is not silently substituted and causes `check-*` to @@ -35,6 +37,8 @@ which options accept `[:name]` and which consume a value. The listing is built from the parser's option metadata, so it cannot advertise a syntax the parser does not recognize. A `:` suffix on flags or unrelated valued options is rejected rather than silently activating the underlying option. +Native aliases from the same donor metadata are accepted, including +`--pubkey-cert`, `--binary`, and command-local `-h`. ## Examples @@ -60,6 +64,10 @@ Verification accepts `-` as the conventional stdin marker. For documents with multiple signatures, `--node-id ` selects an ID-bearing start node and verifies the single `Signature` in its subtree; missing and duplicate IDs fail closed. +XPath and XPath Filter 2.0 verification uses libxmlsec1's legacy `here()` +binding at this CLI compatibility boundary. The Rust library API retains the +XMLDSig specification binding by default and requires an explicit opt-in for +legacy documents. When the selected signature contains `KeyName`, named raw public-key and explicit certificate inputs must match it; `--lax-key-search` is the explicit opt-out. A signature without `KeyName` does not request a different identity, @@ -99,6 +107,8 @@ Named AES and RSA decryption keys obey the selected `EncryptedData` or nested routing them through XML document replacement. `--binary-data` rejects templates explicitly typed as XML `Element` or `Content`; use `--xml-data` for those templates so ciphertext metadata cannot mislabel arbitrary bytes as XML. +Supplying `--binary-data` and `--xml-data` together is rejected before either +payload is read; encryption requires exactly one payload mode. Generate an AES key store using the upstream command shape: diff --git a/tools/xmlsec1/src/args.rs b/tools/xmlsec1/src/args.rs index cf7d972..b41761e 100644 --- a/tools/xmlsec1/src/args.rs +++ b/tools/xmlsec1/src/args.rs @@ -20,7 +20,6 @@ pub enum Command { Keys, Sign, Verify, - SignTemplate, Encrypt, Decrypt, } @@ -43,7 +42,6 @@ impl Command { "keys" => Self::Keys, "sign" => Self::Sign, "verify" => Self::Verify, - "sign-tmpl" | "sign-template" => Self::SignTemplate, "encrypt" => Self::Encrypt, "decrypt" => Self::Decrypt, _ => return None, @@ -196,7 +194,7 @@ pub(crate) const OPTION_SPECS: &[OptionSpec] = &[ }, OptionSpec { canonical: "pubkey-cert-pem", - aliases: &[], + aliases: &["pubkey-cert"], arity: VALUE, accepts_parameter: true, }, @@ -358,7 +356,7 @@ pub(crate) const OPTION_SPECS: &[OptionSpec] = &[ }, OptionSpec { canonical: "binary-data", - aliases: &[], + aliases: &["binary"], arity: VALUE, accepts_parameter: false, }, @@ -388,7 +386,7 @@ pub(crate) const OPTION_SPECS: &[OptionSpec] = &[ }, OptionSpec { canonical: "help", - aliases: &[], + aliases: &["h"], arity: FLAG, accepts_parameter: false, }, @@ -519,7 +517,7 @@ mod tests { fn parses_alias_named_and_repeated_options() { let parsed = parse(&[ "xmlsec1", - "sign-tmpl", + "sign", "-o", "signed.xml", "--privkey-pem:signer", @@ -531,7 +529,7 @@ mod tests { "input.xml", ]) .expect("valid donor-shaped arguments must parse"); - assert_eq!(parsed.command, Command::SignTemplate); + assert_eq!(parsed.command, Command::Sign); assert_eq!(parsed.last_value("output"), Some(OsStr::new("signed.xml"))); assert_eq!(parsed.values("trusted-pem").count(), 2); assert_eq!( @@ -639,6 +637,34 @@ mod tests { } } + #[test] + fn recognizes_all_aliases_for_native_donor_options() { + // The pinned donor metadata is the source of truth for aliases; accepting + // only each canonical spelling breaks otherwise native command lines. + for (alias, canonical) in [ + ("--pubkey-cert", "pubkey-cert-pem"), + ("--binary", "binary-data"), + ] { + let parsed = parse(&["xmlsec1", "verify", alias, "value", "input.xml"]) + .expect("native donor alias must parse"); + assert_eq!(parsed.values(canonical).count(), 1, "alias {alias}"); + } + + let parsed = parse(&["xmlsec1", "verify", "-h", "input.xml"]) + .expect("short command-help alias must parse"); + assert!(parsed.flag("help")); + } + + #[test] + fn rejects_commands_absent_from_the_pinned_donor_surface() { + // libxmlsec1 1.3.13 has no sign-tmpl command. Advertising it as an alias + // for sign would claim template generation while requiring a template. + assert!(matches!( + parse(&["xmlsec1", "sign-tmpl"]), + Err(ParseError::UnknownCommand(command)) if command == "sign-tmpl" + )); + } + #[cfg(unix)] #[test] fn preserves_non_utf8_filesystem_arguments() { diff --git a/tools/xmlsec1/src/commands.rs b/tools/xmlsec1/src/commands.rs index 6f10f21..4416fba 100644 --- a/tools/xmlsec1/src/commands.rs +++ b/tools/xmlsec1/src/commands.rs @@ -13,7 +13,7 @@ use xml_sec::{ xmldsig::{ DefaultKeyResolver, DsigStatus, KeyInfoWriter, KeyResolver, KeyResolverConfig, SignContext, SignatureAlgorithm, UriTypeSet, VerifyContext, X509CertificateKeyInfoWriter, - parse_key_info, + XPathHereSemantics, parse_key_info, }, xmlenc::{ DataEncryptionAlgorithm, DecryptContext, DecryptedContent, DecryptionKeyResolver, @@ -117,7 +117,7 @@ pub fn execute( } } Command::Keys => keys(&invocation, stdout), - Command::Sign | Command::SignTemplate => sign(&invocation, stdout), + Command::Sign => sign(&invocation, stdout), Command::Verify => verify(&invocation, stdout), Command::Encrypt => encrypt(&invocation, stdout), Command::Decrypt => decrypt(&invocation, stdout), @@ -140,7 +140,7 @@ fn help_all(output: &mut dyn Write) -> Result<(), CommandError> { output, "Commands: help help-all help-dsig help-enc help-keys help-x509 version \ list-key-data check-key-data list-transforms check-transforms keys sign \ - verify sign-tmpl encrypt decrypt" + verify encrypt decrypt" ) .map_err(stdout_error)?; writeln!(output, "Options:").map_err(stdout_error)?; @@ -466,6 +466,7 @@ fn xmlsec_compatibility_verification_policy(invocation: &Invocation) -> Verifica process_manifests: !invocation.flag("ignore-manifests"), reference_uri_types: UriTypeSet::ALL, retrieval_uri_types: UriTypeSet::ALL, + xpath_here_semantics: XPathHereSemantics::XmlSecLegacy, ..VerificationPolicy::default() }; policy.key_trust.allowed_legacy_signature_algorithms = HashSet::from([ @@ -690,6 +691,13 @@ fn encrypt(invocation: &Invocation, stdout: &mut dyn Write) -> Result<(), Comman ], )?; reject_unimplemented_selectors(invocation, &[])?; + let has_binary_data = invocation.last_value("binary-data").is_some(); + let has_xml_data = invocation.last_value("xml-data").is_some(); + if has_binary_data == has_xml_data { + return Err(CommandError::Usage( + "encrypt requires exactly one of --binary-data or --xml-data".into(), + )); + } let policy = EncryptionPolicy::default(); let maximum_document_bytes = policy.resources.max_xml_document_bytes; let maximum_plaintext_bytes = policy.resources.max_encryption_plaintext_bytes; @@ -1377,6 +1385,21 @@ mod tests { assert!(matches!(error, CommandError::UnsupportedOption(_))); } + #[test] + fn compatibility_verification_uses_libxmlsec_here_semantics() { + // The CLI compatibility boundary must verify the same node set as the + // donor when an XPath transform uses its non-standard here() binding. + let policy = xmlsec_compatibility_verification_policy(&invocation(&[ + "xmlsec1", + "verify", + "input.xml", + ])); + assert_eq!( + policy.xpath_here_semantics, + xml_sec::xmldsig::XPathHereSemantics::XmlSecLegacy + ); + } + #[test] fn help_all_enumerates_the_registered_surface() { let mut output = Vec::new(); @@ -1387,9 +1410,10 @@ mod tests { ) .unwrap(); let help = String::from_utf8(output).unwrap(); - for command in ["help-dsig", "check-key-data", "sign-tmpl", "decrypt"] { + for command in ["help-dsig", "check-key-data", "decrypt"] { assert!(help.contains(command), "missing command {command}"); } + assert!(!help.contains("sign-tmpl")); for option in OPTION_SPECS { assert!( help.contains(&format!("--{}", option.canonical)), diff --git a/tools/xmlsec1/tests/process_contract.rs b/tools/xmlsec1/tests/process_contract.rs index 7e03a65..178974f 100644 --- a/tools/xmlsec1/tests/process_contract.rs +++ b/tools/xmlsec1/tests/process_contract.rs @@ -9,6 +9,13 @@ use rsa::{ RsaPrivateKey, pkcs8::{DecodePrivateKey as _, EncodePrivateKey as _}, }; +use xml_sec::{ + c14n::{C14nAlgorithm, C14nMode}, + xmldsig::{ + DigestAlgorithm, ReferenceBuilder, RsaSigningKey, SignContext, SignatureAlgorithm, + SignatureBuilder, Transform, XPathExpression, XPathHereSemantics, + }, +}; fn binary() -> &'static str { env!("CARGO_BIN_EXE_xmlsec1") @@ -83,6 +90,59 @@ fn signs_verifies_and_rejects_tampering_through_process_api() { assert!(String::from_utf8_lossy(&rejected.stderr).contains("invalid")); } +#[test] +fn short_command_help_alias_reaches_process_dispatch() { + // Parsing an alias is insufficient if command validation later rejects its + // canonical option, so exercise the complete process route for donor `-h`. + let output = Command::new(binary()) + .args(["verify", "-h"]) + .output() + .unwrap(); + assert!(output.status.success()); + assert!(String::from_utf8_lossy(&output.stdout).starts_with("Usage:")); +} + +#[test] +fn verifies_libxmlsec_legacy_here_semantics_through_process_api() { + // The expression selects different nodes under specification and libxmlsec + // semantics, so process success proves the CLI policy reaches transforms. + let temp = tempfile::tempdir().unwrap(); + let signed_path = temp.path().join("legacy-here.xml"); + let private_key = + fs::read_to_string(project_root().join("tests/fixtures/keys/rsa/rsa-2048-key.pem")) + .unwrap(); + let key = RsaSigningKey::from_pkcs8_pem(&private_key).unwrap(); + let builder = SignatureBuilder::new( + C14nAlgorithm::new(C14nMode::Exclusive1_0, false), + SignatureAlgorithm::RsaSha256, + ) + .add_reference( + ReferenceBuilder::new(DigestAlgorithm::Sha256) + .uri("") + .transform(Transform::XPath(XPathExpression::new( + "count(. | here()) = 1", + ))), + ); + let signed = SignContext::new(&key) + .xpath_here_semantics(XPathHereSemantics::XmlSecLegacy) + .sign_with_builder("legacy here", &builder) + .unwrap(); + fs::write(&signed_path, signed).unwrap(); + + let public_key = project_root().join("tests/fixtures/keys/rsa/rsa-2048-pubkey.pem"); + let verify = Command::new(binary()) + .args(["verify", "--pubkey-pem"]) + .arg(public_key) + .arg(signed_path) + .output() + .unwrap(); + assert!( + verify.status.success(), + "{}", + String::from_utf8_lossy(&verify.stderr) + ); +} + #[test] fn named_public_key_obeys_signature_key_name_unless_lax() { // Named direct keys participate in the same strict KeyName contract as a @@ -508,6 +568,42 @@ fn binary_encryption_rejects_xml_typed_templates() { assert!(String::from_utf8_lossy(&result.stderr).contains("binary-data")); } +#[test] +fn encryption_rejects_simultaneous_binary_and_xml_payloads() { + // Selecting one option by branch order can encrypt the wrong input while + // reporting success, so payload mode must be an exclusive choice. + let temp = tempfile::tempdir().unwrap(); + let template = temp.path().join("template.xml"); + let binary_payload = temp.path().join("payload.bin"); + let xml = temp.path().join("payload.xml"); + let key = temp.path().join("content.key"); + let output = temp.path().join("encrypted.xml"); + fs::write( + &template, + r#""#, + ) + .unwrap(); + fs::write(&binary_payload, b"binary payload").unwrap(); + fs::write(&xml, b"xml payload").unwrap(); + fs::write(&key, [0x31; 16]).unwrap(); + + let result = Command::new(binary()) + .args(["encrypt", "--aes-key"]) + .arg(key) + .args(["--binary-data"]) + .arg(binary_payload) + .args(["--xml-data"]) + .arg(xml) + .args(["--output"]) + .arg(&output) + .arg(template) + .output() + .unwrap(); + assert!(!result.status.success()); + assert!(String::from_utf8_lossy(&result.stderr).contains("exactly one")); + assert!(!output.exists()); +} + #[test] fn direct_aes_key_name_must_match_the_template_unless_lax() { let temp = tempfile::tempdir().unwrap(); From b11e45e9eea8689353588eb71682cb15bb59e8ef Mon Sep 17 00:00:00 2001 From: Dmitry Prudnikov Date: Fri, 14 Aug 2026 17:41:24 +0300 Subject: [PATCH 10/27] test(cli): pin node ceiling failure --- tools/xmlsec1/src/commands.rs | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/tools/xmlsec1/src/commands.rs b/tools/xmlsec1/src/commands.rs index 4416fba..cd4cac4 100644 --- a/tools/xmlsec1/src/commands.rs +++ b/tools/xmlsec1/src/commands.rs @@ -1483,7 +1483,14 @@ mod tests { } xml.push_str(""); - assert!(encryption_template(&xml, &EncryptionPolicy::default()).is_err()); + let error = match encryption_template(&xml, &EncryptionPolicy::default()) { + Ok(_) => panic!("over-budget template must fail"), + Err(error) => error, + }; + assert!( + matches!(&error, CommandError::Encryption(message) if message.contains("nodes limit")), + "expected the parser node ceiling, got: {error}" + ); } #[test] From 06f26264089794e40f1e2157f911a576450c0859 Mon Sep 17 00:00:00 2001 From: Dmitry Prudnikov Date: Fri, 14 Aug 2026 18:59:27 +0300 Subject: [PATCH 11/27] fix(cli): honor donor selection contracts - apply operation start-node selection across signing and encryption pipelines - render command-scoped help from validator contracts - accept RSA recipient certificates and align CLI documentation --- docs/cli.md | 13 +- src/xmldsig/mutation.rs | 54 +++- src/xmldsig/sign.rs | 108 ++++++-- src/xmlenc/decrypt.rs | 96 ++++++- tools/xmlsec1/README.md | 11 +- tools/xmlsec1/src/commands.rs | 330 +++++++++++++++--------- tools/xmlsec1/src/key_material.rs | 27 +- tools/xmlsec1/tests/process_contract.rs | 277 ++++++++++++++++++++ 8 files changed, 761 insertions(+), 155 deletions(-) diff --git a/docs/cli.md b/docs/cli.md index 4ab6b68..89a39d1 100644 --- a/docs/cli.md +++ b/docs/cli.md @@ -63,7 +63,8 @@ template unconstrained when one explicit key is supplied. Verification accepts `-` as the conventional stdin marker. For documents with multiple signatures, `--node-id ` selects an ID-bearing start node and verifies the single `Signature` in its subtree; missing and duplicate IDs fail -closed. +closed. Signing applies the same start-node contract and mutates only the +selected template's digest, signature, and optional key-info placeholders. XPath and XPath Filter 2.0 verification uses libxmlsec1's legacy `here()` binding at this CLI compatibility boundary. The Rust library API retains the XMLDSig specification binding by default and requires an explicit opt-in for @@ -90,7 +91,8 @@ xmlsec1 decrypt --aeskey:content content.key \ Files passed through `--aeskey` use libxmlsec1's binary-key contract: their bytes are consumed verbatim rather than guessed to be Base64 text. `decrypt` accepts both standalone `EncryptedData` and encrypted elements embedded in a -larger XML document; `--node-id` selects an embedded `EncryptedData` by `Id`. +larger XML document; `--node-id` selects an ID-bearing operation start node and +then requires exactly one `EncryptedData` in its subtree. Encryption preserves the template's `Id`, `Type`, `MimeType`, `KeyInfo`, `EncryptionProperties`, and RSA-OAEP parameters while replacing only the cryptographic `CipherValue` payloads. @@ -126,9 +128,10 @@ algorithms, selectors, and policy controls remain capability-limited. Current private-key loading accepts unencrypted PKCS#8 RSA, P-256, and P-384 plus PKCS#1 RSA in PEM or DER; `--privkey-p8-pem` and `--privkey-p8-der` are accepted as upstream PKCS#8 aliases. Public verification accepts SubjectPublicKeyInfo, -PKCS#1 RSA public keys, and X.509 certificates. Explicit certificate options -pin verification to that certificate's public key instead of permitting an -embedded `KeyInfo` to select another identity. When `--trusted-pem` or +PKCS#1 RSA public keys, and X.509 certificates. Encryption accepts RSA public +keys or RSA X.509 recipient certificates in PEM or DER. Explicit verification +certificate options pin verification to that certificate's public key instead +of permitting an embedded `KeyInfo` to select another identity. When `--trusted-pem` or `--trusted-der` is also supplied, the explicit certificate must build a valid path through any `--untrusted-*` intermediates to a supplied anchor; `--insecure` is the explicit opt-out. Direct XMLEnc keys accept diff --git a/src/xmldsig/mutation.rs b/src/xmldsig/mutation.rs index b13e8e0..63399ad 100644 --- a/src/xmldsig/mutation.rs +++ b/src/xmldsig/mutation.rs @@ -155,6 +155,20 @@ pub(super) fn fill_signed_info_digest_values_with_options( values: I, policy: Option<&crate::policy::SigningPolicy>, ) -> Result +where + I: IntoIterator, + S: AsRef, +{ + let target_signature = last_signature_index(xml, policy)?; + fill_signed_info_digest_values_at_index_with_options(xml, values, target_signature, policy) +} + +pub(super) fn fill_signed_info_digest_values_at_index_with_options( + xml: &str, + values: I, + target_signature: usize, + policy: Option<&crate::policy::SigningPolicy>, +) -> Result where I: IntoIterator, S: AsRef, @@ -163,8 +177,7 @@ where .into_iter() .map(|value| value.as_ref().to_owned()) .collect(); - let target_signature = last_signature_index(xml, policy)?; - let expected = count_signed_info_digest_values(xml, policy)?; + let expected = count_signed_info_digest_values(xml, target_signature, policy)?; if expected != values.len() { return Err(XmlMutationError::ValueCountMismatch { element: "DigestValue", @@ -198,7 +211,16 @@ pub(super) fn fill_signature_value_with_options( policy: Option<&crate::policy::SigningPolicy>, ) -> Result { let target_signature = last_signature_index(xml, policy)?; - let expected = count_direct_signature_values(xml, policy)?; + fill_signature_value_at_index_with_options(xml, value, target_signature, policy) +} + +pub(super) fn fill_signature_value_at_index_with_options( + xml: &str, + value: &str, + target_signature: usize, + policy: Option<&crate::policy::SigningPolicy>, +) -> Result { + let expected = count_direct_signature_values(xml, target_signature, policy)?; if expected != 1 { return Err(XmlMutationError::ValueCountMismatch { element: "SignatureValue", @@ -227,7 +249,16 @@ pub(super) fn fill_key_info_with_options( policy: Option<&crate::policy::SigningPolicy>, ) -> Result { let target_signature = last_signature_index(xml, policy)?; - let expected = count_direct_key_infos(xml, policy)?; + fill_key_info_at_index_with_options(xml, key_info_content, target_signature, policy) +} + +pub(super) fn fill_key_info_at_index_with_options( + xml: &str, + key_info_content: &str, + target_signature: usize, + policy: Option<&crate::policy::SigningPolicy>, +) -> Result { + let expected = count_direct_key_infos(xml, target_signature, policy)?; if expected != 1 { return Err(XmlMutationError::ValueCountMismatch { element: "KeyInfo", @@ -505,10 +536,11 @@ fn count_dsig_elements(xml: &str, local_name: &str) -> Result, ) -> Result { let document = parse_with_options(xml, policy)?; - let Some(signature) = last_signature_node(&document) else { + let Some(signature) = signature_node(&document, target_signature) else { return Ok(0); }; Ok(document @@ -519,10 +551,11 @@ fn count_signed_info_digest_values( fn count_direct_signature_values( xml: &str, + target_signature: usize, policy: Option<&crate::policy::SigningPolicy>, ) -> Result { let document = parse_with_options(xml, policy)?; - let Some(signature) = last_signature_node(&document) else { + let Some(signature) = signature_node(&document, target_signature) else { return Ok(0); }; Ok(document @@ -538,10 +571,11 @@ fn count_direct_signature_values( fn count_direct_key_infos( xml: &str, + target_signature: usize, policy: Option<&crate::policy::SigningPolicy>, ) -> Result { let document = parse_with_options(xml, policy)?; - let Some(signature) = last_signature_node(&document) else { + let Some(signature) = signature_node(&document, target_signature) else { return Ok(0); }; Ok(document @@ -555,12 +589,14 @@ fn count_direct_key_infos( .count()) } -fn last_signature_node<'a>( +fn signature_node<'a>( document: &'a roxmltree::Document<'a>, + target_signature: usize, ) -> Option> { document .descendants() - .rfind(|node| is_dsig_node(*node, "Signature")) + .filter(|node| is_dsig_node(*node, "Signature")) + .nth(target_signature) } fn last_signature_index( diff --git a/src/xmldsig/sign.rs b/src/xmldsig/sign.rs index 40ba3f4..f361da0 100644 --- a/src/xmldsig/sign.rs +++ b/src/xmldsig/sign.rs @@ -25,8 +25,9 @@ use crate::c14n::{canonicalize_bounded_with_xml_base_budget, is_output_limit_err use super::builder::{SignatureBuilder, SignatureBuilderError}; use super::digest::DigestAlgorithm; use super::mutation::{ - XmlMutationError, append_signature_to_root_with_options, fill_key_info_with_options, - fill_signature_value_with_options, fill_signed_info_digest_values, + XmlMutationError, append_signature_to_root_with_options, fill_key_info_at_index_with_options, + fill_signature_value_at_index_with_options, fill_signed_info_digest_values, + fill_signed_info_digest_values_at_index_with_options, fill_signed_info_digest_values_with_options, }; use super::parse::{ @@ -675,6 +676,7 @@ impl SigningKey for EcdsaP384SigningKey { pub struct SignContext<'a> { signing_key: &'a dyn SigningKey, key_info_writer: Option<&'a dyn KeyInfoWriter>, + start_node_id: Option<&'a str>, policy: crate::policy::SigningPolicy, provider: &'a dyn crate::provider::CryptoProvider, } @@ -685,6 +687,7 @@ impl<'a> SignContext<'a> { Self { signing_key, key_info_writer: None, + start_node_id: None, policy: crate::policy::SigningPolicy::default(), provider: crate::provider::default_provider(), } @@ -711,6 +714,14 @@ impl<'a> SignContext<'a> { self } + /// Select an operation start node by ID and sign the first XMLDSig + /// `` in that node's subtree. + #[must_use] + pub fn start_node_id(mut self, id: &'a str) -> Self { + self.start_node_id = Some(id); + self + } + /// Select the node returned by XPath's `here()` extension function. /// /// The default follows XMLDSig and returns the `` parameter. @@ -731,6 +742,9 @@ impl<'a> SignContext<'a> { pub fn sign_template(&self, xml: &str) -> Result { self.policy.validate()?; self.policy.resources.validate_xml_document_len(xml.len())?; + let document = parse_signing_document(xml, Some(&self.policy)) + .map_err(SigningDigestError::XmlParse)?; + let target_signature = signing_signature_index(&document, self.start_node_id)?; let execution_budget = TransformExecutionBudget::from_resources(&self.policy.resources); let transform_options = TransformOptions::default() .allow_internal_dtd(self.policy.xml.allow_internal_dtd) @@ -741,12 +755,17 @@ impl<'a> SignContext<'a> { Some(&self.policy), self.provider, &execution_budget, + Some(target_signature), )?; self.policy .resources .validate_xml_document_len(with_digests.len())?; - let (algorithm, canonical_signed_info) = - canonicalize_signed_info(&with_digests, &self.policy, &execution_budget)?; + let (algorithm, canonical_signed_info) = canonicalize_signed_info( + &with_digests, + &self.policy, + &execution_budget, + target_signature, + )?; execution_budget .charge_c14n_output(canonical_signed_info.len()) .map_err(SigningDigestError::Transform)?; @@ -770,15 +789,23 @@ impl<'a> SignContext<'a> { .sign(self.signing_key, algorithm, &canonical_signed_info)?; validate_signature_output(expected_signature_len, &signature_value)?; let signature_b64 = base64::engine::general_purpose::STANDARD.encode(signature_value); - let signed = - fill_signature_value_with_options(&with_digests, &signature_b64, Some(&self.policy))?; + let signed = fill_signature_value_at_index_with_options( + &with_digests, + &signature_b64, + target_signature, + Some(&self.policy), + )?; self.policy .resources .validate_xml_document_len(signed.len())?; if let Some(writer) = self.key_info_writer { let key_info_content = writer.write_key_info(self.signing_key)?; - let signed = - fill_key_info_with_options(&signed, &key_info_content, Some(&self.policy))?; + let signed = fill_key_info_at_index_with_options( + &signed, + &key_info_content, + target_signature, + Some(&self.policy), + )?; self.policy .resources .validate_xml_document_len(signed.len())?; @@ -825,6 +852,7 @@ pub fn compute_reference_digest_values( None, crate::provider::default_provider(), &execution_budget, + None, ) } @@ -834,9 +862,10 @@ fn compute_reference_digest_values_with_options( policy: Option<&crate::policy::SigningPolicy>, provider: &dyn crate::provider::CryptoProvider, execution_budget: &TransformExecutionBudget, + target_signature: Option, ) -> Result, SigningDigestError> { let doc = parse_signing_document(xml, policy)?; - let signature = find_signing_signature_node(&doc)?; + let signature = find_signing_signature_node(&doc, target_signature)?; let signed_info = find_required_child(signature, "SignedInfo")?; let references = parse_signing_references(signed_info)?; if let Some(policy) = policy { @@ -939,6 +968,7 @@ pub fn fill_reference_digest_values(xml: &str) -> Result, provider: &dyn crate::provider::CryptoProvider, execution_budget: &TransformExecutionBudget, + target_signature: Option, ) -> Result { let digest_values = compute_reference_digest_values_with_options( xml, @@ -955,10 +986,18 @@ fn fill_reference_digest_values_with_options( policy, provider, execution_budget, + target_signature, )? .into_iter() .map(|digest| digest.digest_value); - Ok(if let Some(policy) = policy { + Ok(if let Some(target_signature) = target_signature { + fill_signed_info_digest_values_at_index_with_options( + xml, + digest_values, + target_signature, + policy, + )? + } else if let Some(policy) = policy { fill_signed_info_digest_values_with_options(xml, digest_values, Some(policy))? } else { fill_signed_info_digest_values(xml, digest_values)? @@ -969,9 +1008,11 @@ fn canonicalize_signed_info( xml: &str, policy: &crate::policy::SigningPolicy, execution_budget: &TransformExecutionBudget, + target_signature: usize, ) -> Result<(SignatureAlgorithm, Vec), SigningError> { let doc = parse_signing_document(xml, Some(policy)).map_err(SigningDigestError::XmlParse)?; - let signature = find_signing_signature_node(&doc).map_err(SigningError::Digest)?; + let signature = + find_signing_signature_node(&doc, Some(target_signature)).map_err(SigningError::Digest)?; let signed_info_node = find_required_child(signature, "SignedInfo").map_err(SigningError::Digest)?; let signed_info = parse_signed_info(signed_info_node)?; @@ -1034,13 +1075,48 @@ fn parse_private_key_pem(private_key_pem: &str) -> Result, SigningKeyErr fn find_signing_signature_node<'a>( doc: &'a Document<'a>, + target_signature: Option, ) -> Result, SigningDigestError> { + let mut signatures = doc.descendants().filter(|node| { + node.is_element() + && node.tag_name().name() == "Signature" + && node.tag_name().namespace() == Some(XMLDSIG_NS) + }); + match target_signature { + Some(index) => signatures.nth(index), + None => signatures.next_back(), + } + .ok_or(SigningDigestError::MissingElement { + element: "Signature", + }) +} + +fn signing_signature_index( + doc: &Document<'_>, + start_node_id: Option<&str>, +) -> Result { + let selected = if let Some(id) = start_node_id { + let start = UriReferenceResolver::new(doc) + .node_for_id(id) + .ok_or_else(|| { + SigningDigestError::InvalidStructure(format!( + "selected node ID is missing or ambiguous: {id}" + )) + })?; + start + .descendants() + .find(|node| node.has_tag_name((XMLDSIG_NS, "Signature"))) + .ok_or_else(|| { + SigningDigestError::InvalidStructure(format!( + "selected node subtree has no Signature: {id}" + )) + })? + } else { + find_signing_signature_node(doc, None)? + }; doc.descendants() - .rfind(|node| { - node.is_element() - && node.tag_name().name() == "Signature" - && node.tag_name().namespace() == Some(XMLDSIG_NS) - }) + .filter(|node| node.has_tag_name((XMLDSIG_NS, "Signature"))) + .position(|node| node == selected) .ok_or(SigningDigestError::MissingElement { element: "Signature", }) diff --git a/src/xmlenc/decrypt.rs b/src/xmlenc/decrypt.rs index 466d8cc..df2a7c9 100644 --- a/src/xmlenc/decrypt.rs +++ b/src/xmlenc/decrypt.rs @@ -16,6 +16,7 @@ use super::{ KeyTransportAlgorithm, KeyWrapAlgorithm, OaepDigestAlgorithm, RsaOaepParameters, XmlEncError, has_single_element_with_boundary_trivia, }; +use crate::xmldsig::uri::UriReferenceResolver; #[cfg(test)] use super::parse_encrypted_data; @@ -151,7 +152,25 @@ impl<'a> DecryptContext<'a> { xml: &str, encrypted_data_id: Option<&str>, ) -> Result { - decrypt_document_with_context(xml, encrypted_data_id, self) + decrypt_document_with_context( + xml, + DocumentEncryptedDataSelector::EncryptedDataId(encrypted_data_id), + self, + ) + } + + /// Decrypt and replace the sole `EncryptedData` below an operation start + /// node selected by ID. + pub fn decrypt_document_from_start_node( + &self, + xml: &str, + start_node_id: Option<&str>, + ) -> Result { + decrypt_document_with_context( + xml, + DocumentEncryptedDataSelector::StartNodeId(start_node_id), + self, + ) } } @@ -436,16 +455,39 @@ pub fn decrypt_document_with_options( .decrypt_document(xml, options.encrypted_data_id) } +#[derive(Clone, Copy)] +enum DocumentEncryptedDataSelector<'a> { + EncryptedDataId(Option<&'a str>), + StartNodeId(Option<&'a str>), +} + fn decrypt_document_with_context( xml: &str, - encrypted_data_id: Option<&str>, + selector: DocumentEncryptedDataSelector<'_>, context: &DecryptContext<'_>, ) -> Result { context.policy.resources.validate()?; validate_encryption_document_len(xml.len(), &context.policy)?; let parsing_options = || decryption_parsing_options(&context.policy); let document = Document::parse_with_options(xml, parsing_options())?; - let mut matches = document.descendants().filter(|node| { + let start = match selector { + DocumentEncryptedDataSelector::StartNodeId(Some(id)) => { + UriReferenceResolver::new(&document) + .node_for_id(id) + .ok_or_else(|| { + XmlEncError::InvalidStructure(format!( + "selected node ID is missing or ambiguous: {id}" + )) + })? + } + DocumentEncryptedDataSelector::StartNodeId(None) + | DocumentEncryptedDataSelector::EncryptedDataId(_) => document.root(), + }; + let encrypted_data_id = match selector { + DocumentEncryptedDataSelector::EncryptedDataId(id) => id, + DocumentEncryptedDataSelector::StartNodeId(_) => None, + }; + let mut matches = start.descendants().filter(|node| { node.has_tag_name((XMLENC_NS, "EncryptedData")) && encrypted_data_id.is_none_or(|id| node.attribute("Id") == Some(id)) }); @@ -2357,6 +2399,54 @@ mod tests { )); } + #[test] + fn selects_encrypted_data_below_a_unique_operation_start_node() { + // CLI-compatible selection starts at an arbitrary ID-bearing ancestor; + // missing/duplicate IDs and multiple encrypted descendants fail closed. + let key = [0x42_u8; 16]; + let first = encrypted_gcm_element( + "http://www.w3.org/2001/04/xmlenc#Content", + "first", + None, + false, + &key, + ); + let second = encrypted_gcm_element( + "http://www.w3.org/2001/04/xmlenc#Content", + "second", + None, + false, + &key, + ); + let document = format!( + "{first}{second}" + ); + let resolver = SymmetricKeyDecryptor::new(key); + let context = DecryptContext::new(&resolver); + let replaced = context + .decrypt_document_from_start_node(&document, Some("second")) + .expect("ancestor ID must select its encrypted descendant"); + assert!(replaced.contains("second")); + assert!(replaced.contains("{first}{second}" + ); + assert!(matches!( + context.decrypt_document_from_start_node(&ambiguous, Some("selected")), + Err(XmlEncError::AmbiguousEncryptedData) + )); + } + #[test] fn rejects_non_xml_or_malformed_document_replacement_plaintext() { // The document API must not expose binary content or return a document diff --git a/tools/xmlsec1/README.md b/tools/xmlsec1/README.md index 703d1f6..f3ffcd3 100644 --- a/tools/xmlsec1/README.md +++ b/tools/xmlsec1/README.md @@ -14,12 +14,13 @@ See the repository's [CLI compatibility guide](https://github.com/structured-wor examples, supported key formats, fail-closed behavior, and upstream runner coverage. -`--aeskey` files are raw binary key material. Decryption accepts standalone -`EncryptedData` and performs in-document replacement, optionally selected by +`--aeskey` files are raw binary key material. Decrypting a standalone +`EncryptedData` returns opaque decrypted bytes; embedded encrypted data uses +in-document replacement and supports operation-start selection with `--node-id`. Encryption retains template metadata and RSA-OAEP parameters; -PKCS#1 RSA and PKCS#8/SPKI/X.509 PEM or DER key material is normalized into the -same core signing, verification, and encryption pipelines. Signing options -validate every certificate from `key,leaf,intermediate,...` and embed the chain +PKCS#1 RSA, unencrypted PKCS#8, SPKI, and X.509 PEM or DER key material is +normalized into the same core signing, verification, and encryption pipelines. +Signing options validate every certificate from `key,leaf,intermediate,...` and embed the chain when the template provides a `KeyInfo` placeholder; verification accepts stdin as `-` and can select one signature subtree with `--node-id`. Output paths support the upstream `{inputfile}` basename template, and `--gen-key[:name]` diff --git a/tools/xmlsec1/src/commands.rs b/tools/xmlsec1/src/commands.rs index cd4cac4..15283b4 100644 --- a/tools/xmlsec1/src/commands.rs +++ b/tools/xmlsec1/src/commands.rs @@ -13,7 +13,7 @@ use xml_sec::{ xmldsig::{ DefaultKeyResolver, DsigStatus, KeyInfoWriter, KeyResolver, KeyResolverConfig, SignContext, SignatureAlgorithm, UriTypeSet, VerifyContext, X509CertificateKeyInfoWriter, - XPathHereSemantics, parse_key_info, + XPathHereSemantics, parse_key_info, uri::UriReferenceResolver, }, xmlenc::{ DataEncryptionAlgorithm, DecryptContext, DecryptedContent, DecryptionKeyResolver, @@ -40,6 +40,78 @@ const GENERIC_OPTIONS: &[&str] = &[ "print-xml-debug", "help", ]; +const SIGN_OPTIONS: &[&str] = &[ + "output", + "privkey-pem", + "privkey-der", + "pkcs8-pem", + "pkcs8-der", + "pwd", + "lax-key-search", + "node-id", + "node-name", + "node-xpath", + "id-attr", + "add-id-attr", +]; +const VERIFY_OPTIONS: &[&str] = &[ + "pubkey-pem", + "pubkey-der", + "pubkey-cert-pem", + "pubkey-cert-der", + "trusted-pem", + "trusted-der", + "untrusted-pem", + "untrusted-der", + "enabled-reference-uris", + "enabled-retrieval-uris", + "ignore-manifests", + "lax-key-search", + "verify-crls", + "X509-skip-time-checks", + "X509-skip-strict-checks", + "insecure", + "verification-time", + "depth", + "node-id", + "node-name", + "node-xpath", + "id-attr", + "add-id-attr", + "url-map", +]; +const ENCRYPT_OPTIONS: &[&str] = &[ + "output", + "binary-data", + "xml-data", + "aes-key", + "pubkey-pem", + "pubkey-der", + "pubkey-cert-pem", + "pubkey-cert-der", + "lax-key-search", + "node-id", + "node-name", + "node-xpath", + "id-attr", + "add-id-attr", +]; +const DECRYPT_OPTIONS: &[&str] = &[ + "output", + "aes-key", + "privkey-pem", + "privkey-der", + "pkcs8-pem", + "pkcs8-der", + "pwd", + "lax-key-search", + "node-id", + "node-name", + "node-xpath", + "id-attr", + "add-id-attr", +]; +const KEYS_OPTIONS: &[&str] = &["gen-key"]; const XMLDSIG_NS: &str = "http://www.w3.org/2000/09/xmldsig#"; const XMLENC_NS: &str = "http://www.w3.org/2001/04/xmlenc#"; const XMLENC11_NS: &str = "http://www.w3.org/2009/xmlenc11#"; @@ -81,16 +153,20 @@ pub fn execute( _stderr: &mut dyn Write, ) -> Result<(), CommandError> { if invocation.flag("help") { - return help(stdout); + return command_help(invocation.command, stdout); } validate_provider(&invocation)?; validate_crypto_config(&invocation)?; match invocation.command { Command::Help => help(stdout), Command::HelpAll => help_all(stdout), - Command::HelpDsig | Command::HelpEnc | Command::HelpKeys | Command::HelpX509 => { - help(stdout) - } + Command::HelpDsig => topic_help(&[Command::Sign, Command::Verify], stdout), + Command::HelpEnc => topic_help(&[Command::Encrypt, Command::Decrypt], stdout), + Command::HelpKeys => topic_help( + &[Command::Keys, Command::ListKeyData, Command::CheckKeyData], + stdout, + ), + Command::HelpX509 => topic_help(&[Command::Verify], stdout), Command::Version => writeln!(stdout, "xmlsec1 1.3.13 (rustcrypto)").map_err(stdout_error), Command::ListTransforms => { validate_options(&invocation, &[])?; @@ -160,6 +236,57 @@ fn help_all(output: &mut dyn Write) -> Result<(), CommandError> { Ok(()) } +fn command_help(command: Command, output: &mut dyn Write) -> Result<(), CommandError> { + let Some((name, options)) = command_contract(command) else { + return help(output); + }; + writeln!(output, "Usage: xmlsec1 {name} [options] [files]").map_err(stdout_error)?; + writeln!(output, "Options:").map_err(stdout_error)?; + for option in GENERIC_OPTIONS.iter().chain(options) { + let spec = OPTION_SPECS + .iter() + .find(|spec| spec.canonical == *option) + .expect("command option must exist in OPTION_SPECS"); + let parameter = if spec.accepts_parameter { + "[:name]" + } else { + "" + }; + let value = if matches!(spec.arity, Arity::Value) { + " " + } else { + "" + }; + writeln!(output, " --{}{parameter}{value}", spec.canonical).map_err(stdout_error)?; + } + Ok(()) +} + +fn topic_help(commands: &[Command], output: &mut dyn Write) -> Result<(), CommandError> { + for (index, command) in commands.iter().copied().enumerate() { + if index != 0 { + writeln!(output).map_err(stdout_error)?; + } + command_help(command, output)?; + } + Ok(()) +} + +fn command_contract(command: Command) -> Option<(&'static str, &'static [&'static str])> { + Some(match command { + Command::Sign => ("sign", SIGN_OPTIONS), + Command::Verify => ("verify", VERIFY_OPTIONS), + Command::Encrypt => ("encrypt", ENCRYPT_OPTIONS), + Command::Decrypt => ("decrypt", DECRYPT_OPTIONS), + Command::Keys => ("keys", KEYS_OPTIONS), + Command::ListKeyData => ("list-key-data", &[]), + Command::CheckKeyData => ("check-key-data", &[]), + Command::ListTransforms => ("list-transforms", &[]), + Command::CheckTransforms => ("check-transforms", &[]), + _ => return None, + }) +} + fn validate_provider(invocation: &Invocation) -> Result<(), CommandError> { if let Some(provider) = option_text(invocation, "crypto")? && !matches!(provider, "rustcrypto" | "default") @@ -328,30 +455,15 @@ fn option_value_text(option: &crate::OptionValue) -> Result<&str, CommandError> } fn sign(invocation: &Invocation, stdout: &mut dyn Write) -> Result<(), CommandError> { - validate_options( - invocation, - &[ - "output", - "privkey-pem", - "privkey-der", - "pkcs8-pem", - "pkcs8-der", - "pwd", - "lax-key-search", - "node-id", - "node-name", - "node-xpath", - "id-attr", - "add-id-attr", - ], - )?; - reject_unimplemented_selectors(invocation, &[])?; + validate_options(invocation, SIGN_OPTIONS)?; + reject_unimplemented_selectors(invocation, &["node-id"])?; if invocation.last_value("pwd").is_some() { return Err(CommandError::UnsupportedOption("pwd".into())); } let policy = SigningPolicy::default(); let xml = read_input(invocation, policy.resources.max_xml_document_bytes)?; - let signature = key_material::signing_signature_metadata(&xml, &policy)?; + let start_node_id = option_text(invocation, "node-id")?; + let signature = key_material::signing_signature_metadata(&xml, start_node_id, &policy)?; let (key_option, certificate_is_der) = select_signing_key(invocation, signature.key_name.as_deref())?; let value = key_option.value.as_deref().unwrap_or_default(); @@ -378,6 +490,9 @@ fn sign(invocation: &Invocation, stdout: &mut dyn Write) -> Result<(), CommandEr ) }; let mut context = SignContext::new(key.as_ref()).policy(policy); + if let Some(id) = start_node_id { + context = context.start_node_id(id); + } if let Some(writer) = &writer { if signature.has_key_info { context = context.key_info_writer(writer); @@ -482,38 +597,7 @@ fn xmlsec_compatibility_verification_policy(invocation: &Invocation) -> Verifica } fn verify(invocation: &Invocation, stdout: &mut dyn Write) -> Result<(), CommandError> { - validate_options( - invocation, - &[ - "pubkey-pem", - "pubkey-der", - "pubkey-cert-pem", - "pubkey-cert-der", - "trusted-pem", - "trusted-der", - "untrusted-pem", - "untrusted-der", - "enabled-reference-uris", - "enabled-retrieval-uris", - "ignore-manifests", - "lax-key-search", - "verify-crls", - "X509-skip-time-checks", - // libxmlsec uses this to relax provider security levels for legacy - // certificate signatures. RustCrypto has no provider strict mode and - // already verifies every certificate signature algorithm it implements. - "X509-skip-strict-checks", - "insecure", - "verification-time", - "depth", - "node-id", - "node-name", - "node-xpath", - "id-attr", - "add-id-attr", - "url-map", - ], - )?; + validate_options(invocation, VERIFY_OPTIONS)?; reject_unimplemented_selectors(invocation, &["node-id"])?; reject_unimplemented_verification_policy(invocation)?; let direct_keys = ["pubkey-pem", "pubkey-der"] @@ -673,24 +757,8 @@ fn verify_with_explicit_certificate( } fn encrypt(invocation: &Invocation, stdout: &mut dyn Write) -> Result<(), CommandError> { - validate_options( - invocation, - &[ - "output", - "binary-data", - "xml-data", - "aes-key", - "pubkey-pem", - "pubkey-der", - "lax-key-search", - "node-id", - "node-name", - "node-xpath", - "id-attr", - "add-id-attr", - ], - )?; - reject_unimplemented_selectors(invocation, &[])?; + validate_options(invocation, ENCRYPT_OPTIONS)?; + reject_unimplemented_selectors(invocation, &["node-id"])?; let has_binary_data = invocation.last_value("binary-data").is_some(); let has_xml_data = invocation.last_value("xml-data").is_some(); if has_binary_data == has_xml_data { @@ -702,16 +770,26 @@ fn encrypt(invocation: &Invocation, stdout: &mut dyn Write) -> Result<(), Comman let maximum_document_bytes = policy.resources.max_xml_document_bytes; let maximum_plaintext_bytes = policy.resources.max_encryption_plaintext_bytes; let template = read_input(invocation, policy.resources.max_xml_document_bytes)?; - let metadata = encryption_template(&template, &policy)?; + let start_node_id = option_text(invocation, "node-id")?; + let metadata = encryption_template(&template, start_node_id, &policy)?; let algorithm = metadata.algorithm; let encrypted_type = metadata.encrypted_type; let explicit_encrypted_type = metadata.explicit_encrypted_type; let mut builder = EncryptedDataBuilder::new(algorithm).policy(policy.clone()); let aes_keys = invocation.values("aes-key").collect::>(); - let public_keys = ["pubkey-pem", "pubkey-der"] - .into_iter() - .flat_map(|name| invocation.values(name)) - .collect::>(); + let public_keys = [ + ("pubkey-pem", false), + ("pubkey-der", false), + ("pubkey-cert-pem", true), + ("pubkey-cert-der", true), + ] + .into_iter() + .flat_map(|(name, certificate)| { + invocation + .values(name) + .map(move |option| (option, certificate)) + }) + .collect::>(); if aes_keys.len() + public_keys.len() > 1 { return Err(CommandError::Usage( "encrypt accepts exactly one AES key or RSA public key".into(), @@ -732,7 +810,7 @@ fn encrypt(invocation: &Invocation, stdout: &mut dyn Write) -> Result<(), Comman if let Some(name) = option.parameter.as_deref() { builder = builder.direct_key_name(name); } - } else if let [option] = public_keys.as_slice() { + } else if let [(option, certificate)] = public_keys.as_slice() { enforce_named_key_match( option, metadata.recipient_key_name.as_deref(), @@ -740,14 +818,19 @@ fn encrypt(invocation: &Invocation, stdout: &mut dyn Write) -> Result<(), Comman "RSA recipient key", )?; let path = option.value.as_deref().unwrap_or_default(); - let mut recipient = EncryptionRecipient::rsa_oaep(key_material::load_rsa_public(path)?); + let public_key = if *certificate { + key_material::load_rsa_certificate_public(path)? + } else { + key_material::load_rsa_public(path)? + }; + let mut recipient = EncryptionRecipient::rsa_oaep(public_key); if let Some(parameters) = metadata.oaep_parameters { recipient = recipient.oaep_parameters(parameters); } builder = builder.add_recipient(recipient); } else { return Err(CommandError::Usage( - "encrypt requires --aes-key or --pubkey-pem".into(), + "encrypt requires --aes-key, an RSA public key, or an RSA certificate".into(), )); } builder = builder.encryption_type(encrypted_type); @@ -769,7 +852,12 @@ fn encrypt(invocation: &Invocation, stdout: &mut dyn Write) -> Result<(), Comman )); } .map_err(|error| CommandError::Encryption(error.to_string()))?; - let rendered = apply_encryption_template(&template, &result.encrypted_data_xml, &policy)?; + let rendered = apply_encryption_template( + &template, + &result.encrypted_data_xml, + start_node_id, + &policy, + )?; if rendered.len() > maximum_document_bytes { return Err(CommandError::Encryption( "encrypted template output exceeds XML document policy".into(), @@ -859,11 +947,12 @@ fn oaep_mgf_from_uri(uri: &str) -> Result { fn apply_encryption_template( template: &str, generated: &str, + start_node_id: Option<&str>, policy: &EncryptionPolicy, ) -> Result { let template_document = parse_encryption_document(template, policy)?; let generated_document = parse_encryption_document(generated, policy)?; - let template_data = select_encrypted_data(&template_document, None)?; + let template_data = select_encrypted_data(&template_document, start_node_id)?; let generated_data = generated_document.root_element(); let template_cipher = encrypted_data_cipher_value(template_data) .ok_or_else(|| CommandError::Encryption("template has no CipherValue".into()))?; @@ -978,24 +1067,7 @@ fn encrypted_data_cipher_value<'a, 'input>( } fn decrypt(invocation: &Invocation, stdout: &mut dyn Write) -> Result<(), CommandError> { - validate_options( - invocation, - &[ - "output", - "aes-key", - "privkey-pem", - "privkey-der", - "pkcs8-pem", - "pkcs8-der", - "pwd", - "lax-key-search", - "node-id", - "node-name", - "node-xpath", - "id-attr", - "add-id-attr", - ], - )?; + validate_options(invocation, DECRYPT_OPTIONS)?; reject_unimplemented_selectors(invocation, &["node-id"])?; if invocation.last_value("pwd").is_some() { return Err(CommandError::UnsupportedOption("pwd".into())); @@ -1073,7 +1145,7 @@ fn decrypt_input( .map_err(|error| CommandError::Encryption(error.to_string())); } context - .decrypt_document(xml, encrypted_data_id) + .decrypt_document_from_start_node(xml, encrypted_data_id) .map(String::into_bytes) .map_err(|error| CommandError::Encryption(error.to_string())) } @@ -1089,10 +1161,11 @@ struct EncryptionTemplateMetadata { fn encryption_template( xml: &str, + start_node_id: Option<&str>, policy: &EncryptionPolicy, ) -> Result { let document = parse_encryption_document(xml, policy)?; - let encrypted_data = select_encrypted_data(&document, None)?; + let encrypted_data = select_encrypted_data(&document, start_node_id)?; let method = encrypted_data .children() .find(|node| node.has_tag_name((XMLENC_NS, "EncryptionMethod"))) @@ -1157,14 +1230,22 @@ fn parse_encryption_document<'a>( .map_err(|error| CommandError::Encryption(error.to_string())) } -fn select_encrypted_data<'a, 'input>( - document: &'a Document<'input>, - id: Option<&str>, -) -> Result, CommandError> { - let mut matches = document.descendants().filter(|node| { - node.has_tag_name((XMLENC_NS, "EncryptedData")) - && id.is_none_or(|id| node.attribute("Id") == Some(id)) - }); +fn select_encrypted_data<'a>( + document: &'a Document<'a>, + start_node_id: Option<&str>, +) -> Result, CommandError> { + let start = if let Some(id) = start_node_id { + UriReferenceResolver::new(document) + .node_for_id(id) + .ok_or_else(|| { + CommandError::Encryption(format!("selected node ID is missing or ambiguous: {id}")) + })? + } else { + document.root() + }; + let mut matches = start + .descendants() + .filter(|node| node.has_tag_name((XMLENC_NS, "EncryptedData"))); let selected = matches .next() .ok_or_else(|| CommandError::Encryption("document has no EncryptedData".into()))?; @@ -1203,7 +1284,7 @@ fn enforce_named_key_match( } fn keys(invocation: &Invocation, stdout: &mut dyn Write) -> Result<(), CommandError> { - validate_options(invocation, &["gen-key"])?; + validate_options(invocation, KEYS_OPTIONS)?; let generated = invocation.values("gen-key").collect::>(); if generated.is_empty() { return Err(CommandError::Usage( @@ -1374,7 +1455,23 @@ mod tests { &mut Vec::new(), ) .unwrap(); - assert!(String::from_utf8(output).unwrap().starts_with("Usage:")); + let help = String::from_utf8(output).unwrap(); + assert!(help.starts_with("Usage: xmlsec1 verify")); + assert!(help.contains("--pubkey-cert-pem")); + assert!(!help.contains("--binary-data")); + + let mut topic = Vec::new(); + execute( + invocation(&["xmlsec1", "help-enc"]), + &mut topic, + &mut Vec::new(), + ) + .unwrap(); + let topic = String::from_utf8(topic).unwrap(); + assert!(topic.contains("Usage: xmlsec1 encrypt")); + assert!(topic.contains("Usage: xmlsec1 decrypt")); + assert!(topic.contains("--binary-data")); + assert!(topic.contains("--privkey-pem")); let error = execute( invocation(&["xmlsec1", "verify", "--lax-key-search", "input.xml"]), @@ -1437,7 +1534,8 @@ mod tests { ); let rendered = - apply_encryption_template(&template, &generated, &EncryptionPolicy::default()).unwrap(); + apply_encryption_template(&template, &generated, None, &EncryptionPolicy::default()) + .unwrap(); let document = Document::parse(&rendered) .expect("injected KeyInfo prefixes must remain namespace-bound"); assert!( @@ -1483,7 +1581,7 @@ mod tests { } xml.push_str(""); - let error = match encryption_template(&xml, &EncryptionPolicy::default()) { + let error = match encryption_template(&xml, None, &EncryptionPolicy::default()) { Ok(_) => panic!("over-budget template must fail"), Err(error) => error, }; diff --git a/tools/xmlsec1/src/key_material.rs b/tools/xmlsec1/src/key_material.rs index f5f62a7..3a9358c 100644 --- a/tools/xmlsec1/src/key_material.rs +++ b/tools/xmlsec1/src/key_material.rs @@ -108,6 +108,7 @@ pub fn verification_signature_metadata( pub fn signing_signature_metadata( xml: &str, + start_node_id: Option<&str>, policy: &SigningPolicy, ) -> Result { policy.validate()?; @@ -116,7 +117,20 @@ pub fn signing_signature_metadata( policy.xml.allow_internal_dtd, policy.resources.max_xml_nodes, )?; - let signature = find_signature_node(&document).ok_or(KeyMaterialError::MissingSignedInfo)?; + let signature = match start_node_id { + Some(id) => { + let start = UriReferenceResolver::new(&document) + .node_for_id(id) + .ok_or_else(|| KeyMaterialError::SelectedNodeUnavailable(id.to_owned()))?; + start + .descendants() + .find(|node| node.has_tag_name(("http://www.w3.org/2000/09/xmldsig#", "Signature"))) + } + None => document + .descendants() + .rfind(|node| node.has_tag_name(("http://www.w3.org/2000/09/xmldsig#", "Signature"))), + } + .ok_or(KeyMaterialError::MissingSignedInfo)?; Ok(SigningTemplateMetadata { key_name: signature_key_name(signature), has_key_info: signature_key_info(signature).is_some(), @@ -306,6 +320,17 @@ pub fn load_rsa_public(path: impl AsRef) -> Result, +) -> Result { + let path = path.as_ref(); + let der = load_certificate(path)?; + let (_, certificate) = x509_parser::certificate::X509Certificate::from_der(&der) + .map_err(|_| KeyMaterialError::InvalidCertificate(path.to_owned()))?; + RsaPublicKey::from_public_key_der(certificate.public_key().raw) + .map_err(|_| KeyMaterialError::UnsupportedPublicKey(path.to_owned())) +} + pub fn load_symmetric( path: impl AsRef, expected: Option, diff --git a/tools/xmlsec1/tests/process_contract.rs b/tools/xmlsec1/tests/process_contract.rs index 178974f..cf1abea 100644 --- a/tools/xmlsec1/tests/process_contract.rs +++ b/tools/xmlsec1/tests/process_contract.rs @@ -386,6 +386,87 @@ fn verification_node_id_selects_one_signature_subtree() { assert!(String::from_utf8_lossy(&ambiguous.stderr).contains("ambiguous")); } +#[test] +fn signing_node_id_selects_one_signature_subtree() { + // The donor treats --node-id as an operation start node. Signing must fill + // only the Signature below that node while preserving unrelated templates. + let temp = tempfile::tempdir().unwrap(); + let original = fs::read_to_string( + project_root() + .join("tests/fixtures/xmldsig/aleksey-xmldsig-01/enveloping-sha256-rsa-sha256.tmpl"), + ) + .unwrap(); + let body = original + .split_once("?>") + .map_or(original.as_str(), |(_, body)| body); + let template = temp.path().join("multiple-templates.xml"); + let first = body + .replace("#object", "#first-object") + .replace("Id=\"object\"", "Id=\"first-object\""); + let second = body + .replace("#object", "#second-object") + .replace("Id=\"object\"", "Id=\"second-object\""); + fs::write( + &template, + format!( + "{first}{second}" + ), + ) + .unwrap(); + let private_key = project_root().join("tests/fixtures/keys/rsa/rsa-4096-key.pem"); + + let output = Command::new(binary()) + .args(["sign", "--privkey-pem"]) + .arg(&private_key) + .args(["--node-id", "first"]) + .arg(&template) + .output() + .unwrap(); + + assert!( + output.status.success(), + "{}", + String::from_utf8_lossy(&output.stderr) + ); + let signed = String::from_utf8(output.stdout).unwrap(); + let document = roxmltree::Document::parse(&signed).unwrap(); + let values = document + .descendants() + .filter(|node| node.has_tag_name(("http://www.w3.org/2000/09/xmldsig#", "SignatureValue"))) + .map(|node| node.text().unwrap_or_default()) + .collect::>(); + assert!(!values[0].trim().is_empty()); + assert!(values[1].trim().is_empty()); + + let missing = Command::new(binary()) + .args(["sign", "--privkey-pem"]) + .arg(&private_key) + .args(["--node-id", "missing"]) + .arg(&template) + .output() + .unwrap(); + assert!(!missing.status.success()); + assert!(String::from_utf8_lossy(&missing.stderr).contains("missing or ambiguous")); + + let duplicate = temp.path().join("duplicate-start.xml"); + fs::write( + &duplicate, + fs::read_to_string(&template) + .unwrap() + .replace("Id=\"second\"", "Id=\"first\""), + ) + .unwrap(); + let ambiguous = Command::new(binary()) + .args(["sign", "--privkey-pem"]) + .arg(&private_key) + .args(["--node-id", "first"]) + .arg(&duplicate) + .output() + .unwrap(); + assert!(!ambiguous.status.success()); + assert!(String::from_utf8_lossy(&ambiguous.stderr).contains("missing or ambiguous")); +} + #[test] fn output_template_expands_the_extensionless_input_basename() { // libxmlsec1 automation uses one output template across many input files; @@ -540,6 +621,89 @@ fn encryption_preserves_template_metadata_and_supports_id_selection() { ); } +#[test] +fn encryption_node_id_selects_one_template_subtree() { + // --node-id selects the operation start node, not EncryptedData/@Id. Both + // template inspection and replacement must stay within that subtree. + let temp = tempfile::tempdir().unwrap(); + let template = temp.path().join("multiple-templates.xml"); + let plaintext = temp.path().join("plaintext.bin"); + let key = temp.path().join("key.bin"); + let encrypted_data = |id: &str| { + format!( + r#""# + ) + }; + fs::write( + &template, + format!( + "{}{}", + encrypted_data("first-template"), + encrypted_data("second-template") + ), + ) + .unwrap(); + fs::write(&plaintext, b"selected payload").unwrap(); + fs::write(&key, b"0123456789abcdef").unwrap(); + + let output = Command::new(binary()) + .args(["encrypt", "--aes-key"]) + .arg(&key) + .args(["--binary-data"]) + .arg(&plaintext) + .args(["--node-id", "first"]) + .arg(&template) + .output() + .unwrap(); + + assert!( + output.status.success(), + "{}", + String::from_utf8_lossy(&output.stderr) + ); + let encrypted = String::from_utf8(output.stdout).unwrap(); + let document = roxmltree::Document::parse(&encrypted).unwrap(); + let values = document + .descendants() + .filter(|node| node.has_tag_name(("http://www.w3.org/2001/04/xmlenc#", "CipherValue"))) + .map(|node| node.text().unwrap_or_default()) + .collect::>(); + assert!(!values[0].trim().is_empty()); + assert!(values[1].trim().is_empty()); + + let missing = Command::new(binary()) + .args(["encrypt", "--aes-key"]) + .arg(&key) + .args(["--binary-data"]) + .arg(&plaintext) + .args(["--node-id", "missing"]) + .arg(&template) + .output() + .unwrap(); + assert!(!missing.status.success()); + assert!(String::from_utf8_lossy(&missing.stderr).contains("missing or ambiguous")); + + let duplicate = temp.path().join("duplicate-start.xml"); + fs::write( + &duplicate, + fs::read_to_string(&template) + .unwrap() + .replace("Id=\"second\"", "Id=\"first\""), + ) + .unwrap(); + let ambiguous = Command::new(binary()) + .args(["encrypt", "--aes-key"]) + .arg(&key) + .args(["--binary-data"]) + .arg(&plaintext) + .args(["--node-id", "first"]) + .arg(&duplicate) + .output() + .unwrap(); + assert!(!ambiguous.status.success()); + assert!(String::from_utf8_lossy(&ambiguous.stderr).contains("missing or ambiguous")); +} + #[test] fn binary_encryption_rejects_xml_typed_templates() { // Binary payloads cannot truthfully carry the XML Element or Content type; @@ -749,6 +913,63 @@ fn standalone_binary_decryption_accepts_its_root_node_id() { assert!(!missing.status.success()); } +#[test] +fn embedded_decryption_node_id_selects_an_operation_subtree() { + // The selected ID belongs to an ancestor operation node; EncryptedData is + // intentionally anonymous, matching libxmlsec1's start-node contract. + let temp = tempfile::tempdir().unwrap(); + let template = temp.path().join("template.xml"); + let plaintext = temp.path().join("plaintext.xml"); + let encrypted = temp.path().join("encrypted.xml"); + let document = temp.path().join("document.xml"); + let key = temp.path().join("key.bin"); + fs::write( + &template, + r#""#, + ) + .unwrap(); + fs::write(&plaintext, b"selected subtree").unwrap(); + fs::write(&key, b"0123456789abcdef").unwrap(); + let encryption = Command::new(binary()) + .args(["encrypt", "--aes-key"]) + .arg(&key) + .args(["--xml-data"]) + .arg(&plaintext) + .args(["--output"]) + .arg(&encrypted) + .arg(&template) + .output() + .unwrap(); + assert!(encryption.status.success()); + fs::write( + &document, + format!( + "{}", + fs::read_to_string(&encrypted).unwrap() + ), + ) + .unwrap(); + + let output = Command::new(binary()) + .args(["decrypt", "--aes-key"]) + .arg(&key) + .args(["--node-id", "selected"]) + .arg(&document) + .output() + .unwrap(); + + assert!( + output.status.success(), + "{}", + String::from_utf8_lossy(&output.stderr) + ); + assert!( + String::from_utf8(output.stdout) + .unwrap() + .contains("selected subtree") + ); +} + #[test] fn rsa_recipient_name_must_match_the_template_unless_lax() { // A named RSA key selects the nested EncryptedKey recipient identity, not @@ -886,6 +1107,62 @@ fn encrypts_and_decrypts_with_an_rsa_oaep_recipient() { assert_eq!(decrypt.stdout, fs::read(&plaintext).unwrap()); } +#[test] +fn encrypts_with_rsa_recipient_certificates() { + // Donor certificate options extract the recipient public key from either + // PEM or DER X.509 input and feed the same RSA-OAEP encryption path. + let temp = tempfile::tempdir().unwrap(); + let template = temp.path().join("template.xml"); + let plaintext = temp.path().join("plaintext.bin"); + let private_key = project_root().join("tests/fixtures/keys/rsa/rsa-4096-key.pem"); + let certificate_pem = project_root().join("tests/fixtures/keys/rsa/rsa-4096-cert.pem"); + let certificate_der = temp.path().join("certificate.der"); + let pem = fs::read(&certificate_pem).unwrap(); + let (_, certificate) = x509_parser::pem::parse_x509_pem(&pem).unwrap(); + fs::write(&certificate_der, certificate.contents).unwrap(); + fs::write( + &template, + r#""#, + ) + .unwrap(); + fs::write(&plaintext, b"certificate recipient").unwrap(); + + for (option, certificate) in [ + ("--pubkey-cert-pem", certificate_pem.as_path()), + ("--pubkey-cert-der", certificate_der.as_path()), + ] { + let encrypted = temp.path().join(format!("{option}.xml")); + let encryption = Command::new(binary()) + .args(["encrypt", option]) + .arg(certificate) + .args(["--binary-data"]) + .arg(&plaintext) + .args(["--output"]) + .arg(&encrypted) + .arg(&template) + .output() + .unwrap(); + assert!( + encryption.status.success(), + "{option}: {}", + String::from_utf8_lossy(&encryption.stderr) + ); + + let decryption = Command::new(binary()) + .args(["decrypt", "--privkey-pem"]) + .arg(&private_key) + .arg(&encrypted) + .output() + .unwrap(); + assert!( + decryption.status.success(), + "{option}: {}", + String::from_utf8_lossy(&decryption.stderr) + ); + assert_eq!(decryption.stdout, b"certificate recipient"); + } +} + #[test] fn rsa_decryption_accepts_private_key_certificate_companions() { // libxmlsec private-key options permit certificate companions after the From 468e33dedfef286ba594ff627a96cb8fe3dccca0 Mon Sep 17 00:00:00 2001 From: Dmitry Prudnikov Date: Fri, 14 Aug 2026 20:14:23 +0300 Subject: [PATCH 12/27] fix(cli): select encrypted recipients by name - Match named RSA keys against every encrypted recipient - Share duplicate-safe XML ID indexing across feature boundaries - Cover later-recipient selection and standalone XMLEnc builds --- src/xml.rs | 95 ++++++++++++++++++++++++- src/xmldsig/uri.rs | 78 +++----------------- src/xmlenc/decrypt.rs | 14 ++-- tools/xmlsec1/src/commands.rs | 76 ++++++++++++++++---- tools/xmlsec1/tests/process_contract.rs | 44 +++++++++++- 5 files changed, 217 insertions(+), 90 deletions(-) diff --git a/src/xml.rs b/src/xml.rs index 062835a..3039d25 100644 --- a/src/xml.rs +++ b/src/xml.rs @@ -1,5 +1,81 @@ //! Shared XML lexical invariants used before serialization. +use std::collections::{HashMap, HashSet, hash_map::Entry}; + +use roxmltree::{Document, Node}; + +#[cfg(feature = "xmldsig")] +use roxmltree::NodeId; + +/// Default ID attribute names shared by XMLDSig and XMLEnc selection. +const DEFAULT_ID_ATTRS: &[&str] = &["ID", "Id", "id"]; + +/// Duplicate-safe index of XML ID attributes in one parsed document. +pub(crate) struct XmlIdIndex<'a> { + nodes: HashMap<&'a str, Node<'a, 'a>>, +} + +impl<'a> XmlIdIndex<'a> { + /// Index the standard `ID`, `Id`, and `id` spellings. + #[cfg(any(feature = "xmlenc", test))] + pub(crate) fn new(document: &'a Document<'a>) -> Self { + Self::with_extra_attrs(document, &[]) + } + + /// Index standard ID spellings plus caller-declared local attribute names. + pub(crate) fn with_extra_attrs(document: &'a Document<'a>, extra_attrs: &[&str]) -> Self { + let mut names = DEFAULT_ID_ATTRS.to_vec(); + for name in extra_attrs { + if !names.contains(name) { + names.push(name); + } + } + + let mut nodes = HashMap::new(); + let mut duplicates = HashSet::new(); + for node in document.descendants().filter(Node::is_element) { + for name in &names { + let Some(value) = node.attribute(*name) else { + continue; + }; + if duplicates.contains(value) { + continue; + } + match nodes.entry(value) { + Entry::Vacant(entry) => { + entry.insert(node); + } + Entry::Occupied(entry) if entry.get().id() != node.id() => { + entry.remove(); + duplicates.insert(value); + } + Entry::Occupied(_) => {} + } + } + } + Self { nodes } + } + + #[cfg(feature = "xmldsig")] + pub(crate) fn contains(&self, id: &str) -> bool { + self.nodes.contains_key(id) + } + + #[cfg(feature = "xmldsig")] + pub(crate) fn node_id(&self, id: &str) -> Option { + self.nodes.get(id).map(Node::id) + } + + pub(crate) fn node(&self, id: &str) -> Option> { + self.nodes.get(id).copied() + } + + #[cfg(feature = "xmldsig")] + pub(crate) fn len(&self) -> usize { + self.nodes.len() + } +} + /// Return whether a Unicode scalar is permitted by XML 1.0 Fifth Edition [2]. pub(crate) fn is_xml_1_0_character(character: char) -> bool { // Rust `char` cannot represent the surrogate range between D7FF and E000, @@ -29,7 +105,9 @@ pub(crate) fn is_xml_ncname(value: &str) -> bool { #[cfg(test)] mod tests { - use super::{is_xml_1_0_character, is_xml_ncname}; + use roxmltree::Document; + + use super::{XmlIdIndex, is_xml_1_0_character, is_xml_ncname}; #[test] fn xml_1_0_character_boundaries_match_production_two() { @@ -63,4 +141,19 @@ mod tests { assert!(!is_xml_ncname(invalid), "{invalid:?}"); } } + + #[test] + fn id_index_rejects_duplicate_values_but_not_duplicate_attributes_on_one_node() { + // Ambiguous IDs must fail closed across every consumer, while one node + // carrying equivalent ID spellings still denotes one stable target. + let document = Document::parse( + r#""#, + ) + .expect("ID index fixture must be valid XML"); + let index = XmlIdIndex::new(&document); + + assert!(index.contains("same")); + assert!(!index.contains("duplicate")); + assert_eq!(index.len(), 1); + } } diff --git a/src/xmldsig/uri.rs b/src/xmldsig/uri.rs index c02b50e..9e90b4a 100644 --- a/src/xmldsig/uri.rs +++ b/src/xmldsig/uri.rs @@ -13,25 +13,17 @@ //! module never performs network or filesystem I/O. use std::cell::Cell; -use std::collections::hash_map::Entry; -use std::collections::{HashMap, HashSet}; +use std::collections::HashMap; use roxmltree::{Document, Node, NodeId}; use crate::c14n::xml_base::{ XmlBaseResolutionBudget, XmlBaseResolutionError, resolve_uri_from_node_with_budget, }; +use crate::xml::XmlIdIndex; use super::types::{NodeSet, NodeSetMaterializationBudget, TransformData, TransformError}; -/// Default ID attribute names to scan when building the ID index. -/// -/// These cover the most common conventions: -/// - `ID` — SAML 2.0 (``) -/// - `Id` — XMLDSig (``) -/// - `id` — general XML -const DEFAULT_ID_ATTRS: &[&str] = &["ID", "Id", "id"]; - struct ExternalResourceBudget { remaining_total_bytes: Cell, max_resource_bytes: usize, @@ -101,8 +93,7 @@ impl ExternalResourceBudget { /// ``` pub struct UriReferenceResolver<'a> { doc: &'a Document<'a>, - /// ID → element node mapping for O(1) fragment lookups. - id_map: HashMap<&'a str, Node<'a, 'a>>, + id_index: XmlIdIndex<'a>, external_resources: Option<&'a HashMap>>, external_resource_budget: ExternalResourceBudget, } @@ -110,7 +101,7 @@ pub struct UriReferenceResolver<'a> { impl<'a> UriReferenceResolver<'a> { /// Build a resolver with default ID attribute names (`ID`, `Id`, `id`). pub fn new(doc: &'a Document<'a>) -> Self { - Self::with_id_attrs(doc, DEFAULT_ID_ATTRS) + Self::with_id_attrs(doc, &[]) } /// Build a resolver scanning additional ID attribute names beyond the defaults. @@ -124,56 +115,9 @@ impl<'a> UriReferenceResolver<'a> { /// simply `Id` by `roxmltree`, so callers **must** pass `"Id"`, not /// `"wsu:Id"` or `"{namespace}Id"`. pub fn with_id_attrs(doc: &'a Document<'a>, extra_attrs: &[&str]) -> Self { - let mut id_map = HashMap::new(); - // Track IDs seen more than once so they are never reinserted - // after being removed (handles 3+ occurrences correctly). - let mut duplicate_ids: HashSet<&'a str> = HashSet::new(); - - // Merge default + extra attribute names, dedup - let mut attr_names: Vec<&str> = DEFAULT_ID_ATTRS.to_vec(); - for name in extra_attrs { - if !attr_names.contains(name) { - attr_names.push(name); - } - } - - // Scan all elements for ID attributes - for node in doc.descendants() { - if node.is_element() { - for attr_name in &attr_names { - if let Some(value) = node.attribute(*attr_name) { - // Skip IDs already marked as duplicate - if duplicate_ids.contains(value) { - continue; - } - - // Duplicate IDs are invalid per XML spec and can enable - // signature-wrapping attacks. Remove the entry so that - // lookups for ambiguous IDs fail with ElementNotFound - // rather than silently picking an arbitrary node. - match id_map.entry(value) { - Entry::Vacant(v) => { - v.insert(node); - } - Entry::Occupied(o) => { - // Only treat as duplicate if a *different* element - // maps the same ID value. The same element can - // expose the same value via multiple scanned attrs - // (e.g., both `ID="x"` and `Id="x"`). - if o.get().id() != node.id() { - o.remove(); - duplicate_ids.insert(value); - } - } - } - } - } - } - } - Self { doc, - id_map, + id_index: XmlIdIndex::with_extra_attrs(doc, extra_attrs), external_resources: None, external_resource_budget: ExternalResourceBudget::default(), } @@ -330,8 +274,8 @@ impl<'a> UriReferenceResolver<'a> { budget: Option<&NodeSetMaterializationBudget>, with_comments: bool, ) -> Result, TransformError> { - match self.id_map.get(id) { - Some(&element) => { + match self.id_index.node(id) { + Some(element) => { let nodes = if with_comments { match budget { Some(budget) => NodeSet::subtree_with_budget(element, budget)?, @@ -348,7 +292,7 @@ impl<'a> UriReferenceResolver<'a> { /// Check if an ID is registered in the resolver's index. pub fn has_id(&self, id: &str) -> bool { - self.id_map.contains_key(id) + self.id_index.contains(id) } /// Resolve a same-document ID token to a stable node identity. @@ -356,7 +300,7 @@ impl<'a> UriReferenceResolver<'a> { /// Returns `None` when the ID is absent or ambiguous (duplicate ID collision), /// matching the resolver behavior used by `dereference()`. pub(crate) fn node_id_for_id(&self, id: &str) -> Option { - self.id_map.get(id).map(|node| node.id()) + self.id_index.node_id(id) } /// Resolve an unambiguous XML ID to its element node. @@ -364,7 +308,7 @@ impl<'a> UriReferenceResolver<'a> { /// Returns `None` when the ID is absent or duplicated, matching fragment /// dereferencing and operation start-node selection. pub fn node_for_id(&self, id: &str) -> Option> { - self.id_map.get(id).copied() + self.id_index.node(id) } pub(crate) fn node_for_node_id(&self, id: NodeId) -> Option> { @@ -373,7 +317,7 @@ impl<'a> UriReferenceResolver<'a> { /// Get the number of registered IDs. pub fn id_count(&self) -> usize { - self.id_map.len() + self.id_index.len() } } diff --git a/src/xmlenc/decrypt.rs b/src/xmlenc/decrypt.rs index df2a7c9..273396f 100644 --- a/src/xmlenc/decrypt.rs +++ b/src/xmlenc/decrypt.rs @@ -16,7 +16,7 @@ use super::{ KeyTransportAlgorithm, KeyWrapAlgorithm, OaepDigestAlgorithm, RsaOaepParameters, XmlEncError, has_single_element_with_boundary_trivia, }; -use crate::xmldsig::uri::UriReferenceResolver; +use crate::xml::XmlIdIndex; #[cfg(test)] use super::parse_encrypted_data; @@ -472,13 +472,11 @@ fn decrypt_document_with_context( let document = Document::parse_with_options(xml, parsing_options())?; let start = match selector { DocumentEncryptedDataSelector::StartNodeId(Some(id)) => { - UriReferenceResolver::new(&document) - .node_for_id(id) - .ok_or_else(|| { - XmlEncError::InvalidStructure(format!( - "selected node ID is missing or ambiguous: {id}" - )) - })? + XmlIdIndex::new(&document).node(id).ok_or_else(|| { + XmlEncError::InvalidStructure(format!( + "selected node ID is missing or ambiguous: {id}" + )) + })? } DocumentEncryptedDataSelector::StartNodeId(None) | DocumentEncryptedDataSelector::EncryptedDataId(_) => document.root(), diff --git a/tools/xmlsec1/src/commands.rs b/tools/xmlsec1/src/commands.rs index 15283b4..706a8bb 100644 --- a/tools/xmlsec1/src/commands.rs +++ b/tools/xmlsec1/src/commands.rs @@ -9,7 +9,7 @@ use std::{ use roxmltree::{Document, Node, ParsingOptions}; use xml_sec::{ policy::{DecryptionPolicy, EncryptionPolicy, SigningPolicy, VerificationPolicy}, - provider::default_provider, + provider::{CryptoProvider, default_provider}, xmldsig::{ DefaultKeyResolver, DsigStatus, KeyInfoWriter, KeyResolver, KeyResolverConfig, SignContext, SignatureAlgorithm, UriTypeSet, VerifyContext, X509CertificateKeyInfoWriter, @@ -17,8 +17,9 @@ use xml_sec::{ }, xmlenc::{ DataEncryptionAlgorithm, DecryptContext, DecryptedContent, DecryptionKeyResolver, - EncryptedDataBuilder, EncryptedDataType, EncryptionRecipient, KeyTransportAlgorithm, - OaepDigestAlgorithm, PrivateKeyDecryptor, RsaOaepParameters, SymmetricKeyDecryptor, + EncryptedDataBuilder, EncryptedDataType, EncryptedKey, EncryptionRecipient, + KeyTransportAlgorithm, OaepDigestAlgorithm, PrivateKeyDecryptor, RsaOaepParameters, + SymmetricKeyDecryptor, XmlEncError, }, }; @@ -1079,7 +1080,7 @@ fn decrypt(invocation: &Invocation, stdout: &mut dyn Write) -> Result<(), Comman let encrypted_data = select_encrypted_data(&document, encrypted_data_id)?; let standalone = encrypted_data == document.root_element(); let content_key_name = encrypted_data_key_name(encrypted_data); - let recipient_key_name = encrypted_key_recipient_name(encrypted_data); + let recipient_key_names = encrypted_key_recipient_names(encrypted_data); let aes_keys = invocation.values("aes-key").collect::>(); let private_keys = ["privkey-pem", "privkey-der", "pkcs8-pem", "pkcs8-der"] .into_iter() @@ -1106,18 +1107,20 @@ fn decrypt(invocation: &Invocation, stdout: &mut dyn Write) -> Result<(), Comman policy, )? } else if let [option] = private_keys.as_slice() { - enforce_named_key_match( + let recipient_filter = named_recipient_filter( option, - recipient_key_name.as_deref(), + &recipient_key_names, invocation.flag("lax-key-search"), - "RSA recipient key", )?; let (path, certificate_paths) = split_key_and_certificates(option.value.as_deref().unwrap_or_default())?; for certificate in certificate_paths { key_material::load_certificate(certificate)?; } - let resolver = PrivateKeyDecryptor::new(key_material::load_rsa_private(path)?); + let resolver = NamedRecipientDecryptor { + inner: PrivateKeyDecryptor::new(key_material::load_rsa_private(path)?), + key_name: recipient_filter, + }; decrypt_input(&resolver, &xml, encrypted_data_id, standalone, policy)? } else { return Err(CommandError::Usage( @@ -1127,6 +1130,27 @@ fn decrypt(invocation: &Invocation, stdout: &mut dyn Write) -> Result<(), Comman write_output(invocation, &bytes, stdout) } +struct NamedRecipientDecryptor<'a> { + inner: PrivateKeyDecryptor, + key_name: Option<&'a str>, +} + +impl DecryptionKeyResolver for NamedRecipientDecryptor<'_> { + fn resolve_key( + &self, + provider: &dyn CryptoProvider, + algorithm: DataEncryptionAlgorithm, + encrypted_key: Option<&EncryptedKey>, + ) -> Result, XmlEncError> { + if let (Some(expected), Some(candidate)) = (self.key_name, encrypted_key) + && candidate.key_name.as_deref() != Some(expected) + { + return Err(XmlEncError::KeyNotFound); + } + self.inner.resolve_key(provider, algorithm, encrypted_key) + } +} + fn decrypt_input( resolver: &dyn DecryptionKeyResolver, xml: &str, @@ -1201,12 +1225,40 @@ fn encrypted_data_key_name(encrypted_data: Node<'_, '_>) -> Option { } fn encrypted_key_recipient_name(encrypted_data: Node<'_, '_>) -> Option { + encrypted_key_recipient_names(encrypted_data) + .into_iter() + .next() +} + +fn encrypted_key_recipient_names(encrypted_data: Node<'_, '_>) -> Vec { direct_child_element(encrypted_data, XMLDSIG_NS, "KeyInfo") - .and_then(|key_info| direct_child_element(key_info, XMLENC_NS, "EncryptedKey")) - .and_then(|encrypted_key| direct_child_element(encrypted_key, XMLDSIG_NS, "KeyInfo")) - .and_then(|key_info| direct_child_element(key_info, XMLDSIG_NS, "KeyName")) - .and_then(|key_name| key_name.text()) + .into_iter() + .flat_map(|key_info| key_info.children()) + .filter(|node| node.has_tag_name((XMLENC_NS, "EncryptedKey"))) + .filter_map(|encrypted_key| direct_child_element(encrypted_key, XMLDSIG_NS, "KeyInfo")) + .filter_map(|key_info| direct_child_element(key_info, XMLDSIG_NS, "KeyName")) + .filter_map(|key_name| key_name.text()) .map(str::to_owned) + .collect() +} + +fn named_recipient_filter<'a>( + option: &'a crate::OptionValue, + recipient_names: &[String], + lax_key_search: bool, +) -> Result, CommandError> { + let Some(option_name) = option.parameter.as_deref() else { + return Ok(None); + }; + if lax_key_search || recipient_names.is_empty() { + return Ok(None); + } + if recipient_names.iter().any(|name| name == option_name) { + return Ok(Some(option_name)); + } + Err(CommandError::Usage(format!( + "template recipient KeyNames do not contain named RSA recipient key {option_name}" + ))) } fn parse_encryption_document<'a>( diff --git a/tools/xmlsec1/tests/process_contract.rs b/tools/xmlsec1/tests/process_contract.rs index cf1abea..83afe19 100644 --- a/tools/xmlsec1/tests/process_contract.rs +++ b/tools/xmlsec1/tests/process_contract.rs @@ -6,8 +6,8 @@ use std::{ }; use rsa::{ - RsaPrivateKey, - pkcs8::{DecodePrivateKey as _, EncodePrivateKey as _}, + RsaPrivateKey, RsaPublicKey, + pkcs8::{DecodePrivateKey as _, DecodePublicKey as _, EncodePrivateKey as _}, }; use xml_sec::{ c14n::{C14nAlgorithm, C14nMode}, @@ -15,6 +15,7 @@ use xml_sec::{ DigestAlgorithm, ReferenceBuilder, RsaSigningKey, SignContext, SignatureAlgorithm, SignatureBuilder, Transform, XPathExpression, XPathHereSemantics, }, + xmlenc::{DataEncryptionAlgorithm, EncryptedDataBuilder, EncryptionRecipient}, }; fn binary() -> &'static str { @@ -1053,6 +1054,45 @@ fn rsa_recipient_name_must_match_the_template_unless_lax() { assert_eq!(lax_decrypt.stdout, b"named recipient"); } +#[test] +fn named_decryption_key_selects_a_later_recipient() { + // A document KeyName selects among all EncryptedKey recipients. Checking + // only the first recipient would reject a valid key before core decryption + // can reach the matching wrapped content key. + let temp = tempfile::tempdir().unwrap(); + let encrypted = temp.path().join("encrypted.xml"); + let matching_private = project_root().join("tests/fixtures/keys/rsa/rsa-4096-key.pem"); + let matching_public = RsaPublicKey::from_public_key_pem( + &fs::read_to_string(project_root().join("tests/fixtures/keys/rsa/rsa-4096-pubkey.pem")) + .unwrap(), + ) + .unwrap(); + let other_public = RsaPublicKey::from_public_key_pem( + &fs::read_to_string(project_root().join("tests/fixtures/keys/rsa/rsa-2048-pubkey.pem")) + .unwrap(), + ) + .unwrap(); + let generated = EncryptedDataBuilder::new(DataEncryptionAlgorithm::Aes128Gcm) + .add_recipient(EncryptionRecipient::rsa_oaep(other_public).key_name("other")) + .add_recipient(EncryptionRecipient::rsa_oaep(matching_public).key_name("matching")) + .encrypt_binary(b"later recipient") + .unwrap(); + fs::write(&encrypted, generated.encrypted_data_xml).unwrap(); + + let output = Command::new(binary()) + .args(["decrypt", "--privkey-pem:matching"]) + .arg(&matching_private) + .arg(&encrypted) + .output() + .unwrap(); + assert!( + output.status.success(), + "{}", + String::from_utf8_lossy(&output.stderr) + ); + assert_eq!(output.stdout, b"later recipient"); +} + #[test] fn encrypts_and_decrypts_with_an_rsa_oaep_recipient() { // The advertised RSA path must emit XML Encryption 1.1 OAEP and unwrap its From d47f22a72996195acf0995878a98021ca74c7c9a Mon Sep 17 00:00:00 2001 From: Dmitry Prudnikov Date: Fri, 14 Aug 2026 20:21:24 +0300 Subject: [PATCH 13/27] fix(cli): derive help from command registry - Enumerate canonical command names from one typed source - Return a typed error for invalid option contracts - Document the canonical AES key spelling --- tools/xmlsec1/README.md | 3 +- tools/xmlsec1/src/args.rs | 40 +++++++++++++++++ tools/xmlsec1/src/commands.rs | 82 +++++++++++++++++++++++------------ 3 files changed, 96 insertions(+), 29 deletions(-) diff --git a/tools/xmlsec1/README.md b/tools/xmlsec1/README.md index f3ffcd3..6390818 100644 --- a/tools/xmlsec1/README.md +++ b/tools/xmlsec1/README.md @@ -14,7 +14,8 @@ See the repository's [CLI compatibility guide](https://github.com/structured-wor examples, supported key formats, fail-closed behavior, and upstream runner coverage. -`--aeskey` files are raw binary key material. Decrypting a standalone +`--aes-key` files are raw binary key material (`--aeskey` remains a compatible +alias). Decrypting a standalone `EncryptedData` returns opaque decrypted bytes; embedded encrypted data uses in-document replacement and supports operation-start selection with `--node-id`. Encryption retains template metadata and RSA-OAEP parameters; diff --git a/tools/xmlsec1/src/args.rs b/tools/xmlsec1/src/args.rs index b41761e..1cc0b9d 100644 --- a/tools/xmlsec1/src/args.rs +++ b/tools/xmlsec1/src/args.rs @@ -25,6 +25,46 @@ pub enum Command { } impl Command { + pub(crate) const ALL: &[Self] = &[ + Self::Help, + Self::HelpAll, + Self::HelpDsig, + Self::HelpEnc, + Self::HelpKeys, + Self::HelpX509, + Self::Version, + Self::ListKeyData, + Self::CheckKeyData, + Self::ListTransforms, + Self::CheckTransforms, + Self::Keys, + Self::Sign, + Self::Verify, + Self::Encrypt, + Self::Decrypt, + ]; + + pub(crate) const fn canonical_name(self) -> &'static str { + match self { + Self::Help => "help", + Self::HelpAll => "help-all", + Self::HelpDsig => "help-dsig", + Self::HelpEnc => "help-enc", + Self::HelpKeys => "help-keys", + Self::HelpX509 => "help-x509", + Self::Version => "version", + Self::ListKeyData => "list-key-data", + Self::CheckKeyData => "check-key-data", + Self::ListTransforms => "list-transforms", + Self::CheckTransforms => "check-transforms", + Self::Keys => "keys", + Self::Sign => "sign", + Self::Verify => "verify", + Self::Encrypt => "encrypt", + Self::Decrypt => "decrypt", + } + } + fn parse(value: &str) -> Option { let value = value.strip_prefix("--").unwrap_or(value); Some(match value { diff --git a/tools/xmlsec1/src/commands.rs b/tools/xmlsec1/src/commands.rs index 706a8bb..0dc74dc 100644 --- a/tools/xmlsec1/src/commands.rs +++ b/tools/xmlsec1/src/commands.rs @@ -116,6 +116,17 @@ const KEYS_OPTIONS: &[&str] = &["gen-key"]; const XMLDSIG_NS: &str = "http://www.w3.org/2000/09/xmldsig#"; const XMLENC_NS: &str = "http://www.w3.org/2001/04/xmlenc#"; const XMLENC11_NS: &str = "http://www.w3.org/2009/xmlenc11#"; +const PRIMARY_COMMANDS: &[Command] = &[ + Command::Sign, + Command::Verify, + Command::Encrypt, + Command::Decrypt, + Command::Keys, + Command::ListTransforms, + Command::CheckTransforms, + Command::ListKeyData, + Command::CheckKeyData, +]; #[derive(Debug, thiserror::Error)] pub enum CommandError { @@ -146,6 +157,8 @@ pub enum CommandError { Encryption(String), #[error("requested capability is not available")] CapabilityUnavailable, + #[error("invalid internal command contract: {0}")] + InvalidContract(&'static str), } pub fn execute( @@ -202,24 +215,13 @@ pub fn execute( } fn help(output: &mut dyn Write) -> Result<(), CommandError> { - writeln!( - output, - "Usage: xmlsec1 [options] [files]\n\ - Commands: sign verify encrypt decrypt keys list-transforms check-transforms \ - list-key-data check-key-data" - ) - .map_err(stdout_error) + writeln!(output, "Usage: xmlsec1 [options] [files]").map_err(stdout_error)?; + write_command_list(PRIMARY_COMMANDS, output) } fn help_all(output: &mut dyn Write) -> Result<(), CommandError> { writeln!(output, "Usage: xmlsec1 [options] [files]").map_err(stdout_error)?; - writeln!( - output, - "Commands: help help-all help-dsig help-enc help-keys help-x509 version \ - list-key-data check-key-data list-transforms check-transforms keys sign \ - verify encrypt decrypt" - ) - .map_err(stdout_error)?; + write_command_list(Command::ALL, output)?; writeln!(output, "Options:").map_err(stdout_error)?; for spec in OPTION_SPECS { let parameter = if spec.accepts_parameter { @@ -237,6 +239,14 @@ fn help_all(output: &mut dyn Write) -> Result<(), CommandError> { Ok(()) } +fn write_command_list(commands: &[Command], output: &mut dyn Write) -> Result<(), CommandError> { + write!(output, "Commands:").map_err(stdout_error)?; + for command in commands { + write!(output, " {}", command.canonical_name()).map_err(stdout_error)?; + } + writeln!(output).map_err(stdout_error) +} + fn command_help(command: Command, output: &mut dyn Write) -> Result<(), CommandError> { let Some((name, options)) = command_contract(command) else { return help(output); @@ -247,7 +257,9 @@ fn command_help(command: Command, output: &mut dyn Write) -> Result<(), CommandE let spec = OPTION_SPECS .iter() .find(|spec| spec.canonical == *option) - .expect("command option must exist in OPTION_SPECS"); + .ok_or(CommandError::InvalidContract( + "command option is absent from OPTION_SPECS", + ))?; let parameter = if spec.accepts_parameter { "[:name]" } else { @@ -274,18 +286,19 @@ fn topic_help(commands: &[Command], output: &mut dyn Write) -> Result<(), Comman } fn command_contract(command: Command) -> Option<(&'static str, &'static [&'static str])> { - Some(match command { - Command::Sign => ("sign", SIGN_OPTIONS), - Command::Verify => ("verify", VERIFY_OPTIONS), - Command::Encrypt => ("encrypt", ENCRYPT_OPTIONS), - Command::Decrypt => ("decrypt", DECRYPT_OPTIONS), - Command::Keys => ("keys", KEYS_OPTIONS), - Command::ListKeyData => ("list-key-data", &[]), - Command::CheckKeyData => ("check-key-data", &[]), - Command::ListTransforms => ("list-transforms", &[]), - Command::CheckTransforms => ("check-transforms", &[]), + let options = match command { + Command::Sign => SIGN_OPTIONS, + Command::Verify => VERIFY_OPTIONS, + Command::Encrypt => ENCRYPT_OPTIONS, + Command::Decrypt => DECRYPT_OPTIONS, + Command::Keys => KEYS_OPTIONS, + Command::ListKeyData + | Command::CheckKeyData + | Command::ListTransforms + | Command::CheckTransforms => &[], _ => return None, - }) + }; + Some((command.canonical_name(), options)) } fn validate_provider(invocation: &Invocation) -> Result<(), CommandError> { @@ -1559,8 +1572,21 @@ mod tests { ) .unwrap(); let help = String::from_utf8(output).unwrap(); - for command in ["help-dsig", "check-key-data", "decrypt"] { - assert!(help.contains(command), "missing command {command}"); + for command in Command::ALL { + assert!( + help.contains(command.canonical_name()), + "missing command {}", + command.canonical_name() + ); + if command_contract(*command).is_some() { + let mut command_output = Vec::new(); + command_help(*command, &mut command_output).unwrap(); + assert!( + String::from_utf8(command_output) + .unwrap() + .starts_with(&format!("Usage: xmlsec1 {}", command.canonical_name())) + ); + } } assert!(!help.contains("sign-tmpl")); for option in OPTION_SPECS { From 2ce9654e27dadceff3f2cfca28804dcf33004e70 Mon Sep 17 00:00:00 2001 From: Dmitry Prudnikov Date: Fri, 14 Aug 2026 21:34:15 +0300 Subject: [PATCH 14/27] fix(cli): enforce template trust contracts - preserve inferred XML encryption types on rendered templates - require trust for document-selected X.509 certificates - reject stale recipient metadata with direct AES keys --- README.md | 7 +- docs/cli.md | 23 ++-- tools/xmlsec1/README.md | 6 +- tools/xmlsec1/src/commands.rs | 54 ++++++++- tools/xmlsec1/tests/process_contract.rs | 139 ++++++++++++++++++++++++ 5 files changed, 215 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index 7185702..8821d82 100644 --- a/README.md +++ b/README.md @@ -85,8 +85,11 @@ commands and options accepted by the parser. Named signing keys require a template `KeyName`, while named verification and encryption/decryption keys obey a selected XML `KeyName` unless lax lookup is requested; unnamed templates still use their sole explicit verification or encryption key. Certificate -companions are validated even when no output `KeyInfo` placeholder is present. -Its process tests run a minimal checked-in +companions are validated even when no output `KeyInfo` placeholder is present; +document-supplied X.509 certificates require a caller trust anchor unless +`--insecure` is explicit. XML payload encryption materializes inferred Element +metadata, and direct AES keys reject templates containing recipient +`EncryptedKey` metadata they cannot refresh. Its process tests run a minimal checked-in snapshot of the unmodified upstream DSig, Enc, and Keys runners without network access or a system `xmlsec1` installation. Unsupported algorithms, key formats, providers, and policy controls fail closed diff --git a/docs/cli.md b/docs/cli.md index 89a39d1..4e45c17 100644 --- a/docs/cli.md +++ b/docs/cli.md @@ -82,13 +82,14 @@ its final extension, for example `--output 'signed-{inputfile}.xml'` with Encrypt and decrypt binary data with a direct AES key: ```sh -xmlsec1 encrypt --aeskey:content content.key \ +xmlsec1 encrypt --aes-key:content content.key \ --binary-data plaintext.bin --output encrypted.xml encrypted-data.tmpl -xmlsec1 decrypt --aeskey:content content.key \ +xmlsec1 decrypt --aes-key:content content.key \ --output plaintext.bin encrypted.xml ``` -Files passed through `--aeskey` use libxmlsec1's binary-key contract: their +Files passed through `--aes-key` (`--aeskey` is an alias) use libxmlsec1's +binary-key contract: their bytes are consumed verbatim rather than guessed to be Base64 text. `decrypt` accepts both standalone `EncryptedData` and encrypted elements embedded in a larger XML document; `--node-id` selects an ID-bearing operation start node and @@ -96,6 +97,8 @@ then requires exactly one `EncryptedData` in its subtree. Encryption preserves the template's `Id`, `Type`, `MimeType`, `KeyInfo`, `EncryptionProperties`, and RSA-OAEP parameters while replacing only the cryptographic `CipherValue` payloads. +For `--xml-data`, a missing template `Type` is materialized as XML Element +metadata so a later embedded-document decrypt can perform XML replacement. When an encryption template contains a direct content-key `KeyName`, a named AES key must match it. Likewise, an RSA wrapping key must match a recipient `KeyName` inside `EncryptedKey`. An unnamed template does not constrain the sole @@ -111,6 +114,9 @@ templates explicitly typed as XML `Element` or `Content`; use `--xml-data` for those templates so ciphertext metadata cannot mislabel arbitrary bytes as XML. Supplying `--binary-data` and `--xml-data` together is rejected before either payload is read; encryption requires exactly one payload mode. +A direct `--aes-key` cannot satisfy an `EncryptedKey` recipient embedded in the +template, so that inconsistent combination is rejected rather than preserving +a stale wrapped key. Generate an AES key store using the upstream command shape: @@ -131,10 +137,13 @@ as upstream PKCS#8 aliases. Public verification accepts SubjectPublicKeyInfo, PKCS#1 RSA public keys, and X.509 certificates. Encryption accepts RSA public keys or RSA X.509 recipient certificates in PEM or DER. Explicit verification certificate options pin verification to that certificate's public key instead -of permitting an embedded `KeyInfo` to select another identity. When `--trusted-pem` or -`--trusted-der` is also supplied, the explicit certificate must build a valid -path through any `--untrusted-*` intermediates to a supplied anchor; `--insecure` -is the explicit opt-out. Direct XMLEnc keys accept +of permitting an embedded `KeyInfo` to select another identity. Certificates +discovered from document-controlled `X509Data` require a path through any +`--untrusted-*` intermediates to a caller-supplied `--trusted-pem` or +`--trusted-der` anchor. They are accepted without an anchor only when +`--insecure` explicitly disables trust validation. An explicit certificate +remains a caller-pinned identity; when separate anchors are supplied, it must +also build a valid path to one of them. Direct XMLEnc keys accept AES-128/256; RSA-OAEP supports both the XMLEnc 1.0 `rsa-oaep-mgf1p` and XMLEnc 1.1 parameter contracts. Encrypted PKCS#8, PKCS#12, platform crypto stores, external DTDs, implicit network access, diff --git a/tools/xmlsec1/README.md b/tools/xmlsec1/README.md index 6390818..e0a9936 100644 --- a/tools/xmlsec1/README.md +++ b/tools/xmlsec1/README.md @@ -21,6 +21,8 @@ in-document replacement and supports operation-start selection with `--node-id`. Encryption retains template metadata and RSA-OAEP parameters; PKCS#1 RSA, unencrypted PKCS#8, SPKI, and X.509 PEM or DER key material is normalized into the same core signing, verification, and encryption pipelines. +Untyped `--xml-data` templates gain the inferred XML Element type, while direct +AES keys reject recipient `EncryptedKey` templates they cannot refresh. Signing options validate every certificate from `key,leaf,intermediate,...` and embed the chain when the template provides a `KeyInfo` placeholder; verification accepts stdin as `-` and can select one signature subtree with `--node-id`. Output paths @@ -29,5 +31,7 @@ emits both named and unnamed AES key-store entries. `help-all` is generated from the parser registry. Named signing keys require a template `KeyName`; named verification and encryption/decryption keys require an exact match when the selected XML names a key, unless lax lookup is explicit. Unnamed verification -and encryption templates leave the sole explicit key unconstrained. Binary +and encryption templates leave the sole explicit key unconstrained. Embedded +X.509 certificates require a caller trust anchor unless `--insecure` is +explicit; an explicit certificate remains a caller-pinned identity. Binary payloads cannot be emitted with XML Element/Content type metadata. diff --git a/tools/xmlsec1/src/commands.rs b/tools/xmlsec1/src/commands.rs index 0dc74dc..02c240d 100644 --- a/tools/xmlsec1/src/commands.rs +++ b/tools/xmlsec1/src/commands.rs @@ -604,9 +604,10 @@ fn xmlsec_compatibility_verification_policy(invocation: &Invocation) -> Verifica SignatureAlgorithm::HmacSha1, ]); policy.key_trust.check_crls = invocation.flag("verify-crls"); - policy.key_trust.verify_x509_chains = !invocation.flag("insecure") - && (invocation.values("trusted-pem").next().is_some() - || invocation.values("trusted-der").next().is_some()); + // X509Data is controlled by the signed document and therefore cannot + // establish its own trust. Only an explicit insecure opt-out disables + // path validation for resolver-selected certificates. + policy.key_trust.verify_x509_chains = !invocation.flag("insecure"); policy } @@ -759,9 +760,16 @@ fn verify_with_explicit_certificate( )?); } } + let mut resolver_policy = policy.clone(); + if config.trusted_certs.is_empty() { + // An explicit certificate pins the caller-selected identity. In the + // absence of separate anchors this path is direct key verification, + // unlike document-controlled X509Data discovery. + resolver_policy.key_trust.verify_x509_chains = false; + } let resolver = DefaultKeyResolver::new(config); let key = resolver - .resolve_with_policy(Some(&key_info), algorithm, &policy) + .resolve_with_policy(Some(&key_info), algorithm, &resolver_policy) .map_err(|error| CommandError::Signature(error.to_string()))? .ok_or_else(|| CommandError::Signature("explicit certificate was not resolved".into()))?; verification_context(policy, start_node_id) @@ -810,6 +818,11 @@ fn encrypt(invocation: &Invocation, stdout: &mut dyn Write) -> Result<(), Comman )); } if let [option] = aes_keys.as_slice() { + if metadata.has_encrypted_key_recipient { + return Err(CommandError::Usage( + "direct AES key cannot satisfy an EncryptedKey recipient in the template".into(), + )); + } enforce_named_key_match( option, metadata.content_key_name.as_deref(), @@ -976,6 +989,19 @@ fn apply_encryption_template( template_cipher.range(), standalone_cipher_value(generated_cipher), )]; + if template_data.attribute("Type").is_none() + && let Some(generated_type) = generated_data.attribute("Type") + { + let opening_end = opening_tag_end(&template[template_data.range().start..]) + .map(|offset| template_data.range().start + offset) + .ok_or_else(|| { + CommandError::Encryption("template EncryptedData is malformed".into()) + })?; + replacements.push(( + opening_end..opening_end, + format!(" Type=\"{}\"", quick_xml::escape::escape(generated_type)), + )); + } let template_key_info = direct_child_element(template_data, XMLDSIG_NS, "KeyInfo"); let generated_key_info = direct_child_element(generated_data, XMLDSIG_NS, "KeyInfo"); @@ -1023,6 +1049,19 @@ fn apply_encryption_template( Ok(output) } +fn opening_tag_end(fragment: &str) -> Option { + let mut quote = None; + for (offset, ch) in fragment.char_indices() { + match (quote, ch) { + (None, '\'' | '"') => quote = Some(ch), + (Some(delimiter), current) if delimiter == current => quote = None, + (None, '>') => return Some(offset), + _ => {} + } + } + None +} + fn standalone_cipher_value(node: roxmltree::Node<'_, '_>) -> String { format!( "{}", @@ -1191,6 +1230,7 @@ struct EncryptionTemplateMetadata { algorithm: DataEncryptionAlgorithm, encrypted_type: EncryptedDataType, explicit_encrypted_type: bool, + has_encrypted_key_recipient: bool, content_key_name: Option, recipient_key_name: Option, oaep_parameters: Option, @@ -1224,6 +1264,12 @@ fn encryption_template( algorithm, encrypted_type, explicit_encrypted_type, + has_encrypted_key_recipient: direct_child_element(encrypted_data, XMLDSIG_NS, "KeyInfo") + .is_some_and(|key_info| { + key_info + .children() + .any(|node| node.has_tag_name((XMLENC_NS, "EncryptedKey"))) + }), content_key_name: encrypted_data_key_name(encrypted_data), recipient_key_name: encrypted_key_recipient_name(encrypted_data), oaep_parameters: template_oaep_parameters(encrypted_data)?, diff --git a/tools/xmlsec1/tests/process_contract.rs b/tools/xmlsec1/tests/process_contract.rs index 83afe19..0bf0562 100644 --- a/tools/xmlsec1/tests/process_contract.rs +++ b/tools/xmlsec1/tests/process_contract.rs @@ -769,6 +769,100 @@ fn encryption_rejects_simultaneous_binary_and_xml_payloads() { assert!(!output.exists()); } +#[test] +fn untyped_xml_template_round_trips_when_embedded() { + // --xml-data infers Element semantics. The rendered wire document must + // retain that inference so embedded decryption performs XML replacement. + let temp = tempfile::tempdir().unwrap(); + let template = temp.path().join("template.xml"); + let plaintext = temp.path().join("plaintext.xml"); + let encrypted = temp.path().join("encrypted.xml"); + let document = temp.path().join("document.xml"); + let key = temp.path().join("key.bin"); + fs::write( + &template, + r#""#, + ) + .unwrap(); + fs::write(&plaintext, b"inferred XML").unwrap(); + fs::write(&key, b"0123456789abcdef").unwrap(); + + let encryption = Command::new(binary()) + .args(["encrypt", "--aes-key"]) + .arg(&key) + .arg("--xml-data") + .arg(&plaintext) + .arg("--output") + .arg(&encrypted) + .arg(&template) + .output() + .unwrap(); + assert!( + encryption.status.success(), + "{}", + String::from_utf8_lossy(&encryption.stderr) + ); + let encrypted_xml = fs::read_to_string(&encrypted).unwrap(); + assert!( + encrypted_xml.contains("Id=\"quoted>delimiter\" Type="), + "{encrypted_xml}" + ); + fs::write( + &document, + format!( + "{}", + encrypted_xml + ), + ) + .unwrap(); + + let decryption = Command::new(binary()) + .args(["decrypt", "--aes-key"]) + .arg(&key) + .args(["--node-id", "selected"]) + .arg(&document) + .output() + .unwrap(); + assert!( + decryption.status.success(), + "{}", + String::from_utf8_lossy(&decryption.stderr) + ); + assert!( + String::from_utf8(decryption.stdout) + .unwrap() + .contains("inferred XML") + ); +} + +#[test] +fn direct_aes_encryption_rejects_recipient_templates() { + // A direct content key cannot refresh an EncryptedKey recipient. Emitting + // the untouched wrapped key would create internally inconsistent XML. + let temp = tempfile::tempdir().unwrap(); + let template = temp.path().join("recipient-template.xml"); + let plaintext = temp.path().join("plaintext.bin"); + let key = temp.path().join("key.bin"); + fs::write( + &template, + r#"c3RhbGU="#, + ) + .unwrap(); + fs::write(&plaintext, b"recipient mismatch").unwrap(); + fs::write(&key, b"0123456789abcdef").unwrap(); + + let output = Command::new(binary()) + .args(["encrypt", "--aes-key"]) + .arg(&key) + .arg("--binary-data") + .arg(&plaintext) + .arg(&template) + .output() + .unwrap(); + assert!(!output.status.success()); + assert!(String::from_utf8_lossy(&output.stderr).contains("recipient")); +} + #[test] fn direct_aes_key_name_must_match_the_template_unless_lax() { let temp = tempfile::tempdir().unwrap(); @@ -1547,6 +1641,51 @@ fn explicit_certificate_pins_the_verification_identity() { ); } +#[test] +fn embedded_certificate_requires_trust_unless_insecure() { + // Document-controlled X509Data is an identity claim, not a trust anchor. + // Only an explicit insecure opt-out may verify it without caller trust. + let temp = tempfile::tempdir().unwrap(); + let template = project_root() + .join("tests/fixtures/xmldsig/aleksey-xmldsig-01/enveloping-sha256-rsa-sha256.tmpl"); + let private_key = project_root().join("tests/fixtures/keys/rsa/rsa-4096-key.pem"); + let certificate = project_root().join("tests/fixtures/keys/rsa/rsa-4096-cert.pem"); + let signed = temp.path().join("signed.xml"); + let compound = format!("{},{}", private_key.display(), certificate.display()); + let sign = Command::new(binary()) + .args(["sign", "--privkey-pem"]) + .arg(compound) + .arg("--output") + .arg(&signed) + .arg(&template) + .output() + .unwrap(); + assert!( + sign.status.success(), + "{}", + String::from_utf8_lossy(&sign.stderr) + ); + + let untrusted = Command::new(binary()) + .arg("verify") + .arg(&signed) + .output() + .unwrap(); + assert!(!untrusted.status.success()); + assert!(String::from_utf8_lossy(&untrusted.stderr).contains("trusted")); + + let insecure = Command::new(binary()) + .args(["verify", "--insecure"]) + .arg(&signed) + .output() + .unwrap(); + assert!( + insecure.status.success(), + "{}", + String::from_utf8_lossy(&insecure.stderr) + ); +} + #[test] fn signing_embeds_every_certificate_from_the_private_key_option() { // libxmlsec1 treats every comma-separated path after the private key as a From da0b15e39eaa85636eba404d5224669572cae669 Mon Sep 17 00:00:00 2001 From: Dmitry Prudnikov Date: Sat, 15 Aug 2026 00:32:04 +0300 Subject: [PATCH 15/27] fix(cli): align donor signing contracts - emit parseable donor verification diagnostics and enforce option multiplicity - isolate nested signature mutation and scope builder placement to selected nodes - add unit, integration, process, and feature-only regression coverage Closes #112 --- README.md | 7 +- docs/cli.md | 9 ++ src/xml.rs | 8 +- src/xmldsig/mutation.rs | 106 ++++++++++++++++++++++-- src/xmldsig/sign.rs | 75 ++++++++++++++--- tests/signing_digest.rs | 72 ++++++++++++++++ tools/xmlsec1/README.md | 5 ++ tools/xmlsec1/src/args.rs | 54 ++++++++++++ tools/xmlsec1/src/commands.rs | 75 +++++++++++++++-- tools/xmlsec1/tests/process_contract.rs | 88 ++++++++++++++++++++ 10 files changed, 472 insertions(+), 27 deletions(-) diff --git a/README.md b/README.md index 8821d82..7def698 100644 --- a/README.md +++ b/README.md @@ -81,7 +81,9 @@ The native binary supports sign/verify, template-preserving encrypt/decrypt, AES key generation, capability checks, libxmlsec1 key aliases and option syntax, certificate-chain embedding, stdin input, signature selection by node ID, and deterministic process statuses. `help-all` enumerates the same registered -commands and options accepted by the parser. Named signing keys require a +commands and options accepted by the parser; donor option multiplicity is +enforced across canonical and alias spellings, and `--print-xml-debug` emits +parseable verification diagnostics. Named signing keys require a template `KeyName`, while named verification and encryption/decryption keys obey a selected XML `KeyName` unless lax lookup is requested; unnamed templates still use their sole explicit verification or encryption key. Certificate @@ -89,7 +91,8 @@ companions are validated even when no output `KeyInfo` placeholder is present; document-supplied X.509 certificates require a caller trust anchor unless `--insecure` is explicit. XML payload encryption materializes inferred Element metadata, and direct AES keys reject templates containing recipient -`EncryptedKey` metadata they cannot refresh. Its process tests run a minimal checked-in +`EncryptedKey` metadata they cannot refresh. Its process tests run a minimal +checked-in snapshot of the unmodified upstream DSig, Enc, and Keys runners without network access or a system `xmlsec1` installation. Unsupported algorithms, key formats, providers, and policy controls fail closed diff --git a/docs/cli.md b/docs/cli.md index 4e45c17..931e8b1 100644 --- a/docs/cli.md +++ b/docs/cli.md @@ -23,6 +23,11 @@ non-zero. Commands absent from the pinned 1.3.13 surface are not advertised; historical `sign-tmpl` spellings are rejected instead of being routed to `sign` without template-generation semantics. +`--print-debug` emits the donor-style text status, while `--print-xml-debug` +emits a well-formed `VerificationContext` with donor `OK`/`FAILED` status and +failure-reason vocabulary. XML diagnostics are emitted for invalid signatures +before the command returns its required non-zero status. + Capability checks and runtime dispatch use one registry. A transform or key-data class absent from `list-*` is not silently substituted and causes `check-*` to fail. Backend selection is equally strict: `--crypto rustcrypto` and @@ -39,6 +44,10 @@ does not recognize. A `:` suffix on flags or unrelated valued options is rejected rather than silently activating the underlying option. Native aliases from the same donor metadata are accepted, including `--pubkey-cert`, `--binary`, and command-local `-h`. +The same metadata enforces option multiplicity: key, certificate, ID-attribute, +and key-generation options may repeat where libxmlsec1 marks them as +multi-value; repeating singleton output, provider, payload, selector, password, +or policy options is rejected even when canonical and alias spellings are mixed. ## Examples diff --git a/src/xml.rs b/src/xml.rs index 3039d25..9f124b5 100644 --- a/src/xml.rs +++ b/src/xml.rs @@ -152,8 +152,10 @@ mod tests { .expect("ID index fixture must be valid XML"); let index = XmlIdIndex::new(&document); - assert!(index.contains("same")); - assert!(!index.contains("duplicate")); - assert_eq!(index.len(), 1); + assert_eq!( + index.node("same").map(|node| node.tag_name().name()), + Some("one") + ); + assert!(index.node("duplicate").is_none()); } } diff --git a/src/xmldsig/mutation.rs b/src/xmldsig/mutation.rs index 63399ad..44c803b 100644 --- a/src/xmldsig/mutation.rs +++ b/src/xmldsig/mutation.rs @@ -3,7 +3,7 @@ //! Signing cannot mutate `roxmltree`'s read-only DOM. These helpers validate //! structure with `roxmltree`, then rewrite the document with `quick-xml`. -use std::io::Write; +use std::{io::Write, ops::Range}; use quick_xml::events::{BytesText, Event}; use quick_xml::name::{Namespace, ResolveResult}; @@ -60,6 +60,9 @@ pub enum XmlMutationError { /// The source XML did not contain a root element that can receive a signature. #[error("source XML must contain a root element")] MissingRootElement, + /// The selected source element cannot receive an appended signature. + #[error("selected source element cannot receive a signature")] + InvalidAppendTarget, } /// Append a generated XMLDSig `` template as the last child of the @@ -129,6 +132,56 @@ pub(super) fn append_signature_to_root_with_options( Ok(output) } +pub(super) fn append_signature_to_element_with_options( + xml: &str, + signature_template: &str, + target: Range, + policy: Option<&crate::policy::SigningPolicy>, +) -> Result { + validate_signature_template(signature_template)?; + let _source = parse_with_options(xml, policy)?; + let fragment = xml + .get(target.clone()) + .ok_or(XmlMutationError::InvalidAppendTarget)?; + let opening_end = opening_tag_end(fragment).ok_or(XmlMutationError::InvalidAppendTarget)?; + let mut output = xml.to_owned(); + if fragment[..opening_end].trim_end().ends_with('/') { + let slash = fragment[..opening_end] + .rfind('/') + .ok_or(XmlMutationError::InvalidAppendTarget)?; + let name_end = fragment[1..] + .find(|character: char| character.is_whitespace() || matches!(character, '/' | '>')) + .map(|offset| offset + 1) + .ok_or(XmlMutationError::InvalidAppendTarget)?; + let qualified_name = &fragment[1..name_end]; + let replacement = format!( + "{}>{signature_template}", + &fragment[..slash] + ); + output.replace_range(target, &replacement); + } else { + let closing_start = fragment + .rfind(" Option { + let mut quote = None; + for (offset, character) in fragment.char_indices() { + match (quote, character) { + (None, '\'' | '"') => quote = Some(character), + (Some(delimiter), current) if delimiter == current => quote = None, + (None, '>') => return Some(offset), + _ => {} + } + } + None +} + /// Fill XMLDSig `` elements in document order. pub fn fill_digest_values(xml: &str, values: I) -> Result where @@ -680,11 +733,8 @@ fn is_in_target_signature( element_stack .iter() .rev() - .any(|(is_dsig, local_name, signature)| { - *is_dsig - && local_name.as_slice() == b"Signature" - && *signature == Some(target_signature) - }) + .find(|(is_dsig, local_name, _)| *is_dsig && local_name.as_slice() == b"Signature") + .is_some_and(|(_, _, signature)| *signature == Some(target_signature)) } fn is_dsig_element(namespace: &ResolveResult<'_>, local: &[u8], expected_local: &str) -> bool { @@ -769,6 +819,32 @@ mod tests { ); } + #[test] + fn appends_signature_template_to_selected_empty_element() { + // Selected builder targets may be self-closing; insertion must expand + // the element without dropping its qualified name or attributes. + let source = r#""#; + let document = roxmltree::Document::parse(source).expect("source must parse"); + let scope = document + .descendants() + .find(|node| node.attribute("Id") == Some("selected")) + .expect("selected scope"); + let signed = + append_signature_to_element_with_options(source, &template(1), scope.range(), None) + .expect("selected empty element must accept a signature"); + let output = roxmltree::Document::parse(&signed).expect("output must parse"); + let scope = output + .descendants() + .find(|node| node.has_tag_name(("urn:scope", "scope"))) + .expect("qualified scope must remain"); + + assert!( + scope + .children() + .any(|node| node.has_tag_name((XMLDSIG_NS, "Signature"))) + ); + } + #[test] fn rejects_non_signature_template() { let err = append_signature_to_root("", "") @@ -872,4 +948,22 @@ mod tests { } )); } + + #[test] + fn indexed_digest_replacement_ignores_nested_signatures() { + // Digest counts and replacements must use the same nearest-Signature + // boundary or a nested Object signature can exhaust the value list. + let source = r#"outer-oldinner-keep"#; + let filled = + fill_signed_info_digest_values_at_index_with_options(source, ["outer-new"], 0, None) + .expect("outer signature replacement must ignore nested signatures"); + let document = roxmltree::Document::parse(&filled).expect("filled XML must parse"); + let values = document + .descendants() + .filter(|node| node.has_tag_name((XMLDSIG_NS, "DigestValue"))) + .filter_map(|node| node.text()) + .collect::>(); + + assert_eq!(values, ["outer-new", "inner-keep"]); + } } diff --git a/src/xmldsig/sign.rs b/src/xmldsig/sign.rs index f361da0..143069a 100644 --- a/src/xmldsig/sign.rs +++ b/src/xmldsig/sign.rs @@ -25,7 +25,8 @@ use crate::c14n::{canonicalize_bounded_with_xml_base_budget, is_output_limit_err use super::builder::{SignatureBuilder, SignatureBuilderError}; use super::digest::DigestAlgorithm; use super::mutation::{ - XmlMutationError, append_signature_to_root_with_options, fill_key_info_at_index_with_options, + XmlMutationError, append_signature_to_element_with_options, + append_signature_to_root_with_options, fill_key_info_at_index_with_options, fill_signature_value_at_index_with_options, fill_signed_info_digest_values, fill_signed_info_digest_values_at_index_with_options, fill_signed_info_digest_values_with_options, @@ -745,6 +746,14 @@ impl<'a> SignContext<'a> { let document = parse_signing_document(xml, Some(&self.policy)) .map_err(SigningDigestError::XmlParse)?; let target_signature = signing_signature_index(&document, self.start_node_id)?; + self.sign_template_at_index(xml, target_signature) + } + + fn sign_template_at_index( + &self, + xml: &str, + target_signature: usize, + ) -> Result { let execution_budget = TransformExecutionBudget::from_resources(&self.policy.resources); let transform_options = TransformOptions::default() .allow_internal_dtd(self.policy.xml.allow_internal_dtd) @@ -815,7 +824,8 @@ impl<'a> SignContext<'a> { } } - /// Build a signature template, append it to the source root, then sign it. + /// Build a signature template, append it to the selected start node (or + /// the document root when no selector is set), then sign that new template. pub fn sign_with_builder( &self, xml: &str, @@ -824,8 +834,37 @@ impl<'a> SignContext<'a> { self.policy.validate()?; self.policy.resources.validate_xml_document_len(xml.len())?; let template = builder.build_template()?; - let templated = append_signature_to_root_with_options(xml, &template, Some(&self.policy))?; - self.sign_template(&templated) + let templated = if let Some(id) = self.start_node_id { + let document = parse_signing_document(xml, Some(&self.policy)) + .map_err(SigningDigestError::XmlParse)?; + let start = signing_start_node(&document, id)?; + append_signature_to_element_with_options( + xml, + &template, + start.range(), + Some(&self.policy), + )? + } else { + append_signature_to_root_with_options(xml, &template, Some(&self.policy))? + }; + self.policy + .resources + .validate_xml_document_len(templated.len())?; + let document = parse_signing_document(&templated, Some(&self.policy)) + .map_err(SigningDigestError::XmlParse)?; + let target_signature = if let Some(id) = self.start_node_id { + let start = signing_start_node(&document, id)?; + let appended = start + .children() + .rfind(|node| node.has_tag_name((XMLDSIG_NS, "Signature"))) + .ok_or(SigningDigestError::MissingElement { + element: "Signature", + })?; + signature_index(&document, appended)? + } else { + signing_signature_index(&document, None)? + }; + self.sign_template_at_index(&templated, target_signature) } } @@ -1096,13 +1135,7 @@ fn signing_signature_index( start_node_id: Option<&str>, ) -> Result { let selected = if let Some(id) = start_node_id { - let start = UriReferenceResolver::new(doc) - .node_for_id(id) - .ok_or_else(|| { - SigningDigestError::InvalidStructure(format!( - "selected node ID is missing or ambiguous: {id}" - )) - })?; + let start = signing_start_node(doc, id)?; start .descendants() .find(|node| node.has_tag_name((XMLDSIG_NS, "Signature"))) @@ -1114,6 +1147,26 @@ fn signing_signature_index( } else { find_signing_signature_node(doc, None)? }; + signature_index(doc, selected) +} + +fn signing_start_node<'a>( + doc: &'a Document<'a>, + id: &str, +) -> Result, SigningDigestError> { + UriReferenceResolver::new(doc) + .node_for_id(id) + .ok_or_else(|| { + SigningDigestError::InvalidStructure(format!( + "selected node ID is missing or ambiguous: {id}" + )) + }) +} + +fn signature_index( + doc: &Document<'_>, + selected: Node<'_, '_>, +) -> Result { doc.descendants() .filter(|node| node.has_tag_name((XMLDSIG_NS, "Signature"))) .position(|node| node == selected) diff --git a/tests/signing_digest.rs b/tests/signing_digest.rs index f871a1e..6d3522d 100644 --- a/tests/signing_digest.rs +++ b/tests/signing_digest.rs @@ -1039,6 +1039,78 @@ fn sign_with_builder_targets_appended_signature_when_existing_key_info_is_presen assert!(!second_signed.contains("")); } +#[test] +fn sign_with_builder_appends_and_targets_within_the_selected_start_node() { + // A start-node selector scopes both template placement and target choice; + // an older template in that subtree must remain untouched. + let private_key = + RsaSigningKey::from_pkcs8_pem(&read_fixture("tests/fixtures/keys/rsa/rsa-2048-key.pem")) + .expect("RSA private key fixture must parse"); + let old_builder = SignatureBuilder::new(exclusive_c14n(), SignatureAlgorithm::RsaSha256) + .add_reference( + ReferenceBuilder::new(DigestAlgorithm::Sha256) + .uri("#old") + .transform(Transform::C14n(exclusive_c14n())), + ); + let old_template = old_builder + .build_template() + .expect("old template must build"); + let xml = format!( + "oldnew{old_template}" + ); + let new_builder = SignatureBuilder::new(exclusive_c14n(), SignatureAlgorithm::RsaSha256) + .add_reference( + ReferenceBuilder::new(DigestAlgorithm::Sha256) + .uri("#new") + .transform(Transform::C14n(exclusive_c14n())), + ); + + let signed = SignContext::new(&private_key) + .start_node_id("selected") + .sign_with_builder(&xml, &new_builder) + .expect("builder signing must target its appended selected-node template"); + let document = roxmltree::Document::parse(&signed).expect("signed XML must parse"); + let scope = document + .descendants() + .find(|node| node.attribute("Id") == Some("selected")) + .expect("selected scope must remain"); + let signatures = scope + .children() + .filter(|node| node.has_tag_name(("http://www.w3.org/2000/09/xmldsig#", "Signature"))) + .collect::>(); + assert_eq!(signatures.len(), 2); + assert_eq!( + signatures[0] + .descendants() + .find(|node| node.has_tag_name(("http://www.w3.org/2000/09/xmldsig#", "DigestValue"))) + .and_then(|node| node.text()), + None + ); + assert_eq!( + signatures[1] + .descendants() + .find(|node| node.has_tag_name(("http://www.w3.org/2000/09/xmldsig#", "Reference"))) + .and_then(|node| node.attribute("URI")), + Some("#new") + ); + assert!( + signatures[1] + .descendants() + .find(|node| node.has_tag_name(("http://www.w3.org/2000/09/xmldsig#", "DigestValue"))) + .and_then(|node| node.text()) + .is_some() + ); + assert!( + signatures[1] + .children() + .find(|node| { + node.has_tag_name(("http://www.w3.org/2000/09/xmldsig#", "SignatureValue")) + }) + .and_then(|node| node.text()) + .is_some() + ); +} + #[test] fn signing_fills_only_top_level_signature_value() { // Object payloads may contain SignatureValue-named XMLDSig elements. The diff --git a/tools/xmlsec1/README.md b/tools/xmlsec1/README.md index e0a9936..ace18ff 100644 --- a/tools/xmlsec1/README.md +++ b/tools/xmlsec1/README.md @@ -14,6 +14,11 @@ See the repository's [CLI compatibility guide](https://github.com/structured-wor examples, supported key formats, fail-closed behavior, and upstream runner coverage. +The parser rejects repeated singleton options, including mixed canonical and +alias spellings, while preserving donor multi-value key and certificate inputs. +`--print-debug` is text; `--print-xml-debug` emits a parseable donor-shaped +`VerificationContext` for both successful and invalid verification results. + `--aes-key` files are raw binary key material (`--aeskey` remains a compatible alias). Decrypting a standalone `EncryptedData` returns opaque decrypted bytes; embedded encrypted data uses diff --git a/tools/xmlsec1/src/args.rs b/tools/xmlsec1/src/args.rs index 1cc0b9d..43b7ff3 100644 --- a/tools/xmlsec1/src/args.rs +++ b/tools/xmlsec1/src/args.rs @@ -115,6 +115,8 @@ pub enum ParseError { UnsupportedOption(String), #[error("option {0} does not accept a name parameter")] UnexpectedOptionParameter(String), + #[error("option {0} cannot be repeated")] + RepeatedOption(String), #[error("arguments are not valid UTF-8")] NonUtf8, } @@ -132,6 +134,26 @@ pub(crate) struct OptionSpec { pub accepts_parameter: bool, } +#[derive(Clone, Copy, PartialEq, Eq)] +enum Repeatability { + Singleton, + Multiple, +} + +impl OptionSpec { + fn repeatability(&self) -> Repeatability { + match self.canonical { + "keys-file" | "gen-key" | "privkey-pem" | "privkey-der" | "pkcs8-pem" | "pkcs8-der" + | "pubkey-pem" | "pubkey-der" | "pubkey-cert-pem" | "pubkey-cert-der" + | "trusted-pem" | "trusted-der" | "untrusted-pem" | "untrusted-der" | "aes-key" + | "hmac-key" | "enabled-key-data" | "id-attr" | "add-id-attr" | "url-map" => { + Repeatability::Multiple + } + _ => Repeatability::Singleton, + } + } +} + const FLAG: Arity = Arity::Flag; const VALUE: Arity = Arity::Value; @@ -478,6 +500,11 @@ impl Invocation { argument_text.to_owned(), )); } + if option_spec(name).repeatability() == Repeatability::Singleton + && options.contains_key(name) + { + return Err(ParseError::RepeatedOption(format!("--{name}"))); + } let value = match option_arity(name) { Arity::Flag => None, @@ -636,6 +663,33 @@ mod tests { ); } + #[test] + fn rejects_repeated_singleton_options_across_aliases() { + for arguments in [ + &[ + "xmlsec1", + "sign", + "--output", + "first.xml", + "-o", + "second.xml", + "input.xml", + ][..], + &[ + "xmlsec1", + "verify", + "--node-id", + "first", + "--node-id", + "second", + "input.xml", + ], + &["xmlsec1", "verify", "--insecure", "--insecure", "input.xml"], + ] { + assert!(parse(arguments).is_err(), "{arguments:?}"); + } + } + #[test] fn consumes_dash_prefixed_option_values_verbatim() { // Option arity, not the first byte of its value, determines parsing. diff --git a/tools/xmlsec1/src/commands.rs b/tools/xmlsec1/src/commands.rs index 02c240d..84a1e28 100644 --- a/tools/xmlsec1/src/commands.rs +++ b/tools/xmlsec1/src/commands.rs @@ -11,9 +11,10 @@ use xml_sec::{ policy::{DecryptionPolicy, EncryptionPolicy, SigningPolicy, VerificationPolicy}, provider::{CryptoProvider, default_provider}, xmldsig::{ - DefaultKeyResolver, DsigStatus, KeyInfoWriter, KeyResolver, KeyResolverConfig, SignContext, - SignatureAlgorithm, UriTypeSet, VerifyContext, X509CertificateKeyInfoWriter, - XPathHereSemantics, parse_key_info, uri::UriReferenceResolver, + DefaultKeyResolver, DsigStatus, FailureReason, KeyInfoWriter, KeyResolver, + KeyResolverConfig, ReferenceResult, SignContext, SignatureAlgorithm, UriTypeSet, + VerifyContext, VerifyResult, X509CertificateKeyInfoWriter, XPathHereSemantics, + parse_key_info, uri::UriReferenceResolver, }, xmlenc::{ DataEncryptionAlgorithm, DecryptContext, DecryptedContent, DecryptionKeyResolver, @@ -697,6 +698,7 @@ fn verify(invocation: &Invocation, stdout: &mut dyn Write) -> Result<(), Command .verify(&xml) .map_err(|error| CommandError::Signature(error.to_string()))? }; + write_verification_diagnostics(invocation, &result, stdout)?; if result.status != DsigStatus::Valid || result .manifest_references @@ -705,12 +707,75 @@ fn verify(invocation: &Invocation, stdout: &mut dyn Write) -> Result<(), Command { return Err(CommandError::InvalidSignature); } - if invocation.flag("print-debug") || invocation.flag("print-xml-debug") { - writeln!(stdout, "Status: valid").map_err(stdout_error)?; + Ok(()) +} + +fn write_verification_diagnostics( + invocation: &Invocation, + result: &VerifyResult, + stdout: &mut dyn Write, +) -> Result<(), CommandError> { + if invocation.flag("print-debug") { + let status = if result.status == DsigStatus::Valid { + "valid" + } else { + "invalid" + }; + writeln!(stdout, "Status: {status}").map_err(stdout_error)?; + } + if invocation.flag("print-xml-debug") { + let (status, failure_reason) = donor_dsig_status(result.status); + writeln!( + stdout, + "" + ) + .map_err(stdout_error)?; + write_reference_diagnostics( + stdout, + "SignedInfoReferences", + &result.signed_info_references, + )?; + write_reference_diagnostics(stdout, "ManifestReferences", &result.manifest_references)?; + writeln!(stdout, "").map_err(stdout_error)?; } Ok(()) } +fn write_reference_diagnostics( + stdout: &mut dyn Write, + container: &str, + references: &[ReferenceResult], +) -> Result<(), CommandError> { + writeln!(stdout, "<{container}>").map_err(stdout_error)?; + for reference in references { + let (status, _) = donor_dsig_status(reference.status); + writeln!(stdout, "") + .map_err(stdout_error)?; + writeln!( + stdout, + "{}", + quick_xml::escape::escape(&reference.uri) + ) + .map_err(stdout_error)?; + writeln!(stdout, "").map_err(stdout_error)?; + } + writeln!(stdout, "").map_err(stdout_error) +} + +fn donor_dsig_status(status: DsigStatus) -> (&'static str, &'static str) { + match status { + DsigStatus::Valid => ("OK", "UNKNOWN"), + DsigStatus::Invalid(FailureReason::ReferenceDigestMismatch { .. }) + | DsigStatus::Invalid(FailureReason::ReferencePolicyViolation { .. }) + | DsigStatus::Invalid(FailureReason::ReferenceProcessingFailure { .. }) => { + ("FAILED", "REFERENCE") + } + DsigStatus::Invalid(FailureReason::SignatureMismatch) => ("FAILED", "SIGNATURE"), + DsigStatus::Invalid(FailureReason::KeyNotFound) => ("FAILED", "KEY-NOT-FOUND"), + _ => ("ERROR", "UNKNOWN"), + } +} + fn verification_context( policy: VerificationPolicy, start_node_id: Option<&str>, diff --git a/tools/xmlsec1/tests/process_contract.rs b/tools/xmlsec1/tests/process_contract.rs index 0bf0562..28ee57e 100644 --- a/tools/xmlsec1/tests/process_contract.rs +++ b/tools/xmlsec1/tests/process_contract.rs @@ -91,6 +91,67 @@ fn signs_verifies_and_rejects_tampering_through_process_api() { assert!(String::from_utf8_lossy(&rejected.stderr).contains("invalid")); } +#[test] +fn xml_debug_verification_output_matches_the_donor_xml_contract() { + // The upstream runner parses --print-xml-debug output with xmllint, so the + // diagnostic mode must not share the plain-text debug renderer. + let temp = tempfile::tempdir().unwrap(); + let template = project_root() + .join("tests/fixtures/xmldsig/aleksey-xmldsig-01/enveloping-sha256-rsa-sha256.tmpl"); + let private_key = project_root().join("tests/fixtures/keys/rsa/rsa-4096-key.pem"); + let public_key = project_root().join("tests/fixtures/keys/rsa/rsa-4096-pubkey.pem"); + let signed = temp.path().join("signed.xml"); + let sign = Command::new(binary()) + .args(["sign", "--privkey-pem"]) + .arg(&private_key) + .arg("--output") + .arg(&signed) + .arg(&template) + .output() + .unwrap(); + assert!(sign.status.success()); + + let verify = Command::new(binary()) + .args(["verify", "--print-xml-debug", "--pubkey-pem"]) + .arg(&public_key) + .arg(&signed) + .output() + .unwrap(); + assert!( + verify.status.success(), + "{}", + String::from_utf8_lossy(&verify.stderr) + ); + let debug_xml = String::from_utf8(verify.stdout).unwrap(); + let document = roxmltree::Document::parse(&debug_xml).expect("debug output must be XML"); + let root = document.root_element(); + assert_eq!(root.tag_name().name(), "VerificationContext"); + assert_eq!(root.attribute("status"), Some("OK")); + assert_eq!(root.attribute("failureReason"), Some("UNKNOWN")); + + let tampered = temp.path().join("tampered.xml"); + fs::write( + &tampered, + fs::read_to_string(&signed) + .unwrap() + .replace("some text", "tampered text"), + ) + .unwrap(); + let invalid = Command::new(binary()) + .args(["verify", "--print-xml-debug", "--pubkey-pem"]) + .arg(&public_key) + .arg(&tampered) + .output() + .unwrap(); + assert!(!invalid.status.success()); + let invalid_xml = String::from_utf8(invalid.stdout).unwrap(); + let invalid_document = + roxmltree::Document::parse(&invalid_xml).expect("invalid debug output must still be XML"); + let invalid_root = invalid_document.root_element(); + assert_eq!(invalid_root.attribute("status"), Some("FAILED")); + assert_eq!(invalid_root.attribute("failureReason"), Some("REFERENCE")); +} + #[test] fn short_command_help_alias_reaches_process_dispatch() { // Parsing an alias is insufficient if command validation later rejects its @@ -498,6 +559,33 @@ fn output_template_expands_the_extensionless_input_basename() { assert!(expected.is_file()); } +#[test] +fn repeated_output_options_fail_before_creating_files() { + // Canonical and alias spellings identify one donor singleton; accepting + // both would silently redirect output through last-value wins behavior. + let temp = tempfile::tempdir().unwrap(); + let first = temp.path().join("first.xml"); + let second = temp.path().join("second.xml"); + let template = project_root() + .join("tests/fixtures/xmldsig/aleksey-xmldsig-01/enveloping-sha256-rsa-sha256.tmpl"); + let private_key = project_root().join("tests/fixtures/keys/rsa/rsa-4096-key.pem"); + let output = Command::new(binary()) + .args(["sign", "--privkey-pem"]) + .arg(&private_key) + .arg("--output") + .arg(&first) + .arg("-o") + .arg(&second) + .arg(&template) + .output() + .unwrap(); + + assert!(!output.status.success()); + assert!(String::from_utf8_lossy(&output.stderr).contains("cannot be repeated")); + assert!(!first.exists()); + assert!(!second.exists()); +} + #[test] fn encrypts_decrypts_and_rejects_wrong_symmetric_key() { // A reciprocal binary round trip must preserve non-UTF-8 bytes, while an From c4bd1514141cbb76098576e2fe62497cd14aa42d Mon Sep 17 00:00:00 2001 From: Dmitry Prudnikov Date: Sat, 15 Aug 2026 02:01:42 +0300 Subject: [PATCH 16/27] docs(cli): clarify key format scopes Separate signing, verification, and encryption key-format support so the short CLI README does not imply unsupported command and format combinations. --- tools/xmlsec1/README.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/tools/xmlsec1/README.md b/tools/xmlsec1/README.md index ace18ff..7f452eb 100644 --- a/tools/xmlsec1/README.md +++ b/tools/xmlsec1/README.md @@ -24,8 +24,10 @@ alias). Decrypting a standalone `EncryptedData` returns opaque decrypted bytes; embedded encrypted data uses in-document replacement and supports operation-start selection with `--node-id`. Encryption retains template metadata and RSA-OAEP parameters; -PKCS#1 RSA, unencrypted PKCS#8, SPKI, and X.509 PEM or DER key material is -normalized into the same core signing, verification, and encryption pipelines. +signing accepts PKCS#1 RSA and unencrypted PKCS#8 private keys, verification +accepts SPKI, PKCS#1 RSA public keys, and X.509 certificates, and RSA encryption +accepts public keys or recipient certificates. Supported formats are normalized +into the corresponding core pipeline from PEM or DER. Untyped `--xml-data` templates gain the inferred XML Element type, while direct AES keys reject recipient `EncryptedKey` templates they cannot refresh. Signing options validate every certificate from `key,leaf,intermediate,...` and embed the chain From 9cddb71c74a8e5853b0e7a8cbf3a24eb50afbf10 Mon Sep 17 00:00:00 2001 From: Dmitry Prudnikov Date: Sat, 15 Aug 2026 04:49:55 +0300 Subject: [PATCH 17/27] fix(cli): complete key manager semantics - select repeated signing, verification, and encryption keys by every applicable KeyName - preserve template KeyInfo sources when embedding certificates - enforce donor trust precedence and post-merge XML limits - add process regressions and synchronize CLI documentation --- README.md | 10 +- docs/cli.md | 25 ++- src/xmldsig/mutation.rs | 76 +++++++ src/xmldsig/sign.rs | 9 +- tools/xmlsec1/README.md | 8 +- tools/xmlsec1/src/commands.rs | 274 +++++++++++++++--------- tools/xmlsec1/src/key_material.rs | 40 +++- tools/xmlsec1/tests/process_contract.rs | 263 +++++++++++++++++++++-- 8 files changed, 552 insertions(+), 153 deletions(-) diff --git a/README.md b/README.md index 7def698..c14afbc 100644 --- a/README.md +++ b/README.md @@ -84,10 +84,12 @@ deterministic process statuses. `help-all` enumerates the same registered commands and options accepted by the parser; donor option multiplicity is enforced across canonical and alias spellings, and `--print-xml-debug` emits parseable verification diagnostics. Named signing keys require a -template `KeyName`, while named verification and encryption/decryption keys -obey a selected XML `KeyName` unless lax lookup is requested; unnamed templates -still use their sole explicit verification or encryption key. Certificate -companions are validated even when no output `KeyInfo` placeholder is present; +template `KeyName`, while repeatable named verification and encryption/decryption +options form key sets from which the selected XML `KeyName` must identify exactly +one key unless lax lookup is requested; unnamed templates still use their sole +explicit verification or encryption key. Certificate companions are validated +even when no output `KeyInfo` placeholder is present; embedding a chain fills an +empty `X509Data` placeholder without discarding sibling `KeyInfo` sources. document-supplied X.509 certificates require a caller trust anchor unless `--insecure` is explicit. XML payload encryption materializes inferred Element metadata, and direct AES keys reject templates containing recipient diff --git a/docs/cli.md b/docs/cli.md index 931e8b1..058d4b4 100644 --- a/docs/cli.md +++ b/docs/cli.md @@ -62,8 +62,10 @@ Signing key options accept libxmlsec1's comma-separated certificate form, `key.pem,leaf.pem,intermediate.pem,...`. Every certificate is structurally validated, and the first certificate must contain the signing key. When the template contains the optional direct `KeyInfo` -placeholder, the chain is embedded there in order under `X509Data`; omitting -that placeholder leaves the signed output without `KeyInfo`. Named signing keys +placeholder, the chain is embedded there in order under `X509Data`. An empty +`X509Data` child is populated in place, preserving sibling sources such as the +`KeyName` used to select the signing key; omitting `KeyInfo` leaves the signed +output without `KeyInfo`. Named signing keys require a matching template `KeyName` even when only one key is supplied. A named key with no template `KeyName` fails unless `--lax-key-search` explicitly opts out of lookup. Verification and encryption instead leave a `KeyName`-less @@ -78,10 +80,12 @@ XPath and XPath Filter 2.0 verification uses libxmlsec1's legacy `here()` binding at this CLI compatibility boundary. The Rust library API retains the XMLDSig specification binding by default and requires an explicit opt-in for legacy documents. -When the selected signature contains `KeyName`, named raw public-key and -explicit certificate inputs must match it; `--lax-key-search` is the explicit -opt-out. A signature without `KeyName` does not request a different identity, -so its sole explicit key remains usable even when that key has a registry name. +Repeatable named raw public-key and explicit-certificate options form a key set. +Every direct `KeyName` in the selected signature participates in lookup and must +identify exactly one supplied key; duplicate matches fail as ambiguous. +`--lax-key-search` is the explicit opt-out. A signature without `KeyName` does +not request a different identity, so its sole explicit key remains usable even +when that key has a registry name. `--output` follows the upstream filename-template contract. The first `{inputfile}` token is replaced with the input file's basename after removing @@ -108,10 +112,11 @@ Encryption preserves the template's `Id`, `Type`, `MimeType`, `KeyInfo`, cryptographic `CipherValue` payloads. For `--xml-data`, a missing template `Type` is materialized as XML Element metadata so a later embedded-document decrypt can perform XML replacement. -When an encryption template contains a direct content-key `KeyName`, a named -AES key must match it. Likewise, an RSA wrapping key must match a recipient -`KeyName` inside `EncryptedKey`. An unnamed template does not constrain the sole -explicit key; an explicit mismatch fails unless `--lax-key-search` is supplied. +Repeatable AES or RSA key options form a key set. A direct content-key `KeyName` +must select exactly one AES key, while a recipient `KeyName` inside +`EncryptedKey` must select exactly one RSA wrapping key. An unnamed template +does not constrain the sole explicit key; missing and duplicate matches fail +unless `--lax-key-search` is supplied. RSA private-key decryption accepts the upstream `key.pem,certificate.pem,...` option syntax, consumes the first component as the decryption key, and validates every certificate companion before decrypting. diff --git a/src/xmldsig/mutation.rs b/src/xmldsig/mutation.rs index 44c803b..7de625f 100644 --- a/src/xmldsig/mutation.rs +++ b/src/xmldsig/mutation.rs @@ -329,6 +329,82 @@ pub(super) fn fill_key_info_at_index_with_options( ) } +pub(super) fn merge_key_info_source_at_index_with_options( + xml: &str, + key_info_source: &str, + target_signature: usize, + policy: Option<&crate::policy::SigningPolicy>, +) -> Result { + let document = parse_with_options(xml, policy)?; + let source_document = roxmltree::Document::parse(key_info_source)?; + let source = source_document.root_element(); + let Some(signature) = signature_node(&document, target_signature) else { + return Err(XmlMutationError::ValueCountMismatch { + element: "Signature", + expected: 1, + actual: 0, + }); + }; + let key_infos = signature + .children() + .filter(|node| is_dsig_node(*node, "KeyInfo")) + .collect::>(); + if key_infos.len() != 1 { + return Err(XmlMutationError::ValueCountMismatch { + element: "KeyInfo", + expected: key_infos.len(), + actual: 1, + }); + } + let key_info = key_infos[0]; + + // A writer contributes one KeyInfo source, not the whole KeyInfo value. + // Fill an empty matching placeholder when present; otherwise append the + // source while preserving every template-provided sibling and its order. + if let Some(placeholder) = key_info.children().find(|node| { + node.is_element() + && node.tag_name() == source.tag_name() + && !node.children().any(|child| child.is_element()) + && node.text().is_none_or(|text| text.trim().is_empty()) + }) { + let mut output = xml.to_owned(); + output.replace_range(placeholder.range(), key_info_source); + parse_with_options(&output, policy)?; + return Ok(output); + } + + let range = key_info.range(); + let raw_key_info = &xml[range.clone()]; + let mut output = xml.to_owned(); + if raw_key_info.trim_end().ends_with("/>") { + let name_end = raw_key_info[1..] + .find(|character: char| { + character.is_ascii_whitespace() || character == '/' || character == '>' + }) + .map(|offset| offset + 1) + .ok_or(XmlMutationError::InvalidAppendTarget)?; + let qualified_name = &raw_key_info[1..name_end]; + let empty_end = raw_key_info + .rfind("/>") + .ok_or(XmlMutationError::InvalidAppendTarget)?; + let expanded = format!( + "{}>{}", + &raw_key_info[..empty_end], + key_info_source, + qualified_name + ); + output.replace_range(range, &expanded); + } else { + let closing = raw_key_info + .rfind("( xml: &str, local_name: &'static str, diff --git a/src/xmldsig/sign.rs b/src/xmldsig/sign.rs index 143069a..3846867 100644 --- a/src/xmldsig/sign.rs +++ b/src/xmldsig/sign.rs @@ -26,10 +26,9 @@ use super::builder::{SignatureBuilder, SignatureBuilderError}; use super::digest::DigestAlgorithm; use super::mutation::{ XmlMutationError, append_signature_to_element_with_options, - append_signature_to_root_with_options, fill_key_info_at_index_with_options, - fill_signature_value_at_index_with_options, fill_signed_info_digest_values, - fill_signed_info_digest_values_at_index_with_options, - fill_signed_info_digest_values_with_options, + append_signature_to_root_with_options, fill_signature_value_at_index_with_options, + fill_signed_info_digest_values, fill_signed_info_digest_values_at_index_with_options, + fill_signed_info_digest_values_with_options, merge_key_info_source_at_index_with_options, }; use super::parse::{ MAX_REFERENCES_PER_SIGNATURE, SignatureAlgorithm, XMLDSIG_NS, parse_signed_info, @@ -809,7 +808,7 @@ impl<'a> SignContext<'a> { .validate_xml_document_len(signed.len())?; if let Some(writer) = self.key_info_writer { let key_info_content = writer.write_key_info(self.signing_key)?; - let signed = fill_key_info_at_index_with_options( + let signed = merge_key_info_source_at_index_with_options( &signed, &key_info_content, target_signature, diff --git a/tools/xmlsec1/README.md b/tools/xmlsec1/README.md index 7f452eb..57582b1 100644 --- a/tools/xmlsec1/README.md +++ b/tools/xmlsec1/README.md @@ -31,13 +31,15 @@ into the corresponding core pipeline from PEM or DER. Untyped `--xml-data` templates gain the inferred XML Element type, while direct AES keys reject recipient `EncryptedKey` templates they cannot refresh. Signing options validate every certificate from `key,leaf,intermediate,...` and embed the chain -when the template provides a `KeyInfo` placeholder; verification accepts +when the template provides a `KeyInfo` placeholder, filling an empty `X509Data` +without erasing sibling key sources; verification accepts stdin as `-` and can select one signature subtree with `--node-id`. Output paths support the upstream `{inputfile}` basename template, and `--gen-key[:name]` emits both named and unnamed AES key-store entries. `help-all` is generated from the parser registry. Named signing keys require a template `KeyName`; named -verification and encryption/decryption keys require an exact match when the -selected XML names a key, unless lax lookup is explicit. Unnamed verification +verification and encryption/decryption options form key sets and require the +selected XML `KeyName` to identify exactly one entry, unless lax lookup is +explicit. Unnamed verification and encryption templates leave the sole explicit key unconstrained. Embedded X.509 certificates require a caller trust anchor unless `--insecure` is explicit; an explicit certificate remains a caller-pinned identity. Binary diff --git a/tools/xmlsec1/src/commands.rs b/tools/xmlsec1/src/commands.rs index 84a1e28..579dee3 100644 --- a/tools/xmlsec1/src/commands.rs +++ b/tools/xmlsec1/src/commands.rs @@ -469,6 +469,48 @@ fn option_value_text(option: &crate::OptionValue) -> Result<&str, CommandError> .ok_or_else(|| CommandError::Usage(format!("--{} value must be valid UTF-8", option.name))) } +fn select_named_candidate<'a, T: Copy>( + candidates: &[(&'a crate::OptionValue, T)], + requested_names: &[String], + lax_key_search: bool, + allow_unconstrained_named_singleton: bool, + key_kind: &str, +) -> Result<(&'a crate::OptionValue, T), CommandError> { + if let [selected] = candidates + && (selected.0.parameter.is_none() + || lax_key_search + || (requested_names.is_empty() && allow_unconstrained_named_singleton)) + { + return Ok(*selected); + } + if !requested_names.is_empty() { + let matching = candidates + .iter() + .copied() + .filter(|(key, _)| { + key.parameter + .as_deref() + .is_some_and(|name| requested_names.iter().any(|requested| requested == name)) + }) + .collect::>(); + return match matching.as_slice() { + [selected] => Ok(*selected), + [] => Err(CommandError::Usage(format!( + "template requests unknown KeyName for supplied {key_kind}" + ))), + _ => Err(CommandError::Usage(format!( + "multiple {key_kind} inputs match template KeyNames" + ))), + }; + } + let message = if candidates.len() == 1 { + format!("a named {key_kind} requires a template KeyName; use --lax-key-search to opt out") + } else { + format!("multiple {key_kind} inputs require a template KeyName and named options") + }; + Err(CommandError::Usage(message)) +} + fn sign(invocation: &Invocation, stdout: &mut dyn Write) -> Result<(), CommandError> { validate_options(invocation, SIGN_OPTIONS)?; reject_unimplemented_selectors(invocation, &["node-id"])?; @@ -479,8 +521,7 @@ fn sign(invocation: &Invocation, stdout: &mut dyn Write) -> Result<(), CommandEr let xml = read_input(invocation, policy.resources.max_xml_document_bytes)?; let start_node_id = option_text(invocation, "node-id")?; let signature = key_material::signing_signature_metadata(&xml, start_node_id, &policy)?; - let (key_option, certificate_is_der) = - select_signing_key(invocation, signature.key_name.as_deref())?; + let (key_option, certificate_is_der) = select_signing_key(invocation, &signature.key_names)?; let value = key_option.value.as_deref().unwrap_or_default(); let (key_path, certificate_paths) = split_key_and_certificates(value)?; let key = key_material::load_signing_key(key_path)?; @@ -527,7 +568,7 @@ fn sign(invocation: &Invocation, stdout: &mut dyn Write) -> Result<(), CommandEr fn select_signing_key<'a>( invocation: &'a Invocation, - requested_name: Option<&str>, + requested_names: &[String], ) -> Result<(&'a crate::OptionValue, bool), CommandError> { let mut keys = Vec::new(); for (name, certificate_is_der) in [ @@ -543,32 +584,13 @@ fn select_signing_key<'a>( "sign requires --privkey-pem or --pkcs8-pem/der".into(), )); } - if let [selected] = keys.as_slice() - && (selected.0.parameter.is_none() || invocation.flag("lax-key-search")) - { - return Ok(*selected); - } - if let Some(requested_name) = requested_name { - let matching = keys - .into_iter() - .filter(|(key, _)| key.parameter.as_deref() == Some(requested_name)) - .collect::>(); - return match matching.as_slice() { - [selected] => Ok(*selected), - [] => Err(CommandError::Usage(format!( - "signature template requests unknown KeyName {requested_name}" - ))), - _ => Err(CommandError::Usage(format!( - "multiple private keys use KeyName {requested_name}" - ))), - }; - } - let message = if keys.len() == 1 { - "a named private key requires a template KeyName; use --lax-key-search to opt out" - } else { - "multiple private keys require a template KeyName and named options" - }; - Err(CommandError::Usage(message.into())) + select_named_candidate( + &keys, + requested_names, + invocation.flag("lax-key-search"), + false, + "private key", + ) } fn split_key_and_certificates(value: &OsStr) -> Result<(&OsStr, Vec<&OsStr>), CommandError> { @@ -604,11 +626,14 @@ fn xmlsec_compatibility_verification_policy(invocation: &Invocation) -> Verifica SignatureAlgorithm::DsaSha1, SignatureAlgorithm::HmacSha1, ]); - policy.key_trust.check_crls = invocation.flag("verify-crls"); // X509Data is controlled by the signed document and therefore cannot // establish its own trust. Only an explicit insecure opt-out disables - // path validation for resolver-selected certificates. - policy.key_trust.verify_x509_chains = !invocation.flag("insecure"); + // path validation for resolver-selected certificates. libxmlsec1 makes + // that opt-out authoritative over --verify-crls as well: CRLs are part of + // path validation and cannot remain enabled after trust checks are bypassed. + let insecure = invocation.flag("insecure"); + policy.key_trust.verify_x509_chains = !insecure; + policy.key_trust.check_crls = invocation.flag("verify-crls") && !insecure; policy } @@ -616,28 +641,22 @@ fn verify(invocation: &Invocation, stdout: &mut dyn Write) -> Result<(), Command validate_options(invocation, VERIFY_OPTIONS)?; reject_unimplemented_selectors(invocation, &["node-id"])?; reject_unimplemented_verification_policy(invocation)?; - let direct_keys = ["pubkey-pem", "pubkey-der"] - .into_iter() - .flat_map(|name| invocation.values(name)) - .collect::>(); - let explicit_certificates = ["pubkey-cert-pem", "pubkey-cert-der"] - .into_iter() - .flat_map(|name| invocation.values(name)) - .collect::>(); - if direct_keys.len() + explicit_certificates.len() > 1 { - return Err(CommandError::Usage( - "verify accepts exactly one explicit public key or certificate".into(), - )); - } - let direct_path = direct_keys - .first() - .and_then(|option| option.value.as_deref()); + let explicit_keys = [ + ("pubkey-pem", false), + ("pubkey-der", false), + ("pubkey-cert-pem", true), + ("pubkey-cert-der", true), + ] + .into_iter() + .flat_map(|(name, certificate)| { + invocation + .values(name) + .map(move |option| (option, certificate)) + }) + .collect::>(); // With an explicit public key there is no key-manager search to relax. // Reject the flag on resolver-backed paths until its semantics exist. - if invocation.flag("lax-key-search") - && direct_path.is_none() - && explicit_certificates.is_empty() - { + if invocation.flag("lax-key-search") && explicit_keys.is_empty() { return Err(CommandError::UnsupportedOption("lax-key-search".into())); } let policy = xmlsec_compatibility_verification_policy(invocation); @@ -645,24 +664,25 @@ fn verify(invocation: &Invocation, stdout: &mut dyn Write) -> Result<(), Command let start_node_id = option_text(invocation, "node-id")?; let signature = key_material::verification_signature_metadata(&xml, start_node_id, &policy)?; let algorithm = signature.algorithm; - if let Some(identity) = direct_keys - .first() - .or_else(|| explicit_certificates.first()) - { - enforce_named_key_match( - identity, - signature.key_name.as_deref(), + let selected_key = if explicit_keys.is_empty() { + None + } else { + Some(select_named_candidate( + &explicit_keys, + &signature.key_names, invocation.flag("lax-key-search"), + true, "verification key", - )?; - } - let result = if let Some(path) = direct_path { + )?) + }; + let result = if let Some((option, false)) = selected_key { + let path = option.value.as_deref().unwrap_or_default(); let key = key_material::load_verification_key(path, algorithm)?; verification_context(policy, start_node_id) .key(&key) .verify(&xml) .map_err(|error| CommandError::Signature(error.to_string()))? - } else if let [certificate] = explicit_certificates.as_slice() { + } else if let Some((certificate, true)) = selected_key { verify_with_explicit_certificate( invocation, certificate, @@ -877,21 +897,32 @@ fn encrypt(invocation: &Invocation, stdout: &mut dyn Write) -> Result<(), Comman .map(move |option| (option, certificate)) }) .collect::>(); - if aes_keys.len() + public_keys.len() > 1 { + if !aes_keys.is_empty() && !public_keys.is_empty() { return Err(CommandError::Usage( - "encrypt accepts exactly one AES key or RSA public key".into(), + "encrypt cannot combine explicit AES and RSA recipient keys".into(), )); } - if let [option] = aes_keys.as_slice() { + if !aes_keys.is_empty() { if metadata.has_encrypted_key_recipient { return Err(CommandError::Usage( "direct AES key cannot satisfy an EncryptedKey recipient in the template".into(), )); } - enforce_named_key_match( - option, - metadata.content_key_name.as_deref(), + let candidates = aes_keys + .iter() + .copied() + .map(|option| (option, ())) + .collect::>(); + let requested_names = metadata + .content_key_name + .iter() + .cloned() + .collect::>(); + let (option, ()) = select_named_candidate( + &candidates, + &requested_names, invocation.flag("lax-key-search"), + true, "AES key", )?; let key = key_material::load_symmetric( @@ -902,15 +933,21 @@ fn encrypt(invocation: &Invocation, stdout: &mut dyn Write) -> Result<(), Comman if let Some(name) = option.parameter.as_deref() { builder = builder.direct_key_name(name); } - } else if let [(option, certificate)] = public_keys.as_slice() { - enforce_named_key_match( - option, - metadata.recipient_key_name.as_deref(), + } else if !public_keys.is_empty() { + let requested_names = metadata + .recipient_key_name + .iter() + .cloned() + .collect::>(); + let (option, certificate) = select_named_candidate( + &public_keys, + &requested_names, invocation.flag("lax-key-search"), + true, "RSA recipient key", )?; let path = option.value.as_deref().unwrap_or_default(); - let public_key = if *certificate { + let public_key = if certificate { key_material::load_rsa_certificate_public(path)? } else { key_material::load_rsa_public(path)? @@ -1111,6 +1148,7 @@ fn apply_encryption_template( for (range, replacement) in replacements { output.replace_range(range, &replacement); } + parse_encryption_document(&output, policy)?; Ok(output) } @@ -1203,16 +1241,23 @@ fn decrypt(invocation: &Invocation, stdout: &mut dyn Write) -> Result<(), Comman .into_iter() .flat_map(|name| invocation.values(name)) .collect::>(); - if aes_keys.len() + private_keys.len() > 1 { + if !aes_keys.is_empty() && !private_keys.is_empty() { return Err(CommandError::Usage( - "decrypt accepts exactly one AES key or RSA private key".into(), + "decrypt cannot combine explicit AES and RSA private keys".into(), )); } - let bytes = if let [option] = aes_keys.as_slice() { - enforce_named_key_match( - option, - content_key_name.as_deref(), + let bytes = if !aes_keys.is_empty() { + let candidates = aes_keys + .iter() + .copied() + .map(|option| (option, ())) + .collect::>(); + let requested_names = content_key_name.iter().cloned().collect::>(); + let (option, ()) = select_named_candidate( + &candidates, + &requested_names, invocation.flag("lax-key-search"), + true, "AES key", )?; let key = key_material::load_symmetric(option.value.as_deref().unwrap_or_default(), None)?; @@ -1223,7 +1268,19 @@ fn decrypt(invocation: &Invocation, stdout: &mut dyn Write) -> Result<(), Comman standalone, policy, )? - } else if let [option] = private_keys.as_slice() { + } else if !private_keys.is_empty() { + let candidates = private_keys + .iter() + .copied() + .map(|option| (option, ())) + .collect::>(); + let (option, ()) = select_named_candidate( + &candidates, + &recipient_key_names, + invocation.flag("lax-key-search"), + true, + "RSA private key", + )?; let recipient_filter = named_recipient_filter( option, &recipient_key_names, @@ -1433,32 +1490,6 @@ fn select_encrypted_data<'a>( Ok(selected) } -fn enforce_named_key_match( - option: &crate::OptionValue, - template_name: Option<&str>, - lax_key_search: bool, - key_kind: &str, -) -> Result<(), CommandError> { - let Some(option_name) = option.parameter.as_deref() else { - return Ok(()); - }; - // A missing KeyName does not request a different identity: with one - // explicit key, libxmlsec uses that key regardless of its registry name. - // Strict lookup applies when the document actually names an identity. - if lax_key_search { - return Ok(()); - } - let Some(template_name) = template_name else { - return Ok(()); - }; - if template_name == option_name { - return Ok(()); - } - Err(CommandError::Usage(format!( - "template KeyName {template_name} does not match named {key_kind} {option_name}" - ))) -} - fn keys(invocation: &Invocation, stdout: &mut dyn Write) -> Result<(), CommandError> { validate_options(invocation, KEYS_OPTIONS)?; let generated = invocation.values("gen-key").collect::>(); @@ -1739,6 +1770,35 @@ mod tests { ); } + #[test] + fn merged_encryption_template_obeys_the_aggregate_node_ceiling() { + // Template and generated output cross the trust boundary separately, + // but the returned document must also fit the same operation policy. + let extras = "".repeat(24); + let template = format!( + "{extras}" + ); + let generated = format!( + "a2V5ZGF0YQ==" + ); + let individual_node_ceiling = [template.as_str(), generated.as_str()] + .into_iter() + .map(|xml| Document::parse(xml).unwrap().descendants().count()) + .max() + .unwrap(); + let policy = EncryptionPolicy { + resources: xml_sec::policy::ResourcePolicy { + max_xml_nodes: individual_node_ceiling, + ..xml_sec::policy::ResourcePolicy::default() + }, + ..EncryptionPolicy::default() + }; + + let error = apply_encryption_template(&template, &generated, None, &policy) + .expect_err("the aggregate merged document must be reparsed under policy"); + assert!(error.to_string().contains("nodes limit"), "{error}"); + } + #[test] fn input_reader_enforces_the_compiled_policy_limit_before_parsing() { // The reader must stop at maximum + 1 rather than allocating an entire diff --git a/tools/xmlsec1/src/key_material.rs b/tools/xmlsec1/src/key_material.rs index 3a9358c..787eaf2 100644 --- a/tools/xmlsec1/src/key_material.rs +++ b/tools/xmlsec1/src/key_material.rs @@ -48,12 +48,12 @@ pub enum KeyMaterialError { #[derive(Debug, Eq, PartialEq)] pub struct SignatureMetadata { pub algorithm: SignatureAlgorithm, - pub key_name: Option, + pub key_names: Vec, } #[derive(Debug, Eq, PartialEq)] pub struct SigningTemplateMetadata { - pub key_name: Option, + pub key_names: Vec, pub has_key_info: bool, } @@ -102,7 +102,7 @@ pub fn verification_signature_metadata( .map_err(|error| KeyMaterialError::Signature(error.to_string()))?; Ok(SignatureMetadata { algorithm, - key_name: signature_key_name(signature), + key_names: signature_key_names(signature), }) } @@ -132,7 +132,7 @@ pub fn signing_signature_metadata( } .ok_or(KeyMaterialError::MissingSignedInfo)?; Ok(SigningTemplateMetadata { - key_name: signature_key_name(signature), + key_names: signature_key_names(signature), has_key_info: signature_key_info(signature).is_some(), }) } @@ -156,15 +156,14 @@ fn parse_signature_document( .map_err(|error| KeyMaterialError::Signature(error.to_string())) } -fn signature_key_name(signature: roxmltree::Node<'_, '_>) -> Option { +fn signature_key_names(signature: roxmltree::Node<'_, '_>) -> Vec { signature_key_info(signature) - .and_then(|key_info| { - key_info - .children() - .find(|node| node.has_tag_name(("http://www.w3.org/2000/09/xmldsig#", "KeyName"))) - }) - .and_then(|key_name| key_name.text()) + .into_iter() + .flat_map(|key_info| key_info.children()) + .filter(|node| node.has_tag_name(("http://www.w3.org/2000/09/xmldsig#", "KeyName"))) + .filter_map(|key_name| key_name.text()) .map(str::to_owned) + .collect() } fn signature_key_info<'a, 'input>( @@ -424,6 +423,25 @@ mod tests { assert_eq!(metadata.algorithm, SignatureAlgorithm::EcdsaSha256); } + #[test] + fn signature_metadata_preserves_every_direct_key_name() { + // KeyInfo is an ordered list of lookup sources; collapsing it to the + // first KeyName makes later valid key-manager entries unreachable. + let digest = base64::Engine::encode(&base64::engine::general_purpose::STANDARD, [0_u8; 32]); + let xml = format!( + r#"{digest}AA==oldwanted"# + ); + + let metadata = verification_signature_metadata( + &xml, + None, + &xml_sec::policy::VerificationPolicy::default(), + ) + .unwrap(); + + assert_eq!(metadata.key_names, ["old", "wanted"]); + } + #[test] fn signature_discovery_obeys_the_verification_node_ceiling() { // Metadata discovery runs before cryptographic verification and must diff --git a/tools/xmlsec1/tests/process_contract.rs b/tools/xmlsec1/tests/process_contract.rs index 28ee57e..31efdcb 100644 --- a/tools/xmlsec1/tests/process_contract.rs +++ b/tools/xmlsec1/tests/process_contract.rs @@ -1043,6 +1043,70 @@ fn named_aes_decryption_obeys_encrypted_data_key_name_unless_lax() { assert_eq!(lax.stdout, b"named decrypt"); } +#[test] +fn named_aes_key_ring_selects_one_key_for_encryption_and_decryption() { + // Repeatable AES options form a key ring. The document name must select + // exactly one entry rather than making every multi-key invocation invalid. + let temp = tempfile::tempdir().unwrap(); + let template = temp.path().join("named-template.xml"); + let plaintext = temp.path().join("plaintext.bin"); + let encrypted = temp.path().join("encrypted.xml"); + let wrong_key = temp.path().join("wrong.bin"); + let matching_key = temp.path().join("matching.bin"); + fs::write( + &template, + r#"selected"#, + ) + .unwrap(); + fs::write(&plaintext, b"selected AES key").unwrap(); + fs::write(&wrong_key, b"fedcba9876543210").unwrap(); + fs::write(&matching_key, b"0123456789abcdef").unwrap(); + + let encrypt = Command::new(binary()) + .args(["encrypt", "--aes-key:wrong"]) + .arg(&wrong_key) + .args(["--aes-key:selected"]) + .arg(&matching_key) + .arg("--binary-data") + .arg(&plaintext) + .arg("--output") + .arg(&encrypted) + .arg(&template) + .output() + .unwrap(); + assert!( + encrypt.status.success(), + "{}", + String::from_utf8_lossy(&encrypt.stderr) + ); + + let decrypt = Command::new(binary()) + .args(["decrypt", "--aes-key:wrong"]) + .arg(&wrong_key) + .args(["--aes-key:selected"]) + .arg(&matching_key) + .arg(&encrypted) + .output() + .unwrap(); + assert!( + decrypt.status.success(), + "{}", + String::from_utf8_lossy(&decrypt.stderr) + ); + assert_eq!(decrypt.stdout, b"selected AES key"); + + let ambiguous = Command::new(binary()) + .args(["decrypt", "--aes-key:selected"]) + .arg(&wrong_key) + .args(["--aes-key:selected"]) + .arg(&matching_key) + .arg(&encrypted) + .output() + .unwrap(); + assert!(!ambiguous.status.success()); + assert!(String::from_utf8_lossy(&ambiguous.stderr).contains("multiple AES key")); +} + #[test] fn standalone_binary_decryption_accepts_its_root_node_id() { // --node-id selects an operation start point; selecting the standalone root @@ -1236,6 +1300,59 @@ fn rsa_recipient_name_must_match_the_template_unless_lax() { assert_eq!(lax_decrypt.stdout, b"named recipient"); } +#[test] +fn named_rsa_key_ring_selects_one_recipient_for_encryption_and_decryption() { + // Recipient KeyName selects one public/private key pair from repeatable + // options on both sides of the process boundary. + let temp = tempfile::tempdir().unwrap(); + let template = temp.path().join("named-rsa-template.xml"); + let plaintext = temp.path().join("plaintext.bin"); + let encrypted = temp.path().join("encrypted.xml"); + let wrong_public = project_root().join("tests/fixtures/keys/rsa/rsa-2048-pubkey.pem"); + let matching_public = project_root().join("tests/fixtures/keys/rsa/rsa-4096-pubkey.pem"); + let wrong_private = project_root().join("tests/fixtures/keys/rsa/rsa-2048-key.pem"); + let matching_private = project_root().join("tests/fixtures/keys/rsa/rsa-4096-key.pem"); + fs::write( + &template, + r#"selected"#, + ) + .unwrap(); + fs::write(&plaintext, b"selected RSA recipient").unwrap(); + + let encrypt = Command::new(binary()) + .args(["encrypt", "--pubkey-pem:wrong"]) + .arg(&wrong_public) + .args(["--pubkey-pem:selected"]) + .arg(&matching_public) + .arg("--binary-data") + .arg(&plaintext) + .arg("--output") + .arg(&encrypted) + .arg(&template) + .output() + .unwrap(); + assert!( + encrypt.status.success(), + "{}", + String::from_utf8_lossy(&encrypt.stderr) + ); + + let decrypt = Command::new(binary()) + .args(["decrypt", "--privkey-pem:wrong"]) + .arg(&wrong_private) + .args(["--privkey-pem:selected"]) + .arg(&matching_private) + .arg(&encrypted) + .output() + .unwrap(); + assert!( + decrypt.status.success(), + "{}", + String::from_utf8_lossy(&decrypt.stderr) + ); + assert_eq!(decrypt.stdout, b"selected RSA recipient"); +} + #[test] fn named_decryption_key_selects_a_later_recipient() { // A document KeyName selects among all EncryptedKey recipients. Checking @@ -1772,6 +1889,17 @@ fn embedded_certificate_requires_trust_unless_insecure() { "{}", String::from_utf8_lossy(&insecure.stderr) ); + + let insecure_with_crls = Command::new(binary()) + .args(["verify", "--insecure", "--verify-crls"]) + .arg(&signed) + .output() + .unwrap(); + assert!( + insecure_with_crls.status.success(), + "{}", + String::from_utf8_lossy(&insecure_with_crls.stderr) + ); } #[test] @@ -1818,6 +1946,75 @@ fn signing_embeds_every_certificate_from_the_private_key_option() { ); } +#[test] +fn certificate_embedding_preserves_existing_key_info_sources() { + // Populating X509Data must not erase the KeyName that selected the signing + // key or create a second X509Data beside the template placeholder. + let temp = tempfile::tempdir().unwrap(); + let source = project_root() + .join("tests/fixtures/xmldsig/aleksey-xmldsig-01/enveloping-sha256-rsa-sha256.tmpl"); + let template = temp.path().join("key-info-template.xml"); + let private_key = project_root().join("tests/fixtures/keys/rsa/rsa-4096-key.pem"); + let public_key = project_root().join("tests/fixtures/keys/rsa/rsa-4096-pubkey.pem"); + let certificate = project_root().join("tests/fixtures/keys/rsa/rsa-4096-cert.pem"); + let signed = temp.path().join("signed.xml"); + let template_xml = fs::read_to_string(source).unwrap(); + fs::write(&template, template_xml).unwrap(); + let compound = format!("{},{}", private_key.display(), certificate.display()); + + let result = Command::new(binary()) + .args(["sign", "--privkey-pem:TestKeyName-rsa-2048"]) + .arg(compound) + .arg("--output") + .arg(&signed) + .arg(&template) + .output() + .unwrap(); + assert!( + result.status.success(), + "{}", + String::from_utf8_lossy(&result.stderr) + ); + let xml = fs::read_to_string(&signed).unwrap(); + let document = roxmltree::Document::parse(&xml).unwrap(); + assert_eq!( + document + .descendants() + .filter(|node| node.has_tag_name(("http://www.w3.org/2000/09/xmldsig#", "KeyName"))) + .filter_map(|node| node.text()) + .collect::>(), + ["TestKeyName-rsa-2048"] + ); + assert_eq!( + document + .descendants() + .filter(|node| node.has_tag_name(("http://www.w3.org/2000/09/xmldsig#", "X509Data"))) + .count(), + 1 + ); + assert_eq!( + document + .descendants() + .filter(|node| { + node.has_tag_name(("http://www.w3.org/2000/09/xmldsig#", "X509Certificate")) + }) + .count(), + 1 + ); + + let verify = Command::new(binary()) + .args(["verify", "--pubkey-pem:TestKeyName-rsa-2048"]) + .arg(&public_key) + .arg(&signed) + .output() + .unwrap(); + assert!( + verify.status.success(), + "{}", + String::from_utf8_lossy(&verify.stderr) + ); +} + #[test] fn signing_rejects_a_malformed_secondary_certificate() { // Every certificate path is parsed before signing; a malformed trailing @@ -2024,6 +2221,46 @@ fn multiple_signing_keys_require_the_matching_template_name() { assert!(String::from_utf8_lossy(&missing.stderr).contains("unknown KeyName")); } +#[test] +fn verification_selects_a_later_named_key_from_every_template_key_name() { + // Repeated public-key inputs model a key manager, and every direct + // KeyName is an ordered lookup source rather than only the first one. + let temp = tempfile::tempdir().unwrap(); + let template = project_root() + .join("tests/fixtures/xmldsig/aleksey-xmldsig-01/enveloping-sha256-rsa-sha256.tmpl"); + let private_key = project_root().join("tests/fixtures/keys/rsa/rsa-4096-key.pem"); + let matching_public = project_root().join("tests/fixtures/keys/rsa/rsa-4096-pubkey.pem"); + let wrong_public = project_root().join("tests/fixtures/keys/rsa/rsa-2048-pubkey.pem"); + let signed = Command::new(binary()) + .args(["sign", "--privkey-pem"]) + .arg(&private_key) + .arg(&template) + .output() + .unwrap(); + assert!(signed.status.success()); + let signed_path = temp.path().join("multiple-key-names.xml"); + let signed_xml = String::from_utf8(signed.stdout).unwrap().replace( + "TestKeyName-rsa-2048", + "staleselected", + ); + fs::write(&signed_path, signed_xml).unwrap(); + + let verified = Command::new(binary()) + .args(["verify", "--pubkey-pem:wrong"]) + .arg(&wrong_public) + .args(["--pubkey-pem:selected"]) + .arg(&matching_public) + .arg(&signed_path) + .output() + .unwrap(); + + assert!( + verified.status.success(), + "{}", + String::from_utf8_lossy(&verified.stderr) + ); +} + #[test] fn singleton_named_signing_key_obeys_template_key_name() { // Naming one key enables strict KeyName lookup even when the key set has a @@ -2180,25 +2417,25 @@ fn capability_queries_report_supported_and_unsupported_names() { } #[test] -fn conflicting_verification_keys_fail_before_input_parsing() { - let temp = tempfile::tempdir().unwrap(); - let malformed = temp.path().join("malformed.xml"); - fs::write(&malformed, "").unwrap(); +fn duplicate_named_verification_keys_are_rejected_as_ambiguous() { + // Repeatable keys are a lookup set, but one KeyName must never select two + // entries because silently choosing by option order would be unstable. + let signed = project_root() + .join("tests/fixtures/xmldsig/aleksey-xmldsig-01/enveloping-sha256-rsa-sha256.xml"); + let first = project_root().join("tests/fixtures/keys/rsa/rsa-2048-pubkey.pem"); + let second = project_root().join("tests/fixtures/keys/rsa/rsa-4096-pubkey.pem"); let conflicting_keys = Command::new(binary()) - .args([ - "verify", - "--pubkey-pem", - "first.pem", - "--pubkey-pem", - "second.pem", - ]) - .arg(&malformed) + .args(["verify", "--pubkey-pem:TestKeyName-rsa-2048"]) + .arg(&first) + .arg("--pubkey-pem:TestKeyName-rsa-2048") + .arg(&second) + .arg(&signed) .output() .unwrap(); assert!(!conflicting_keys.status.success()); assert!( String::from_utf8_lossy(&conflicting_keys.stderr) - .contains("exactly one explicit public key") + .contains("multiple verification key inputs match template KeyNames") ); } From 33ed66fea355afe392c355e1984bba01171aa542 Mon Sep 17 00:00:00 2001 From: Dmitry Prudnikov Date: Sat, 15 Aug 2026 14:29:36 +0300 Subject: [PATCH 18/27] fix(cli): preserve key metadata integrity - preserve KeyInfo placeholder identity before digesting references - reject recipient metadata that contradicts the selected RSA key - aggregate Manifest failures in donor-compatible diagnostics --- README.md | 3 + docs/cli.md | 12 +- src/xmldsig/mutation.rs | 127 +++++++++++++++- src/xmldsig/sign.rs | 42 +++--- tests/signing_digest.rs | 47 ++++++ tests/xpath_transform_integration.rs | 3 +- tools/xmlsec1/README.md | 6 +- tools/xmlsec1/src/commands.rs | 157 ++++++++++++++++++-- tools/xmlsec1/tests/process_contract.rs | 190 ++++++++++++++++++++++++ 9 files changed, 551 insertions(+), 36 deletions(-) diff --git a/README.md b/README.md index c14afbc..fa92374 100644 --- a/README.md +++ b/README.md @@ -90,6 +90,9 @@ one key unless lax lookup is requested; unnamed templates still use their sole explicit verification or encryption key. Certificate companions are validated even when no output `KeyInfo` placeholder is present; embedding a chain fills an empty `X509Data` placeholder without discarding sibling `KeyInfo` sources. +Populated `KeyInfo` is materialized before reference digests, allowing it to be +signed by ID. Preserved XMLEnc recipient key or certificate metadata must match +the selected RSA wrapping key instead of describing a stale recipient. document-supplied X.509 certificates require a caller trust anchor unless `--insecure` is explicit. XML payload encryption materializes inferred Element metadata, and direct AES keys reject templates containing recipient diff --git a/docs/cli.md b/docs/cli.md index 058d4b4..1f53222 100644 --- a/docs/cli.md +++ b/docs/cli.md @@ -64,8 +64,10 @@ validated, and the first certificate must contain the signing key. When the template contains the optional direct `KeyInfo` placeholder, the chain is embedded there in order under `X509Data`. An empty `X509Data` child is populated in place, preserving sibling sources such as the -`KeyName` used to select the signing key; omitting `KeyInfo` leaves the signed -output without `KeyInfo`. Named signing keys +`KeyName` used to select the signing key and attributes such as an `Id`. KeyInfo +materialization occurs before reference digest computation, so a template may +sign its populated `KeyInfo` by ID. Omitting `KeyInfo` leaves the signed output +without `KeyInfo`. Named signing keys require a matching template `KeyName` even when only one key is supplied. A named key with no template `KeyName` fails unless `--lax-key-search` explicitly opts out of lookup. Verification and encryption instead leave a `KeyName`-less @@ -110,6 +112,12 @@ then requires exactly one `EncryptedData` in its subtree. Encryption preserves the template's `Id`, `Type`, `MimeType`, `KeyInfo`, `EncryptionProperties`, and RSA-OAEP parameters while replacing only the cryptographic `CipherValue` payloads. +When nested recipient `KeyInfo` already carries `RSAKeyValue`, an X.509 +certificate, or `DEREncodedKeyValue`, that cryptographic identity must match +the selected RSA wrapping key. Unmatchable or contradictory metadata is +rejected before encryption rather than being preserved beside ciphertext for +a different recipient. `KeyName` remains a lookup hint and empty `X509Data` +remains a non-binding placeholder. For `--xml-data`, a missing template `Type` is materialized as XML Element metadata so a later embedded-document decrypt can perform XML replacement. Repeatable AES or RSA key options form a key set. A direct content-key `KeyName` diff --git a/src/xmldsig/mutation.rs b/src/xmldsig/mutation.rs index 7de625f..d5d6594 100644 --- a/src/xmldsig/mutation.rs +++ b/src/xmldsig/mutation.rs @@ -338,6 +338,7 @@ pub(super) fn merge_key_info_source_at_index_with_options( let document = parse_with_options(xml, policy)?; let source_document = roxmltree::Document::parse(key_info_source)?; let source = source_document.root_element(); + let source_content = element_inner_xml(key_info_source, source.range())?; let Some(signature) = signature_node(&document, target_signature) else { return Err(XmlMutationError::ValueCountMismatch { element: "Signature", @@ -367,8 +368,27 @@ pub(super) fn merge_key_info_source_at_index_with_options( && !node.children().any(|child| child.is_element()) && node.text().is_none_or(|text| text.trim().is_empty()) }) { - let mut output = xml.to_owned(); - output.replace_range(placeholder.range(), key_info_source); + let generated_namespace_attributes = source + .namespaces() + .filter(|namespace| { + placeholder.lookup_namespace_uri(namespace.name()) != Some(namespace.uri()) + }) + .map(|namespace| { + let attribute = namespace + .name() + .map_or_else(|| "xmlns".to_owned(), |prefix| format!("xmlns:{prefix}")); + format!( + " {attribute}=\"{}\"", + quick_xml::escape::escape(namespace.uri()) + ) + }) + .collect::(); + let output = replace_element_content( + xml, + placeholder.range(), + source_content, + &generated_namespace_attributes, + )?; parse_with_options(&output, policy)?; return Ok(output); } @@ -405,6 +425,79 @@ pub(super) fn merge_key_info_source_at_index_with_options( Ok(output) } +fn element_inner_xml(xml: &str, range: Range) -> Result<&str, XmlMutationError> { + let element = &xml[range]; + if element.trim_end().ends_with("/>") { + return Ok(""); + } + let content_start = + element_opening_end(element).ok_or(XmlMutationError::InvalidAppendTarget)?; + let content_end = element + .rfind(", + content: &str, + namespace_attributes: &str, +) -> Result { + let element = &xml[range.clone()]; + let mut output = xml.to_owned(); + if element.trim_end().ends_with("/>") { + let name_end = element[1..] + .find(|character: char| { + character.is_ascii_whitespace() || character == '/' || character == '>' + }) + .map(|offset| offset + 1) + .ok_or(XmlMutationError::InvalidAppendTarget)?; + let qualified_name = &element[1..name_end]; + let empty_end = element + .rfind("/>") + .ok_or(XmlMutationError::InvalidAppendTarget)?; + output.replace_range( + range, + &format!( + "{}{}>{}", + &element[..empty_end], + namespace_attributes, + content, + qualified_name + ), + ); + } else { + let content_start = + element_opening_end(element).ok_or(XmlMutationError::InvalidAppendTarget)?; + let content_end = element + .rfind("{}{}", + &element[..content_start - 1], + namespace_attributes, + content, + &element[content_end..] + ); + output.replace_range(range, &replacement); + } + Ok(output) +} + +fn element_opening_end(fragment: &str) -> Option { + let mut quote = None; + for (offset, character) in fragment.char_indices() { + match (quote, character) { + (None, '\'' | '"') => quote = Some(character), + (Some(delimiter), current) if delimiter == current => quote = None, + (None, '>') => return Some(offset + 1), + _ => {} + } + } + None +} + fn fill_dsig_values( xml: &str, local_name: &'static str, @@ -1042,4 +1135,34 @@ mod tests { assert_eq!(values, ["outer-new", "inner-keep"]); } + + #[test] + fn key_info_source_merge_preserves_placeholder_attributes() { + // Placeholder identity can be referenced from SignedInfo, so filling + // its children must not replace the element that owns the ID. + let source = r#""#; + let generated = r#"Y2VydA=="#; + + let merged = merge_key_info_source_at_index_with_options(source, generated, 0, None) + .expect("matching source must populate the placeholder"); + let document = roxmltree::Document::parse(&merged).expect("merged XML must parse"); + let x509_data = document + .descendants() + .find(|node| node.has_tag_name((XMLDSIG_NS, "X509Data"))) + .expect("X509Data"); + + assert_eq!(x509_data.attribute("Id"), Some("key-info")); + assert_eq!( + x509_data + .children() + .find(|node| node.has_tag_name((XMLDSIG_NS, "X509Certificate"))) + .and_then(|node| node.text()), + Some("Y2VydA==") + ); + assert!( + x509_data + .children() + .any(|node| node.has_tag_name(("urn:example:key-info", "Metadata"))) + ); + } } diff --git a/src/xmldsig/sign.rs b/src/xmldsig/sign.rs index 3846867..127d6f1 100644 --- a/src/xmldsig/sign.rs +++ b/src/xmldsig/sign.rs @@ -736,9 +736,11 @@ impl<'a> SignContext<'a> { /// Sign XML that already contains a `` template. /// /// The template must include empty `` and `` - /// targets. The pipeline fills reference digests, reparses the result, - /// canonicalizes ``, signs those canonical bytes, and fills the - /// base64 ``. + /// targets. The pipeline first materializes configured `` content, + /// then fills reference digests, reparses the result, canonicalizes + /// ``, signs those canonical bytes, and fills the base64 + /// ``. This ordering permits `` to be referenced + /// from `` without producing a stale digest. pub fn sign_template(&self, xml: &str) -> Result { self.policy.validate()?; self.policy.resources.validate_xml_document_len(xml.len())?; @@ -757,8 +759,24 @@ impl<'a> SignContext<'a> { let transform_options = TransformOptions::default() .allow_internal_dtd(self.policy.xml.allow_internal_dtd) .xpath_here_semantics(self.policy.xpath_here_semantics); + let with_key_info = if let Some(writer) = self.key_info_writer { + let key_info_content = writer.write_key_info(self.signing_key)?; + let populated = merge_key_info_source_at_index_with_options( + xml, + &key_info_content, + target_signature, + Some(&self.policy), + )?; + self.policy + .resources + .validate_xml_document_len(populated.len())?; + Some(populated) + } else { + None + }; + let prepared_xml = with_key_info.as_deref().unwrap_or(xml); let with_digests = fill_reference_digest_values_with_options( - xml, + prepared_xml, transform_options, Some(&self.policy), self.provider, @@ -806,21 +824,7 @@ impl<'a> SignContext<'a> { self.policy .resources .validate_xml_document_len(signed.len())?; - if let Some(writer) = self.key_info_writer { - let key_info_content = writer.write_key_info(self.signing_key)?; - let signed = merge_key_info_source_at_index_with_options( - &signed, - &key_info_content, - target_signature, - Some(&self.policy), - )?; - self.policy - .resources - .validate_xml_document_len(signed.len())?; - Ok(signed) - } else { - Ok(signed) - } + Ok(signed) } /// Build a signature template, append it to the selected start node (or diff --git a/tests/signing_digest.rs b/tests/signing_digest.rs index 6d3522d..9cfbdc1 100644 --- a/tests/signing_digest.rs +++ b/tests/signing_digest.rs @@ -871,6 +871,53 @@ fn signs_rsa_template_with_embedded_x509_key_info() { assert!(signed.contains("")); } +#[test] +fn key_info_writer_populates_signed_key_info_before_reference_digests() { + // KeyInfo may itself be a signed reference. Populate its template source + // before digesting so the final embedded certificate is what was signed. + let private_key = + RsaSigningKey::from_pkcs8_pem(&read_fixture("tests/fixtures/keys/rsa/rsa-2048-key.pem")) + .expect("RSA private key fixture must parse"); + let key_info_writer = X509CertificateKeyInfoWriter::from_pem(&read_fixture( + "tests/fixtures/keys/rsa/rsa-2048-cert.pem", + )) + .expect("RSA certificate fixture must parse"); + let template = SignatureBuilder::new(exclusive_c14n(), SignatureAlgorithm::RsaSha256) + .key_info(true) + .add_reference( + ReferenceBuilder::new(DigestAlgorithm::Sha256) + .uri("#payload") + .transform(Transform::C14n(exclusive_c14n())), + ) + .add_reference( + ReferenceBuilder::new(DigestAlgorithm::Sha256) + .uri("#key-info") + .transform(Transform::C14n(exclusive_c14n())), + ) + .build_template() + .expect("valid signature template") + .replace( + "", + "", + ); + let xml = append_signature_to_root( + "hello", + &template, + ) + .expect("append signature"); + + let signed = SignContext::new(&private_key) + .key_info_writer(&key_info_writer) + .sign_template(&xml) + .expect("signed KeyInfo template must succeed"); + let result = xml_sec::xmldsig::VerifyContext::new() + .key_resolver(&DefaultKeyResolver::default()) + .verify(&signed) + .expect("signed KeyInfo reference must verify without pipeline errors"); + + assert_eq!(result.status, DsigStatus::Valid); +} + #[test] fn key_info_writer_requires_direct_template_placeholder() { // The writer is intentionally opt-in and template-scoped. Without a direct diff --git a/tests/xpath_transform_integration.rs b/tests/xpath_transform_integration.rs index cc42f3a..6e148e3 100644 --- a/tests/xpath_transform_integration.rs +++ b/tests/xpath_transform_integration.rs @@ -250,7 +250,8 @@ fn signing_shares_xpath_work_across_references() { // Each Reference is below the per-transform limit, but resetting the meter // between References would allow aggregate work to grow with signature size. let document = format!("{}", "".repeat(1_000)); - let mut builder = SignatureBuilder::new(exclusive_c14n(), SignatureAlgorithm::RsaSha256); + let mut builder = + SignatureBuilder::new(exclusive_c14n(), SignatureAlgorithm::RsaSha256).key_info(true); for _ in 0..6 { builder = builder.add_reference( ReferenceBuilder::new(DigestAlgorithm::Sha256) diff --git a/tools/xmlsec1/README.md b/tools/xmlsec1/README.md index 57582b1..a649b46 100644 --- a/tools/xmlsec1/README.md +++ b/tools/xmlsec1/README.md @@ -17,7 +17,8 @@ coverage. The parser rejects repeated singleton options, including mixed canonical and alias spellings, while preserving donor multi-value key and certificate inputs. `--print-debug` is text; `--print-xml-debug` emits a parseable donor-shaped -`VerificationContext` for both successful and invalid verification results. +`VerificationContext` for both successful and invalid verification results, +including aggregate failures from authenticated Manifest references. `--aes-key` files are raw binary key material (`--aeskey` remains a compatible alias). Decrypting a standalone @@ -28,6 +29,9 @@ signing accepts PKCS#1 RSA and unencrypted PKCS#8 private keys, verification accepts SPKI, PKCS#1 RSA public keys, and X.509 certificates, and RSA encryption accepts public keys or recipient certificates. Supported formats are normalized into the corresponding core pipeline from PEM or DER. +Preserved recipient `RSAKeyValue`, X.509 certificate, and +`DEREncodedKeyValue` metadata must identify the selected RSA wrapping key; +contradictory metadata fails before ciphertext is emitted. Untyped `--xml-data` templates gain the inferred XML Element type, while direct AES keys reject recipient `EncryptedKey` templates they cannot refresh. Signing options validate every certificate from `key,leaf,intermediate,...` and embed the chain diff --git a/tools/xmlsec1/src/commands.rs b/tools/xmlsec1/src/commands.rs index 579dee3..1653878 100644 --- a/tools/xmlsec1/src/commands.rs +++ b/tools/xmlsec1/src/commands.rs @@ -7,13 +7,15 @@ use std::{ }; use roxmltree::{Document, Node, ParsingOptions}; +use rsa::{RsaPublicKey, pkcs8::DecodePublicKey as _, traits::PublicKeyParts as _}; +use x509_parser::prelude::FromDer as _; use xml_sec::{ policy::{DecryptionPolicy, EncryptionPolicy, SigningPolicy, VerificationPolicy}, provider::{CryptoProvider, default_provider}, xmldsig::{ - DefaultKeyResolver, DsigStatus, FailureReason, KeyInfoWriter, KeyResolver, - KeyResolverConfig, ReferenceResult, SignContext, SignatureAlgorithm, UriTypeSet, - VerifyContext, VerifyResult, X509CertificateKeyInfoWriter, XPathHereSemantics, + DefaultKeyResolver, DsigStatus, FailureReason, KeyInfoSource, KeyInfoWriter, KeyResolver, + KeyResolverConfig, KeyValueInfo, ReferenceResult, SignContext, SignatureAlgorithm, + UriTypeSet, VerifyContext, VerifyResult, X509CertificateKeyInfoWriter, XPathHereSemantics, parse_key_info, uri::UriReferenceResolver, }, xmlenc::{ @@ -719,12 +721,7 @@ fn verify(invocation: &Invocation, stdout: &mut dyn Write) -> Result<(), Command .map_err(|error| CommandError::Signature(error.to_string()))? }; write_verification_diagnostics(invocation, &result, stdout)?; - if result.status != DsigStatus::Valid - || result - .manifest_references - .iter() - .any(|reference| reference.status != DsigStatus::Valid) - { + if aggregate_verification_status(&result) != DsigStatus::Valid { return Err(CommandError::InvalidSignature); } Ok(()) @@ -735,8 +732,9 @@ fn write_verification_diagnostics( result: &VerifyResult, stdout: &mut dyn Write, ) -> Result<(), CommandError> { + let aggregate_status = aggregate_verification_status(result); if invocation.flag("print-debug") { - let status = if result.status == DsigStatus::Valid { + let status = if aggregate_status == DsigStatus::Valid { "valid" } else { "invalid" @@ -744,7 +742,7 @@ fn write_verification_diagnostics( writeln!(stdout, "Status: {status}").map_err(stdout_error)?; } if invocation.flag("print-xml-debug") { - let (status, failure_reason) = donor_dsig_status(result.status); + let (status, failure_reason) = donor_dsig_status(aggregate_status); writeln!( stdout, "" @@ -761,6 +759,29 @@ fn write_verification_diagnostics( Ok(()) } +fn aggregate_verification_status(result: &VerifyResult) -> DsigStatus { + aggregate_statuses( + result.status, + result + .manifest_references + .iter() + .map(|reference| reference.status), + ) +} + +fn aggregate_statuses( + core_status: DsigStatus, + manifest_statuses: impl IntoIterator, +) -> DsigStatus { + if core_status != DsigStatus::Valid { + return core_status; + } + manifest_statuses + .into_iter() + .find(|status| *status != DsigStatus::Valid) + .unwrap_or(DsigStatus::Valid) +} + fn write_reference_diagnostics( stdout: &mut dyn Write, container: &str, @@ -952,6 +973,7 @@ fn encrypt(invocation: &Invocation, stdout: &mut dyn Write) -> Result<(), Comman } else { key_material::load_rsa_public(path)? }; + validate_recipient_key_metadata(&template, start_node_id, &policy, &public_key)?; let mut recipient = EncryptionRecipient::rsa_oaep(public_key); if let Some(parameters) = metadata.oaep_parameters { recipient = recipient.oaep_parameters(parameters); @@ -995,6 +1017,105 @@ fn encrypt(invocation: &Invocation, stdout: &mut dyn Write) -> Result<(), Comman write_output(invocation, rendered.as_bytes(), stdout) } +fn validate_recipient_key_metadata( + template: &str, + start_node_id: Option<&str>, + policy: &EncryptionPolicy, + selected_key: &RsaPublicKey, +) -> Result<(), CommandError> { + let document = parse_encryption_document(template, policy)?; + let encrypted_data = select_encrypted_data(&document, start_node_id)?; + let key_infos = direct_child_element(encrypted_data, XMLDSIG_NS, "KeyInfo") + .into_iter() + .flat_map(|key_info| key_info.children()) + .filter(|node| node.has_tag_name((XMLENC_NS, "EncryptedKey"))) + .filter_map(|encrypted_key| direct_child_element(encrypted_key, XMLDSIG_NS, "KeyInfo")); + + for key_info_node in key_infos { + let key_info = parse_key_info(key_info_node) + .map_err(|error| CommandError::Encryption(error.to_string()))?; + for source in key_info.sources { + let matches = match source { + KeyInfoSource::KeyName(_) => continue, + KeyInfoSource::KeyValue(KeyValueInfo::Rsa { modulus, exponent }) => { + rsa_components_match(selected_key, &modulus, &exponent) + } + KeyInfoSource::X509Data(data) => { + if data.certificates.is_empty() + && data.subject_names.is_empty() + && data.issuer_serials.is_empty() + && data.skis.is_empty() + && data.digests.is_empty() + { + // An empty placeholder (or CRL-only source) makes no + // recipient identity claim and is safe to preserve. + continue; + } + let Some(certificate_index) = data + .certificate_chain + .first() + .copied() + .or_else(|| (!data.certificates.is_empty()).then_some(0)) + else { + return Err(recipient_metadata_error( + "X509Data identity does not contain a certificate that can be matched", + )); + }; + let certificate = + data.certificates.get(certificate_index).ok_or_else(|| { + recipient_metadata_error("X509Data certificate chain is inconsistent") + })?; + let (_, certificate) = + x509_parser::certificate::X509Certificate::from_der(certificate) + .map_err(|_| recipient_metadata_error("X509Certificate is invalid"))?; + let public_key = RsaPublicKey::from_public_key_der( + certificate.public_key().raw, + ) + .map_err(|_| { + recipient_metadata_error("X509Certificate does not contain an RSA key") + })?; + rsa_public_keys_match(selected_key, &public_key) + } + KeyInfoSource::DerEncodedKeyValue(der) => { + let public_key = RsaPublicKey::from_public_key_der(&der).map_err(|_| { + recipient_metadata_error("DEREncodedKeyValue is not an RSA public key") + })?; + rsa_public_keys_match(selected_key, &public_key) + } + KeyInfoSource::KeyValue(_) | KeyInfoSource::RetrievalMethod { .. } => { + return Err(recipient_metadata_error( + "recipient key source cannot be matched to the selected RSA key", + )); + } + _ => { + return Err(recipient_metadata_error( + "recipient key source cannot be matched to the selected RSA key", + )); + } + }; + if !matches { + return Err(recipient_metadata_error( + "recipient key metadata does not match the selected RSA key", + )); + } + } + } + Ok(()) +} + +fn rsa_components_match(key: &RsaPublicKey, modulus: &[u8], exponent: &[u8]) -> bool { + key.n().to_be_bytes_trimmed_vartime().as_ref() == modulus + && key.e().to_be_bytes_trimmed_vartime().as_ref() == exponent +} + +fn rsa_public_keys_match(left: &RsaPublicKey, right: &RsaPublicKey) -> bool { + left.n() == right.n() && left.e() == right.e() +} + +fn recipient_metadata_error(message: &str) -> CommandError { + CommandError::Encryption(format!("recipient key metadata is inconsistent: {message}")) +} + fn template_oaep_parameters( encrypted_data: Node<'_, '_>, ) -> Result, CommandError> { @@ -1852,4 +1973,18 @@ mod tests { Err(CommandError::PlaintextTooLarge { maximum: 4 }) )); } + + #[test] + fn verification_diagnostics_aggregate_manifest_failures() { + // libxmlsec1 reports the operation as failed when a processed Manifest + // reference fails, even though the core SignatureValue remains valid. + let aggregate = aggregate_statuses( + DsigStatus::Valid, + [DsigStatus::Invalid( + FailureReason::ReferenceDigestMismatch { ref_index: 0 }, + )], + ); + + assert_eq!(donor_dsig_status(aggregate), ("FAILED", "REFERENCE")); + } } diff --git a/tools/xmlsec1/tests/process_contract.rs b/tools/xmlsec1/tests/process_contract.rs index 31efdcb..3d16955 100644 --- a/tools/xmlsec1/tests/process_contract.rs +++ b/tools/xmlsec1/tests/process_contract.rs @@ -5,15 +5,18 @@ use std::{ process::{Command, Stdio}, }; +use base64::Engine as _; use rsa::{ RsaPrivateKey, RsaPublicKey, pkcs8::{DecodePrivateKey as _, DecodePublicKey as _, EncodePrivateKey as _}, + traits::PublicKeyParts as _, }; use xml_sec::{ c14n::{C14nAlgorithm, C14nMode}, xmldsig::{ DigestAlgorithm, ReferenceBuilder, RsaSigningKey, SignContext, SignatureAlgorithm, SignatureBuilder, Transform, XPathExpression, XPathHereSemantics, + mutation::append_signature_to_root, }, xmlenc::{DataEncryptionAlgorithm, EncryptedDataBuilder, EncryptionRecipient}, }; @@ -41,6 +44,23 @@ fn signature_template_without_key_info() -> &'static str { "## } +fn rsa_key_value(public_key: &RsaPublicKey) -> String { + let base64 = &base64::engine::general_purpose::STANDARD; + format!( + "{}{}", + base64.encode(public_key.n().to_be_bytes_trimmed_vartime()), + base64.encode(public_key.e().to_be_bytes_trimmed_vartime()) + ) +} + +fn x509_certificate_value(path: &Path) -> String { + let (_, pem) = x509_parser::pem::parse_x509_pem(&fs::read(path).unwrap()).unwrap(); + format!( + "{}", + base64::engine::general_purpose::STANDARD.encode(pem.contents) + ) +} + #[test] fn signs_verifies_and_rejects_tampering_through_process_api() { // Exercise the process boundary and prove a post-signature content change @@ -152,6 +172,75 @@ fn xml_debug_verification_output_matches_the_donor_xml_contract() { assert_eq!(invalid_root.attribute("failureReason"), Some("REFERENCE")); } +#[test] +fn xml_debug_reports_authenticated_manifest_reference_failure_at_the_root() { + // A Manifest digest failure invalidates the CLI operation even when the + // outer SignedInfo and SignatureValue remain cryptographically valid. + let temp = tempfile::tempdir().unwrap(); + let private_key_pem = + fs::read_to_string(project_root().join("tests/fixtures/keys/rsa/rsa-2048-key.pem")) + .unwrap(); + let private_key = RsaSigningKey::from_pkcs8_pem(&private_key_pem).unwrap(); + let public_key = project_root().join("tests/fixtures/keys/rsa/rsa-2048-pubkey.pem"); + let c14n = C14nAlgorithm::new(C14nMode::Exclusive1_0, false); + let digest_probe = SignatureBuilder::new(c14n.clone(), SignatureAlgorithm::RsaSha256) + .add_reference( + ReferenceBuilder::new(DigestAlgorithm::Sha256) + .uri("#manifest-payload") + .transform(Transform::C14n(c14n.clone())), + ); + let probe = SignContext::new(&private_key) + .sign_with_builder( + "original", + &digest_probe, + ) + .unwrap(); + let probe_document = roxmltree::Document::parse(&probe).unwrap(); + let manifest_digest = probe_document + .descendants() + .find(|node| node.has_tag_name(("http://www.w3.org/2000/09/xmldsig#", "DigestValue"))) + .and_then(|node| node.text()) + .unwrap(); + let template = SignatureBuilder::new(c14n.clone(), SignatureAlgorithm::RsaSha256) + .add_reference( + ReferenceBuilder::new(DigestAlgorithm::Sha256) + .uri("#manifest") + .transform(Transform::C14n(c14n)), + ) + .build_template() + .unwrap() + .replace( + "", + &format!( + "{manifest_digest}" + ), + ); + let unsigned = append_signature_to_root( + "original", + &template, + ) + .unwrap(); + let signed = SignContext::new(&private_key) + .sign_template(&unsigned) + .unwrap(); + let tampered = temp.path().join("manifest-tampered.xml"); + fs::write(&tampered, signed.replace(">original<", ">tampered<")).unwrap(); + + let output = Command::new(binary()) + .args(["verify", "--print-xml-debug", "--pubkey-pem"]) + .arg(&public_key) + .arg(&tampered) + .output() + .unwrap(); + + assert!(!output.status.success()); + let diagnostics = String::from_utf8(output.stdout).unwrap(); + let document = roxmltree::Document::parse(&diagnostics).unwrap(); + let root = document.root_element(); + assert_eq!(root.attribute("status"), Some("FAILED")); + assert_eq!(root.attribute("failureReason"), Some("REFERENCE")); +} + #[test] fn short_command_help_alias_reaches_process_dispatch() { // Parsing an alias is insufficient if command validation later rejects its @@ -1300,6 +1389,107 @@ fn rsa_recipient_name_must_match_the_template_unless_lax() { assert_eq!(lax_decrypt.stdout, b"named recipient"); } +#[test] +fn rsa_encryption_rejects_stale_recipient_key_value_metadata() { + // A template KeyValue is a cryptographic identity, not a selection hint. + // Preserving it while wrapping for another key would emit contradictory XML. + let temp = tempfile::tempdir().unwrap(); + let template = temp.path().join("rsa-key-value-template.xml"); + let plaintext = temp.path().join("plaintext.bin"); + let matching_path = project_root().join("tests/fixtures/keys/rsa/rsa-4096-pubkey.pem"); + let mismatching_path = project_root().join("tests/fixtures/keys/rsa/rsa-2048-pubkey.pem"); + let matching = + RsaPublicKey::from_public_key_pem(&fs::read_to_string(&matching_path).unwrap()).unwrap(); + fs::write( + &template, + format!( + r#"{}"#, + rsa_key_value(&matching) + ), + ) + .unwrap(); + fs::write(&plaintext, b"recipient identity").unwrap(); + + let accepted = Command::new(binary()) + .args(["encrypt", "--pubkey-pem"]) + .arg(&matching_path) + .arg("--binary-data") + .arg(&plaintext) + .arg(&template) + .output() + .unwrap(); + assert!( + accepted.status.success(), + "{}", + String::from_utf8_lossy(&accepted.stderr) + ); + + let rejected = Command::new(binary()) + .args(["encrypt", "--pubkey-pem"]) + .arg(&mismatching_path) + .arg("--binary-data") + .arg(&plaintext) + .arg(&template) + .output() + .unwrap(); + assert!(!rejected.status.success()); + assert!( + String::from_utf8_lossy(&rejected.stderr).contains("recipient key metadata"), + "{}", + String::from_utf8_lossy(&rejected.stderr) + ); +} + +#[test] +fn rsa_encryption_rejects_stale_recipient_certificate_metadata() { + // X509Data identifies the same recipient as the wrapped content key. A + // certificate for another key must fail before any contradictory output. + let temp = tempfile::tempdir().unwrap(); + let template = temp.path().join("x509-recipient-template.xml"); + let plaintext = temp.path().join("plaintext.bin"); + let certificate = project_root().join("tests/fixtures/keys/rsa/rsa-4096-cert.pem"); + let matching_key = project_root().join("tests/fixtures/keys/rsa/rsa-4096-pubkey.pem"); + let mismatching_key = project_root().join("tests/fixtures/keys/rsa/rsa-2048-pubkey.pem"); + fs::write( + &template, + format!( + r#"{}"#, + x509_certificate_value(&certificate) + ), + ) + .unwrap(); + fs::write(&plaintext, b"certificate recipient identity").unwrap(); + + let accepted = Command::new(binary()) + .args(["encrypt", "--pubkey-pem"]) + .arg(&matching_key) + .arg("--binary-data") + .arg(&plaintext) + .arg(&template) + .output() + .unwrap(); + assert!( + accepted.status.success(), + "{}", + String::from_utf8_lossy(&accepted.stderr) + ); + + let rejected = Command::new(binary()) + .args(["encrypt", "--pubkey-pem"]) + .arg(&mismatching_key) + .arg("--binary-data") + .arg(&plaintext) + .arg(&template) + .output() + .unwrap(); + assert!(!rejected.status.success()); + assert!( + String::from_utf8_lossy(&rejected.stderr).contains("recipient key metadata"), + "{}", + String::from_utf8_lossy(&rejected.stderr) + ); +} + #[test] fn named_rsa_key_ring_selects_one_recipient_for_encryption_and_decryption() { // Recipient KeyName selects one public/private key pair from repeatable From 7e1f27cb79756a79e1f5cc9b31ef90c7558d2bb9 Mon Sep 17 00:00:00 2001 From: Dmitry Prudnikov Date: Sat, 15 Aug 2026 14:51:43 +0300 Subject: [PATCH 19/27] fix(cli): preserve generated key metadata - merge complete KeyInfo writer fragments and replace stale key sources - normalize RSA recipient metadata and populate reserved encryption KeyInfo - strengthen signing, manifest, and recipient process regressions --- README.md | 2 +- src/xmldsig/mutation.rs | 210 ++++++++++++++++++++++-- tests/signing_digest.rs | 113 +++++++++++++ tools/xmlsec1/src/commands.rs | 102 ++++++++++-- tools/xmlsec1/tests/process_contract.rs | 156 +++++++++++++++++- 5 files changed, 548 insertions(+), 35 deletions(-) diff --git a/README.md b/README.md index fa92374..8b89ba2 100644 --- a/README.md +++ b/README.md @@ -93,7 +93,7 @@ empty `X509Data` placeholder without discarding sibling `KeyInfo` sources. Populated `KeyInfo` is materialized before reference digests, allowing it to be signed by ID. Preserved XMLEnc recipient key or certificate metadata must match the selected RSA wrapping key instead of describing a stale recipient. -document-supplied X.509 certificates require a caller trust anchor unless +Document-supplied X.509 certificates require a caller trust anchor unless `--insecure` is explicit. XML payload encryption materializes inferred Element metadata, and direct AES keys reject templates containing recipient `EncryptedKey` metadata they cannot refresh. Its process tests run a minimal diff --git a/src/xmldsig/mutation.rs b/src/xmldsig/mutation.rs index d5d6594..7de27d5 100644 --- a/src/xmldsig/mutation.rs +++ b/src/xmldsig/mutation.rs @@ -334,6 +334,92 @@ pub(super) fn merge_key_info_source_at_index_with_options( key_info_source: &str, target_signature: usize, policy: Option<&crate::policy::SigningPolicy>, +) -> Result { + let document = parse_with_options(xml, policy)?; + let Some(signature) = signature_node(&document, target_signature) else { + return Err(XmlMutationError::ValueCountMismatch { + element: "Signature", + expected: 1, + actual: 0, + }); + }; + let key_infos = signature + .children() + .filter(|node| is_dsig_node(*node, "KeyInfo")) + .collect::>(); + if key_infos.len() != 1 { + return Err(XmlMutationError::ValueCountMismatch { + element: "KeyInfo", + expected: key_infos.len(), + actual: 1, + }); + } + let key_info = key_infos[0]; + + // The writer contract is XML child content, not a standalone document. + // Parse it under the template's namespace context so multiple siblings and + // inherited prefixes have exactly the semantics they will have in KeyInfo. + let wrapped_source = wrap_key_info_children(key_info_source, key_info); + let source_document = roxmltree::Document::parse(&wrapped_source)?; + let sources = source_document + .root_element() + .children() + .filter(|node| node.is_element()) + .map(|node| { + Ok(( + node.tag_name().namespace().map(str::to_owned), + node.tag_name().name().to_owned(), + standalone_element(&wrapped_source, node)?, + )) + }) + .collect::, XmlMutationError>>()?; + if sources.is_empty() { + return Err(XmlMutationError::InvalidAppendTarget); + } + + let generated_key_sources = sources + .iter() + .filter(|(namespace, name, _)| is_cryptographic_key_info_source(namespace.as_deref(), name)) + .map(|(namespace, name, _)| (namespace.as_deref(), name.as_str())) + .collect::>(); + let mut output = xml.to_owned(); + if !generated_key_sources.is_empty() { + // Writer-provided cryptographic identity is authoritative. Retaining a + // populated template source could make a resolver select a stale key; + // selection metadata and extension elements remain untouched. + let mut stale_ranges = key_info + .children() + .filter(|node| node.is_element()) + .filter(|node| { + is_cryptographic_key_info_source( + node.tag_name().namespace(), + node.tag_name().name(), + ) && !is_matching_empty_placeholder(*node, &generated_key_sources) + }) + .map(|node| node.range()) + .collect::>(); + stale_ranges.sort_by_key(|range| std::cmp::Reverse(range.start)); + for range in stale_ranges { + output.replace_range(range, ""); + } + } + + for (_, _, source) in sources { + output = merge_one_key_info_source_at_index_with_options( + &output, + &source, + target_signature, + policy, + )?; + } + Ok(output) +} + +fn merge_one_key_info_source_at_index_with_options( + xml: &str, + key_info_source: &str, + target_signature: usize, + policy: Option<&crate::policy::SigningPolicy>, ) -> Result { let document = parse_with_options(xml, policy)?; let source_document = roxmltree::Document::parse(key_info_source)?; @@ -359,30 +445,35 @@ pub(super) fn merge_key_info_source_at_index_with_options( } let key_info = key_infos[0]; - // A writer contributes one KeyInfo source, not the whole KeyInfo value. - // Fill an empty matching placeholder when present; otherwise append the - // source while preserving every template-provided sibling and its order. if let Some(placeholder) = key_info.children().find(|node| { node.is_element() && node.tag_name() == source.tag_name() && !node.children().any(|child| child.is_element()) && node.text().is_none_or(|text| text.trim().is_empty()) }) { - let generated_namespace_attributes = source - .namespaces() - .filter(|namespace| { - placeholder.lookup_namespace_uri(namespace.name()) != Some(namespace.uri()) - }) - .map(|namespace| { - let attribute = namespace - .name() - .map_or_else(|| "xmlns".to_owned(), |prefix| format!("xmlns:{prefix}")); - format!( - " {attribute}=\"{}\"", - quick_xml::escape::escape(namespace.uri()) - ) - }) - .collect::(); + let generated_namespace_attributes = + source + .namespaces() + .try_fold(String::new(), |mut attributes, namespace| { + let current = placeholder.lookup_namespace_uri(namespace.name()); + if current == Some(namespace.uri()) { + return Ok(attributes); + } + let inherited = placeholder + .parent_element() + .and_then(|parent| parent.lookup_namespace_uri(namespace.name())); + if current.is_some() && current != inherited { + return Err(XmlMutationError::InvalidAppendTarget); + } + let attribute = namespace + .name() + .map_or_else(|| "xmlns".to_owned(), |prefix| format!("xmlns:{prefix}")); + attributes.push_str(&format!( + " {attribute}=\"{}\"", + quick_xml::escape::escape(namespace.uri()) + )); + Ok(attributes) + })?; let output = replace_element_content( xml, placeholder.range(), @@ -425,6 +516,76 @@ pub(super) fn merge_key_info_source_at_index_with_options( Ok(output) } +fn wrap_key_info_children(source: &str, key_info: roxmltree::Node<'_, '_>) -> String { + let mut wrapper = String::from("'); + wrapper.push_str(source); + wrapper.push_str(""); + wrapper +} + +fn standalone_element( + source: &str, + node: roxmltree::Node<'_, '_>, +) -> Result { + let fragment = &source[node.range()]; + let opening_end = element_opening_end(fragment).ok_or(XmlMutationError::InvalidAppendTarget)?; + let opening = &fragment[..opening_end - 1]; + let name_end = opening[1..] + .find(|character: char| character.is_ascii_whitespace()) + .map_or(opening.len(), |offset| offset + 1); + let namespace_insertion = opening.strip_suffix('/').map_or(opening.len(), str::len); + let mut output = opening[..namespace_insertion].to_owned(); + for namespace in node.namespaces() { + let declaration = namespace + .name() + .map_or_else(|| "xmlns".to_owned(), |prefix| format!("xmlns:{prefix}")); + if !opening[name_end..].contains(&format!("{declaration}=")) { + output.push_str(&format!( + " {declaration}=\"{}\"", + quick_xml::escape::escape(namespace.uri()) + )); + } + } + output.push_str(&opening[namespace_insertion..]); + output.push_str(&fragment[opening_end - 1..]); + Ok(output) +} + +fn is_matching_empty_placeholder( + node: roxmltree::Node<'_, '_>, + generated_sources: &[(Option<&str>, &str)], +) -> bool { + generated_sources.iter().any(|(namespace, name)| { + node.tag_name().namespace() == *namespace + && node.tag_name().name() == *name + && !node.children().any(|child| child.is_element()) + && node.text().is_none_or(|text| text.trim().is_empty()) + }) +} + +fn is_cryptographic_key_info_source(namespace: Option<&str>, name: &str) -> bool { + matches!( + (namespace, name), + ( + Some(XMLDSIG_NS), + "KeyValue" | "RetrievalMethod" | "X509Data" | "PGPData" | "SPKIData" + ) | ( + Some("http://www.w3.org/2009/xmldsig11#"), + "DEREncodedKeyValue" | "KeyInfoReference" + ) + ) +} + fn element_inner_xml(xml: &str, range: Range) -> Result<&str, XmlMutationError> { let element = &xml[range]; if element.trim_end().ends_with("/>") { @@ -1165,4 +1326,17 @@ mod tests { .any(|node| node.has_tag_name(("urn:example:key-info", "Metadata"))) ); } + + #[test] + fn key_info_source_merge_rejects_conflicting_placeholder_namespaces() { + // A generated child cannot reuse a prefix that the placeholder owns + // with another URI; emitting both declarations would create invalid XML. + let source = r#""#; + let generated = r#""#; + + let error = merge_key_info_source_at_index_with_options(source, generated, 0, None) + .expect_err("conflicting namespace bindings must fail before serialization"); + + assert!(matches!(error, XmlMutationError::InvalidAppendTarget)); + } } diff --git a/tests/signing_digest.rs b/tests/signing_digest.rs index 9cfbdc1..469bbd4 100644 --- a/tests/signing_digest.rs +++ b/tests/signing_digest.rs @@ -916,6 +916,119 @@ fn key_info_writer_populates_signed_key_info_before_reference_digests() { .expect("signed KeyInfo reference must verify without pipeline errors"); assert_eq!(result.status, DsigStatus::Valid); + let document = roxmltree::Document::parse(&signed).expect("signed XML must parse"); + let certificates = document + .descendants() + .filter(|node| node.has_tag_name(("http://www.w3.org/2000/09/xmldsig#", "X509Certificate"))) + .collect::>(); + assert_eq!(certificates.len(), 1); + assert!(certificates[0].text().is_some_and(|text| !text.is_empty())); +} + +#[test] +fn key_info_writer_accepts_multiple_direct_child_fragments() { + // KeyInfoWriter returns child content, so sibling sources are valid output + // and must be merged without requiring a synthetic single root from callers. + struct MultiSourceWriter(X509CertificateKeyInfoWriter); + + impl KeyInfoWriter for MultiSourceWriter { + fn write_key_info( + &self, + signing_key: &dyn SigningKey, + ) -> Result { + Ok(format!( + "selected{}", + "http://www.w3.org/2000/09/xmldsig#", + self.0.write_key_info(signing_key)? + )) + } + } + + let private_key = + RsaSigningKey::from_pkcs8_pem(&read_fixture("tests/fixtures/keys/rsa/rsa-2048-key.pem")) + .expect("RSA private key fixture must parse"); + let writer = MultiSourceWriter( + X509CertificateKeyInfoWriter::from_pem(&read_fixture( + "tests/fixtures/keys/rsa/rsa-2048-cert.pem", + )) + .expect("RSA certificate fixture must parse"), + ); + let builder = SignatureBuilder::new(exclusive_c14n(), SignatureAlgorithm::RsaSha256) + .key_info(true) + .add_reference( + ReferenceBuilder::new(DigestAlgorithm::Sha256) + .uri("#payload") + .transform(Transform::C14n(exclusive_c14n())), + ); + + let signed = SignContext::new(&private_key) + .key_info_writer(&writer) + .sign_with_builder( + "hello", + &builder, + ) + .expect("multiple KeyInfo child fragments must be accepted"); + let result = xml_sec::xmldsig::VerifyContext::new() + .key_resolver(&DefaultKeyResolver::default()) + .verify(&signed) + .expect("writer certificate must resolve"); + + assert_eq!(result.status, DsigStatus::Valid); + assert!(signed.contains(">selected")); +} + +#[test] +fn key_info_writer_replaces_stale_cryptographic_sources() { + // Writer-provided key material is authoritative. Keeping an older source + // first would make the default resolver verify with the wrong public key. + let stale_key = + RsaSigningKey::from_pkcs8_pem(&read_fixture("tests/fixtures/keys/rsa/rsa-4096-key.pem")) + .expect("stale RSA private key fixture must parse"); + let stale_writer = X509CertificateKeyInfoWriter::from_pem(&read_fixture( + "tests/fixtures/keys/rsa/rsa-4096-cert.pem", + )) + .expect("stale RSA certificate fixture must parse"); + let stale_source = stale_writer + .write_key_info(&stale_key) + .expect("stale KeyInfo source must render"); + let private_key = + RsaSigningKey::from_pkcs8_pem(&read_fixture("tests/fixtures/keys/rsa/rsa-2048-key.pem")) + .expect("RSA private key fixture must parse"); + let writer = X509CertificateKeyInfoWriter::from_pem(&read_fixture( + "tests/fixtures/keys/rsa/rsa-2048-cert.pem", + )) + .expect("RSA certificate fixture must parse"); + let template = SignatureBuilder::new(exclusive_c14n(), SignatureAlgorithm::RsaSha256) + .key_info(true) + .add_reference( + ReferenceBuilder::new(DigestAlgorithm::Sha256) + .uri("#payload") + .transform(Transform::C14n(exclusive_c14n())), + ) + .build_template() + .expect("valid signature template") + .replace( + "", + &format!("selected{stale_source}"), + ); + let xml = append_signature_to_root( + "hello", + &template, + ) + .expect("append signature"); + + let signed = SignContext::new(&private_key) + .key_info_writer(&writer) + .sign_template(&xml) + .expect("authoritative KeyInfo source must replace stale material"); + let result = xml_sec::xmldsig::VerifyContext::new() + .key_resolver(&DefaultKeyResolver::default()) + .verify(&signed) + .expect("replacement certificate must resolve"); + + assert_eq!(result.status, DsigStatus::Valid); + assert!(signed.contains(">selected")); + assert_eq!(signed.matches(" bool { - key.n().to_be_bytes_trimmed_vartime().as_ref() == modulus - && key.e().to_be_bytes_trimmed_vartime().as_ref() == exponent + key.n().to_be_bytes_trimmed_vartime().as_ref() == trim_crypto_binary_zeroes(modulus) + && key.e().to_be_bytes_trimmed_vartime().as_ref() == trim_crypto_binary_zeroes(exponent) +} + +fn trim_crypto_binary_zeroes(value: &[u8]) -> &[u8] { + let first_nonzero = value + .iter() + .position(|byte| *byte != 0) + .unwrap_or(value.len()); + &value[first_nonzero..] } fn rsa_public_keys_match(left: &RsaPublicKey, right: &RsaPublicKey) -> bool { @@ -1238,20 +1246,37 @@ fn apply_encryption_template( .descendants() .filter(|node| node.has_tag_name((XMLENC_NS, "CipherValue"))) .collect::>(); - if template_values.len() != generated_values.len() { + if template_values.is_empty() && !generated_values.is_empty() { + let generated_keys = generated_key_info + .children() + .filter(|node| node.has_tag_name((XMLENC_NS, "EncryptedKey"))) + .map(|node| standalone_element(generated, node)) + .collect::, _>>()?; + if generated_keys.len() != generated_values.len() { + return Err(CommandError::Encryption( + "generated KeyInfo does not contain one direct EncryptedKey per recipient" + .into(), + )); + } + replacements.push(( + template_key_info.range(), + append_element_children(template, template_key_info, &generated_keys.concat())?, + )); + } else if template_values.len() != generated_values.len() { return Err(CommandError::Encryption( "template KeyInfo does not contain one CipherValue per generated recipient" .into(), )); + } else { + replacements.extend(template_values.into_iter().zip(generated_values).map( + |(template_value, generated_value)| { + ( + template_value.range(), + standalone_cipher_value(generated_value), + ) + }, + )); } - replacements.extend(template_values.into_iter().zip(generated_values).map( - |(template_value, generated_value)| { - ( - template_value.range(), - standalone_cipher_value(generated_value), - ) - }, - )); } (None, Some(generated_key_info)) => { let cipher_data = direct_child_element(template_data, XMLENC_NS, "CipherData") @@ -1273,6 +1298,36 @@ fn apply_encryption_template( Ok(output) } +fn append_element_children( + source: &str, + node: roxmltree::Node<'_, '_>, + children: &str, +) -> Result { + let fragment = &source[node.range()]; + if fragment.trim_end().ends_with("/>") { + let empty_end = fragment + .rfind("/>") + .ok_or_else(|| CommandError::Encryption("template KeyInfo is malformed".into()))?; + let name_end = fragment[1..] + .find(|ch: char| ch.is_ascii_whitespace() || matches!(ch, '/' | '>')) + .map(|offset| offset + 1) + .ok_or_else(|| CommandError::Encryption("template KeyInfo is malformed".into()))?; + let qualified_name = &fragment[1..name_end]; + return Ok(format!( + "{}>{children}", + &fragment[..empty_end] + )); + } + let closing = fragment + .rfind(" Option { let mut quote = None; for (offset, ch) in fragment.char_indices() { @@ -1891,6 +1946,31 @@ mod tests { ); } + #[test] + fn generated_recipient_expands_an_empty_key_info_placeholder() { + // An empty KeyInfo reserves the schema position but not an EncryptedKey + // skeleton; generated recipient metadata must expand it in place. + let template = format!( + "" + ); + let generated = format!( + "a2V5ZGF0YQ==" + ); + + let rendered = + apply_encryption_template(&template, &generated, None, &EncryptionPolicy::default()) + .expect("empty KeyInfo must accept a generated recipient"); + let document = Document::parse(&rendered).expect("merged output must parse"); + + assert_eq!( + document + .descendants() + .filter(|node| node.has_tag_name((XMLENC_NS, "EncryptedKey"))) + .count(), + 1 + ); + } + #[test] fn merged_encryption_template_obeys_the_aggregate_node_ceiling() { // Template and generated output cross the trust boundary separately, diff --git a/tools/xmlsec1/tests/process_contract.rs b/tools/xmlsec1/tests/process_contract.rs index 3d16955..a17f923 100644 --- a/tools/xmlsec1/tests/process_contract.rs +++ b/tools/xmlsec1/tests/process_contract.rs @@ -8,7 +8,9 @@ use std::{ use base64::Engine as _; use rsa::{ RsaPrivateKey, RsaPublicKey, - pkcs8::{DecodePrivateKey as _, DecodePublicKey as _, EncodePrivateKey as _}, + pkcs8::{ + DecodePrivateKey as _, DecodePublicKey as _, EncodePrivateKey as _, EncodePublicKey as _, + }, traits::PublicKeyParts as _, }; use xml_sec::{ @@ -44,12 +46,30 @@ fn signature_template_without_key_info() -> &'static str { "## } -fn rsa_key_value(public_key: &RsaPublicKey) -> String { +fn rsa_key_value_with_leading_zeroes( + public_key: &RsaPublicKey, + modulus_zeroes: usize, + exponent_zeroes: usize, +) -> String { let base64 = &base64::engine::general_purpose::STANDARD; + let mut modulus = vec![0; modulus_zeroes]; + modulus.extend(public_key.n().to_be_bytes_trimmed_vartime()); + let mut exponent = vec![0; exponent_zeroes]; + exponent.extend(public_key.e().to_be_bytes_trimmed_vartime()); format!( "{}{}", - base64.encode(public_key.n().to_be_bytes_trimmed_vartime()), - base64.encode(public_key.e().to_be_bytes_trimmed_vartime()) + base64.encode(modulus), + base64.encode(exponent) + ) +} + +fn der_encoded_key_value(public_key: &RsaPublicKey) -> String { + let der = public_key + .to_public_key_der() + .expect("fixture public key must encode as SPKI"); + format!( + "{}", + base64::engine::general_purpose::STANDARD.encode(der.as_bytes()) ) } @@ -239,6 +259,24 @@ fn xml_debug_reports_authenticated_manifest_reference_failure_at_the_root() { let root = document.root_element(); assert_eq!(root.attribute("status"), Some("FAILED")); assert_eq!(root.attribute("failureReason"), Some("REFERENCE")); + let signed_info_statuses = root + .children() + .find(|node| node.has_tag_name("SignedInfoReferences")) + .expect("SignedInfo diagnostics") + .children() + .filter(|node| node.has_tag_name("ReferenceVerificationContext")) + .filter_map(|node| node.attribute("status")) + .collect::>(); + let manifest_statuses = root + .children() + .find(|node| node.has_tag_name("ManifestReferences")) + .expect("Manifest diagnostics") + .children() + .filter(|node| node.has_tag_name("ReferenceVerificationContext")) + .filter_map(|node| node.attribute("status")) + .collect::>(); + assert_eq!(signed_info_statuses, ["OK"]); + assert_eq!(manifest_statuses, ["FAILED"]); } #[test] @@ -1404,7 +1442,7 @@ fn rsa_encryption_rejects_stale_recipient_key_value_metadata() { &template, format!( r#"{}"#, - rsa_key_value(&matching) + rsa_key_value_with_leading_zeroes(&matching, 1, 2) ), ) .unwrap(); @@ -1440,6 +1478,53 @@ fn rsa_encryption_rejects_stale_recipient_key_value_metadata() { ); } +#[test] +fn rsa_encryption_validates_der_encoded_recipient_metadata() { + // DEREncodedKeyValue identifies the wrapping recipient just like + // RSAKeyValue and X509Data: matching SPKI is accepted, stale SPKI is not. + let temp = tempfile::tempdir().unwrap(); + let template = temp.path().join("der-recipient-template.xml"); + let plaintext = temp.path().join("plaintext.bin"); + let matching_path = project_root().join("tests/fixtures/keys/rsa/rsa-4096-pubkey.pem"); + let mismatching_path = project_root().join("tests/fixtures/keys/rsa/rsa-2048-pubkey.pem"); + let matching = + RsaPublicKey::from_public_key_pem(&fs::read_to_string(&matching_path).unwrap()).unwrap(); + fs::write( + &template, + format!( + r#"{}"#, + der_encoded_key_value(&matching) + ), + ) + .unwrap(); + fs::write(&plaintext, b"DER recipient identity").unwrap(); + + let accepted = Command::new(binary()) + .args(["encrypt", "--pubkey-pem"]) + .arg(&matching_path) + .arg("--binary-data") + .arg(&plaintext) + .arg(&template) + .output() + .unwrap(); + assert!( + accepted.status.success(), + "{}", + String::from_utf8_lossy(&accepted.stderr) + ); + + let rejected = Command::new(binary()) + .args(["encrypt", "--pubkey-pem"]) + .arg(&mismatching_path) + .arg("--binary-data") + .arg(&plaintext) + .arg(&template) + .output() + .unwrap(); + assert!(!rejected.status.success()); + assert!(String::from_utf8_lossy(&rejected.stderr).contains("recipient key metadata")); +} + #[test] fn rsa_encryption_rejects_stale_recipient_certificate_metadata() { // X509Data identifies the same recipient as the wrapped content key. A @@ -1543,6 +1628,67 @@ fn named_rsa_key_ring_selects_one_recipient_for_encryption_and_decryption() { assert_eq!(decrypt.stdout, b"selected RSA recipient"); } +#[test] +fn rsa_encryption_populates_an_existing_key_info_container() { + // A template may reserve KeyInfo for non-cryptographic metadata without + // pre-creating EncryptedKey. Encryption must add the generated recipient + // while preserving those sibling sources. + let temp = tempfile::tempdir().unwrap(); + let template = temp.path().join("key-info-container-template.xml"); + let plaintext = temp.path().join("plaintext.bin"); + let encrypted = temp.path().join("encrypted.xml"); + let public_key = project_root().join("tests/fixtures/keys/rsa/rsa-4096-pubkey.pem"); + let private_key = project_root().join("tests/fixtures/keys/rsa/rsa-4096-key.pem"); + fs::write( + &template, + r#"content-key"#, + ) + .unwrap(); + fs::write(&plaintext, b"existing KeyInfo container").unwrap(); + + let encrypt = Command::new(binary()) + .args(["encrypt", "--pubkey-pem"]) + .arg(&public_key) + .arg("--binary-data") + .arg(&plaintext) + .arg("--output") + .arg(&encrypted) + .arg(&template) + .output() + .unwrap(); + assert!( + encrypt.status.success(), + "{}", + String::from_utf8_lossy(&encrypt.stderr) + ); + let encrypted_xml = fs::read_to_string(&encrypted).unwrap(); + let document = roxmltree::Document::parse(&encrypted_xml).unwrap(); + assert!(document.descendants().any(|node| { + node.has_tag_name(("http://www.w3.org/2000/09/xmldsig#", "KeyName")) + && node.text() == Some("content-key") + })); + assert_eq!( + document + .descendants() + .filter(|node| node.has_tag_name(("http://www.w3.org/2001/04/xmlenc#", "EncryptedKey"))) + .count(), + 1 + ); + + let decrypt = Command::new(binary()) + .args(["decrypt", "--privkey-pem"]) + .arg(&private_key) + .arg(&encrypted) + .output() + .unwrap(); + assert!( + decrypt.status.success(), + "{}", + String::from_utf8_lossy(&decrypt.stderr) + ); + assert_eq!(decrypt.stdout, b"existing KeyInfo container"); +} + #[test] fn named_decryption_key_selects_a_later_recipient() { // A document KeyName selects among all EncryptedKey recipients. Checking From 79d5f0c87c387fdfa32aee080123b54d94bb6a5f Mon Sep 17 00:00:00 2001 From: Dmitry Prudnikov Date: Sat, 15 Aug 2026 15:48:06 +0300 Subject: [PATCH 20/27] fix(cli): complete recipient metadata handling - resolve every named encryption recipient with its own OAEP parameters - preserve writer attributes when reusing KeyInfo placeholders - make local-name ID indexing explicit and cover namespaced selectors --- README.md | 6 +- docs/cli.md | 13 ++- src/xml.rs | 31 +++++- src/xmldsig/mutation.rs | 69 ++++++++++++- tests/signing_digest.rs | 62 +++++++++++ tools/xmlsec1/src/commands.rs | 131 +++++++++++++++--------- tools/xmlsec1/tests/process_contract.rs | 121 ++++++++++++++++++++++ 7 files changed, 374 insertions(+), 59 deletions(-) diff --git a/README.md b/README.md index 8b89ba2..f7f5d06 100644 --- a/README.md +++ b/README.md @@ -91,8 +91,10 @@ explicit verification or encryption key. Certificate companions are validated even when no output `KeyInfo` placeholder is present; embedding a chain fills an empty `X509Data` placeholder without discarding sibling `KeyInfo` sources. Populated `KeyInfo` is materialized before reference digests, allowing it to be -signed by ID. Preserved XMLEnc recipient key or certificate metadata must match -the selected RSA wrapping key instead of describing a stale recipient. +signed by ID; writer attributes are merged without overwriting conflicting +template identity. Preserved XMLEnc recipient key or certificate metadata must +match its selected RSA wrapping key, and multi-recipient templates wrap the +content key independently for every named recipient. Document-supplied X.509 certificates require a caller trust anchor unless `--insecure` is explicit. XML payload encryption materializes inferred Element metadata, and direct AES keys reject templates containing recipient diff --git a/docs/cli.md b/docs/cli.md index 1f53222..558869f 100644 --- a/docs/cli.md +++ b/docs/cli.md @@ -64,7 +64,9 @@ validated, and the first certificate must contain the signing key. When the template contains the optional direct `KeyInfo` placeholder, the chain is embedded there in order under `X509Data`. An empty `X509Data` child is populated in place, preserving sibling sources such as the -`KeyName` used to select the signing key and attributes such as an `Id`. KeyInfo +`KeyName` used to select the signing key. Placeholder attributes are preserved; +non-conflicting attributes emitted by the writer are added, while conflicting +expanded names fail closed. KeyInfo materialization occurs before reference digest computation, so a template may sign its populated `KeyInfo` by ID. Omitting `KeyInfo` leaves the signed output without `KeyInfo`. Named signing keys @@ -76,7 +78,9 @@ template unconstrained when one explicit key is supplied. Verification accepts `-` as the conventional stdin marker. For documents with multiple signatures, `--node-id ` selects an ID-bearing start node and verifies the single `Signature` in its subtree; missing and duplicate IDs fail -closed. Signing applies the same start-node contract and mutates only the +closed. The standard `ID`, `Id`, and `id` local names are recognized whether +unqualified or namespace-qualified, including `wsu:Id` and `xml:id`. Signing +applies the same start-node contract and mutates only the selected template's digest, signature, and optional key-info placeholders. XPath and XPath Filter 2.0 verification uses libxmlsec1's legacy `here()` binding at this CLI compatibility boundary. The Rust library API retains the @@ -122,7 +126,10 @@ For `--xml-data`, a missing template `Type` is materialized as XML Element metadata so a later embedded-document decrypt can perform XML replacement. Repeatable AES or RSA key options form a key set. A direct content-key `KeyName` must select exactly one AES key, while a recipient `KeyName` inside -`EncryptedKey` must select exactly one RSA wrapping key. An unnamed template +each `EncryptedKey` must select exactly one RSA wrapping key. Multi-recipient +templates build one wrapped content key per recipient, preserving each +recipient's OAEP parameters and document order; every recipient must resolve +without missing or duplicate key matches. An unnamed template does not constrain the sole explicit key; missing and duplicate matches fail unless `--lax-key-search` is supplied. RSA private-key decryption accepts the upstream diff --git a/src/xml.rs b/src/xml.rs index 9f124b5..62a96d7 100644 --- a/src/xml.rs +++ b/src/xml.rs @@ -34,10 +34,13 @@ impl<'a> XmlIdIndex<'a> { let mut nodes = HashMap::new(); let mut duplicates = HashSet::new(); for node in document.descendants().filter(Node::is_element) { - for name in &names { - let Some(value) = node.attribute(*name) else { - continue; - }; + // ID registration is local-name based: qualified profile attributes + // such as wsu:Id and xml:id participate alongside unqualified Id. + for value in node + .attributes() + .filter(|attribute| names.contains(&attribute.name())) + .map(|attribute| attribute.value()) + { if duplicates.contains(value) { continue; } @@ -158,4 +161,24 @@ mod tests { ); assert!(index.node("duplicate").is_none()); } + + #[test] + fn id_index_matches_supported_local_names_in_any_namespace() { + // ID registration is defined by local attribute name. Common security + // profiles qualify Id with wsu or xml, but the target remains the same. + let document = Document::parse( + r#""#, + ) + .expect("namespaced ID fixture must parse"); + let index = XmlIdIndex::new(&document); + + assert_eq!( + index.node("wsu-target").map(|node| node.tag_name().name()), + Some("one") + ); + assert_eq!( + index.node("xml-target").map(|node| node.tag_name().name()), + Some("two") + ); + } } diff --git a/src/xmldsig/mutation.rs b/src/xmldsig/mutation.rs index 7de27d5..d2c8796 100644 --- a/src/xmldsig/mutation.rs +++ b/src/xmldsig/mutation.rs @@ -474,11 +474,43 @@ fn merge_one_key_info_source_at_index_with_options( )); Ok(attributes) })?; + let generated_attributes = + source + .attributes() + .try_fold(String::new(), |mut attributes, attribute| { + let existing = placeholder.attributes().find(|candidate| { + candidate.namespace() == attribute.namespace() + && candidate.name() == attribute.name() + }); + if let Some(existing) = existing { + if existing.value() != attribute.value() { + return Err(XmlMutationError::InvalidAppendTarget); + } + return Ok(attributes); + } + let qualified_name = match attribute.namespace() { + None => attribute.name().to_owned(), + Some("http://www.w3.org/XML/1998/namespace") => { + format!("xml:{}", attribute.name()) + } + Some(namespace) => { + let prefix = source + .lookup_prefix(namespace) + .ok_or(XmlMutationError::InvalidAppendTarget)?; + format!("{prefix}:{}", attribute.name()) + } + }; + attributes.push_str(&format!( + " {qualified_name}=\"{}\"", + quick_xml::escape::escape(attribute.value()) + )); + Ok(attributes) + })?; let output = replace_element_content( xml, placeholder.range(), source_content, - &generated_namespace_attributes, + &format!("{generated_namespace_attributes}{generated_attributes}"), )?; parse_with_options(&output, policy)?; return Ok(output); @@ -1339,4 +1371,39 @@ mod tests { assert!(matches!(error, XmlMutationError::InvalidAppendTarget)); } + + #[test] + fn key_info_source_merge_preserves_generated_attributes() { + // Writer-owned identity must survive placeholder reuse so later + // reference resolution observes the same element the writer emitted. + let source = r#""#; + let generated = r#"Y2VydA=="#; + + let merged = merge_key_info_source_at_index_with_options(source, generated, 0, None) + .expect("generated attributes must populate the placeholder"); + let document = roxmltree::Document::parse(&merged).expect("merged XML must parse"); + let x509_data = document + .descendants() + .find(|node| node.has_tag_name((XMLDSIG_NS, "X509Data"))) + .expect("X509Data"); + + assert_eq!(x509_data.attribute("Id"), Some("generated")); + assert_eq!( + x509_data.attribute(("urn:key-info", "role")), + Some("signing") + ); + } + + #[test] + fn key_info_source_merge_rejects_conflicting_generated_attributes() { + // Silently choosing template or writer identity would make signed + // references ambiguous, so incompatible expanded attributes fail. + let source = r#""#; + let generated = r#"Y2VydA=="#; + + let error = merge_key_info_source_at_index_with_options(source, generated, 0, None) + .expect_err("conflicting attributes must fail before serialization"); + + assert!(matches!(error, XmlMutationError::InvalidAppendTarget)); + } } diff --git a/tests/signing_digest.rs b/tests/signing_digest.rs index 469bbd4..5cf5631 100644 --- a/tests/signing_digest.rs +++ b/tests/signing_digest.rs @@ -1031,6 +1031,68 @@ fn key_info_writer_replaces_stale_cryptographic_sources() { assert_eq!(signed.matches(" Result { + Ok(self.0.write_key_info(signing_key)?.replacen( + "", ""); + let xml = append_signature_to_root( + "hello", + &template, + ) + .expect("append signature"); + + let signed = SignContext::new(&private_key) + .key_info_writer(&writer) + .sign_template(&xml) + .expect("writer-generated ID must resolve during digesting"); + let result = xml_sec::xmldsig::VerifyContext::new() + .key_resolver(&DefaultKeyResolver::default()) + .verify(&signed) + .expect("signed generated KeyInfo source must verify"); + + assert_eq!(result.status, DsigStatus::Valid); + assert!(signed.contains("Id=\"generated\"")); +} + #[test] fn key_info_writer_requires_direct_template_placeholder() { // The writer is intentionally opt-in and template-scoped. Without a direct diff --git a/tools/xmlsec1/src/commands.rs b/tools/xmlsec1/src/commands.rs index 59d3bae..1297fdc 100644 --- a/tools/xmlsec1/src/commands.rs +++ b/tools/xmlsec1/src/commands.rs @@ -955,30 +955,49 @@ fn encrypt(invocation: &Invocation, stdout: &mut dyn Write) -> Result<(), Comman builder = builder.direct_key_name(name); } } else if !public_keys.is_empty() { - let requested_names = metadata - .recipient_key_name - .iter() - .cloned() - .collect::>(); - let (option, certificate) = select_named_candidate( - &public_keys, - &requested_names, - invocation.flag("lax-key-search"), - true, - "RSA recipient key", - )?; - let path = option.value.as_deref().unwrap_or_default(); - let public_key = if certificate { - key_material::load_rsa_certificate_public(path)? + let template_recipients = if metadata.recipients.is_empty() { + vec![EncryptionTemplateRecipient { + key_name: None, + oaep_parameters: None, + }] } else { - key_material::load_rsa_public(path)? + metadata.recipients }; - validate_recipient_key_metadata(&template, start_node_id, &policy, &public_key)?; - let mut recipient = EncryptionRecipient::rsa_oaep(public_key); - if let Some(parameters) = metadata.oaep_parameters { - recipient = recipient.oaep_parameters(parameters); + let mut selected_recipients = Vec::with_capacity(template_recipients.len()); + for template_recipient in template_recipients { + let requested_names = template_recipient + .key_name + .iter() + .cloned() + .collect::>(); + let (option, certificate) = select_named_candidate( + &public_keys, + &requested_names, + invocation.flag("lax-key-search"), + true, + "RSA recipient key", + )?; + let path = option.value.as_deref().unwrap_or_default(); + let public_key = if certificate { + key_material::load_rsa_certificate_public(path)? + } else { + key_material::load_rsa_public(path)? + }; + selected_recipients.push((public_key, template_recipient.oaep_parameters)); + } + validate_recipient_key_metadata( + &template, + start_node_id, + &policy, + selected_recipients.iter().map(|(key, _)| key), + )?; + for (public_key, parameters) in selected_recipients { + let mut recipient = EncryptionRecipient::rsa_oaep(public_key); + if let Some(parameters) = parameters { + recipient = recipient.oaep_parameters(parameters); + } + builder = builder.add_recipient(recipient); } - builder = builder.add_recipient(recipient); } else { return Err(CommandError::Usage( "encrypt requires --aes-key, an RSA public key, or an RSA certificate".into(), @@ -1017,21 +1036,33 @@ fn encrypt(invocation: &Invocation, stdout: &mut dyn Write) -> Result<(), Comman write_output(invocation, rendered.as_bytes(), stdout) } -fn validate_recipient_key_metadata( +fn validate_recipient_key_metadata<'a>( template: &str, start_node_id: Option<&str>, policy: &EncryptionPolicy, - selected_key: &RsaPublicKey, + selected_keys: impl IntoIterator, ) -> Result<(), CommandError> { let document = parse_encryption_document(template, policy)?; let encrypted_data = select_encrypted_data(&document, start_node_id)?; - let key_infos = direct_child_element(encrypted_data, XMLDSIG_NS, "KeyInfo") + let encrypted_keys = direct_child_element(encrypted_data, XMLDSIG_NS, "KeyInfo") .into_iter() .flat_map(|key_info| key_info.children()) .filter(|node| node.has_tag_name((XMLENC_NS, "EncryptedKey"))) - .filter_map(|encrypted_key| direct_child_element(encrypted_key, XMLDSIG_NS, "KeyInfo")); + .collect::>(); + let selected_keys = selected_keys.into_iter().collect::>(); + if encrypted_keys.is_empty() { + return Ok(()); + } + if encrypted_keys.len() != selected_keys.len() { + return Err(recipient_metadata_error( + "selected RSA key count does not match template recipients", + )); + } - for key_info_node in key_infos { + for (encrypted_key, selected_key) in encrypted_keys.into_iter().zip(selected_keys) { + let Some(key_info_node) = direct_child_element(encrypted_key, XMLDSIG_NS, "KeyInfo") else { + continue; + }; let key_info = parse_key_info(key_info_node) .map_err(|error| CommandError::Encryption(error.to_string()))?; for source in key_info.sources { @@ -1125,15 +1156,11 @@ fn recipient_metadata_error(message: &str) -> CommandError { } fn template_oaep_parameters( - encrypted_data: Node<'_, '_>, + encrypted_key: Node<'_, '_>, ) -> Result, CommandError> { - let Some(method) = encrypted_data - .descendants() - .find(|node| node.has_tag_name((XMLENC_NS, "EncryptedKey"))) - .and_then(|key| { - key.children() - .find(|node| node.has_tag_name((XMLENC_NS, "EncryptionMethod"))) - }) + let Some(method) = encrypted_key + .children() + .find(|node| node.has_tag_name((XMLENC_NS, "EncryptionMethod"))) else { return Ok(None); }; @@ -1530,7 +1557,11 @@ struct EncryptionTemplateMetadata { explicit_encrypted_type: bool, has_encrypted_key_recipient: bool, content_key_name: Option, - recipient_key_name: Option, + recipients: Vec, +} + +struct EncryptionTemplateRecipient { + key_name: Option, oaep_parameters: Option, } @@ -1558,19 +1589,27 @@ fn encryption_template( ))); } }; + let recipients = direct_child_element(encrypted_data, XMLDSIG_NS, "KeyInfo") + .into_iter() + .flat_map(|key_info| key_info.children()) + .filter(|node| node.has_tag_name((XMLENC_NS, "EncryptedKey"))) + .map(|encrypted_key| { + Ok(EncryptionTemplateRecipient { + key_name: direct_child_element(encrypted_key, XMLDSIG_NS, "KeyInfo") + .and_then(|key_info| direct_child_element(key_info, XMLDSIG_NS, "KeyName")) + .and_then(|key_name| key_name.text()) + .map(str::to_owned), + oaep_parameters: template_oaep_parameters(encrypted_key)?, + }) + }) + .collect::, CommandError>>()?; Ok(EncryptionTemplateMetadata { algorithm, encrypted_type, explicit_encrypted_type, - has_encrypted_key_recipient: direct_child_element(encrypted_data, XMLDSIG_NS, "KeyInfo") - .is_some_and(|key_info| { - key_info - .children() - .any(|node| node.has_tag_name((XMLENC_NS, "EncryptedKey"))) - }), + has_encrypted_key_recipient: !recipients.is_empty(), content_key_name: encrypted_data_key_name(encrypted_data), - recipient_key_name: encrypted_key_recipient_name(encrypted_data), - oaep_parameters: template_oaep_parameters(encrypted_data)?, + recipients, }) } @@ -1581,12 +1620,6 @@ fn encrypted_data_key_name(encrypted_data: Node<'_, '_>) -> Option { .map(str::to_owned) } -fn encrypted_key_recipient_name(encrypted_data: Node<'_, '_>) -> Option { - encrypted_key_recipient_names(encrypted_data) - .into_iter() - .next() -} - fn encrypted_key_recipient_names(encrypted_data: Node<'_, '_>) -> Vec { direct_child_element(encrypted_data, XMLDSIG_NS, "KeyInfo") .into_iter() diff --git a/tools/xmlsec1/tests/process_contract.rs b/tools/xmlsec1/tests/process_contract.rs index a17f923..d999f67 100644 --- a/tools/xmlsec1/tests/process_contract.rs +++ b/tools/xmlsec1/tests/process_contract.rs @@ -1689,6 +1689,127 @@ fn rsa_encryption_populates_an_existing_key_info_container() { assert_eq!(decrypt.stdout, b"existing KeyInfo container"); } +#[test] +fn rsa_encryption_builds_every_named_recipient() { + // Repeatable public keys map one-for-one to template recipients. Every + // generated wrapped key must remain decryptable, while missing or duplicate + // matches fail before emitting a partial recipient set. + let temp = tempfile::tempdir().unwrap(); + let template = temp.path().join("multi-recipient-template.xml"); + let plaintext = temp.path().join("plaintext.bin"); + let encrypted = temp.path().join("encrypted.xml"); + let public_a = project_root().join("tests/fixtures/keys/rsa/rsa-2048-pubkey.pem"); + let public_b = project_root().join("tests/fixtures/keys/rsa/rsa-4096-pubkey.pem"); + let private_a = project_root().join("tests/fixtures/keys/rsa/rsa-2048-key.pem"); + let private_b = project_root().join("tests/fixtures/keys/rsa/rsa-4096-key.pem"); + fs::write( + &template, + r#"ab"#, + ) + .unwrap(); + fs::write(&plaintext, b"multiple RSA recipients").unwrap(); + + let encrypt = Command::new(binary()) + .args(["encrypt", "--pubkey-pem:a"]) + .arg(&public_a) + .args(["--pubkey-pem:b"]) + .arg(&public_b) + .arg("--binary-data") + .arg(&plaintext) + .arg("--output") + .arg(&encrypted) + .arg(&template) + .output() + .unwrap(); + assert!( + encrypt.status.success(), + "{}", + String::from_utf8_lossy(&encrypt.stderr) + ); + let encrypted_xml = fs::read_to_string(&encrypted).unwrap(); + let document = roxmltree::Document::parse(&encrypted_xml).unwrap(); + assert_eq!( + document + .descendants() + .filter(|node| node.has_tag_name(("http://www.w3.org/2001/04/xmlenc#", "EncryptedKey"))) + .count(), + 2 + ); + + for (name, private_key) in [("a", &private_a), ("b", &private_b)] { + let decrypt = Command::new(binary()) + .arg("decrypt") + .arg(format!("--privkey-pem:{name}")) + .arg(private_key) + .arg(&encrypted) + .output() + .unwrap(); + assert!( + decrypt.status.success(), + "{}", + String::from_utf8_lossy(&decrypt.stderr) + ); + assert_eq!(decrypt.stdout, b"multiple RSA recipients"); + } + + let missing = Command::new(binary()) + .args(["encrypt", "--pubkey-pem:a"]) + .arg(&public_a) + .arg("--binary-data") + .arg(&plaintext) + .arg(&template) + .output() + .unwrap(); + assert!(!missing.status.success()); + assert!(String::from_utf8_lossy(&missing.stderr).contains("unknown KeyName")); + + let duplicate = Command::new(binary()) + .args(["encrypt", "--pubkey-pem:a"]) + .arg(&public_a) + .args(["--pubkey-pem:a"]) + .arg(&public_b) + .arg("--binary-data") + .arg(&plaintext) + .arg(&template) + .output() + .unwrap(); + assert!(!duplicate.status.success()); + assert!(String::from_utf8_lossy(&duplicate.stderr).contains("multiple RSA recipient key")); +} + +#[test] +fn encryption_node_id_accepts_namespaced_id_attributes() { + // CLI node selection shares local-name ID semantics with reference + // resolution, including profile-standard qualified wsu:Id attributes. + let temp = tempfile::tempdir().unwrap(); + let template = temp.path().join("namespaced-node-id-template.xml"); + let plaintext = temp.path().join("plaintext.bin"); + let aes_key = temp.path().join("aes.key"); + fs::write( + &template, + r#""#, + ) + .unwrap(); + fs::write(&plaintext, b"namespaced node ID").unwrap(); + fs::write(&aes_key, [7_u8; 16]).unwrap(); + + let encrypt = Command::new(binary()) + .args(["encrypt", "--node-id", "target", "--aes-key"]) + .arg(&aes_key) + .arg("--binary-data") + .arg(&plaintext) + .arg(&template) + .output() + .unwrap(); + + assert!( + encrypt.status.success(), + "{}", + String::from_utf8_lossy(&encrypt.stderr) + ); + assert!(String::from_utf8_lossy(&encrypt.stdout).contains("wsu:Id=\"target\"")); +} + #[test] fn named_decryption_key_selects_a_later_recipient() { // A document KeyName selects among all EncryptedKey recipients. Checking From 28ad257a2acd74c9f0d2ab9f191e58d4ae829c89 Mon Sep 17 00:00:00 2001 From: Dmitry Prudnikov Date: Sat, 15 Aug 2026 16:59:38 +0300 Subject: [PATCH 21/27] fix(cli): validate encryption templates --- tools/xmlsec1/src/commands.rs | 57 +++++++---- tools/xmlsec1/tests/process_contract.rs | 123 ++++++++++++++++++++++++ 2 files changed, 162 insertions(+), 18 deletions(-) diff --git a/tools/xmlsec1/src/commands.rs b/tools/xmlsec1/src/commands.rs index 1297fdc..3868d59 100644 --- a/tools/xmlsec1/src/commands.rs +++ b/tools/xmlsec1/src/commands.rs @@ -1173,10 +1173,15 @@ fn template_oaep_parameters( .children() .find(|node| node.has_tag_name((XMLDSIG_NS, "DigestMethod"))) .and_then(|node| node.attribute("Algorithm")); - let mgf = method + let mgf_node = method .children() - .find(|node| node.has_tag_name((XMLENC11_NS, "MGF"))) - .and_then(|node| node.attribute("Algorithm")); + .find(|node| node.has_tag_name((XMLENC11_NS, "MGF"))); + if transport == KeyTransportAlgorithm::RsaOaepMgf1p && mgf_node.is_some() { + return Err(CommandError::Encryption( + "legacy rsa-oaep-mgf1p does not permit an XML Encryption 1.1 MGF parameter".into(), + )); + } + let mgf = mgf_node.and_then(|node| node.attribute("Algorithm")); let digest = oaep_digest_from_uri(digest.unwrap_or(OaepDigestAlgorithm::Sha1.uri()))?; let mgf_digest = if transport == KeyTransportAlgorithm::RsaOaepMgf1p { OaepDigestAlgorithm::Sha1 @@ -1265,14 +1270,8 @@ fn apply_encryption_template( let generated_key_info = direct_child_element(generated_data, XMLDSIG_NS, "KeyInfo"); match (template_key_info, generated_key_info) { (Some(template_key_info), Some(generated_key_info)) => { - let template_values = template_key_info - .descendants() - .filter(|node| node.has_tag_name((XMLENC_NS, "CipherValue"))) - .collect::>(); - let generated_values = generated_key_info - .descendants() - .filter(|node| node.has_tag_name((XMLENC_NS, "CipherValue"))) - .collect::>(); + let template_values = encrypted_key_cipher_values(template_key_info); + let generated_values = encrypted_key_cipher_values(generated_key_info); if template_values.is_empty() && !generated_values.is_empty() { let generated_keys = generated_key_info .children() @@ -1425,6 +1424,19 @@ fn encrypted_data_cipher_value<'a, 'input>( .and_then(|cipher| direct_child_element(cipher, XMLENC_NS, "CipherValue")) } +fn encrypted_key_cipher_values<'a, 'input>( + key_info: roxmltree::Node<'a, 'input>, +) -> Vec> { + key_info + .children() + .filter(|node| node.has_tag_name((XMLENC_NS, "EncryptedKey"))) + .filter_map(|encrypted_key| { + direct_child_element(encrypted_key, XMLENC_NS, "CipherData") + .and_then(|cipher| direct_child_element(cipher, XMLENC_NS, "CipherValue")) + }) + .collect() +} + fn decrypt(invocation: &Invocation, stdout: &mut dyn Write) -> Result<(), CommandError> { validate_options(invocation, DECRYPT_OPTIONS)?; reject_unimplemented_selectors(invocation, &["node-id"])?; @@ -1437,7 +1449,7 @@ fn decrypt(invocation: &Invocation, stdout: &mut dyn Write) -> Result<(), Comman let document = parse_encryption_document(&xml, &policy)?; let encrypted_data = select_encrypted_data(&document, encrypted_data_id)?; let standalone = encrypted_data == document.root_element(); - let content_key_name = encrypted_data_key_name(encrypted_data); + let content_key_name = encrypted_data_key_name(encrypted_data)?; let recipient_key_names = encrypted_key_recipient_names(encrypted_data); let aes_keys = invocation.values("aes-key").collect::>(); let private_keys = ["privkey-pem", "privkey-der", "pkcs8-pem", "pkcs8-der"] @@ -1608,16 +1620,25 @@ fn encryption_template( encrypted_type, explicit_encrypted_type, has_encrypted_key_recipient: !recipients.is_empty(), - content_key_name: encrypted_data_key_name(encrypted_data), + content_key_name: encrypted_data_key_name(encrypted_data)?, recipients, }) } -fn encrypted_data_key_name(encrypted_data: Node<'_, '_>) -> Option { - direct_child_element(encrypted_data, XMLDSIG_NS, "KeyInfo") - .and_then(|key_info| direct_child_element(key_info, XMLDSIG_NS, "KeyName")) - .and_then(|key_name| key_name.text()) - .map(str::to_owned) +fn encrypted_data_key_name(encrypted_data: Node<'_, '_>) -> Result, CommandError> { + let Some(key_info) = direct_child_element(encrypted_data, XMLDSIG_NS, "KeyInfo") else { + return Ok(None); + }; + let mut key_names = key_info + .children() + .filter(|node| node.has_tag_name((XMLDSIG_NS, "KeyName"))); + let key_name = key_names.next(); + if key_names.next().is_some() { + return Err(CommandError::Encryption( + "KeyInfo contains more than one direct KeyName".into(), + )); + } + Ok(key_name.and_then(|node| node.text()).map(str::to_owned)) } fn encrypted_key_recipient_names(encrypted_data: Node<'_, '_>) -> Vec { diff --git a/tools/xmlsec1/tests/process_contract.rs b/tools/xmlsec1/tests/process_contract.rs index d999f67..d033141 100644 --- a/tools/xmlsec1/tests/process_contract.rs +++ b/tools/xmlsec1/tests/process_contract.rs @@ -1118,6 +1118,34 @@ fn direct_aes_key_name_must_match_the_template_unless_lax() { ); } +#[test] +fn encryption_rejects_duplicate_content_key_names() { + // The core XMLEnc parser permits at most one direct KeyName. Reject the + // template before encryption rather than emitting ciphertext we cannot read. + let temp = tempfile::tempdir().unwrap(); + let template = temp.path().join("duplicate-key-name.xml"); + let plaintext = temp.path().join("plaintext.bin"); + let key = temp.path().join("key.bin"); + fs::write( + &template, + r#"firstsecond"#, + ) + .unwrap(); + fs::write(&plaintext, b"duplicate content key names").unwrap(); + fs::write(&key, b"0123456789abcdef").unwrap(); + + let rejected = Command::new(binary()) + .args(["encrypt", "--aes-key"]) + .arg(&key) + .arg("--binary-data") + .arg(&plaintext) + .arg(&template) + .output() + .unwrap(); + assert!(!rejected.status.success()); + assert!(String::from_utf8_lossy(&rejected.stderr).contains("more than one direct KeyName")); +} + #[test] fn named_aes_decryption_obeys_encrypted_data_key_name_unless_lax() { // Decryption key selection must enforce the same document identity contract @@ -1689,6 +1717,74 @@ fn rsa_encryption_populates_an_existing_key_info_container() { assert_eq!(decrypt.stdout, b"existing KeyInfo container"); } +#[test] +fn rsa_encryption_preserves_extension_cipher_values() { + // CipherValue is meaningful only along an EncryptedKey recipient path. + // Extension-owned values must remain caller metadata, not placeholders. + let temp = tempfile::tempdir().unwrap(); + let template = temp.path().join("extension-cipher-value.xml"); + let plaintext = temp.path().join("plaintext.bin"); + let encrypted = temp.path().join("encrypted.xml"); + let public_key = project_root().join("tests/fixtures/keys/rsa/rsa-4096-pubkey.pem"); + let private_key = project_root().join("tests/fixtures/keys/rsa/rsa-4096-key.pem"); + fs::write( + &template, + r#"ZXh0ZW5zaW9u"#, + ) + .unwrap(); + fs::write(&plaintext, b"extension metadata").unwrap(); + + let encrypt = Command::new(binary()) + .args(["encrypt", "--pubkey-pem"]) + .arg(&public_key) + .arg("--binary-data") + .arg(&plaintext) + .arg("--output") + .arg(&encrypted) + .arg(&template) + .output() + .unwrap(); + assert!( + encrypt.status.success(), + "{}", + String::from_utf8_lossy(&encrypt.stderr) + ); + let encrypted_xml = fs::read_to_string(&encrypted).unwrap(); + let document = roxmltree::Document::parse(&encrypted_xml).unwrap(); + assert_eq!( + document + .descendants() + .find(|node| node.has_tag_name(("urn:test", "Metadata"))) + .and_then(|node| { + node.children().find(|child| { + child.has_tag_name(("http://www.w3.org/2001/04/xmlenc#", "CipherValue")) + }) + }) + .and_then(|node| node.text()), + Some("ZXh0ZW5zaW9u") + ); + assert_eq!( + document + .descendants() + .filter(|node| node.has_tag_name(("http://www.w3.org/2001/04/xmlenc#", "EncryptedKey"))) + .count(), + 1 + ); + + let decrypt = Command::new(binary()) + .args(["decrypt", "--privkey-pem"]) + .arg(&private_key) + .arg(&encrypted) + .output() + .unwrap(); + assert!( + decrypt.status.success(), + "{}", + String::from_utf8_lossy(&decrypt.stderr) + ); + assert_eq!(decrypt.stdout, b"extension metadata"); +} + #[test] fn rsa_encryption_builds_every_named_recipient() { // Repeatable public keys map one-for-one to template recipients. Every @@ -2083,6 +2179,33 @@ fn honors_legacy_rsa_oaep_parameters_from_the_template() { assert_eq!(decrypt.stdout, b"legacy OAEP payload"); } +#[test] +fn legacy_rsa_oaep_rejects_xmlenc11_mgf_parameters() { + // XML Encryption 1.0 fixes MGF1 to SHA-1. Preserving an XML Encryption 1.1 + // MGF child would make the emitted structure contradict the wrapping mode. + let temp = tempfile::tempdir().unwrap(); + let template = temp.path().join("legacy-oaep-with-mgf.xml"); + let plaintext = temp.path().join("plaintext.bin"); + let public_key = project_root().join("tests/fixtures/keys/rsa/rsa-4096-pubkey.pem"); + fs::write( + &template, + r#""#, + ) + .unwrap(); + fs::write(&plaintext, b"invalid legacy OAEP metadata").unwrap(); + + let rejected = Command::new(binary()) + .args(["encrypt", "--pubkey-pem"]) + .arg(&public_key) + .arg("--binary-data") + .arg(&plaintext) + .arg(&template) + .output() + .unwrap(); + assert!(!rejected.status.success()); + assert!(String::from_utf8_lossy(&rejected.stderr).contains("MGF")); +} + #[test] fn decrypts_encrypted_data_embedded_in_a_document() { // libxmlsec1 decrypt replaces EncryptedData in its containing document; a From ba60657321df12fb98e54f8680edc9686106236c Mon Sep 17 00:00:00 2001 From: Dmitry Prudnikov Date: Sat, 15 Aug 2026 17:12:19 +0300 Subject: [PATCH 22/27] fix(xmldsig): validate key info mutation --- src/xmldsig/mutation.rs | 79 +++++++++++++++++++++++++++++++++-------- tests/signing_digest.rs | 8 ++--- 2 files changed, 69 insertions(+), 18 deletions(-) diff --git a/src/xmldsig/mutation.rs b/src/xmldsig/mutation.rs index d2c8796..31feb39 100644 --- a/src/xmldsig/mutation.rs +++ b/src/xmldsig/mutation.rs @@ -311,12 +311,12 @@ pub(super) fn fill_key_info_at_index_with_options( target_signature: usize, policy: Option<&crate::policy::SigningPolicy>, ) -> Result { - let expected = count_direct_key_infos(xml, target_signature, policy)?; - if expected != 1 { + let actual = count_direct_key_infos(xml, target_signature, policy)?; + if actual != 1 { return Err(XmlMutationError::ValueCountMismatch { element: "KeyInfo", - expected, - actual: 1, + expected: 1, + actual, }); } @@ -350,8 +350,8 @@ pub(super) fn merge_key_info_source_at_index_with_options( if key_infos.len() != 1 { return Err(XmlMutationError::ValueCountMismatch { element: "KeyInfo", - expected: key_infos.len(), - actual: 1, + expected: 1, + actual: key_infos.len(), }); } let key_info = key_infos[0]; @@ -439,8 +439,8 @@ fn merge_one_key_info_source_at_index_with_options( if key_infos.len() != 1 { return Err(XmlMutationError::ValueCountMismatch { element: "KeyInfo", - expected: key_infos.len(), - actual: 1, + expected: 1, + actual: key_infos.len(), }); } let key_info = key_infos[0]; @@ -455,15 +455,21 @@ fn merge_one_key_info_source_at_index_with_options( source .namespaces() .try_fold(String::new(), |mut attributes, namespace| { - let current = placeholder.lookup_namespace_uri(namespace.name()); - if current == Some(namespace.uri()) { + if let Some(declared) = placeholder + .namespaces() + .find(|declared| declared.name() == namespace.name()) + { + if declared.uri() != namespace.uri() { + return Err(XmlMutationError::InvalidAppendTarget); + } return Ok(attributes); } - let inherited = placeholder + if placeholder .parent_element() - .and_then(|parent| parent.lookup_namespace_uri(namespace.name())); - if current.is_some() && current != inherited { - return Err(XmlMutationError::InvalidAppendTarget); + .and_then(|parent| parent.lookup_namespace_uri(namespace.name())) + == Some(namespace.uri()) + { + return Ok(attributes); } let attribute = namespace .name() @@ -1359,6 +1365,38 @@ mod tests { ); } + #[test] + fn key_info_source_merge_reports_required_and_observed_counts() { + // Mutation diagnostics are a structured API: expected is the required + // singleton count and actual is the number observed in the template. + for (source, actual) in [ + ( + r#""#, + 0, + ), + ( + r#""#, + 2, + ), + ] { + let error = merge_key_info_source_at_index_with_options( + source, + r#"key"#, + 0, + None, + ) + .expect_err("KeyInfo must be a singleton"); + assert!(matches!( + error, + XmlMutationError::ValueCountMismatch { + element: "KeyInfo", + expected: 1, + actual: observed, + } if observed == actual + )); + } + } + #[test] fn key_info_source_merge_rejects_conflicting_placeholder_namespaces() { // A generated child cannot reuse a prefix that the placeholder owns @@ -1372,6 +1410,19 @@ mod tests { assert!(matches!(error, XmlMutationError::InvalidAppendTarget)); } + #[test] + fn key_info_source_merge_detects_redundant_owned_namespace_conflicts() { + // A direct declaration remains owned by the placeholder even when it + // repeats the parent binding; replacing it would duplicate xmlns:ext. + let source = r#""#; + let generated = r#""#; + + let error = merge_key_info_source_at_index_with_options(source, generated, 0, None) + .expect_err("placeholder-owned namespace conflicts must be typed"); + + assert!(matches!(error, XmlMutationError::InvalidAppendTarget)); + } + #[test] fn key_info_source_merge_preserves_generated_attributes() { // Writer-owned identity must survive placeholder reuse so later diff --git a/tests/signing_digest.rs b/tests/signing_digest.rs index 5cf5631..cef41cb 100644 --- a/tests/signing_digest.rs +++ b/tests/signing_digest.rs @@ -1124,8 +1124,8 @@ fn key_info_writer_requires_direct_template_placeholder() { SigningError::XmlMutation( xml_sec::xmldsig::mutation::XmlMutationError::ValueCountMismatch { element: "KeyInfo", - expected: 0, - actual: 1, + expected: 1, + actual: 0, } ) )); @@ -1168,8 +1168,8 @@ fn key_info_writer_rejects_duplicate_direct_template_placeholders() { SigningError::XmlMutation( xml_sec::xmldsig::mutation::XmlMutationError::ValueCountMismatch { element: "KeyInfo", - expected: 2, - actual: 1, + expected: 1, + actual: 2, } ) )); From 9a38c1883126dec7df9e4815b4317dd4d57257c9 Mon Sep 17 00:00:00 2001 From: Dmitry Prudnikov Date: Sat, 15 Aug 2026 20:28:41 +0300 Subject: [PATCH 23/27] fix(cli): enforce encryption contracts - reject ambiguous recipient KeyName metadata before encryption - emit donor-shaped XML diagnostics for successful decryption - cover both process contracts with regression tests --- tools/xmlsec1/src/commands.rs | 103 +++++++++++++++++++++--- tools/xmlsec1/tests/process_contract.rs | 78 ++++++++++++++++++ 2 files changed, 169 insertions(+), 12 deletions(-) diff --git a/tools/xmlsec1/src/commands.rs b/tools/xmlsec1/src/commands.rs index 3868d59..49c6a69 100644 --- a/tools/xmlsec1/src/commands.rs +++ b/tools/xmlsec1/src/commands.rs @@ -1516,7 +1516,64 @@ fn decrypt(invocation: &Invocation, stdout: &mut dyn Write) -> Result<(), Comman "decrypt requires --aes-key or an RSA private key".into(), )); }; - write_output(invocation, &bytes, stdout) + write_output(invocation, &bytes, stdout)?; + write_decryption_diagnostics(invocation, encrypted_data, !standalone, stdout) +} + +fn write_decryption_diagnostics( + invocation: &Invocation, + encrypted_data: Node<'_, '_>, + result_replaced: bool, + stdout: &mut dyn Write, +) -> Result<(), CommandError> { + if !invocation.flag("print-xml-debug") { + return Ok(()); + } + // Donor testEnc.sh routes plaintext through --output and parses stdout as + // a separate xmlSecEncCtxDebugXmlDump-compatible diagnostics document. + let method = direct_child_element(encrypted_data, XMLENC_NS, "EncryptionMethod") + .and_then(|node| node.attribute("Algorithm")) + .ok_or_else(|| CommandError::Encryption("template has no encryption algorithm".into()))?; + let transform_name = method.rsplit_once('#').map_or(method, |(_, name)| name); + debug_assert!(TRANSFORMS.contains(&transform_name)); + let status = if result_replaced { + "replaced" + } else { + "not-replaced" + }; + writeln!( + stdout, + "" + ) + .map_err(stdout_error)?; + writeln!(stdout, "00000000").map_err(stdout_error)?; + writeln!(stdout, "00000000").map_err(stdout_error)?; + for (element, attribute) in [ + ("Id", "Id"), + ("Type", "Type"), + ("MimeType", "MimeType"), + ("Encoding", "Encoding"), + ] { + let value = encrypted_data.attribute(attribute).unwrap_or("NULL"); + writeln!( + stdout, + "<{element}>{}", + quick_xml::escape::escape(value) + ) + .map_err(stdout_error)?; + } + writeln!(stdout, "NULL").map_err(stdout_error)?; + writeln!(stdout, "NULL").map_err(stdout_error)?; + writeln!(stdout, "").map_err(stdout_error)?; + writeln!( + stdout, + "", + quick_xml::escape::escape(transform_name), + quick_xml::escape::escape(method) + ) + .map_err(stdout_error)?; + writeln!(stdout, "").map_err(stdout_error)?; + writeln!(stdout, "").map_err(stdout_error) } struct NamedRecipientDecryptor<'a> { @@ -1608,9 +1665,16 @@ fn encryption_template( .map(|encrypted_key| { Ok(EncryptionTemplateRecipient { key_name: direct_child_element(encrypted_key, XMLDSIG_NS, "KeyInfo") - .and_then(|key_info| direct_child_element(key_info, XMLDSIG_NS, "KeyName")) - .and_then(|key_name| key_name.text()) - .map(str::to_owned), + .map(|key_info| { + optional_direct_child_text( + key_info, + XMLDSIG_NS, + "KeyName", + "EncryptedKey KeyInfo contains more than one direct KeyName", + ) + }) + .transpose()? + .flatten(), oaep_parameters: template_oaep_parameters(encrypted_key)?, }) }) @@ -1629,16 +1693,31 @@ fn encrypted_data_key_name(encrypted_data: Node<'_, '_>) -> Result, + namespace: &str, + name: &str, + duplicate_error: &str, +) -> Result, CommandError> { + let mut children = parent .children() - .filter(|node| node.has_tag_name((XMLDSIG_NS, "KeyName"))); - let key_name = key_names.next(); - if key_names.next().is_some() { - return Err(CommandError::Encryption( - "KeyInfo contains more than one direct KeyName".into(), - )); + .filter(|node| node.has_tag_name((namespace, name))); + let value = children + .next() + .and_then(|node| node.text()) + .map(str::to_owned); + if children.next().is_some() { + return Err(CommandError::Encryption(duplicate_error.into())); } - Ok(key_name.and_then(|node| node.text()).map(str::to_owned)) + Ok(value) } fn encrypted_key_recipient_names(encrypted_data: Node<'_, '_>) -> Vec { diff --git a/tools/xmlsec1/tests/process_contract.rs b/tools/xmlsec1/tests/process_contract.rs index d033141..6d9ffed 100644 --- a/tools/xmlsec1/tests/process_contract.rs +++ b/tools/xmlsec1/tests/process_contract.rs @@ -777,6 +777,51 @@ fn encrypts_decrypts_and_rejects_wrong_symmetric_key() { assert!(!rejected.status.success()); } +#[test] +fn xml_debug_decryption_writes_diagnostics_separately_from_plaintext() { + // The unmodified donor runner redirects diagnostics from stdout while + // --output receives the decrypted payload, then parses stdout as XML. + let temp = tempfile::tempdir().unwrap(); + let fixtures = project_root().join("tools/xmlsec1/tests/fixtures/upstream"); + let vector = fixtures.join("xmlenc11-interop-2012/xenc11-example-AES128-GCM"); + let decrypted = temp.path().join("decrypted.data"); + + let output = Command::new(binary()) + .args([ + "decrypt", + "--print-xml-debug", + "--lax-key-search", + "--aeskey", + ]) + .arg(vector.with_extension("key")) + .arg("--output") + .arg(&decrypted) + .arg(vector.with_extension("xml")) + .output() + .unwrap(); + + assert!( + output.status.success(), + "{}", + String::from_utf8_lossy(&output.stderr) + ); + assert_eq!( + fs::read(&decrypted).unwrap(), + fs::read(vector.with_extension("data")).unwrap() + ); + let diagnostics = String::from_utf8(output.stdout).unwrap(); + let document = roxmltree::Document::parse(&diagnostics) + .expect("--print-xml-debug stdout must be well-formed XML"); + let root = document.root_element(); + assert_eq!(root.tag_name().name(), "DataDecryptionContext"); + assert_eq!(root.attribute("status"), Some("not-replaced")); + assert_eq!(root.attribute("failureReason"), Some("UNKNOWN")); + assert!(root.descendants().any(|node| { + node.has_tag_name("Transform") + && node.attribute("href") == Some("http://www.w3.org/2009/xmlenc11#aes128-gcm") + })); +} + #[test] fn encryption_preserves_template_metadata_and_supports_id_selection() { // Encryption templates are output contracts. Only CipherValue is mutable; @@ -1455,6 +1500,39 @@ fn rsa_recipient_name_must_match_the_template_unless_lax() { assert_eq!(lax_decrypt.stdout, b"named recipient"); } +#[test] +fn rsa_encryption_rejects_duplicate_recipient_key_names() { + // Recipient identity is a singleton selection hint. Silently selecting the + // first value would emit ciphertext that the reciprocal parser rejects. + let temp = tempfile::tempdir().unwrap(); + let template = temp.path().join("duplicate-recipient-name.xml"); + let plaintext = temp.path().join("plaintext.bin"); + let public_key = project_root().join("tests/fixtures/keys/rsa/rsa-4096-pubkey.pem"); + fs::write( + &template, + r#"firstsecond"#, + ) + .unwrap(); + fs::write(&plaintext, b"ambiguous recipient").unwrap(); + + let output = Command::new(binary()) + .args(["encrypt", "--pubkey-pem:first"]) + .arg(&public_key) + .arg("--binary-data") + .arg(&plaintext) + .arg(&template) + .output() + .unwrap(); + + assert!(!output.status.success()); + assert!(output.stdout.is_empty(), "ciphertext must not be emitted"); + assert!( + String::from_utf8_lossy(&output.stderr).contains("more than one direct KeyName"), + "{}", + String::from_utf8_lossy(&output.stderr) + ); +} + #[test] fn rsa_encryption_rejects_stale_recipient_key_value_metadata() { // A template KeyValue is a cryptographic identity, not a selection hint. From 17bcccb5488a7d5d5ff69cc6529ee77d07567393 Mon Sep 17 00:00:00 2001 From: Dmitry Prudnikov Date: Sat, 15 Aug 2026 22:38:11 +0300 Subject: [PATCH 24/27] fix(cli): complete review contracts - implement typed custom ID registrations across XMLDSig and XMLEnc - reject ambiguous recipient metadata and malformed key-store output - apply signing parser limits to custom KeyInfo fragments --- docs/cli.md | 10 ++ docs/xmldsig.md | 4 + docs/xmlenc.md | 4 + src/lib.rs | 3 + src/xml.rs | 83 +++++++-- src/xmldsig/mutation.rs | 29 +++- src/xmldsig/sign.rs | 32 +++- src/xmldsig/uri.rs | 13 ++ src/xmldsig/verify.rs | 17 +- src/xmlenc/decrypt.rs | 20 ++- tools/xmlsec1/README.md | 7 +- tools/xmlsec1/src/args.rs | 2 +- tools/xmlsec1/src/commands.rs | 222 ++++++++++++++++++------ tools/xmlsec1/src/key_material.rs | 10 +- tools/xmlsec1/tests/process_contract.rs | 196 +++++++++++++++++++++ 15 files changed, 557 insertions(+), 95 deletions(-) diff --git a/docs/cli.md b/docs/cli.md index 558869f..3f25b52 100644 --- a/docs/cli.md +++ b/docs/cli.md @@ -82,6 +82,12 @@ closed. The standard `ID`, `Id`, and `id` local names are recognized whether unqualified or namespace-qualified, including `wsu:Id` and `xml:id`. Signing applies the same start-node contract and mutates only the selected template's digest, signature, and optional key-info placeholders. +Custom ID declarations match libxmlsec1's two request-local forms: +`--add-id-attr NAME` registers an attribute local name on every element, while +`--id-attr[:ATTR] [NAMESPACE-URI:]ELEMENT` (default `ATTR` is `id`) limits the +registration to one expanded element name. Registrations affect both +`--node-id` selection and same-document reference resolution; duplicate ID +values remain ambiguous and fail closed. XPath and XPath Filter 2.0 verification uses libxmlsec1's legacy `here()` binding at this CLI compatibility boundary. The Rust library API retains the XMLDSig specification binding by default and requires an explicit opt-in for @@ -116,6 +122,10 @@ then requires exactly one `EncryptedData` in its subtree. Encryption preserves the template's `Id`, `Type`, `MimeType`, `KeyInfo`, `EncryptionProperties`, and RSA-OAEP parameters while replacing only the cryptographic `CipherValue` payloads. +Each template `EncryptedKey` must contain exactly one direct +`EncryptionMethod`; optional `DigestMethod`, `MGF`, and `OAEPparams` children +must each occur at most once. Ambiguous recipient metadata is rejected before +key wrapping. When nested recipient `KeyInfo` already carries `RSAKeyValue`, an X.509 certificate, or `DEREncodedKeyValue`, that cryptographic identity must match the selected RSA wrapping key. Unmatchable or contradictory metadata is diff --git a/docs/xmldsig.md b/docs/xmldsig.md index f189a5f..5fecb9e 100644 --- a/docs/xmldsig.md +++ b/docs/xmldsig.md @@ -41,6 +41,10 @@ The same immutable policy controls every signing parse and mutation reparse, inc validation in `sign_with_builder`, digest filling, `SignedInfo` parsing, signature filling, and optional `KeyInfo` filling. An internal-DTD opt-in and XML node ceiling therefore cannot be lost between stages. +`IdAttributeRegistration` supplies immutable request context for non-standard ID attributes. +`SignContext::id_attributes` and `VerifyContext::id_attributes` apply the same global or +element-scoped registrations to operation start-node selection and every same-document Reference; +the registration is not stored in policy and never comes from document content. `SigningPolicy::rsa_keys` validates normalized modulus width and public exponent before provider dispatch. The default accepts 2048-8192-bit RSA keys for new signatures; compatibility callers can raise or lower the minimum explicitly, while the 8192-bit implementation ceiling cannot be relaxed. diff --git a/docs/xmlenc.md b/docs/xmlenc.md index 2a4e9a2..d37f032 100644 --- a/docs/xmlenc.md +++ b/docs/xmlenc.md @@ -33,6 +33,10 @@ fn example() -> Result<(), Box> { } ``` +For embedded decryption, `DecryptContext::id_attributes` accepts the same immutable global or +element-scoped `IdAttributeRegistration` request context as XMLDSig. It affects only operation +start-node lookup; policy remains a separate compiled snapshot. + For recipient transport, add one or more `EncryptionRecipient::rsa_oaep` entries with recipient public keys, or use `recipient_aes_kw` with a shared KEK. `EncryptedDataBuilder` obtains each fresh content key through `CryptoProvider::fill_random` and wraps it once per recipient. The default diff --git a/src/lib.rs b/src/lib.rs index 9252123..56d8f53 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -40,6 +40,9 @@ pub mod provider; #[cfg(any(feature = "xmldsig", feature = "xmlenc"))] mod xml; +#[cfg(any(feature = "xmldsig", feature = "xmlenc"))] +pub use xml::IdAttributeRegistration; + #[cfg(feature = "xmldsig")] pub mod xmldsig; diff --git a/src/xml.rs b/src/xml.rs index 62a96d7..2dd80df 100644 --- a/src/xml.rs +++ b/src/xml.rs @@ -10,27 +10,75 @@ use roxmltree::NodeId; /// Default ID attribute names shared by XMLDSig and XMLEnc selection. const DEFAULT_ID_ATTRS: &[&str] = &["ID", "Id", "id"]; +/// Caller-declared XML ID attribute registration. +/// +/// Registrations are request context rather than security policy. A global +/// registration applies an attribute local name to every element; a scoped +/// registration applies only to one expanded element name. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct IdAttributeRegistration { + attribute_local_name: String, + element_local_name: Option, + element_namespace: Option, +} + +impl IdAttributeRegistration { + /// Register an attribute local name as an ID on every element. + #[must_use] + pub fn global(attribute_local_name: impl Into) -> Self { + Self { + attribute_local_name: attribute_local_name.into(), + element_local_name: None, + element_namespace: None, + } + } + + /// Register an attribute as an ID only on matching elements. + /// + /// `element_namespace` is the namespace URI, not an XML prefix. `None` + /// matches only elements without a namespace. + #[must_use] + pub fn scoped( + attribute_local_name: impl Into, + element_local_name: impl Into, + element_namespace: Option<&str>, + ) -> Self { + Self { + attribute_local_name: attribute_local_name.into(), + element_local_name: Some(element_local_name.into()), + element_namespace: element_namespace.map(str::to_owned), + } + } + + fn matches(&self, node: Node<'_, '_>, attribute_name: &str) -> bool { + self.attribute_local_name == attribute_name + && self.element_local_name.as_deref().is_none_or(|name| { + node.tag_name().name() == name + && node.tag_name().namespace() == self.element_namespace.as_deref() + }) + } +} + /// Duplicate-safe index of XML ID attributes in one parsed document. pub(crate) struct XmlIdIndex<'a> { nodes: HashMap<&'a str, Node<'a, 'a>>, } impl<'a> XmlIdIndex<'a> { - /// Index the standard `ID`, `Id`, and `id` spellings. - #[cfg(any(feature = "xmlenc", test))] - pub(crate) fn new(document: &'a Document<'a>) -> Self { - Self::with_extra_attrs(document, &[]) - } - /// Index standard ID spellings plus caller-declared local attribute names. pub(crate) fn with_extra_attrs(document: &'a Document<'a>, extra_attrs: &[&str]) -> Self { - let mut names = DEFAULT_ID_ATTRS.to_vec(); - for name in extra_attrs { - if !names.contains(name) { - names.push(name); - } - } + let registrations = extra_attrs + .iter() + .map(|name| IdAttributeRegistration::global(*name)) + .collect::>(); + Self::with_registrations(document, ®istrations) + } + /// Index standard ID spellings plus caller-declared registrations. + pub(crate) fn with_registrations( + document: &'a Document<'a>, + registrations: &[IdAttributeRegistration], + ) -> Self { let mut nodes = HashMap::new(); let mut duplicates = HashSet::new(); for node in document.descendants().filter(Node::is_element) { @@ -38,7 +86,12 @@ impl<'a> XmlIdIndex<'a> { // such as wsu:Id and xml:id participate alongside unqualified Id. for value in node .attributes() - .filter(|attribute| names.contains(&attribute.name())) + .filter(|attribute| { + DEFAULT_ID_ATTRS.contains(&attribute.name()) + || registrations + .iter() + .any(|registration| registration.matches(node, attribute.name())) + }) .map(|attribute| attribute.value()) { if duplicates.contains(value) { @@ -153,7 +206,7 @@ mod tests { r#""#, ) .expect("ID index fixture must be valid XML"); - let index = XmlIdIndex::new(&document); + let index = XmlIdIndex::with_registrations(&document, &[]); assert_eq!( index.node("same").map(|node| node.tag_name().name()), @@ -170,7 +223,7 @@ mod tests { r#""#, ) .expect("namespaced ID fixture must parse"); - let index = XmlIdIndex::new(&document); + let index = XmlIdIndex::with_registrations(&document, &[]); assert_eq!( index.node("wsu-target").map(|node| node.tag_name().name()), diff --git a/src/xmldsig/mutation.rs b/src/xmldsig/mutation.rs index 31feb39..3426f27 100644 --- a/src/xmldsig/mutation.rs +++ b/src/xmldsig/mutation.rs @@ -360,7 +360,7 @@ pub(super) fn merge_key_info_source_at_index_with_options( // Parse it under the template's namespace context so multiple siblings and // inherited prefixes have exactly the semantics they will have in KeyInfo. let wrapped_source = wrap_key_info_children(key_info_source, key_info); - let source_document = roxmltree::Document::parse(&wrapped_source)?; + let source_document = parse_with_options(&wrapped_source, policy)?; let sources = source_document .root_element() .children() @@ -422,7 +422,7 @@ fn merge_one_key_info_source_at_index_with_options( policy: Option<&crate::policy::SigningPolicy>, ) -> Result { let document = parse_with_options(xml, policy)?; - let source_document = roxmltree::Document::parse(key_info_source)?; + let source_document = parse_with_options(key_info_source, policy)?; let source = source_document.root_element(); let source_content = element_inner_xml(key_info_source, source.range())?; let Some(signature) = signature_node(&document, target_signature) else { @@ -1457,4 +1457,29 @@ mod tests { assert!(matches!(error, XmlMutationError::InvalidAppendTarget)); } + + #[test] + fn key_info_source_merge_applies_policy_to_writer_fragments() { + // A custom writer is an untrusted allocation boundary: its wrapper must + // obey the same node ceiling as the caller's signing template. + let source = r#""#; + let children = (0..64).map(|_| "").collect::(); + let generated = format!( + r#"{children}"# + ); + let policy = crate::policy::SigningPolicy { + resources: crate::policy::ResourcePolicy { + max_xml_nodes: 32, + ..crate::policy::ResourcePolicy::default() + }, + ..crate::policy::SigningPolicy::default() + }; + + let error = + merge_key_info_source_at_index_with_options(source, &generated, 0, Some(&policy)) + .expect_err("writer fragment must obey the signing node ceiling"); + + assert!(matches!(error, XmlMutationError::XmlParse(_))); + assert!(error.to_string().contains("nodes limit")); + } } diff --git a/src/xmldsig/sign.rs b/src/xmldsig/sign.rs index 127d6f1..0ef52c1 100644 --- a/src/xmldsig/sign.rs +++ b/src/xmldsig/sign.rs @@ -677,6 +677,7 @@ pub struct SignContext<'a> { signing_key: &'a dyn SigningKey, key_info_writer: Option<&'a dyn KeyInfoWriter>, start_node_id: Option<&'a str>, + id_attributes: &'a [crate::IdAttributeRegistration], policy: crate::policy::SigningPolicy, provider: &'a dyn crate::provider::CryptoProvider, } @@ -688,6 +689,7 @@ impl<'a> SignContext<'a> { signing_key, key_info_writer: None, start_node_id: None, + id_attributes: &[], policy: crate::policy::SigningPolicy::default(), provider: crate::provider::default_provider(), } @@ -722,6 +724,13 @@ impl<'a> SignContext<'a> { self } + /// Add caller-declared ID attributes for start-node and Reference lookup. + #[must_use] + pub fn id_attributes(mut self, registrations: &'a [crate::IdAttributeRegistration]) -> Self { + self.id_attributes = registrations; + self + } + /// Select the node returned by XPath's `here()` extension function. /// /// The default follows XMLDSig and returns the `` parameter. @@ -746,7 +755,8 @@ impl<'a> SignContext<'a> { self.policy.resources.validate_xml_document_len(xml.len())?; let document = parse_signing_document(xml, Some(&self.policy)) .map_err(SigningDigestError::XmlParse)?; - let target_signature = signing_signature_index(&document, self.start_node_id)?; + let target_signature = + signing_signature_index(&document, self.start_node_id, self.id_attributes)?; self.sign_template_at_index(xml, target_signature) } @@ -782,6 +792,7 @@ impl<'a> SignContext<'a> { self.provider, &execution_budget, Some(target_signature), + self.id_attributes, )?; self.policy .resources @@ -840,7 +851,7 @@ impl<'a> SignContext<'a> { let templated = if let Some(id) = self.start_node_id { let document = parse_signing_document(xml, Some(&self.policy)) .map_err(SigningDigestError::XmlParse)?; - let start = signing_start_node(&document, id)?; + let start = signing_start_node(&document, id, self.id_attributes)?; append_signature_to_element_with_options( xml, &template, @@ -856,7 +867,7 @@ impl<'a> SignContext<'a> { let document = parse_signing_document(&templated, Some(&self.policy)) .map_err(SigningDigestError::XmlParse)?; let target_signature = if let Some(id) = self.start_node_id { - let start = signing_start_node(&document, id)?; + let start = signing_start_node(&document, id, self.id_attributes)?; let appended = start .children() .rfind(|node| node.has_tag_name((XMLDSIG_NS, "Signature"))) @@ -865,7 +876,7 @@ impl<'a> SignContext<'a> { })?; signature_index(&document, appended)? } else { - signing_signature_index(&document, None)? + signing_signature_index(&document, None, self.id_attributes)? }; self.sign_template_at_index(&templated, target_signature) } @@ -895,6 +906,7 @@ pub fn compute_reference_digest_values( crate::provider::default_provider(), &execution_budget, None, + &[], ) } @@ -905,6 +917,7 @@ fn compute_reference_digest_values_with_options( provider: &dyn crate::provider::CryptoProvider, execution_budget: &TransformExecutionBudget, target_signature: Option, + id_attributes: &[crate::IdAttributeRegistration], ) -> Result, SigningDigestError> { let doc = parse_signing_document(xml, policy)?; let signature = find_signing_signature_node(&doc, target_signature)?; @@ -952,7 +965,7 @@ fn compute_reference_digest_values_with_options( } } } - let resolver = UriReferenceResolver::new(&doc); + let resolver = UriReferenceResolver::with_id_registrations(&doc, id_attributes); references .into_iter() .enumerate() @@ -1011,6 +1024,7 @@ pub fn fill_reference_digest_values(xml: &str) -> Result, + id_attributes: &[crate::IdAttributeRegistration], ) -> Result { let digest_values = compute_reference_digest_values_with_options( xml, @@ -1029,6 +1044,7 @@ fn fill_reference_digest_values_with_options( provider, execution_budget, target_signature, + id_attributes, )? .into_iter() .map(|digest| digest.digest_value); @@ -1136,9 +1152,10 @@ fn find_signing_signature_node<'a>( fn signing_signature_index( doc: &Document<'_>, start_node_id: Option<&str>, + id_attributes: &[crate::IdAttributeRegistration], ) -> Result { let selected = if let Some(id) = start_node_id { - let start = signing_start_node(doc, id)?; + let start = signing_start_node(doc, id, id_attributes)?; start .descendants() .find(|node| node.has_tag_name((XMLDSIG_NS, "Signature"))) @@ -1156,8 +1173,9 @@ fn signing_signature_index( fn signing_start_node<'a>( doc: &'a Document<'a>, id: &str, + id_attributes: &[crate::IdAttributeRegistration], ) -> Result, SigningDigestError> { - UriReferenceResolver::new(doc) + UriReferenceResolver::with_id_registrations(doc, id_attributes) .node_for_id(id) .ok_or_else(|| { SigningDigestError::InvalidStructure(format!( diff --git a/src/xmldsig/uri.rs b/src/xmldsig/uri.rs index 9e90b4a..6edf5e9 100644 --- a/src/xmldsig/uri.rs +++ b/src/xmldsig/uri.rs @@ -123,6 +123,19 @@ impl<'a> UriReferenceResolver<'a> { } } + /// Build a resolver with typed global and element-scoped ID registrations. + pub fn with_id_registrations( + doc: &'a Document<'a>, + registrations: &[crate::IdAttributeRegistration], + ) -> Self { + Self { + doc, + id_index: XmlIdIndex::with_registrations(doc, registrations), + external_resources: None, + external_resource_budget: ExternalResourceBudget::default(), + } + } + /// Attach an explicit caller-owned external-resource map. /// /// No network or filesystem access is performed by this resolver. Keys are diff --git a/src/xmldsig/verify.rs b/src/xmldsig/verify.rs index facf679..867b0f3 100644 --- a/src/xmldsig/verify.rs +++ b/src/xmldsig/verify.rs @@ -230,6 +230,7 @@ pub struct VerifyContext<'a> { store_pre_digest: bool, external_resources: Option<&'a HashMap>>, start_node_id: Option<&'a str>, + id_attributes: &'a [crate::IdAttributeRegistration], } impl<'a> VerifyContext<'a> { @@ -250,6 +251,7 @@ impl<'a> VerifyContext<'a> { store_pre_digest: false, external_resources: None, start_node_id: None, + id_attributes: &[], } } @@ -356,6 +358,12 @@ impl<'a> VerifyContext<'a> { self } + /// Add caller-declared ID attributes for start-node and Reference lookup. + pub fn id_attributes(mut self, registrations: &'a [crate::IdAttributeRegistration]) -> Self { + self.id_attributes = registrations; + self + } + /// Allow bounded internal DTD declarations while keeping external entity /// resolution disabled. This is off by default. pub fn allow_internal_dtd(mut self, enabled: bool) -> Self { @@ -1014,10 +1022,11 @@ fn verify_signature_with_context( entity_resolver: None, }, )?; - let resolver = UriReferenceResolver::new(&doc).with_external_resource_limits( - ctx.policy.resources.max_external_resource_bytes, - ctx.policy.resources.max_external_resource_total_bytes, - ); + let resolver = UriReferenceResolver::with_id_registrations(&doc, ctx.id_attributes) + .with_external_resource_limits( + ctx.policy.resources.max_external_resource_bytes, + ctx.policy.resources.max_external_resource_total_bytes, + ); let resolver = match ctx.external_resources { Some(resources) => resolver.with_external_resources(resources), None => resolver, diff --git a/src/xmlenc/decrypt.rs b/src/xmlenc/decrypt.rs index 273396f..fb285a1 100644 --- a/src/xmlenc/decrypt.rs +++ b/src/xmlenc/decrypt.rs @@ -49,6 +49,7 @@ pub struct DecryptContext<'a> { resolver: &'a dyn DecryptionKeyResolver, policy: crate::policy::DecryptionPolicy, provider: &'a dyn crate::provider::CryptoProvider, + id_attributes: &'a [crate::IdAttributeRegistration], } impl<'a> DecryptContext<'a> { @@ -58,6 +59,7 @@ impl<'a> DecryptContext<'a> { resolver, policy: crate::policy::DecryptionPolicy::default(), provider: crate::provider::default_provider(), + id_attributes: &[], } } @@ -73,6 +75,12 @@ impl<'a> DecryptContext<'a> { self } + /// Add caller-declared ID attributes for operation start-node lookup. + pub fn id_attributes(mut self, registrations: &'a [crate::IdAttributeRegistration]) -> Self { + self.id_attributes = registrations; + self + } + /// Parse and decrypt a standalone `EncryptedData` XML fragment. pub fn decrypt(&self, xml: &str) -> Result { let encrypted = parse_encrypted_data_with_policy(xml, &self.policy)?; @@ -472,11 +480,13 @@ fn decrypt_document_with_context( let document = Document::parse_with_options(xml, parsing_options())?; let start = match selector { DocumentEncryptedDataSelector::StartNodeId(Some(id)) => { - XmlIdIndex::new(&document).node(id).ok_or_else(|| { - XmlEncError::InvalidStructure(format!( - "selected node ID is missing or ambiguous: {id}" - )) - })? + XmlIdIndex::with_registrations(&document, context.id_attributes) + .node(id) + .ok_or_else(|| { + XmlEncError::InvalidStructure(format!( + "selected node ID is missing or ambiguous: {id}" + )) + })? } DocumentEncryptedDataSelector::StartNodeId(None) | DocumentEncryptedDataSelector::EncryptedDataId(_) => document.root(), diff --git a/tools/xmlsec1/README.md b/tools/xmlsec1/README.md index a649b46..cbb931b 100644 --- a/tools/xmlsec1/README.md +++ b/tools/xmlsec1/README.md @@ -37,8 +37,11 @@ AES keys reject recipient `EncryptedKey` templates they cannot refresh. Signing options validate every certificate from `key,leaf,intermediate,...` and embed the chain when the template provides a `KeyInfo` placeholder, filling an empty `X509Data` without erasing sibling key sources; verification accepts -stdin as `-` and can select one signature subtree with `--node-id`. Output paths -support the upstream `{inputfile}` basename template, and `--gen-key[:name]` +stdin as `-` and can select one signature subtree with `--node-id`. +support request-local custom IDs through global `--add-id-attr NAME` and +element-scoped `--id-attr[:ATTR] [NAMESPACE-URI:]ELEMENT`; the same +registrations drive Reference resolution and XMLEnc operation selection. +Output paths support the upstream `{inputfile}` basename template, and `--gen-key[:name]` emits both named and unnamed AES key-store entries. `help-all` is generated from the parser registry. Named signing keys require a template `KeyName`; named verification and encryption/decryption options form key sets and require the diff --git a/tools/xmlsec1/src/args.rs b/tools/xmlsec1/src/args.rs index 43b7ff3..5a7b822 100644 --- a/tools/xmlsec1/src/args.rs +++ b/tools/xmlsec1/src/args.rs @@ -414,7 +414,7 @@ pub(crate) const OPTION_SPECS: &[OptionSpec] = &[ canonical: "add-id-attr", aliases: &[], arity: VALUE, - accepts_parameter: true, + accepts_parameter: false, }, OptionSpec { canonical: "binary-data", diff --git a/tools/xmlsec1/src/commands.rs b/tools/xmlsec1/src/commands.rs index 49c6a69..6b615cd 100644 --- a/tools/xmlsec1/src/commands.rs +++ b/tools/xmlsec1/src/commands.rs @@ -10,6 +10,7 @@ use roxmltree::{Document, Node, ParsingOptions}; use rsa::{RsaPublicKey, pkcs8::DecodePublicKey as _, traits::PublicKeyParts as _}; use x509_parser::prelude::FromDer as _; use xml_sec::{ + IdAttributeRegistration, policy::{DecryptionPolicy, EncryptionPolicy, SigningPolicy, VerificationPolicy}, provider::{CryptoProvider, default_provider}, xmldsig::{ @@ -471,6 +472,34 @@ fn option_value_text(option: &crate::OptionValue) -> Result<&str, CommandError> .ok_or_else(|| CommandError::Usage(format!("--{} value must be valid UTF-8", option.name))) } +fn id_attribute_registrations( + invocation: &Invocation, +) -> Result, CommandError> { + let mut registrations = invocation + .values("add-id-attr") + .map(|option| option_value_text(option).map(IdAttributeRegistration::global)) + .collect::, _>>()?; + for option in invocation.values("id-attr") { + let element = option_value_text(option)?; + let (namespace, local_name) = element + .rsplit_once(':') + .map_or((None, element), |(namespace, local_name)| { + (Some(namespace), local_name) + }); + if local_name.is_empty() { + return Err(CommandError::Usage( + "--id-attr element local name cannot be empty".into(), + )); + } + registrations.push(IdAttributeRegistration::scoped( + option.parameter.as_deref().unwrap_or("id"), + local_name, + namespace, + )); + } + Ok(registrations) +} + fn select_named_candidate<'a, T: Copy>( candidates: &[(&'a crate::OptionValue, T)], requested_names: &[String], @@ -515,14 +544,16 @@ fn select_named_candidate<'a, T: Copy>( fn sign(invocation: &Invocation, stdout: &mut dyn Write) -> Result<(), CommandError> { validate_options(invocation, SIGN_OPTIONS)?; - reject_unimplemented_selectors(invocation, &["node-id"])?; + reject_unimplemented_selectors(invocation, &["node-id", "id-attr", "add-id-attr"])?; if invocation.last_value("pwd").is_some() { return Err(CommandError::UnsupportedOption("pwd".into())); } let policy = SigningPolicy::default(); let xml = read_input(invocation, policy.resources.max_xml_document_bytes)?; let start_node_id = option_text(invocation, "node-id")?; - let signature = key_material::signing_signature_metadata(&xml, start_node_id, &policy)?; + let id_attributes = id_attribute_registrations(invocation)?; + let signature = + key_material::signing_signature_metadata(&xml, start_node_id, &id_attributes, &policy)?; let (key_option, certificate_is_der) = select_signing_key(invocation, &signature.key_names)?; let value = key_option.value.as_deref().unwrap_or_default(); let (key_path, certificate_paths) = split_key_and_certificates(value)?; @@ -551,6 +582,7 @@ fn sign(invocation: &Invocation, stdout: &mut dyn Write) -> Result<(), CommandEr if let Some(id) = start_node_id { context = context.start_node_id(id); } + context = context.id_attributes(&id_attributes); if let Some(writer) = &writer { if signature.has_key_info { context = context.key_info_writer(writer); @@ -641,7 +673,7 @@ fn xmlsec_compatibility_verification_policy(invocation: &Invocation) -> Verifica fn verify(invocation: &Invocation, stdout: &mut dyn Write) -> Result<(), CommandError> { validate_options(invocation, VERIFY_OPTIONS)?; - reject_unimplemented_selectors(invocation, &["node-id"])?; + reject_unimplemented_selectors(invocation, &["node-id", "id-attr", "add-id-attr"])?; reject_unimplemented_verification_policy(invocation)?; let explicit_keys = [ ("pubkey-pem", false), @@ -664,7 +696,13 @@ fn verify(invocation: &Invocation, stdout: &mut dyn Write) -> Result<(), Command let policy = xmlsec_compatibility_verification_policy(invocation); let xml = read_input(invocation, policy.resources.max_xml_document_bytes)?; let start_node_id = option_text(invocation, "node-id")?; - let signature = key_material::verification_signature_metadata(&xml, start_node_id, &policy)?; + let id_attributes = id_attribute_registrations(invocation)?; + let signature = key_material::verification_signature_metadata( + &xml, + start_node_id, + &id_attributes, + &policy, + )?; let algorithm = signature.algorithm; let selected_key = if explicit_keys.is_empty() { None @@ -680,7 +718,7 @@ fn verify(invocation: &Invocation, stdout: &mut dyn Write) -> Result<(), Command let result = if let Some((option, false)) = selected_key { let path = option.value.as_deref().unwrap_or_default(); let key = key_material::load_verification_key(path, algorithm)?; - verification_context(policy, start_node_id) + verification_context(policy, start_node_id, &id_attributes) .key(&key) .verify(&xml) .map_err(|error| CommandError::Signature(error.to_string()))? @@ -691,6 +729,7 @@ fn verify(invocation: &Invocation, stdout: &mut dyn Write) -> Result<(), Command algorithm, policy, start_node_id, + &id_attributes, &xml, )? } else { @@ -715,7 +754,7 @@ fn verify(invocation: &Invocation, stdout: &mut dyn Write) -> Result<(), Command } } let resolver = DefaultKeyResolver::new(config); - verification_context(policy, start_node_id) + verification_context(policy, start_node_id, &id_attributes) .key_resolver(&resolver) .verify(&xml) .map_err(|error| CommandError::Signature(error.to_string()))? @@ -817,11 +856,14 @@ fn donor_dsig_status(status: DsigStatus) -> (&'static str, &'static str) { } } -fn verification_context( +fn verification_context<'a>( policy: VerificationPolicy, - start_node_id: Option<&str>, -) -> VerifyContext<'_> { - let context = VerifyContext::new().policy(policy); + start_node_id: Option<&'a str>, + id_attributes: &'a [IdAttributeRegistration], +) -> VerifyContext<'a> { + let context = VerifyContext::new() + .policy(policy) + .id_attributes(id_attributes); match start_node_id { Some(id) => context.start_node_id(id), None => context, @@ -834,6 +876,7 @@ fn verify_with_explicit_certificate( algorithm: SignatureAlgorithm, policy: VerificationPolicy, start_node_id: Option<&str>, + id_attributes: &[IdAttributeRegistration], xml: &str, ) -> Result { let certificate_der = @@ -878,7 +921,7 @@ fn verify_with_explicit_certificate( .resolve_with_policy(Some(&key_info), algorithm, &resolver_policy) .map_err(|error| CommandError::Signature(error.to_string()))? .ok_or_else(|| CommandError::Signature("explicit certificate was not resolved".into()))?; - verification_context(policy, start_node_id) + verification_context(policy, start_node_id, id_attributes) .key(key.as_ref()) .verify(xml) .map_err(|error| CommandError::Signature(error.to_string())) @@ -886,7 +929,7 @@ fn verify_with_explicit_certificate( fn encrypt(invocation: &Invocation, stdout: &mut dyn Write) -> Result<(), CommandError> { validate_options(invocation, ENCRYPT_OPTIONS)?; - reject_unimplemented_selectors(invocation, &["node-id"])?; + reject_unimplemented_selectors(invocation, &["node-id", "id-attr", "add-id-attr"])?; let has_binary_data = invocation.last_value("binary-data").is_some(); let has_xml_data = invocation.last_value("xml-data").is_some(); if has_binary_data == has_xml_data { @@ -899,7 +942,8 @@ fn encrypt(invocation: &Invocation, stdout: &mut dyn Write) -> Result<(), Comman let maximum_plaintext_bytes = policy.resources.max_encryption_plaintext_bytes; let template = read_input(invocation, policy.resources.max_xml_document_bytes)?; let start_node_id = option_text(invocation, "node-id")?; - let metadata = encryption_template(&template, start_node_id, &policy)?; + let id_attributes = id_attribute_registrations(invocation)?; + let metadata = encryption_template(&template, start_node_id, &id_attributes, &policy)?; let algorithm = metadata.algorithm; let encrypted_type = metadata.encrypted_type; let explicit_encrypted_type = metadata.explicit_encrypted_type; @@ -988,6 +1032,7 @@ fn encrypt(invocation: &Invocation, stdout: &mut dyn Write) -> Result<(), Comman validate_recipient_key_metadata( &template, start_node_id, + &id_attributes, &policy, selected_recipients.iter().map(|(key, _)| key), )?; @@ -1026,6 +1071,7 @@ fn encrypt(invocation: &Invocation, stdout: &mut dyn Write) -> Result<(), Comman &template, &result.encrypted_data_xml, start_node_id, + &id_attributes, &policy, )?; if rendered.len() > maximum_document_bytes { @@ -1039,11 +1085,12 @@ fn encrypt(invocation: &Invocation, stdout: &mut dyn Write) -> Result<(), Comman fn validate_recipient_key_metadata<'a>( template: &str, start_node_id: Option<&str>, + id_attributes: &[IdAttributeRegistration], policy: &EncryptionPolicy, selected_keys: impl IntoIterator, ) -> Result<(), CommandError> { let document = parse_encryption_document(template, policy)?; - let encrypted_data = select_encrypted_data(&document, start_node_id)?; + let encrypted_data = select_encrypted_data(&document, start_node_id, id_attributes)?; let encrypted_keys = direct_child_element(encrypted_data, XMLDSIG_NS, "KeyInfo") .into_iter() .flat_map(|key_info| key_info.children()) @@ -1158,24 +1205,40 @@ fn recipient_metadata_error(message: &str) -> CommandError { fn template_oaep_parameters( encrypted_key: Node<'_, '_>, ) -> Result, CommandError> { - let Some(method) = encrypted_key - .children() - .find(|node| node.has_tag_name((XMLENC_NS, "EncryptionMethod"))) - else { - return Ok(None); - }; + let method = singleton_direct_child( + encrypted_key, + XMLENC_NS, + "EncryptionMethod", + "EncryptedKey contains more than one direct EncryptionMethod", + )? + .ok_or_else(|| { + CommandError::Encryption( + "EncryptedKey must contain exactly one direct EncryptionMethod".into(), + ) + })?; let algorithm = method .attribute("Algorithm") .ok_or_else(|| CommandError::Encryption("EncryptedKey has no algorithm".into()))?; let transport = KeyTransportAlgorithm::from_uri(algorithm) .map_err(|error| CommandError::Encryption(error.to_string()))?; - let digest = method - .children() - .find(|node| node.has_tag_name((XMLDSIG_NS, "DigestMethod"))) - .and_then(|node| node.attribute("Algorithm")); - let mgf_node = method - .children() - .find(|node| node.has_tag_name((XMLENC11_NS, "MGF"))); + let digest_node = singleton_direct_child( + method, + XMLDSIG_NS, + "DigestMethod", + "EncryptionMethod contains more than one direct DigestMethod", + )?; + let digest = digest_node + .map(|node| { + node.attribute("Algorithm") + .ok_or_else(|| CommandError::Encryption("DigestMethod has no Algorithm".into())) + }) + .transpose()?; + let mgf_node = singleton_direct_child( + method, + XMLENC11_NS, + "MGF", + "EncryptionMethod contains more than one direct MGF", + )?; if transport == KeyTransportAlgorithm::RsaOaepMgf1p && mgf_node.is_some() { return Err(CommandError::Encryption( "legacy rsa-oaep-mgf1p does not permit an XML Encryption 1.1 MGF parameter".into(), @@ -1188,20 +1251,23 @@ fn template_oaep_parameters( } else { oaep_mgf_from_uri(mgf.unwrap_or(OaepDigestAlgorithm::Sha1.mgf_uri()))? }; - let label = method - .children() - .find(|node| node.has_tag_name((XMLENC_NS, "OAEPparams"))) - .and_then(|node| node.text()) - .map_or_else( - || Ok(Vec::new()), - |encoded| { - base64::Engine::decode( - &base64::engine::general_purpose::STANDARD, - encoded.split_ascii_whitespace().collect::(), - ) - .map_err(|error| CommandError::Encryption(format!("invalid OAEPparams: {error}"))) - }, - )?; + let label = singleton_direct_child( + method, + XMLENC_NS, + "OAEPparams", + "EncryptionMethod contains more than one direct OAEPparams", + )? + .and_then(|node| node.text()) + .map_or_else( + || Ok(Vec::new()), + |encoded| { + base64::Engine::decode( + &base64::engine::general_purpose::STANDARD, + encoded.split_ascii_whitespace().collect::(), + ) + .map_err(|error| CommandError::Encryption(format!("invalid OAEPparams: {error}"))) + }, + )?; Ok(Some(RsaOaepParameters { algorithm: transport, digest, @@ -1210,6 +1276,22 @@ fn template_oaep_parameters( })) } +fn singleton_direct_child<'a, 'input>( + parent: Node<'a, 'input>, + namespace: &str, + name: &str, + cardinality_error: &str, +) -> Result>, CommandError> { + let mut children = parent + .children() + .filter(|node| node.has_tag_name((namespace, name))); + let child = children.next(); + if children.next().is_some() { + return Err(CommandError::Encryption(cardinality_error.into())); + } + Ok(child) +} + fn oaep_digest_from_uri(uri: &str) -> Result { [ OaepDigestAlgorithm::Sha1, @@ -1238,11 +1320,12 @@ fn apply_encryption_template( template: &str, generated: &str, start_node_id: Option<&str>, + id_attributes: &[IdAttributeRegistration], policy: &EncryptionPolicy, ) -> Result { let template_document = parse_encryption_document(template, policy)?; let generated_document = parse_encryption_document(generated, policy)?; - let template_data = select_encrypted_data(&template_document, start_node_id)?; + let template_data = select_encrypted_data(&template_document, start_node_id, id_attributes)?; let generated_data = generated_document.root_element(); let template_cipher = encrypted_data_cipher_value(template_data) .ok_or_else(|| CommandError::Encryption("template has no CipherValue".into()))?; @@ -1439,15 +1522,16 @@ fn encrypted_key_cipher_values<'a, 'input>( fn decrypt(invocation: &Invocation, stdout: &mut dyn Write) -> Result<(), CommandError> { validate_options(invocation, DECRYPT_OPTIONS)?; - reject_unimplemented_selectors(invocation, &["node-id"])?; + reject_unimplemented_selectors(invocation, &["node-id", "id-attr", "add-id-attr"])?; if invocation.last_value("pwd").is_some() { return Err(CommandError::UnsupportedOption("pwd".into())); } let policy = DecryptionPolicy::default(); let xml = read_input(invocation, policy.resources.max_xml_document_bytes)?; let encrypted_data_id = option_text(invocation, "node-id")?; + let id_attributes = id_attribute_registrations(invocation)?; let document = parse_encryption_document(&xml, &policy)?; - let encrypted_data = select_encrypted_data(&document, encrypted_data_id)?; + let encrypted_data = select_encrypted_data(&document, encrypted_data_id, &id_attributes)?; let standalone = encrypted_data == document.root_element(); let content_key_name = encrypted_data_key_name(encrypted_data)?; let recipient_key_names = encrypted_key_recipient_names(encrypted_data); @@ -1482,6 +1566,7 @@ fn decrypt(invocation: &Invocation, stdout: &mut dyn Write) -> Result<(), Comman encrypted_data_id, standalone, policy, + &id_attributes, )? } else if !private_keys.is_empty() { let candidates = private_keys @@ -1510,7 +1595,14 @@ fn decrypt(invocation: &Invocation, stdout: &mut dyn Write) -> Result<(), Comman inner: PrivateKeyDecryptor::new(key_material::load_rsa_private(path)?), key_name: recipient_filter, }; - decrypt_input(&resolver, &xml, encrypted_data_id, standalone, policy)? + decrypt_input( + &resolver, + &xml, + encrypted_data_id, + standalone, + policy, + &id_attributes, + )? } else { return Err(CommandError::Usage( "decrypt requires --aes-key or an RSA private key".into(), @@ -1603,8 +1695,11 @@ fn decrypt_input( encrypted_data_id: Option<&str>, standalone: bool, policy: DecryptionPolicy, + id_attributes: &[IdAttributeRegistration], ) -> Result, CommandError> { - let context = DecryptContext::new(resolver).policy(policy); + let context = DecryptContext::new(resolver) + .policy(policy) + .id_attributes(id_attributes); if standalone { return context .decrypt(xml) @@ -1637,10 +1732,11 @@ struct EncryptionTemplateRecipient { fn encryption_template( xml: &str, start_node_id: Option<&str>, + id_attributes: &[IdAttributeRegistration], policy: &EncryptionPolicy, ) -> Result { let document = parse_encryption_document(xml, policy)?; - let encrypted_data = select_encrypted_data(&document, start_node_id)?; + let encrypted_data = select_encrypted_data(&document, start_node_id, id_attributes)?; let method = encrypted_data .children() .find(|node| node.has_tag_name((XMLENC_NS, "EncryptionMethod"))) @@ -1775,9 +1871,10 @@ fn parse_encryption_document<'a>( fn select_encrypted_data<'a>( document: &'a Document<'a>, start_node_id: Option<&str>, + id_attributes: &[IdAttributeRegistration], ) -> Result, CommandError> { let start = if let Some(id) = start_node_id { - UriReferenceResolver::new(document) + UriReferenceResolver::with_id_registrations(document, id_attributes) .node_for_id(id) .ok_or_else(|| { CommandError::Encryption(format!("selected node ID is missing or ambiguous: {id}")) @@ -1840,6 +1937,9 @@ fn keys(invocation: &Invocation, stdout: &mut dyn Write) -> Result<(), CommandEr "\n\n\ {entries}\n" ); + Document::parse(&document).map_err(|error| { + CommandError::Usage(format!("generated key store is not valid XML: {error}")) + })?; if invocation.positional.len() > 1 { return Err(CommandError::Usage( "keys accepts at most one key-store path".into(), @@ -2062,9 +2162,14 @@ mod tests { "a2V5ZGF0YQ==" ); - let rendered = - apply_encryption_template(&template, &generated, None, &EncryptionPolicy::default()) - .unwrap(); + let rendered = apply_encryption_template( + &template, + &generated, + None, + &[], + &EncryptionPolicy::default(), + ) + .unwrap(); let document = Document::parse(&rendered) .expect("injected KeyInfo prefixes must remain namespace-bound"); assert!( @@ -2090,9 +2195,14 @@ mod tests { "a2V5ZGF0YQ==" ); - let rendered = - apply_encryption_template(&template, &generated, None, &EncryptionPolicy::default()) - .expect("empty KeyInfo must accept a generated recipient"); + let rendered = apply_encryption_template( + &template, + &generated, + None, + &[], + &EncryptionPolicy::default(), + ) + .expect("empty KeyInfo must accept a generated recipient"); let document = Document::parse(&rendered).expect("merged output must parse"); assert_eq!( @@ -2128,7 +2238,7 @@ mod tests { ..EncryptionPolicy::default() }; - let error = apply_encryption_template(&template, &generated, None, &policy) + let error = apply_encryption_template(&template, &generated, None, &[], &policy) .expect_err("the aggregate merged document must be reparsed under policy"); assert!(error.to_string().contains("nodes limit"), "{error}"); } @@ -2164,7 +2274,7 @@ mod tests { } xml.push_str(""); - let error = match encryption_template(&xml, None, &EncryptionPolicy::default()) { + let error = match encryption_template(&xml, None, &[], &EncryptionPolicy::default()) { Ok(_) => panic!("over-budget template must fail"), Err(error) => error, }; diff --git a/tools/xmlsec1/src/key_material.rs b/tools/xmlsec1/src/key_material.rs index 787eaf2..0bf3210 100644 --- a/tools/xmlsec1/src/key_material.rs +++ b/tools/xmlsec1/src/key_material.rs @@ -73,6 +73,7 @@ pub fn read_text(path: impl AsRef) -> Result { pub fn verification_signature_metadata( xml: &str, start_node_id: Option<&str>, + id_attributes: &[xml_sec::IdAttributeRegistration], policy: &VerificationPolicy, ) -> Result { policy.validate()?; @@ -83,7 +84,7 @@ pub fn verification_signature_metadata( )?; let signature = match start_node_id { Some(id) => { - let start = UriReferenceResolver::new(&document) + let start = UriReferenceResolver::with_id_registrations(&document, id_attributes) .node_for_id(id) .ok_or_else(|| KeyMaterialError::SelectedNodeUnavailable(id.to_owned()))?; start @@ -109,6 +110,7 @@ pub fn verification_signature_metadata( pub fn signing_signature_metadata( xml: &str, start_node_id: Option<&str>, + id_attributes: &[xml_sec::IdAttributeRegistration], policy: &SigningPolicy, ) -> Result { policy.validate()?; @@ -119,7 +121,7 @@ pub fn signing_signature_metadata( )?; let signature = match start_node_id { Some(id) => { - let start = UriReferenceResolver::new(&document) + let start = UriReferenceResolver::with_id_registrations(&document, id_attributes) .node_for_id(id) .ok_or_else(|| KeyMaterialError::SelectedNodeUnavailable(id.to_owned()))?; start @@ -417,6 +419,7 @@ mod tests { let metadata = verification_signature_metadata( &xml, Some("ec"), + &[], &xml_sec::policy::VerificationPolicy::default(), ) .unwrap(); @@ -435,6 +438,7 @@ mod tests { let metadata = verification_signature_metadata( &xml, None, + &[], &xml_sec::policy::VerificationPolicy::default(), ) .unwrap(); @@ -458,7 +462,7 @@ mod tests { ..xml_sec::policy::VerificationPolicy::default() }; - let error = verification_signature_metadata(&xml, None, &policy).unwrap_err(); + let error = verification_signature_metadata(&xml, None, &[], &policy).unwrap_err(); assert!(error.to_string().contains("nodes limit")); } } diff --git a/tools/xmlsec1/tests/process_contract.rs b/tools/xmlsec1/tests/process_contract.rs index 6d9ffed..354f248 100644 --- a/tools/xmlsec1/tests/process_contract.rs +++ b/tools/xmlsec1/tests/process_contract.rs @@ -131,6 +131,72 @@ fn signs_verifies_and_rejects_tampering_through_process_api() { assert!(String::from_utf8_lossy(&rejected.stderr).contains("invalid")); } +#[test] +fn scoped_id_attribute_selects_and_signs_the_registered_element() { + // libxmlsec1's --id-attr form is element-scoped: the custom attribute must + // drive both --node-id selection and same-document Reference resolution. + let temp = tempfile::tempdir().unwrap(); + let template = temp.path().join("scoped-id.xml"); + let signed = temp.path().join("scoped-id-signed.xml"); + let private_key = project_root().join("tests/fixtures/keys/rsa/rsa-4096-key.pem"); + let public_key = project_root().join("tests/fixtures/keys/rsa/rsa-4096-pubkey.pem"); + fs::write( + &template, + r##"registered"##, + ) + .unwrap(); + + let sign = Command::new(binary()) + .args([ + "sign", + "--id-attr:Token", + "Envelope", + "--node-id", + "selected", + ]) + .args(["--privkey-pem"]) + .arg(&private_key) + .arg("--output") + .arg(&signed) + .arg(&template) + .output() + .unwrap(); + assert!( + sign.status.success(), + "{}", + String::from_utf8_lossy(&sign.stderr) + ); + + let verify = Command::new(binary()) + .args([ + "verify", + "--id-attr:Token", + "Envelope", + "--node-id", + "selected", + ]) + .arg("--pubkey-pem") + .arg(&public_key) + .arg(&signed) + .output() + .unwrap(); + assert!( + verify.status.success(), + "{}", + String::from_utf8_lossy(&verify.stderr) + ); + + let wrong_element = Command::new(binary()) + .args(["sign", "--id-attr:Token", "Other", "--node-id", "selected"]) + .arg("--privkey-pem") + .arg(&private_key) + .arg(&template) + .output() + .unwrap(); + assert!(!wrong_element.status.success()); + assert!(String::from_utf8_lossy(&wrong_element.stderr).contains("missing or ambiguous")); +} + #[test] fn xml_debug_verification_output_matches_the_donor_xml_contract() { // The upstream runner parses --print-xml-debug output with xmllint, so the @@ -1984,6 +2050,56 @@ fn encryption_node_id_accepts_namespaced_id_attributes() { assert!(String::from_utf8_lossy(&encrypt.stdout).contains("wsu:Id=\"target\"")); } +#[test] +fn global_id_attribute_selects_encrypt_and_decrypt_subtrees() { + // --add-id-attr applies one local attribute name to every element, and the + // registration must survive both CLI-side template parsing and core decrypt. + let temp = tempfile::tempdir().unwrap(); + let template = temp.path().join("custom-id-template.xml"); + let plaintext = temp.path().join("plaintext.xml"); + let encrypted = temp.path().join("encrypted.xml"); + let key = temp.path().join("aes.key"); + fs::write( + &template, + r#""#, + ) + .unwrap(); + fs::write(&plaintext, b"registered").unwrap(); + fs::write(&key, b"0123456789abcdef").unwrap(); + + let encrypt = Command::new(binary()) + .args(["encrypt", "--add-id-attr", "Token", "--node-id", "target"]) + .arg("--aes-key") + .arg(&key) + .arg("--xml-data") + .arg(&plaintext) + .arg("--output") + .arg(&encrypted) + .arg(&template) + .output() + .unwrap(); + assert!( + encrypt.status.success(), + "{}", + String::from_utf8_lossy(&encrypt.stderr) + ); + + let decrypt = Command::new(binary()) + .args(["decrypt", "--add-id-attr", "Token", "--node-id", "target"]) + .arg("--aes-key") + .arg(&key) + .arg(&encrypted) + .output() + .unwrap(); + assert!( + decrypt.status.success(), + "{}", + String::from_utf8_lossy(&decrypt.stderr) + ); + let output = String::from_utf8(decrypt.stdout).unwrap(); + assert!(output.contains("registered")); +} + #[test] fn named_decryption_key_selects_a_later_recipient() { // A document KeyName selects among all EncryptedKey recipients. Checking @@ -2203,6 +2319,67 @@ fn rsa_decryption_accepts_private_key_certificate_companions() { assert!(String::from_utf8_lossy(&missing.stderr).contains("missing.pem")); } +#[test] +fn encryption_rejects_incomplete_or_ambiguous_recipient_methods() { + // Template metadata must describe exactly one self-consistent wrapping + // method; first-match parsing can otherwise emit output our parser rejects. + let temp = tempfile::tempdir().unwrap(); + let plaintext = temp.path().join("plaintext.bin"); + let public_key = project_root().join("tests/fixtures/keys/rsa/rsa-4096-pubkey.pem"); + fs::write(&plaintext, b"recipient metadata").unwrap(); + let method = |children: &str| { + format!( + r#"{children}"# + ) + }; + let digest = r#""#; + let mgf = r#""#; + let label = "YQ=="; + + for (case, recipient_method, expected) in [ + ("missing", String::new(), "EncryptionMethod"), + ( + "duplicate-method", + format!("{}{}", method(""), method("")), + "EncryptionMethod", + ), + ( + "duplicate-digest", + method(&format!("{digest}{digest}")), + "DigestMethod", + ), + ("duplicate-mgf", method(&format!("{mgf}{mgf}")), "MGF"), + ( + "duplicate-label", + method(&format!("{label}{label}")), + "OAEPparams", + ), + ] { + let template = temp.path().join(format!("{case}.xml")); + fs::write( + &template, + format!( + r#"{recipient_method}"# + ), + ) + .unwrap(); + let output = Command::new(binary()) + .args(["encrypt", "--pubkey-pem"]) + .arg(&public_key) + .arg("--binary-data") + .arg(&plaintext) + .arg(&template) + .output() + .unwrap(); + assert!(!output.status.success(), "{case} unexpectedly succeeded"); + assert!( + String::from_utf8_lossy(&output.stderr).contains(expected), + "{case}: {}", + String::from_utf8_lossy(&output.stderr) + ); + } +} + #[test] fn honors_legacy_rsa_oaep_parameters_from_the_template() { // Advertising rsa-oaep-mgf1p requires an actual process round trip, and @@ -2390,6 +2567,25 @@ fn generated_key_store_uses_the_libxmlsec1_xml_shape() { ); } +#[test] +fn generated_key_store_rejects_non_xml_key_names_before_writing() { + // Escaping handles markup but cannot make XML 1.0-forbidden characters + // serializable; a failed command must not leave a malformed secret file. + let temp = tempfile::tempdir().unwrap(); + let key_store = temp.path().join("invalid.xml"); + let output = Command::new(binary()) + .arg("keys") + .arg("--gen-key:invalid\u{1}name") + .arg("aes-128") + .arg(&key_store) + .output() + .unwrap(); + + assert!(!output.status.success()); + assert!(String::from_utf8_lossy(&output.stderr).contains("XML")); + assert!(!key_store.exists()); +} + #[test] fn generated_key_store_allows_an_unnamed_key() { // The optional --gen-key parameter controls KeyName presence; omitting it From 9ee0b5b575a882b625ff3815c2728f9d7168e2a1 Mon Sep 17 00:00:00 2001 From: Dmitry Prudnikov Date: Sat, 15 Aug 2026 23:09:46 +0300 Subject: [PATCH 25/27] fix(cli): complete diagnostic contracts --- README.md | 2 +- docs/cli.md | 12 ++- tools/xmlsec1/README.md | 6 +- tools/xmlsec1/src/commands.rs | 80 +++++++++++++-- tools/xmlsec1/src/key_material.rs | 15 +++ tools/xmlsec1/tests/process_contract.rs | 125 ++++++++++++++++++++++++ 6 files changed, 227 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index f7f5d06..94b3327 100644 --- a/README.md +++ b/README.md @@ -83,7 +83,7 @@ certificate-chain embedding, stdin input, signature selection by node ID, and deterministic process statuses. `help-all` enumerates the same registered commands and options accepted by the parser; donor option multiplicity is enforced across canonical and alias spellings, and `--print-xml-debug` emits -parseable verification diagnostics. Named signing keys require a +parseable operation diagnostics separately from `--output`. Named signing keys require a template `KeyName`, while repeatable named verification and encryption/decryption options form key sets from which the selected XML `KeyName` must identify exactly one key unless lax lookup is requested; unnamed templates still use their sole diff --git a/docs/cli.md b/docs/cli.md index 3f25b52..051e350 100644 --- a/docs/cli.md +++ b/docs/cli.md @@ -23,10 +23,14 @@ non-zero. Commands absent from the pinned 1.3.13 surface are not advertised; historical `sign-tmpl` spellings are rejected instead of being routed to `sign` without template-generation semantics. -`--print-debug` emits the donor-style text status, while `--print-xml-debug` -emits a well-formed `VerificationContext` with donor `OK`/`FAILED` status and -failure-reason vocabulary. XML diagnostics are emitted for invalid signatures -before the command returns its required non-zero status. +`--print-debug` emits donor-style text context diagnostics, while +`--print-xml-debug` emits a well-formed operation context. Signing and +verification use `SignatureContext` and `VerificationContext`; encryption and +decryption use `DataEncryptionContext` and `DataDecryptionContext`. With +`--output`, the transformed payload is written to the requested file and +diagnostics remain on stdout. Verification diagnostics retain donor +`OK`/`FAILED` status and failure-reason vocabulary and are emitted for invalid +signatures before the command returns its required non-zero status. Capability checks and runtime dispatch use one registry. A transform or key-data class absent from `list-*` is not silently substituted and causes `check-*` to diff --git a/tools/xmlsec1/README.md b/tools/xmlsec1/README.md index cbb931b..ade9053 100644 --- a/tools/xmlsec1/README.md +++ b/tools/xmlsec1/README.md @@ -16,8 +16,10 @@ coverage. The parser rejects repeated singleton options, including mixed canonical and alias spellings, while preserving donor multi-value key and certificate inputs. -`--print-debug` is text; `--print-xml-debug` emits a parseable donor-shaped -`VerificationContext` for both successful and invalid verification results, +`--print-debug` is text; `--print-xml-debug` emits parseable donor-shaped +signature, verification, encryption, and decryption contexts. When `--output` +is present, transformed data goes to the file while diagnostics remain on +stdout. Verification output covers both successful and invalid results, including aggregate failures from authenticated Manifest references. `--aes-key` files are raw binary key material (`--aeskey` remains a compatible diff --git a/tools/xmlsec1/src/commands.rs b/tools/xmlsec1/src/commands.rs index 6b615cd..655431b 100644 --- a/tools/xmlsec1/src/commands.rs +++ b/tools/xmlsec1/src/commands.rs @@ -597,7 +597,30 @@ fn sign(invocation: &Invocation, stdout: &mut dyn Write) -> Result<(), CommandEr let signed = context .sign_template(&xml) .map_err(|error| CommandError::Signature(error.to_string()))?; - write_output(invocation, signed.as_bytes(), stdout) + write_output(invocation, signed.as_bytes(), stdout)?; + write_signing_diagnostics(invocation, signature.algorithm, stdout) +} + +fn write_signing_diagnostics( + invocation: &Invocation, + algorithm: SignatureAlgorithm, + stdout: &mut dyn Write, +) -> Result<(), CommandError> { + if invocation.flag("print-debug") { + writeln!(stdout, "== Signature Context").map_err(stdout_error)?; + writeln!(stdout, "Status: succeeded").map_err(stdout_error)?; + writeln!(stdout, "Signature Method: {}", algorithm.uri()).map_err(stdout_error)?; + } + if invocation.flag("print-xml-debug") { + writeln!( + stdout, + "" + ) + .map_err(stdout_error)?; + write_debug_transform(stdout, "SignatureMethod", algorithm.uri())?; + writeln!(stdout, "").map_err(stdout_error)?; + } + Ok(()) } fn select_signing_key<'a>( @@ -1079,7 +1102,47 @@ fn encrypt(invocation: &Invocation, stdout: &mut dyn Write) -> Result<(), Comman "encrypted template output exceeds XML document policy".into(), )); } - write_output(invocation, rendered.as_bytes(), stdout) + write_output(invocation, rendered.as_bytes(), stdout)?; + write_encryption_diagnostics(invocation, algorithm, stdout) +} + +fn write_encryption_diagnostics( + invocation: &Invocation, + algorithm: DataEncryptionAlgorithm, + stdout: &mut dyn Write, +) -> Result<(), CommandError> { + if invocation.flag("print-debug") { + writeln!(stdout, "== Data Encryption Context").map_err(stdout_error)?; + writeln!(stdout, "Status: succeeded").map_err(stdout_error)?; + writeln!(stdout, "Encryption Method: {}", algorithm.uri()).map_err(stdout_error)?; + } + if invocation.flag("print-xml-debug") { + writeln!( + stdout, + "" + ) + .map_err(stdout_error)?; + write_debug_transform(stdout, "EncryptionMethod", algorithm.uri())?; + writeln!(stdout, "").map_err(stdout_error)?; + } + Ok(()) +} + +fn write_debug_transform( + stdout: &mut dyn Write, + container: &str, + uri: &str, +) -> Result<(), CommandError> { + let name = uri.rsplit_once('#').map_or(uri, |(_, name)| name); + writeln!(stdout, "<{container}>").map_err(stdout_error)?; + writeln!( + stdout, + "", + quick_xml::escape::escape(name), + quick_xml::escape::escape(uri) + ) + .map_err(stdout_error)?; + writeln!(stdout, "").map_err(stdout_error) } fn validate_recipient_key_metadata<'a>( @@ -1737,10 +1800,15 @@ fn encryption_template( ) -> Result { let document = parse_encryption_document(xml, policy)?; let encrypted_data = select_encrypted_data(&document, start_node_id, id_attributes)?; - let method = encrypted_data - .children() - .find(|node| node.has_tag_name((XMLENC_NS, "EncryptionMethod"))) - .and_then(|node| node.attribute("Algorithm")) + let method_node = singleton_direct_child( + encrypted_data, + XMLENC_NS, + "EncryptionMethod", + "EncryptedData contains more than one direct EncryptionMethod", + )? + .ok_or_else(|| CommandError::Encryption("template has no encryption algorithm".into()))?; + let method = method_node + .attribute("Algorithm") .ok_or_else(|| CommandError::Encryption("template has no encryption algorithm".into()))?; let algorithm = DataEncryptionAlgorithm::from_uri(method) .map_err(|error| CommandError::Encryption(error.to_string()))?; diff --git a/tools/xmlsec1/src/key_material.rs b/tools/xmlsec1/src/key_material.rs index 0bf3210..ddab2e8 100644 --- a/tools/xmlsec1/src/key_material.rs +++ b/tools/xmlsec1/src/key_material.rs @@ -53,6 +53,7 @@ pub struct SignatureMetadata { #[derive(Debug, Eq, PartialEq)] pub struct SigningTemplateMetadata { + pub algorithm: SignatureAlgorithm, pub key_names: Vec, pub has_key_info: bool, } @@ -133,7 +134,21 @@ pub fn signing_signature_metadata( .rfind(|node| node.has_tag_name(("http://www.w3.org/2000/09/xmldsig#", "Signature"))), } .ok_or(KeyMaterialError::MissingSignedInfo)?; + let algorithm_uri = signature + .children() + .find(|node| node.has_tag_name(("http://www.w3.org/2000/09/xmldsig#", "SignedInfo"))) + .and_then(|signed_info| { + signed_info.children().find(|node| { + node.has_tag_name(("http://www.w3.org/2000/09/xmldsig#", "SignatureMethod")) + }) + }) + .and_then(|method| method.attribute("Algorithm")) + .ok_or(KeyMaterialError::MissingSignedInfo)?; + let algorithm = SignatureAlgorithm::from_uri(algorithm_uri).ok_or_else(|| { + KeyMaterialError::Signature(format!("unsupported signature algorithm: {algorithm_uri}")) + })?; Ok(SigningTemplateMetadata { + algorithm, key_names: signature_key_names(signature), has_key_info: signature_key_info(signature).is_some(), }) diff --git a/tools/xmlsec1/tests/process_contract.rs b/tools/xmlsec1/tests/process_contract.rs index 354f248..ca4d06e 100644 --- a/tools/xmlsec1/tests/process_contract.rs +++ b/tools/xmlsec1/tests/process_contract.rs @@ -131,6 +131,52 @@ fn signs_verifies_and_rejects_tampering_through_process_api() { assert!(String::from_utf8_lossy(&rejected.stderr).contains("invalid")); } +#[test] +fn signing_writes_requested_diagnostics_separately_from_output() { + // Diagnostic flags describe the completed signing context; the signed XML + // remains exclusively in --output so shell callers can consume both streams. + let temp = tempfile::tempdir().unwrap(); + let template = project_root() + .join("tests/fixtures/xmldsig/aleksey-xmldsig-01/enveloping-sha256-rsa-sha256.tmpl"); + let private_key = project_root().join("tests/fixtures/keys/rsa/rsa-4096-key.pem"); + + for (flag, expected) in [ + ("--print-debug", "Status: succeeded"), + ("--print-xml-debug", ""#, + ) + .unwrap(); + let output = Command::new(binary()) + .args(["encrypt", flag, "--aeskey"]) + .arg(&key) + .arg("--binary-data") + .arg(&plaintext) + .arg("--output") + .arg(&encrypted) + .arg(&template) + .output() + .unwrap(); + + assert!( + output.status.success(), + "{}", + String::from_utf8_lossy(&output.stderr) + ); + assert!( + fs::read_to_string(&encrypted) + .unwrap() + .contains("CipherValue") + ); + let diagnostics = String::from_utf8(output.stdout).unwrap(); + assert!(diagnostics.contains(expected), "{diagnostics}"); + if flag == "--print-xml-debug" { + let document = roxmltree::Document::parse(&diagnostics) + .expect("encryption XML diagnostics must be well-formed"); + assert_eq!( + document.root_element().attribute("status"), + Some("replaced") + ); + } + } + + let malformed = temp.path().join("duplicate-method.xml"); + let rejected_output = temp.path().join("must-not-exist.xml"); + fs::write( + &malformed, + r#""#, + ) + .unwrap(); + let rejected = Command::new(binary()) + .args(["encrypt", "--aeskey"]) + .arg(&key) + .arg("--binary-data") + .arg(&plaintext) + .arg("--output") + .arg(&rejected_output) + .arg(&malformed) + .output() + .unwrap(); + + assert!(!rejected.status.success()); + assert!( + String::from_utf8_lossy(&rejected.stderr).contains("more than one direct EncryptionMethod") + ); + assert!(!rejected_output.exists()); +} + #[test] fn xml_debug_decryption_writes_diagnostics_separately_from_plaintext() { // The unmodified donor runner redirects diagnostics from stdout while From 5a16177bc0feab951ae40eb727a9c396f6b06fee Mon Sep 17 00:00:00 2001 From: Dmitry Prudnikov Date: Sat, 15 Aug 2026 23:17:41 +0300 Subject: [PATCH 26/27] fix(xml): distinguish ID namespace scopes --- docs/cli.md | 5 +- docs/xmldsig.md | 6 +- docs/xmlenc.md | 5 +- src/xml.rs | 103 +++++++++++++++++++++--- tools/xmlsec1/README.md | 4 +- tools/xmlsec1/src/commands.rs | 21 ++--- tools/xmlsec1/tests/process_contract.rs | 6 +- 7 files changed, 118 insertions(+), 32 deletions(-) diff --git a/docs/cli.md b/docs/cli.md index 051e350..3b87dfa 100644 --- a/docs/cli.md +++ b/docs/cli.md @@ -89,7 +89,10 @@ selected template's digest, signature, and optional key-info placeholders. Custom ID declarations match libxmlsec1's two request-local forms: `--add-id-attr NAME` registers an attribute local name on every element, while `--id-attr[:ATTR] [NAMESPACE-URI:]ELEMENT` (default `ATTR` is `id`) limits the -registration to one expanded element name. Registrations affect both +registration to one element local name. Without `NAMESPACE-URI:`, that local +name matches elements in any namespace, as in libxmlsec1; with the component +present, it matches that exact namespace (`:ELEMENT` means no namespace). +Registrations affect both `--node-id` selection and same-document reference resolution; duplicate ID values remain ambiguous and fail closed. XPath and XPath Filter 2.0 verification uses libxmlsec1's legacy `here()` diff --git a/docs/xmldsig.md b/docs/xmldsig.md index 5fecb9e..7bc5810 100644 --- a/docs/xmldsig.md +++ b/docs/xmldsig.md @@ -43,8 +43,10 @@ optional `KeyInfo` filling. An internal-DTD opt-in and XML node ceiling therefor between stages. `IdAttributeRegistration` supplies immutable request context for non-standard ID attributes. `SignContext::id_attributes` and `VerifyContext::id_attributes` apply the same global or -element-scoped registrations to operation start-node selection and every same-document Reference; -the registration is not stored in policy and never comes from document content. +element-scoped registrations to operation start-node selection and every same-document Reference. +`scoped_any_namespace` matches one element local name across namespaces, while `scoped` matches +one exact expanded name and uses `None` for no namespace. The registration is not stored in policy +and never comes from document content. `SigningPolicy::rsa_keys` validates normalized modulus width and public exponent before provider dispatch. The default accepts 2048-8192-bit RSA keys for new signatures; compatibility callers can raise or lower the minimum explicitly, while the 8192-bit implementation ceiling cannot be relaxed. diff --git a/docs/xmlenc.md b/docs/xmlenc.md index d37f032..f3721fb 100644 --- a/docs/xmlenc.md +++ b/docs/xmlenc.md @@ -34,8 +34,9 @@ fn example() -> Result<(), Box> { ``` For embedded decryption, `DecryptContext::id_attributes` accepts the same immutable global or -element-scoped `IdAttributeRegistration` request context as XMLDSig. It affects only operation -start-node lookup; policy remains a separate compiled snapshot. +element-scoped `IdAttributeRegistration` request context as XMLDSig. Element scope can match a +local name in any namespace or one exact expanded name. It affects only operation start-node +lookup; policy remains a separate compiled snapshot. For recipient transport, add one or more `EncryptionRecipient::rsa_oaep` entries with recipient public keys, or use `recipient_aes_kw` with a shared KEK. `EncryptedDataBuilder` obtains each fresh diff --git a/src/xml.rs b/src/xml.rs index 2dd80df..bcd784b 100644 --- a/src/xml.rs +++ b/src/xml.rs @@ -14,12 +14,24 @@ const DEFAULT_ID_ATTRS: &[&str] = &["ID", "Id", "id"]; /// /// Registrations are request context rather than security policy. A global /// registration applies an attribute local name to every element; a scoped -/// registration applies only to one expanded element name. +/// registration applies to one element local name in either any namespace or +/// one exact namespace. #[derive(Clone, Debug, PartialEq, Eq)] pub struct IdAttributeRegistration { attribute_local_name: String, - element_local_name: Option, - element_namespace: Option, + element_scope: IdAttributeElementScope, +} + +#[derive(Clone, Debug, PartialEq, Eq)] +enum IdAttributeElementScope { + AnyElement, + AnyNamespace { + local_name: String, + }, + ExpandedName { + local_name: String, + namespace: Option, + }, } impl IdAttributeRegistration { @@ -28,8 +40,23 @@ impl IdAttributeRegistration { pub fn global(attribute_local_name: impl Into) -> Self { Self { attribute_local_name: attribute_local_name.into(), - element_local_name: None, - element_namespace: None, + element_scope: IdAttributeElementScope::AnyElement, + } + } + + /// Register an attribute as an ID on a local element name in any namespace. + /// + /// This models libxmlsec1's unqualified `--id-attr` element-name contract. + #[must_use] + pub fn scoped_any_namespace( + attribute_local_name: impl Into, + element_local_name: impl Into, + ) -> Self { + Self { + attribute_local_name: attribute_local_name.into(), + element_scope: IdAttributeElementScope::AnyNamespace { + local_name: element_local_name.into(), + }, } } @@ -45,17 +72,30 @@ impl IdAttributeRegistration { ) -> Self { Self { attribute_local_name: attribute_local_name.into(), - element_local_name: Some(element_local_name.into()), - element_namespace: element_namespace.map(str::to_owned), + element_scope: IdAttributeElementScope::ExpandedName { + local_name: element_local_name.into(), + namespace: element_namespace.map(str::to_owned), + }, } } fn matches(&self, node: Node<'_, '_>, attribute_name: &str) -> bool { - self.attribute_local_name == attribute_name - && self.element_local_name.as_deref().is_none_or(|name| { - node.tag_name().name() == name - && node.tag_name().namespace() == self.element_namespace.as_deref() - }) + if self.attribute_local_name != attribute_name { + return false; + } + match &self.element_scope { + IdAttributeElementScope::AnyElement => true, + IdAttributeElementScope::AnyNamespace { local_name } => { + node.tag_name().name() == local_name + } + IdAttributeElementScope::ExpandedName { + local_name, + namespace, + } => { + node.tag_name().name() == local_name + && node.tag_name().namespace() == namespace.as_deref() + } + } } } @@ -163,7 +203,7 @@ pub(crate) fn is_xml_ncname(value: &str) -> bool { mod tests { use roxmltree::Document; - use super::{XmlIdIndex, is_xml_1_0_character, is_xml_ncname}; + use super::{IdAttributeRegistration, XmlIdIndex, is_xml_1_0_character, is_xml_ncname}; #[test] fn xml_1_0_character_boundaries_match_production_two() { @@ -234,4 +274,41 @@ mod tests { Some("two") ); } + + #[test] + fn id_registration_distinguishes_any_and_exact_element_namespaces() { + // Donor --id-attr without a namespace matches the local element name + // everywhere, while the public scoped API retains exact-name matching. + let document = Document::parse( + r#""#, + ) + .expect("scope fixture must parse"); + + let any_namespace = XmlIdIndex::with_registrations( + &document, + &[IdAttributeRegistration::scoped_any_namespace( + "Token", "item", + )], + ); + assert!(any_namespace.node("plain").is_some()); + assert!(any_namespace.node("namespaced").is_some()); + + let no_namespace = XmlIdIndex::with_registrations( + &document, + &[IdAttributeRegistration::scoped("Token", "item", None)], + ); + assert!(no_namespace.node("plain").is_some()); + assert!(no_namespace.node("namespaced").is_none()); + + let exact_namespace = XmlIdIndex::with_registrations( + &document, + &[IdAttributeRegistration::scoped( + "Token", + "item", + Some("urn:item"), + )], + ); + assert!(exact_namespace.node("plain").is_none()); + assert!(exact_namespace.node("namespaced").is_some()); + } } diff --git a/tools/xmlsec1/README.md b/tools/xmlsec1/README.md index ade9053..c7ad9fa 100644 --- a/tools/xmlsec1/README.md +++ b/tools/xmlsec1/README.md @@ -42,7 +42,9 @@ without erasing sibling key sources; verification accepts stdin as `-` and can select one signature subtree with `--node-id`. support request-local custom IDs through global `--add-id-attr NAME` and element-scoped `--id-attr[:ATTR] [NAMESPACE-URI:]ELEMENT`; the same -registrations drive Reference resolution and XMLEnc operation selection. +registrations drive Reference resolution and XMLEnc operation selection. An +unqualified element name matches its local name in any namespace; an explicit +namespace component restricts the match to that expanded name. Output paths support the upstream `{inputfile}` basename template, and `--gen-key[:name]` emits both named and unnamed AES key-store entries. `help-all` is generated from the parser registry. Named signing keys require a template `KeyName`; named diff --git a/tools/xmlsec1/src/commands.rs b/tools/xmlsec1/src/commands.rs index 655431b..b7101c4 100644 --- a/tools/xmlsec1/src/commands.rs +++ b/tools/xmlsec1/src/commands.rs @@ -481,21 +481,22 @@ fn id_attribute_registrations( .collect::, _>>()?; for option in invocation.values("id-attr") { let element = option_value_text(option)?; - let (namespace, local_name) = element - .rsplit_once(':') - .map_or((None, element), |(namespace, local_name)| { - (Some(namespace), local_name) - }); + let expanded_name = element.rsplit_once(':'); + let local_name = expanded_name.map_or(element, |(_, local_name)| local_name); if local_name.is_empty() { return Err(CommandError::Usage( "--id-attr element local name cannot be empty".into(), )); } - registrations.push(IdAttributeRegistration::scoped( - option.parameter.as_deref().unwrap_or("id"), - local_name, - namespace, - )); + let attribute_name = option.parameter.as_deref().unwrap_or("id"); + registrations.push(match expanded_name { + None => IdAttributeRegistration::scoped_any_namespace(attribute_name, local_name), + Some((namespace, _)) => IdAttributeRegistration::scoped( + attribute_name, + local_name, + (!namespace.is_empty()).then_some(namespace), + ), + }); } Ok(registrations) } diff --git a/tools/xmlsec1/tests/process_contract.rs b/tools/xmlsec1/tests/process_contract.rs index ca4d06e..d4525be 100644 --- a/tools/xmlsec1/tests/process_contract.rs +++ b/tools/xmlsec1/tests/process_contract.rs @@ -179,8 +179,8 @@ fn signing_writes_requested_diagnostics_separately_from_output() { #[test] fn scoped_id_attribute_selects_and_signs_the_registered_element() { - // libxmlsec1's --id-attr form is element-scoped: the custom attribute must - // drive both --node-id selection and same-document Reference resolution. + // An unqualified libxmlsec1 --id-attr element name is namespace-agnostic; + // the custom attribute must drive selection and Reference resolution. let temp = tempfile::tempdir().unwrap(); let template = temp.path().join("scoped-id.xml"); let signed = temp.path().join("scoped-id-signed.xml"); @@ -188,7 +188,7 @@ fn scoped_id_attribute_selects_and_signs_the_registered_element() { let public_key = project_root().join("tests/fixtures/keys/rsa/rsa-4096-pubkey.pem"); fs::write( &template, - r##"registered"##, + r##"registered"##, ) .unwrap(); From 0b42262e23e2121723ebcbe4504fff4a05e9c478 Mon Sep 17 00:00:00 2001 From: Dmitry Prudnikov Date: Sat, 15 Aug 2026 23:23:55 +0300 Subject: [PATCH 27/27] fix(cli): validate cipher template shape --- docs/cli.md | 10 ++-- tools/xmlsec1/README.md | 2 + tools/xmlsec1/src/commands.rs | 60 ++++++++++++++++------- tools/xmlsec1/tests/process_contract.rs | 63 +++++++++++++++++++++++++ 4 files changed, 113 insertions(+), 22 deletions(-) diff --git a/docs/cli.md b/docs/cli.md index 3b87dfa..069ca0f 100644 --- a/docs/cli.md +++ b/docs/cli.md @@ -129,10 +129,12 @@ then requires exactly one `EncryptedData` in its subtree. Encryption preserves the template's `Id`, `Type`, `MimeType`, `KeyInfo`, `EncryptionProperties`, and RSA-OAEP parameters while replacing only the cryptographic `CipherValue` payloads. -Each template `EncryptedKey` must contain exactly one direct -`EncryptionMethod`; optional `DigestMethod`, `MGF`, and `OAEPparams` children -must each occur at most once. Ambiguous recipient metadata is rejected before -key wrapping. +The selected `EncryptedData` and every existing template `EncryptedKey` must +contain exactly one direct `CipherData` with one direct `CipherValue`. Each +`EncryptedKey` must also contain exactly one direct `EncryptionMethod`; +optional `DigestMethod`, `MGF`, and `OAEPparams` children must each occur at +most once. Ambiguous or incomplete recipient metadata is rejected before key +wrapping. When nested recipient `KeyInfo` already carries `RSAKeyValue`, an X.509 certificate, or `DEREncodedKeyValue`, that cryptographic identity must match the selected RSA wrapping key. Unmatchable or contradictory metadata is diff --git a/tools/xmlsec1/README.md b/tools/xmlsec1/README.md index c7ad9fa..52a7301 100644 --- a/tools/xmlsec1/README.md +++ b/tools/xmlsec1/README.md @@ -34,6 +34,8 @@ into the corresponding core pipeline from PEM or DER. Preserved recipient `RSAKeyValue`, X.509 certificate, and `DEREncodedKeyValue` metadata must identify the selected RSA wrapping key; contradictory metadata fails before ciphertext is emitted. +Content and recipient templates require exactly one direct `CipherData` and +`CipherValue`; incomplete or duplicate payload containers fail before output. Untyped `--xml-data` templates gain the inferred XML Element type, while direct AES keys reject recipient `EncryptedKey` templates they cannot refresh. Signing options validate every certificate from `key,leaf,intermediate,...` and embed the chain diff --git a/tools/xmlsec1/src/commands.rs b/tools/xmlsec1/src/commands.rs index b7101c4..b137049 100644 --- a/tools/xmlsec1/src/commands.rs +++ b/tools/xmlsec1/src/commands.rs @@ -1391,10 +1391,8 @@ fn apply_encryption_template( let generated_document = parse_encryption_document(generated, policy)?; let template_data = select_encrypted_data(&template_document, start_node_id, id_attributes)?; let generated_data = generated_document.root_element(); - let template_cipher = encrypted_data_cipher_value(template_data) - .ok_or_else(|| CommandError::Encryption("template has no CipherValue".into()))?; - let generated_cipher = encrypted_data_cipher_value(generated_data) - .ok_or_else(|| CommandError::Encryption("generated data has no CipherValue".into()))?; + let template_cipher = required_cipher_value(template_data, "template EncryptedData")?; + let generated_cipher = required_cipher_value(generated_data, "generated EncryptedData")?; let mut replacements = vec![( template_cipher.range(), standalone_cipher_value(generated_cipher), @@ -1417,8 +1415,8 @@ fn apply_encryption_template( let generated_key_info = direct_child_element(generated_data, XMLDSIG_NS, "KeyInfo"); match (template_key_info, generated_key_info) { (Some(template_key_info), Some(generated_key_info)) => { - let template_values = encrypted_key_cipher_values(template_key_info); - let generated_values = encrypted_key_cipher_values(generated_key_info); + let template_values = encrypted_key_cipher_values(template_key_info, "template")?; + let generated_values = encrypted_key_cipher_values(generated_key_info, "generated")?; if template_values.is_empty() && !generated_values.is_empty() { let generated_keys = generated_key_info .children() @@ -1564,22 +1562,39 @@ fn direct_child_element<'a, 'input>( .find(|child| child.has_tag_name((namespace, name))) } -fn encrypted_data_cipher_value<'a, 'input>( - data: roxmltree::Node<'a, 'input>, -) -> Option> { - direct_child_element(data, XMLENC_NS, "CipherData") - .and_then(|cipher| direct_child_element(cipher, XMLENC_NS, "CipherValue")) +fn required_cipher_value<'a, 'input>( + parent: roxmltree::Node<'a, 'input>, + owner: &str, +) -> Result, CommandError> { + let cipher_data = singleton_direct_child( + parent, + XMLENC_NS, + "CipherData", + &format!("{owner} contains more than one direct CipherData"), + )? + .ok_or_else(|| CommandError::Encryption(format!("{owner} has no direct CipherData")))?; + singleton_direct_child( + cipher_data, + XMLENC_NS, + "CipherValue", + &format!("{owner} CipherData contains more than one direct CipherValue"), + )? + .ok_or_else(|| CommandError::Encryption(format!("{owner} CipherData has no CipherValue"))) } fn encrypted_key_cipher_values<'a, 'input>( key_info: roxmltree::Node<'a, 'input>, -) -> Vec> { + owner: &str, +) -> Result>, CommandError> { key_info .children() .filter(|node| node.has_tag_name((XMLENC_NS, "EncryptedKey"))) - .filter_map(|encrypted_key| { - direct_child_element(encrypted_key, XMLENC_NS, "CipherData") - .and_then(|cipher| direct_child_element(cipher, XMLENC_NS, "CipherValue")) + .enumerate() + .map(|(index, encrypted_key)| { + required_cipher_value( + encrypted_key, + &format!("{owner} EncryptedKey recipient {}", index + 1), + ) }) .collect() } @@ -1682,11 +1697,9 @@ fn write_decryption_diagnostics( result_replaced: bool, stdout: &mut dyn Write, ) -> Result<(), CommandError> { - if !invocation.flag("print-xml-debug") { + if !invocation.flag("print-debug") && !invocation.flag("print-xml-debug") { return Ok(()); } - // Donor testEnc.sh routes plaintext through --output and parses stdout as - // a separate xmlSecEncCtxDebugXmlDump-compatible diagnostics document. let method = direct_child_element(encrypted_data, XMLENC_NS, "EncryptionMethod") .and_then(|node| node.attribute("Algorithm")) .ok_or_else(|| CommandError::Encryption("template has no encryption algorithm".into()))?; @@ -1697,6 +1710,17 @@ fn write_decryption_diagnostics( } else { "not-replaced" }; + if invocation.flag("print-debug") { + writeln!(stdout, "== Data Decryption Context").map_err(stdout_error)?; + writeln!(stdout, "Status: succeeded").map_err(stdout_error)?; + writeln!(stdout, "Result: {status}").map_err(stdout_error)?; + writeln!(stdout, "Encryption Method: {method}").map_err(stdout_error)?; + } + if !invocation.flag("print-xml-debug") { + return Ok(()); + } + // Donor testEnc.sh routes plaintext through --output and parses stdout as + // a separate xmlSecEncCtxDebugXmlDump-compatible diagnostics document. writeln!( stdout, "" diff --git a/tools/xmlsec1/tests/process_contract.rs b/tools/xmlsec1/tests/process_contract.rs index d4525be..a0b63bb 100644 --- a/tools/xmlsec1/tests/process_contract.rs +++ b/tools/xmlsec1/tests/process_contract.rs @@ -966,6 +966,47 @@ fn encryption_writes_requested_diagnostics_and_rejects_duplicate_methods() { String::from_utf8_lossy(&rejected.stderr).contains("more than one direct EncryptionMethod") ); assert!(!rejected_output.exists()); + + let public_key = project_root().join("tests/fixtures/keys/rsa/rsa-4096-pubkey.pem"); + for (case, malformed_xml, key_args) in [ + ( + "duplicate-cipher-data", + r#""#, + None, + ), + ( + "recipient-without-value", + r#""#, + Some(&public_key), + ), + ( + "recipient-cipher-reference", + r#""#, + Some(&public_key), + ), + ] { + let malformed = temp.path().join(format!("{case}.xml")); + let rejected_output = temp.path().join(format!("{case}-output.xml")); + fs::write(&malformed, malformed_xml).unwrap(); + let mut command = Command::new(binary()); + command.arg("encrypt"); + if let Some(public_key) = key_args { + command.arg("--pubkey-pem").arg(public_key); + } else { + command.arg("--aeskey").arg(&key); + } + let rejected = command + .arg("--binary-data") + .arg(&plaintext) + .arg("--output") + .arg(&rejected_output) + .arg(&malformed) + .output() + .unwrap(); + + assert!(!rejected.status.success(), "{case} unexpectedly succeeded"); + assert!(!rejected_output.exists(), "{case} emitted partial output"); + } } #[test] @@ -977,6 +1018,28 @@ fn xml_debug_decryption_writes_diagnostics_separately_from_plaintext() { let vector = fixtures.join("xmlenc11-interop-2012/xenc11-example-AES128-GCM"); let decrypted = temp.path().join("decrypted.data"); + let text_decrypted = temp.path().join("text-decrypted.data"); + let text_output = Command::new(binary()) + .args(["decrypt", "--print-debug", "--lax-key-search", "--aeskey"]) + .arg(vector.with_extension("key")) + .arg("--output") + .arg(&text_decrypted) + .arg(vector.with_extension("xml")) + .output() + .unwrap(); + assert!( + text_output.status.success(), + "{}", + String::from_utf8_lossy(&text_output.stderr) + ); + assert_eq!( + fs::read(&text_decrypted).unwrap(), + fs::read(vector.with_extension("data")).unwrap() + ); + let text_diagnostics = String::from_utf8(text_output.stdout).unwrap(); + assert!(text_diagnostics.contains("== Data Decryption Context")); + assert!(text_diagnostics.contains("Status: succeeded")); + let output = Command::new(binary()) .args([ "decrypt",