diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1b354aa..fcea66e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -38,6 +38,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 @@ -59,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 @@ -79,20 +81,34 @@ 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 + 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 "$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 @@ -111,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/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..724cd8f 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,47 @@ 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, 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; donor option multiplicity is +enforced across canonical and alias spellings, and `--print-xml-debug` emits +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 +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; 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 or unnamed recipient without reusing +a lax-search key across recipient slots. +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 +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..67b453f 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|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" + }, + { + "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..02b3218 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", @@ -22058,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", @@ -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..7184c81 --- /dev/null +++ b/docs/cli.md @@ -0,0 +1,232 @@ +# 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. 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 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 +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. 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. + +`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. +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 + +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 +``` + +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`. An empty +`X509Data` child is populated in place, preserving sibling sources such as the +`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 +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 +verifies the single `Signature` in its subtree; missing and duplicate IDs fail +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 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 signing and verification use libxmlsec1's legacy +`here()` binding at this CLI compatibility boundary, so CLI-produced signatures +round-trip with the donor. The Rust library API retains the XMLDSig specification +binding by default and requires an explicit opt-in for legacy documents. +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. For a repeated explicit key set, lax lookup +ignores `KeyName` identity and selects the first compatible option in command-line +order, matching libxmlsec1's key-type search rather than treating ambiguity as a +strict lookup failure. + +`--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 +xmlsec1 encrypt --aes-key:content content.key \ + --binary-data plaintext.bin --output encrypted.xml encrypted-data.tmpl +xmlsec1 decrypt --aes-key:content content.key \ + --output plaintext.bin encrypted.xml +``` + +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. Reads are +bounded to the largest supported AES key before allocation. `decrypt` +accepts both standalone `EncryptedData` and encrypted elements embedded in a +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. +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. The content `EncryptionMethod` is validated under the same structural +contract before encryption: optional `KeySize` is singular, positive, and must +match the fixed AES algorithm URI, while OAEP-only children are rejected. +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` +must select exactly one AES key, while a recipient `KeyName` inside +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. Under lax lookup, compatible wrapping +keys are consumed in command-line order rather than reused for every recipient. +Named and unnamed recipient slots remain distinct identities. 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. +Repeatable named private keys form one resolver: each nested `EncryptedKey` is +matched to its own key, and duplicate keys for the same recipient fail as +ambiguous instead of preventing distinct recipients from sharing an invocation. +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. +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: + +```sh +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, +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. 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. 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, +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. + +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. `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/docs/xmldsig.md b/docs/xmldsig.md index f189a5f..7bc5810 100644 --- a/docs/xmldsig.md +++ b/docs/xmldsig.md @@ -41,6 +41,12 @@ 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. +`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 2a4e9a2..e6d3291 100644 --- a/docs/xmlenc.md +++ b/docs/xmlenc.md @@ -33,6 +33,11 @@ fn example() -> Result<(), Box> { } ``` +For embedded decryption, `DecryptContext::id_attributes` accepts the same immutable global or +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 content key through `CryptoProvider::fill_random` and wraps it once per recipient. The default @@ -85,6 +90,13 @@ fn example(encrypted_xml: &str) -> Result<(), Box> { } ``` +Callers that already parsed a containing XML document can use +`parse_encrypted_data_node_with_policy` on the selected `roxmltree::Node`. Encryption frontends +that inspect caller-owned templates use `parse_encrypted_data_template_node_with_policy`, which +applies the same complete structure and metadata limits but permits empty `CipherValue` +placeholders. Both node-oriented APIs avoid serializing a subtree and losing namespace declarations +inherited from its ancestors. + `PrivateKeyDecryptor` unwraps embedded RSA-OAEP `EncryptedKey` values and `KekDecryptor` unwraps AES-KW values. RSA PKCS#1 v1.5 transport, `CipherReference`, and unauthenticated external resource loading are rejected; only inline `CipherValue` is accepted. Encryption plaintext, diff --git a/scripts/import-xmlsec1-cli-fixtures.sh b/scripts/import-xmlsec1-cli-fixtures.sh new file mode 100755 index 0000000..a075307 --- /dev/null +++ b/scripts/import-xmlsec1-cli-fixtures.sh @@ -0,0 +1,89 @@ +#!/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="${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 +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 130' INT +trap 'cleanup 143' TERM +trap 'cleanup 129' HUP + +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")" + file_mode=0644 + if [[ "$asset" == *.sh ]]; then + file_mode=0755 + fi + install -m "$file_mode" "$source" "$staging/$asset" +done + +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 --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..4d36f35 100755 --- a/scripts/install-xmlsec1.sh +++ b/scripts/install-xmlsec1.sh @@ -83,16 +83,39 @@ 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" ]]; 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 + 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/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 062835a..0f915a0 100644 --- a/src/xml.rs +++ b/src/xml.rs @@ -1,5 +1,178 @@ //! 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"]; + +/// 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 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_scope: IdAttributeElementScope, +} + +#[derive(Clone, Debug, PartialEq, Eq)] +enum IdAttributeElementScope { + AnyElement, + AnyNamespace { + local_name: String, + }, + ExpandedName { + local_name: String, + 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_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(), + }, + } + } + + /// 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_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 { + 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() + } + } + } +} + +/// 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 standard ID spellings plus caller-declared local attribute names. + #[cfg(feature = "xmldsig")] + pub(crate) fn with_extra_attrs(document: &'a Document<'a>, extra_attrs: &[&str]) -> Self { + 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) { + // 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| { + DEFAULT_ID_ATTRS.contains(&attribute.name()) + || registrations + .iter() + .any(|registration| registration.matches(node, attribute.name())) + }) + .map(|attribute| attribute.value()) + { + 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 +202,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::{IdAttributeRegistration, XmlIdIndex, is_xml_1_0_character, is_xml_ncname}; #[test] fn xml_1_0_character_boundaries_match_production_two() { @@ -63,4 +238,78 @@ 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::with_registrations(&document, &[]); + + assert_eq!( + index.node("same").map(|node| node.tag_name().name()), + Some("one") + ); + 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::with_registrations(&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") + ); + } + + #[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/src/xmldsig/mutation.rs b/src/xmldsig/mutation.rs index b13e8e0..3426f27 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 @@ -155,6 +208,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 +230,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 +264,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,12 +302,21 @@ 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)?; - if expected != 1 { + 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 actual = count_direct_key_infos(xml, target_signature, policy)?; + if actual != 1 { return Err(XmlMutationError::ValueCountMismatch { element: "KeyInfo", - expected, - actual: 1, + expected: 1, + actual, }); } @@ -245,6 +329,374 @@ pub(super) fn fill_key_info_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 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: 1, + actual: key_infos.len(), + }); + } + 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 = parse_with_options(&wrapped_source, policy)?; + 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 = 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 { + 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: 1, + actual: key_infos.len(), + }); + } + let key_info = key_infos[0]; + + 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() + .try_fold(String::new(), |mut attributes, namespace| { + if let Some(declared) = placeholder + .namespaces() + .find(|declared| declared.name() == namespace.name()) + { + if declared.uri() != namespace.uri() { + return Err(XmlMutationError::InvalidAppendTarget); + } + return Ok(attributes); + } + if placeholder + .parent_element() + .and_then(|parent| parent.lookup_namespace_uri(namespace.name())) + == Some(namespace.uri()) + { + return Ok(attributes); + } + 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 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, + &format!("{generated_namespace_attributes}{generated_attributes}"), + )?; + 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(") -> 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("/>") { + 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, @@ -505,10 +957,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 +972,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 +992,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 +1010,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( @@ -644,11 +1101,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 { @@ -733,6 +1187,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("", "") @@ -836,4 +1316,170 @@ 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"]); + } + + #[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"))) + ); + } + + #[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 + // 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)); + } + + #[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 + // 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)); + } + + #[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 c760810..c985851 100644 --- a/src/xmldsig/sign.rs +++ b/src/xmldsig/sign.rs @@ -25,9 +25,10 @@ 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, - fill_signed_info_digest_values_with_options, + XmlMutationError, append_signature_to_element_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, @@ -39,6 +40,7 @@ use super::transforms::{ }; use super::types::TransformError; use super::uri::UriReferenceResolver; +use super::verify::parse_signature_children; /// Result for one computed signing-template reference digest. #[derive(Debug, Clone, PartialEq, Eq)] @@ -337,6 +339,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 +352,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 +432,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) } } @@ -632,6 +677,8 @@ 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>, + id_attributes: &'a [crate::IdAttributeRegistration], policy: crate::policy::SigningPolicy, provider: &'a dyn crate::provider::CryptoProvider, } @@ -642,6 +689,8 @@ impl<'a> SignContext<'a> { Self { signing_key, key_info_writer: None, + start_node_id: None, + id_attributes: &[], policy: crate::policy::SigningPolicy::default(), provider: crate::provider::default_provider(), } @@ -668,6 +717,24 @@ impl<'a> SignContext<'a> { self } + /// Select an operation start node by ID and sign the first XMLDSig + /// `` in that node's subtree. + /// + /// [`Self::sign_with_builder`] instead uses the selected node as the append + /// location and signs the newly appended direct `` child. + #[must_use] + pub fn start_node_id(mut self, id: &'a str) -> Self { + self.start_node_id = Some(id); + 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. @@ -682,28 +749,69 @@ 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())?; + 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.id_attributes)?; + self.sign_template_at_index(xml, target_signature) + } + + fn sign_template_at_index( + &self, + xml: &str, + target_signature: usize, + ) -> Result { + let document = parse_signing_document(xml, Some(&self.policy)) + .map_err(SigningDigestError::XmlParse)?; + let signature = find_signing_signature_node(&document, Some(target_signature))?; + parse_signature_children(signature) + .map_err(|error| SigningDigestError::InvalidStructure(error.to_string()))?; let execution_budget = TransformExecutionBudget::from_resources(&self.policy.resources); 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, &execution_budget, + Some(target_signature), + self.id_attributes, )?; 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)?; @@ -727,25 +835,20 @@ 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))?; - self.policy - .resources - .validate_xml_document_len(signed.len())?; - Ok(signed) - } else { - Ok(signed) - } + Ok(signed) } - /// 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, @@ -754,8 +857,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, self.id_attributes)?; + 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, self.id_attributes)?; + 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.id_attributes)? + }; + self.sign_template_at_index(&templated, target_signature) } } @@ -782,6 +914,8 @@ pub fn compute_reference_digest_values( None, crate::provider::default_provider(), &execution_budget, + None, + &[], ) } @@ -791,9 +925,11 @@ fn compute_reference_digest_values_with_options( policy: Option<&crate::policy::SigningPolicy>, 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)?; + 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 { @@ -838,7 +974,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() @@ -896,6 +1032,8 @@ pub fn fill_reference_digest_values(xml: &str) -> Result, provider: &dyn crate::provider::CryptoProvider, execution_budget: &TransformExecutionBudget, + target_signature: Option, + id_attributes: &[crate::IdAttributeRegistration], ) -> Result { let digest_values = compute_reference_digest_values_with_options( xml, @@ -912,10 +1052,19 @@ fn fill_reference_digest_values_with_options( policy, provider, execution_budget, + target_signature, + id_attributes, )? .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)? @@ -926,9 +1075,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)?; @@ -991,13 +1142,64 @@ 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> { - doc.descendants() - .rfind(|node| { - node.is_element() - && node.tag_name().name() == "Signature" - && node.tag_name().namespace() == Some(XMLDSIG_NS) + 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>, + id_attributes: &[crate::IdAttributeRegistration], +) -> Result { + let selected = if let Some(id) = start_node_id { + let start = signing_start_node(doc, id, id_attributes)?; + 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)? + }; + signature_index(doc, selected) +} + +fn signing_start_node<'a>( + doc: &'a Document<'a>, + id: &str, + id_attributes: &[crate::IdAttributeRegistration], +) -> Result, SigningDigestError> { + UriReferenceResolver::with_id_registrations(doc, id_attributes) + .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) .ok_or(SigningDigestError::MissingElement { element: "Signature", }) diff --git a/src/xmldsig/uri.rs b/src/xmldsig/uri.rs index f92f327..6edf5e9 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,22 @@ 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_index: XmlIdIndex::with_extra_attrs(doc, extra_attrs), + external_resources: None, + external_resource_budget: ExternalResourceBudget::default(), } + } + /// 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_map, + id_index: XmlIdIndex::with_registrations(doc, registrations), external_resources: None, external_resource_budget: ExternalResourceBudget::default(), } @@ -330,8 +287,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 +305,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,11 +313,15 @@ 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) } - pub(crate) fn node_for_id(&self, id: &str) -> Option> { - self.id_map.get(id).copied() + /// 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_index.node(id) } pub(crate) fn node_for_node_id(&self, id: NodeId) -> Option> { @@ -369,7 +330,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/xmldsig/verify.rs b/src/xmldsig/verify.rs index 1ca7dce..12dd175 100644 --- a/src/xmldsig/verify.rs +++ b/src/xmldsig/verify.rs @@ -229,6 +229,8 @@ 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>, + id_attributes: &'a [crate::IdAttributeRegistration], } impl<'a> VerifyContext<'a> { @@ -248,6 +250,8 @@ impl<'a> VerifyContext<'a> { provider: crate::provider::default_provider(), store_pre_digest: false, external_resources: None, + start_node_id: None, + id_attributes: &[], } } @@ -344,6 +348,22 @@ 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 + } + + /// 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 { @@ -878,6 +898,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 +1022,23 @@ fn verify_signature_with_context( entity_resolver: None, }, )?; + 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, + }; 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 +1150,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, @@ -1855,13 +1889,13 @@ fn enforce_transform_allowed( } #[derive(Debug, Clone, Copy)] -struct SignatureChildNodes<'a, 'input> { +pub(super) struct SignatureChildNodes<'a, 'input> { signed_info_node: Node<'a, 'input>, signature_value_node: Node<'a, 'input>, key_info_node: Option>, } -fn parse_signature_children<'a, 'input>( +pub(super) fn parse_signature_children<'a, 'input>( signature_node: Node<'a, 'input>, ) -> Result, SignatureVerificationPipelineError> { let mut signed_info_node: Option> = None; @@ -5248,6 +5282,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/src/xmlenc/decrypt.rs b/src/xmlenc/decrypt.rs index 466d8cc..750f7e8 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::xml::XmlIdIndex; #[cfg(test)] use super::parse_encrypted_data; @@ -48,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> { @@ -57,6 +59,7 @@ impl<'a> DecryptContext<'a> { resolver, policy: crate::policy::DecryptionPolicy::default(), provider: crate::provider::default_provider(), + id_attributes: &[], } } @@ -72,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)?; @@ -151,7 +160,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, + ) } } @@ -349,14 +376,9 @@ impl PrivateKeyDecryptor { } fn parse_oaep_digest(uri: Option<&str>) -> Result { - match uri.unwrap_or("http://www.w3.org/2000/09/xmldsig#sha1") { - "http://www.w3.org/2000/09/xmldsig#sha1" => Ok(OaepDigestAlgorithm::Sha1), - "http://www.w3.org/2001/04/xmlenc#sha256" => Ok(OaepDigestAlgorithm::Sha256), - "http://www.w3.org/2001/04/xmlenc#sha384" - | "http://www.w3.org/2001/04/xmldsig-more#sha384" => Ok(OaepDigestAlgorithm::Sha384), - "http://www.w3.org/2001/04/xmlenc#sha512" => Ok(OaepDigestAlgorithm::Sha512), - unsupported => Err(XmlEncError::UnsupportedAlgorithm(unsupported.to_owned())), - } + let uri = uri.unwrap_or("http://www.w3.org/2000/09/xmldsig#sha1"); + OaepDigestAlgorithm::from_uri(uri) + .ok_or_else(|| XmlEncError::UnsupportedAlgorithm(uri.to_owned())) } fn parse_oaep_mgf_digest(uri: Option<&str>) -> Result { @@ -436,16 +458,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)) => { + 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(), + }; + 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 +2402,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/src/xmlenc/mod.rs b/src/xmlenc/mod.rs index b59a5ea..8e5d8a4 100644 --- a/src/xmlenc/mod.rs +++ b/src/xmlenc/mod.rs @@ -25,7 +25,10 @@ pub use decrypt::{ decrypt_document_with_options, }; pub use encrypt::EncryptedDataBuilder; -pub use parse::parse_encrypted_data; +pub use parse::{ + parse_encrypted_data, parse_encrypted_data_node_with_policy, + parse_encrypted_data_template_node_with_policy, +}; pub use types::{ CipherData, DataEncryptionAlgorithm, DecryptedContent, DocumentEncryptionOptions, EncryptedData, EncryptedDataType, EncryptedKey, EncryptionMethod, EncryptionRecipient, diff --git a/src/xmlenc/parse.rs b/src/xmlenc/parse.rs index cb1bd65..f94120d 100644 --- a/src/xmlenc/parse.rs +++ b/src/xmlenc/parse.rs @@ -35,9 +35,37 @@ pub(super) fn parse_encrypted_data_with_policy( parse_encrypted_data_node_with_policy(document.root_element(), policy) } -pub(super) fn parse_encrypted_data_node_with_policy( +/// Parse a selected `xenc:EncryptedData` node under an immutable policy snapshot. +/// +/// This is the node-oriented counterpart to [`parse_encrypted_data`]. It lets +/// callers that already parsed a containing document validate the complete +/// encrypted-data structure without serializing the selected subtree and losing +/// namespace declarations inherited from its ancestors. +pub fn parse_encrypted_data_node_with_policy( node: Node<'_, '_>, policy: &crate::policy::DecryptionPolicy, +) -> Result { + policy.validate()?; + parse_encrypted_data_node(node, policy, false) +} + +/// Parse an `xenc:EncryptedData` template under an immutable policy snapshot. +/// +/// This applies the complete encrypted-data grammar and metadata limits while +/// permitting empty `CipherValue` placeholders that encryption will replace. +/// Non-empty placeholders must still be well-formed base64. +pub fn parse_encrypted_data_template_node_with_policy( + node: Node<'_, '_>, + policy: &crate::policy::EncryptionPolicy, +) -> Result { + policy.validate()?; + parse_encrypted_data_node(node, policy, true) +} + +fn parse_encrypted_data_node( + node: Node<'_, '_>, + policy: &crate::policy::DecryptionPolicy, + allow_empty_cipher_values: bool, ) -> Result { require_element(node, XMLENC_NS, "EncryptedData")?; let mut children = element_children(node); @@ -45,18 +73,39 @@ pub(super) fn parse_encrypted_data_node_with_policy( next_required(&mut children, "EncryptionMethod")?, policy.resources.max_encryption_metadata_bytes, )?; + if children + .peek() + .is_some_and(|child| child.has_tag_name((XMLENC_NS, "EncryptionMethod"))) + { + return Err(XmlEncError::InvalidStructure( + "EncryptedData contains more than one direct EncryptionMethod".into(), + )); + } let key_info = match children.peek() { - Some(child) if child.has_tag_name((XMLDSIG_NS, "KeyInfo")) => { - parse_key_info(next_required(&mut children, "KeyInfo")?, policy)? - } + Some(child) if child.has_tag_name((XMLDSIG_NS, "KeyInfo")) => parse_key_info( + next_required(&mut children, "KeyInfo")?, + policy, + allow_empty_cipher_values, + )?, _ => ParsedKeyInfo { key_name: None, encrypted_keys: Vec::new(), }, }; + if children + .peek() + .is_some_and(|child| child.has_tag_name((XMLDSIG_NS, "KeyInfo"))) + { + return Err(XmlEncError::InvalidStructure( + "EncryptedData contains more than one direct KeyInfo".into(), + )); + } - let cipher_data = parse_cipher_data(next_required(&mut children, "CipherData")?)?; + let cipher_data = parse_cipher_data( + next_required(&mut children, "CipherData")?, + allow_empty_cipher_values, + )?; consume_encryption_properties(&mut children); if children.next().is_some() { return Err(XmlEncError::InvalidStructure( @@ -79,6 +128,7 @@ pub(super) fn parse_encrypted_data_node_with_policy( fn parse_key_info( node: Node<'_, '_>, policy: &crate::policy::DecryptionPolicy, + allow_empty_cipher_values: bool, ) -> Result { require_element(node, XMLDSIG_NS, "KeyInfo")?; let mut key_name = None; @@ -101,7 +151,11 @@ fn parse_key_info( } .into()); } - encrypted_keys.push(parse_encrypted_key(child, policy)?); + encrypted_keys.push(parse_encrypted_key( + child, + policy, + allow_empty_cipher_values, + )?); } else if child.has_tag_name((XMLENC_NS, "AgreementMethod")) { // Agreement methods require a separate key-derivation trust boundary. // Keep the URI as a fallback error while allowing another advertised @@ -134,6 +188,7 @@ fn parse_key_info( fn parse_encrypted_key( node: Node<'_, '_>, policy: &crate::policy::DecryptionPolicy, + allow_empty_cipher_values: bool, ) -> Result { require_element(node, XMLENC_NS, "EncryptedKey")?; let mut children = element_children(node); @@ -141,6 +196,14 @@ fn parse_encrypted_key( next_required(&mut children, "EncryptionMethod")?, policy.resources.max_encryption_metadata_bytes, )?; + if children + .peek() + .is_some_and(|child| child.has_tag_name((XMLENC_NS, "EncryptionMethod"))) + { + return Err(XmlEncError::InvalidStructure( + "EncryptedKey contains more than one direct EncryptionMethod".into(), + )); + } let key_name = if children .peek() .is_some_and(|child| child.has_tag_name((XMLDSIG_NS, "KeyInfo"))) @@ -149,7 +212,18 @@ fn parse_encrypted_key( } else { None }; - let cipher_data = parse_cipher_data(next_required(&mut children, "CipherData")?)?; + if children + .peek() + .is_some_and(|child| child.has_tag_name((XMLDSIG_NS, "KeyInfo"))) + { + return Err(XmlEncError::InvalidStructure( + "EncryptedKey contains more than one direct KeyInfo".into(), + )); + } + let cipher_data = parse_cipher_data( + next_required(&mut children, "CipherData")?, + allow_empty_cipher_values, + )?; consume_encryption_properties(&mut children); let reference_list = if children .peek() @@ -374,7 +448,7 @@ fn parse_key_size(node: Node<'_, '_>, metadata_limit: usize) -> Result) -> Result { +fn parse_cipher_data(node: Node<'_, '_>, allow_empty: bool) -> Result { require_element(node, XMLENC_NS, "CipherData")?; let mut children = element_children(node); let value = next_required(&mut children, "CipherValue")?; @@ -385,7 +459,7 @@ fn parse_cipher_data(node: Node<'_, '_>) -> Result { )); } Ok(CipherData { - value: normalize_base64(&simple_text(value, "CipherValue")?)?, + value: normalize_base64_with_empty(&simple_text(value, "CipherValue")?, allow_empty)?, }) } @@ -581,11 +655,6 @@ fn decode_bounded_base64_text(node: Node<'_, '_>, maximum: usize) -> Result Result { - normalize_base64_with_empty(value, false) -} - fn normalize_base64_with_empty(value: &str, allow_empty: bool) -> Result { let mut normalized = String::with_capacity(value.len().min(MAX_CIPHER_VALUE_BASE64_LEN)); for character in value.chars() { @@ -609,6 +678,11 @@ fn normalize_base64_with_empty(value: &str, allow_empty: bool) -> Result Result { + normalize_base64_with_empty(value, false) +} + fn require_element(node: Node<'_, '_>, namespace: &str, name: &str) -> Result<(), XmlEncError> { if node.has_tag_name((namespace, name)) { Ok(()) diff --git a/src/xmlenc/types.rs b/src/xmlenc/types.rs index 807c0d4..411c718 100644 --- a/src/xmlenc/types.rs +++ b/src/xmlenc/types.rs @@ -192,6 +192,18 @@ pub enum OaepDigestAlgorithm { } impl OaepDigestAlgorithm { + /// Parse a digest URI accepted by XML Encryption and libxmlsec1. + pub fn from_uri(uri: &str) -> Option { + match uri { + "http://www.w3.org/2000/09/xmldsig#sha1" => Some(Self::Sha1), + "http://www.w3.org/2001/04/xmlenc#sha256" => Some(Self::Sha256), + "http://www.w3.org/2001/04/xmlenc#sha384" + | "http://www.w3.org/2001/04/xmldsig-more#sha384" => Some(Self::Sha384), + "http://www.w3.org/2001/04/xmlenc#sha512" => Some(Self::Sha512), + _ => None, + } + } + /// Return the standard digest URI. pub const fn uri(self) -> &'static str { match self { @@ -213,6 +225,26 @@ impl OaepDigestAlgorithm { } } +#[cfg(test)] +mod tests { + use super::OaepDigestAlgorithm; + + #[test] + fn oaep_sha384_accepts_both_interoperable_digest_uris() { + // The canonical XML Encryption spelling and libxmlsec1's XMLDSig-more + // spelling identify the same OAEP digest algorithm. + for uri in [ + "http://www.w3.org/2001/04/xmlenc#sha384", + "http://www.w3.org/2001/04/xmldsig-more#sha384", + ] { + assert_eq!( + OaepDigestAlgorithm::from_uri(uri), + Some(OaepDigestAlgorithm::Sha384) + ); + } + } +} + /// RSA-OAEP parameters emitted in an `EncryptedKey`. #[derive(Debug, Clone, PartialEq, Eq)] pub struct RsaOaepParameters { diff --git a/tests/capability_ledger.rs b/tests/capability_ledger.rs index b6a6c7c..7ae29a0 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,82 @@ 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 [ + "--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() + .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, 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, outcome); + assert!( + item.detail.contains(code), + "{status} must record exit code {code}" + ); + } +} + #[test] fn deprecated_surface_is_explicitly_unsupported() { // Deprecated aliases stay visible without adding compatibility shims prematurely. 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/tests/signing_digest.rs b/tests/signing_digest.rs index f871a1e..243c0f3 100644 --- a/tests/signing_digest.rs +++ b/tests/signing_digest.rs @@ -871,6 +871,228 @@ 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); + 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(" 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 @@ -902,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, } ) )); @@ -943,16 +1165,50 @@ fn key_info_writer_rejects_duplicate_direct_template_placeholders() { assert!(matches!( err, - SigningError::XmlMutation( - xml_sec::xmldsig::mutation::XmlMutationError::ValueCountMismatch { - element: "KeyInfo", - expected: 2, - actual: 1, - } - ) + SigningError::Digest(SigningDigestError::InvalidStructure(message)) + if message.contains("KeyInfo must appear at most once") )); } +#[test] +fn signing_without_key_info_writer_rejects_malformed_signature_children() { + // Structural validation is independent of KeyInfo population: signing must + // never emit a document that the verification parser necessarily rejects. + 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 builder = SignatureBuilder::new(exclusive_c14n(), SignatureAlgorithm::RsaSha256) + .key_info(true) + .add_reference( + ReferenceBuilder::new(DigestAlgorithm::Sha256) + .uri("#payload") + .transform(Transform::C14n(exclusive_c14n())), + ); + let template = builder.build_template().expect("valid signature template"); + + let malformed_templates = [ + template.replace("", ""), + template.replace("", ""), + ]; + for malformed in malformed_templates { + let xml = append_signature_to_root( + "hello", + &malformed, + ) + .expect("append malformed signature template"); + let error = SignContext::new(&private_key) + .sign_template(&xml) + .expect_err("malformed Signature children must fail without a writer"); + assert!( + matches!( + error, + SigningError::Digest(SigningDigestError::InvalidStructure(_)) + ), + "{error:?}" + ); + } +} + #[test] fn x509_key_info_writer_rejects_certificate_for_different_key() { // A successful signing call must not produce a document that embeds an @@ -1039,6 +1295,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/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/Cargo.toml b/tools/xmlsec1/Cargo.toml new file mode 100644 index 0000000..6ebf672 --- /dev/null +++ b/tools/xmlsec1/Cargo.toml @@ -0,0 +1,31 @@ +[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] +rand_chacha = "0.10" +tempfile = "3" diff --git a/tools/xmlsec1/README.md b/tools/xmlsec1/README.md new file mode 100644 index 0000000..9db44c5 --- /dev/null +++ b/tools/xmlsec1/README.md @@ -0,0 +1,63 @@ +# 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. + +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 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 bounded raw binary key material (`--aeskey` remains a +compatible alias); they are never guessed to be Base64 text. 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; +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. +Content and recipient templates require exactly one direct `CipherData` and +`CipherValue`; `EncryptedData` children must follow the XML Encryption sequence, +and incomplete, duplicate, or out-of-order payload containers fail before output. +Multi-recipient RSA templates preserve named and unnamed identities and consume +distinct lax-search wrapping keys in command-line order. +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, filling an empty `X509Data` +without erasing sibling key sources; verification accepts +stdin as `-` and can select one signature subtree with `--node-id`. +The CLI supports 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. 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 +verification and encryption/decryption options form key sets and require the +selected XML `KeyName` to identify exactly one entry, unless lax lookup is +explicit. Lax lookup searches compatible entries in command-line order instead +of enforcing XML identity metadata. 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 +payloads cannot be emitted with XML Element/Content type metadata. diff --git a/tools/xmlsec1/src/args.rs b/tools/xmlsec1/src/args.rs new file mode 100644 index 0000000..5a7b822 --- /dev/null +++ b/tools/xmlsec1/src/args.rs @@ -0,0 +1,780 @@ +use std::{ + collections::BTreeMap, + ffi::{OsStr, 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, + Encrypt, + Decrypt, +} + +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 { + "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, + "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("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, +} + +#[derive(Clone, Copy)] +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, +} + +#[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; + +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: &["pubkey-cert"], + 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: false, + }, + OptionSpec { + canonical: "binary-data", + aliases: &["binary"], + 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: &["h"], + arity: FLAG, + accepts_parameter: false, + }, +]; + +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.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 == OsStr::new("--") { + options_finished = true; + index += 1; + continue; + } + let option_text = argument.to_str(); + if options_finished + || argument == OsStr::new("-") + || !option_text.is_some_and(|value| value.starts_with('-')) + { + positional.push(argument.clone()); + options_finished = true; + index += 1; + continue; + } + 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_text.to_owned()))?; + if parameter.is_some() && !option_spec(name).accepts_parameter { + return Err(ParseError::UnexpectedOptionParameter( + 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, + 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() + .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<&OsStr> { + 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> { + OPTION_SPECS + .iter() + .find(|spec| spec.canonical == name || spec.aliases.contains(&name)) + .map(|spec| spec.canonical) +} + +fn option_arity(name: &str) -> Arity { + 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 { + 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", + "-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::Sign); + assert_eq!(parsed.last_value("output"), Some(OsStr::new("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, [OsString::from("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, + [OsString::from("input.xml"), OsString::from("--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(_)) + )); + 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] + 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. + 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 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. + 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 + ); + } + } + + #[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() { + // 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 new file mode 100644 index 0000000..068e485 --- /dev/null +++ b/tools/xmlsec1/src/capabilities.rs @@ -0,0 +1,93 @@ +use std::{ffi::OsString, 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-mgf1p", + "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 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() + .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)] +mod tests { + use super::*; + + #[test] + fn checks_comma_separated_and_repeated_capabilities() { + assert!(all_requested_available( + TRANSFORMS, + &["c14n,rsa-sha256".into(), "sha256".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 new file mode 100644 index 0000000..04ccbd8 --- /dev/null +++ b/tools/xmlsec1/src/commands.rs @@ -0,0 +1,2586 @@ +use std::{ + collections::HashSet, + ffi::{OsStr, OsString}, + fs::{self, File, OpenOptions}, + io::{Read, Write}, + path::{Path, PathBuf}, +}; + +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::{ + DefaultKeyResolver, DsigStatus, FailureReason, KeyInfoSource, KeyInfoWriter, KeyResolver, + KeyResolverConfig, KeyValueInfo, ReferenceResult, SignContext, SignatureAlgorithm, + SigningKey, SigningPublicKeyInfo, UriTypeSet, VerifyContext, VerifyResult, + X509CertificateKeyInfoWriter, XPathHereSemantics, parse_key_info, + uri::UriReferenceResolver, + }, + xmlenc::{ + DataEncryptionAlgorithm, DecryptContext, DecryptedContent, DecryptionKeyResolver, + EncryptedDataBuilder, EncryptedDataType, EncryptedKey, EncryptionMethod, + EncryptionRecipient, KeyTransportAlgorithm, OaepDigestAlgorithm, PrivateKeyDecryptor, + RsaOaepParameters, SymmetricKeyDecryptor, XmlEncError, + parse_encrypted_data_template_node_with_policy, + }, +}; + +use crate::{ + Command, Invocation, + args::{Arity, OPTION_SPECS}, + 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", +]; +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 XMLSEC_COMPATIBILITY_HERE_SEMANTICS: XPathHereSemantics = XPathHereSemantics::XmlSecLegacy; +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 { + #[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 {}: {source}", path.display())] + Io { + 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("encryption plaintext exceeds policy limit of {maximum} bytes")] + PlaintextTooLarge { maximum: usize }, + #[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, + #[error("invalid internal command contract: {0}")] + InvalidContract(&'static str), +} + +pub fn execute( + invocation: Invocation, + stdout: &mut dyn Write, + _stderr: &mut dyn Write, +) -> Result<(), CommandError> { + if invocation.flag("help") { + 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 => 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, &[])?; + capabilities::list("transform klasses", TRANSFORMS, stdout).map_err(stdout_error) + } + Command::CheckTransforms => { + validate_options(&invocation, &[])?; + if capabilities::all_requested_available(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::all_requested_available(KEY_DATA, &invocation.positional) { + Ok(()) + } else { + Err(CommandError::CapabilityUnavailable) + } + } + Command::Keys => keys(&invocation, stdout), + Command::Sign => 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]").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)?; + write_command_list(Command::ALL, output)?; + 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 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); + }; + 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) + .ok_or(CommandError::InvalidContract( + "command option is absent from 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])> { + 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> { + if let Some(provider) = option_text(invocation, "crypto")? + && !matches!(provider, "rustcrypto" | "default") + { + return Err(CommandError::UnsupportedProvider(provider.to_owned())); + } + 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()) { + return Err(CommandError::UnsupportedOption(name.clone())); + } + } + Ok(()) +} + +fn input_path(invocation: &Invocation) -> Result<&OsStr, 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, maximum: usize) -> Result { + let path = input_path(invocation)?; + 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( + invocation: &Invocation, + bytes: &[u8], + stdout: &mut dyn Write, +) -> Result<(), CommandError> { + 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, +) -> 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 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 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(), + )); + } + 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) +} + +fn select_named_candidate<'a, T: Copy>( + candidates: &[(&'a crate::OptionValue, T)], + requested_names: &[String], + allow_unconstrained_named_singleton: bool, + key_kind: &str, +) -> Result<(&'a crate::OptionValue, T), CommandError> { + if let [selected] = candidates + && (selected.0.parameter.is_none() + || (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 unnamed = candidates + .iter() + .copied() + .filter(|(key, _)| key.parameter.is_none()) + .collect::>(); + match unnamed.as_slice() { + [selected] => return Ok(*selected), + [] => {} + _ => { + return Err(CommandError::Usage(format!( + "multiple unnamed {key_kind} inputs match the template recipient" + ))); + } + } + 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 named_candidate_search<'a, T: Copy>( + candidates: &[(&'a crate::OptionValue, T)], + requested_names: &[String], + lax_key_search: bool, + allow_unconstrained_named_singleton: bool, + key_kind: &str, +) -> Result, CommandError> { + if lax_key_search { + if candidates.is_empty() { + return Err(CommandError::Usage(format!( + "no compatible {key_kind} input was supplied" + ))); + } + return Ok(candidates.to_vec()); + } + select_named_candidate( + candidates, + requested_names, + allow_unconstrained_named_singleton, + key_kind, + ) + .map(|selected| vec![selected]) +} + +fn sign(invocation: &Invocation, stdout: &mut dyn Write) -> Result<(), CommandError> { + validate_options(invocation, SIGN_OPTIONS)?; + reject_unimplemented_selectors(invocation, &["node-id", "id-attr", "add-id-attr"])?; + if invocation.last_value("pwd").is_some() { + return Err(CommandError::UnsupportedOption("pwd".into())); + } + // This binary is an explicit libxmlsec1 compatibility boundary. Its sign + // and verify commands must bind XPath here() identically for round trips. + let policy = SigningPolicy { + xpath_here_semantics: XMLSEC_COMPATIBILITY_HERE_SEMANTICS, + ..SigningPolicy::default() + }; + let xml = read_input(invocation, policy.resources.max_xml_document_bytes)?; + let start_node_id = option_text(invocation, "node-id")?; + 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, key) = + select_signing_key(invocation, &signature.key_names, signature.algorithm)?; + let value = key_option.value.as_deref().unwrap_or_default(); + let (_, certificate_paths) = split_key_and_certificates(value)?; + 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(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); + } else { + // 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()))?; + } + } + let signed = context + .sign_template(&xml) + .map_err(|error| CommandError::Signature(error.to_string()))?; + 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>( + invocation: &'a Invocation, + requested_names: &[String], + algorithm: SignatureAlgorithm, +) -> Result<(&'a crate::OptionValue, bool, Box), 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(), + )); + } + let candidates = named_candidate_search( + &keys, + requested_names, + invocation.flag("lax-key-search"), + false, + "private key", + )?; + let mut last_error = None; + for (option, certificate_is_der) in candidates { + let (path, _) = split_key_and_certificates(option.value.as_deref().unwrap_or_default())?; + match key_material::load_signing_key(path) { + Ok(key) if signing_key_supports(key.as_ref(), algorithm) => { + return Ok((option, certificate_is_der, key)); + } + Ok(_) => {} + Err(error) => last_error = Some(error), + } + } + if let Some(error) = last_error { + return Err(error.into()); + } + Err(CommandError::Usage(format!( + "no private key input supports {}", + algorithm.uri() + ))) +} + +fn signing_key_supports(key: &dyn SigningKey, algorithm: SignatureAlgorithm) -> bool { + matches!( + (key.public_key_info(), algorithm), + ( + Ok(SigningPublicKeyInfo::Rsa { .. }), + SignatureAlgorithm::RsaSha1 + | SignatureAlgorithm::RsaSha256 + | SignatureAlgorithm::RsaSha384 + | SignatureAlgorithm::RsaSha512 + ) | ( + Ok(SigningPublicKeyInfo::Ec { .. }), + SignatureAlgorithm::EcdsaSha256 | SignatureAlgorithm::EcdsaSha384 + ) + ) +} + +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 + // 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 and certificate paths must not be empty".into(), + )); + } + Ok((key, certificates)) +} + +fn xmlsec_compatibility_verification_policy(invocation: &Invocation) -> VerificationPolicy { + // Running the xmlsec1-compatible binary is the explicit compatibility + // boundary: both CLI signing and verification use the donor interpretation, + // while the core library retains the XMLDSig binding by default. + let mut policy = VerificationPolicy { + process_manifests: !invocation.flag("ignore-manifests"), + reference_uri_types: UriTypeSet::ALL, + retrieval_uri_types: UriTypeSet::ALL, + xpath_here_semantics: XMLSEC_COMPATIBILITY_HERE_SEMANTICS, + ..VerificationPolicy::default() + }; + policy.key_trust.allowed_legacy_signature_algorithms = HashSet::from([ + SignatureAlgorithm::RsaSha1, + SignatureAlgorithm::DsaSha1, + SignatureAlgorithm::HmacSha1, + ]); + // 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. 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 +} + +fn verify(invocation: &Invocation, stdout: &mut dyn Write) -> Result<(), CommandError> { + validate_options(invocation, VERIFY_OPTIONS)?; + reject_unimplemented_selectors(invocation, &["node-id", "id-attr", "add-id-attr"])?; + reject_unimplemented_verification_policy(invocation)?; + 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") && explicit_keys.is_empty() { + return Err(CommandError::UnsupportedOption("lax-key-search".into())); + } + 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 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_keys = if explicit_keys.is_empty() { + Vec::new() + } else { + named_candidate_search( + &explicit_keys, + &signature.key_names, + invocation.flag("lax-key-search"), + true, + "verification key", + )? + }; + let result = if !selected_keys.is_empty() { + let mut last_result = None; + let mut last_error = None; + for (option, certificate) in selected_keys { + let attempt = if certificate { + verify_with_explicit_certificate( + invocation, + option, + algorithm, + policy.clone(), + start_node_id, + &id_attributes, + &xml, + ) + } else { + key_material::load_verification_key( + option.value.as_deref().unwrap_or_default(), + algorithm, + ) + .map_err(CommandError::from) + .and_then(|key| { + verification_context(policy.clone(), start_node_id, &id_attributes) + .key(&key) + .verify(&xml) + .map_err(|error| CommandError::Signature(error.to_string())) + }) + }; + match attempt { + Ok(result) if aggregate_verification_status(&result) == DsigStatus::Valid => { + last_result = Some(result); + break; + } + Ok(result) => last_result = Some(result), + Err(error) => last_error = Some(error), + } + } + match last_result { + Some(result) => result, + None => return Err(last_error.unwrap_or(CommandError::InvalidSignature)), + } + } 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); + verification_context(policy, start_node_id, &id_attributes) + .key_resolver(&resolver) + .verify(&xml) + .map_err(|error| CommandError::Signature(error.to_string()))? + }; + write_verification_diagnostics(invocation, &result, stdout)?; + if aggregate_verification_status(&result) != DsigStatus::Valid { + return Err(CommandError::InvalidSignature); + } + Ok(()) +} + +fn write_verification_diagnostics( + invocation: &Invocation, + result: &VerifyResult, + stdout: &mut dyn Write, +) -> Result<(), CommandError> { + let aggregate_status = aggregate_verification_status(result); + if invocation.flag("print-debug") { + let status = if aggregate_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(aggregate_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 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, + 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<'a>( + policy: VerificationPolicy, + 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, + } +} + +fn verify_with_explicit_certificate( + invocation: &Invocation, + certificate: &crate::OptionValue, + algorithm: SignatureAlgorithm, + policy: VerificationPolicy, + start_node_id: Option<&str>, + id_attributes: &[IdAttributeRegistration], + 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 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, &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, id_attributes) + .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, ENCRYPT_OPTIONS)?; + 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 { + 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; + let template = read_input(invocation, policy.resources.max_xml_document_bytes)?; + let start_node_id = option_text(invocation, "node-id")?; + 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; + let mut builder = EncryptedDataBuilder::new(algorithm).policy(policy.clone()); + let aes_keys = invocation.values("aes-key").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.is_empty() && !public_keys.is_empty() { + return Err(CommandError::Usage( + "encrypt cannot combine explicit AES and RSA recipient keys".into(), + )); + } + 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(), + )); + } + let candidates = aes_keys + .iter() + .copied() + .map(|option| (option, ())) + .collect::>(); + let requested_names = metadata + .content_key_name + .iter() + .cloned() + .collect::>(); + let candidates = named_candidate_search( + &candidates, + &requested_names, + invocation.flag("lax-key-search"), + true, + "AES key", + )?; + let mut selected = None; + let mut last_error = None; + for (option, ()) in candidates { + match key_material::load_symmetric( + option.value.as_deref().unwrap_or_default(), + Some(algorithm.key_len()), + ) { + Ok(key) => { + selected = Some((option, key)); + break; + } + Err(error) => last_error = Some(error), + } + } + let (option, key) = selected.ok_or_else(|| { + last_error + .map(CommandError::from) + .unwrap_or_else(|| CommandError::Usage("no compatible AES key input".into())) + })?; + builder = builder.direct_key(key); + if let Some(name) = option.parameter.as_deref() { + builder = builder.direct_key_name(name); + } + } else if !public_keys.is_empty() { + let template_recipients = if metadata.recipients.is_empty() { + vec![EncryptionTemplateRecipient { + key_name: None, + oaep_parameters: None, + }] + } else { + metadata.recipients + }; + let lax_key_search = invocation.flag("lax-key-search"); + let mut available_public_keys = public_keys.clone(); + 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 candidates = named_candidate_search( + &available_public_keys, + &requested_names, + lax_key_search, + true, + "RSA recipient key", + )?; + let mut selected = None; + let mut last_error = None; + for (option, certificate) in candidates { + let path = option.value.as_deref().unwrap_or_default(); + let loaded = if certificate { + key_material::load_rsa_certificate_public(path) + } else { + key_material::load_rsa_public(path) + }; + match loaded { + Ok(key) => { + selected = Some((option, certificate, key)); + break; + } + Err(error) => last_error = Some(error), + } + } + let (selected_option, selected_certificate, public_key) = + selected.ok_or_else(|| { + last_error.map(CommandError::from).unwrap_or_else(|| { + CommandError::Usage("no compatible RSA recipient key input".into()) + }) + })?; + if lax_key_search { + let selected_index = available_public_keys + .iter() + .position(|(option, certificate)| { + std::ptr::eq(*option, selected_option) + && *certificate == selected_certificate + }) + .ok_or_else(|| { + CommandError::Encryption( + "selected recipient key is absent from the candidate set".into(), + ) + })?; + available_public_keys.remove(selected_index); + } + selected_recipients.push((public_key, template_recipient.oaep_parameters)); + } + validate_recipient_key_metadata( + &template, + start_node_id, + &id_attributes, + &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); + } + } else { + return Err(CommandError::Usage( + "encrypt requires --aes-key, an RSA public key, or an RSA certificate".into(), + )); + } + 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") { + let data = String::from_utf8(read_plaintext(path, maximum_plaintext_bytes)?) + .map_err(|_| CommandError::InvalidUtf8Input)?; + 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()))?; + let rendered = apply_encryption_template( + &template, + &result.encrypted_data_xml, + start_node_id, + &id_attributes, + &policy, + )?; + 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)?; + 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>( + 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, id_attributes)?; + 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"))) + .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 (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 { + 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() == 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 { + 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( + method: &EncryptionMethod, +) -> Result, CommandError> { + let transport = KeyTransportAlgorithm::from_uri(&method.algorithm) + .map_err(|error| CommandError::Encryption(error.to_string()))?; + let digest = oaep_digest_from_uri( + method + .oaep_digest + .as_deref() + .unwrap_or(OaepDigestAlgorithm::Sha1.uri()), + )?; + let mgf_digest = if transport == KeyTransportAlgorithm::RsaOaepMgf1p { + OaepDigestAlgorithm::Sha1 + } else { + oaep_mgf_from_uri( + method + .mgf_algorithm + .as_deref() + .unwrap_or(OaepDigestAlgorithm::Sha1.mgf_uri()), + )? + }; + Ok(Some(RsaOaepParameters { + algorithm: transport, + digest, + mgf_digest, + label: method.oaep_params.clone().unwrap_or_default(), + })) +} + +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 direct_simple_text(node: Node<'_, '_>, field: &str) -> Result { + if node.children().any(|child| child.is_element()) { + return Err(CommandError::Encryption(format!( + "{field} must not contain element children" + ))); + } + Ok(node + .children() + .filter(Node::is_text) + .filter_map(|child| child.text()) + .collect()) +} + +fn oaep_digest_from_uri(uri: &str) -> Result { + OaepDigestAlgorithm::from_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, + 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, id_attributes)?; + let generated_data = generated_document.root_element(); + 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), + )]; + 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"); + 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, "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() + .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), + ) + }, + )); + } + } + (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 = standalone_element(generated, generated_key_info)?; + replacements.push(( + cipher_data.range().start..cipher_data.range().start, + key_info, + )); + } + _ => {} + } + 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); + } + parse_encryption_document(&output, policy)?; + 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() { + 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!( + "{}", + node.text().unwrap_or_default() + ) +} + +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, + name: &str, +) -> Option> { + node.children() + .find(|child| child.has_tag_name((namespace, name))) +} + +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>, + owner: &str, +) -> Result>, CommandError> { + key_info + .children() + .filter(|node| node.has_tag_name((XMLENC_NS, "EncryptedKey"))) + .enumerate() + .map(|(index, encrypted_key)| { + required_cipher_value( + encrypted_key, + &format!("{owner} EncryptedKey recipient {}", index + 1), + ) + }) + .collect() +} + +fn decrypt(invocation: &Invocation, stdout: &mut dyn Write) -> Result<(), CommandError> { + validate_options(invocation, DECRYPT_OPTIONS)?; + 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, &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)?; + let aes_keys = invocation.values("aes-key").collect::>(); + let private_keys = ["privkey-pem", "privkey-der", "pkcs8-pem", "pkcs8-der"] + .into_iter() + .flat_map(|name| invocation.values(name)) + .collect::>(); + if !aes_keys.is_empty() && !private_keys.is_empty() { + return Err(CommandError::Usage( + "decrypt cannot combine explicit AES and RSA private keys".into(), + )); + } + 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 candidates = named_candidate_search( + &candidates, + &requested_names, + invocation.flag("lax-key-search"), + true, + "AES key", + )?; + let mut decrypted = None; + let mut last_error = None; + for (option, ()) in candidates { + let attempt = + key_material::load_symmetric(option.value.as_deref().unwrap_or_default(), None) + .map_err(CommandError::from) + .and_then(|key| { + decrypt_input( + &SymmetricKeyDecryptor::new(key), + &xml, + encrypted_data_id, + standalone, + policy.clone(), + &id_attributes, + ) + }); + match attempt { + Ok(bytes) => { + decrypted = Some(bytes); + break; + } + Err(error) => last_error = Some(error), + } + } + decrypted.ok_or_else(|| { + last_error.unwrap_or_else(|| CommandError::Usage("no compatible AES key input".into())) + })? + } else if !private_keys.is_empty() { + let selected = select_recipient_private_keys( + &private_keys, + &recipient_key_names, + invocation.flag("lax-key-search"), + )?; + let mut keys = Vec::with_capacity(selected.len()); + for option in selected { + 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)?; + } + keys.push(RecipientPrivateKey { + inner: PrivateKeyDecryptor::new(key_material::load_rsa_private(path)?), + key_name: option.parameter.clone(), + }); + } + let resolver = NamedRecipientDecryptor { + keys, + lax_key_search: invocation.flag("lax-key-search"), + }; + 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(), + )); + }; + 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-debug") && !invocation.flag("print-xml-debug") { + return Ok(()); + } + 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" + }; + 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, + "" + ) + .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 RecipientPrivateKey { + inner: PrivateKeyDecryptor, + key_name: Option, +} + +struct NamedRecipientDecryptor { + keys: Vec, + lax_key_search: bool, +} + +impl DecryptionKeyResolver for NamedRecipientDecryptor { + fn resolve_key( + &self, + provider: &dyn CryptoProvider, + algorithm: DataEncryptionAlgorithm, + encrypted_key: Option<&EncryptedKey>, + ) -> Result, XmlEncError> { + let mut last_error = None; + for key in &self.keys { + if !self.lax_key_search + && let (Some(expected), Some(candidate)) = (key.key_name.as_deref(), encrypted_key) + && candidate.key_name.as_deref() != Some(expected) + { + continue; + } + match key.inner.resolve_key(provider, algorithm, encrypted_key) { + Ok(key) => return Ok(key), + Err(error) => last_error = Some(error), + } + } + Err(last_error.unwrap_or(XmlEncError::KeyNotFound)) + } +} + +fn decrypt_input( + resolver: &dyn DecryptionKeyResolver, + xml: &str, + encrypted_data_id: Option<&str>, + standalone: bool, + policy: DecryptionPolicy, + id_attributes: &[IdAttributeRegistration], +) -> Result, CommandError> { + let context = DecryptContext::new(resolver) + .policy(policy) + .id_attributes(id_attributes); + if standalone { + 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_from_start_node(xml, encrypted_data_id) + .map(String::into_bytes) + .map_err(|error| CommandError::Encryption(error.to_string())) +} + +struct EncryptionTemplateMetadata { + algorithm: DataEncryptionAlgorithm, + encrypted_type: EncryptedDataType, + explicit_encrypted_type: bool, + has_encrypted_key_recipient: bool, + content_key_name: Option, + recipients: Vec, +} + +struct EncryptionTemplateRecipient { + key_name: Option, + oaep_parameters: Option, +} + +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, id_attributes)?; + // Templates preserve every non-cipher field. Parse the selected node through + // the reciprocal core path first so encryption cannot emit a document that + // the same policy snapshot would reject during decryption. + let parsed = parse_encrypted_data_template_node_with_policy(encrypted_data, policy) + .map_err(|error| CommandError::Encryption(error.to_string()))?; + let algorithm = DataEncryptionAlgorithm::from_uri(&parsed.encryption_method.algorithm) + .map_err(|error| CommandError::Encryption(error.to_string()))?; + let explicit_encrypted_type = encrypted_data.attribute("Type").is_some(); + let encrypted_type = match parsed.encrypted_type { + None | Some(EncryptedDataType::Element) => EncryptedDataType::Element, + Some(EncryptedDataType::Content) => EncryptedDataType::Content, + Some(EncryptedDataType::Other(other)) => { + return Err(CommandError::Encryption(format!( + "unsupported EncryptedData Type: {other}" + ))); + } + }; + let recipients = parsed + .encrypted_keys + .into_iter() + .map(|encrypted_key| { + Ok(EncryptionTemplateRecipient { + key_name: encrypted_key.key_name, + oaep_parameters: template_oaep_parameters(&encrypted_key.encryption_method)?, + }) + }) + .collect::, CommandError>>()?; + Ok(EncryptionTemplateMetadata { + algorithm, + encrypted_type, + explicit_encrypted_type, + has_encrypted_key_recipient: !recipients.is_empty(), + content_key_name: parsed.key_name, + recipients, + }) +} + +fn encrypted_data_key_name(encrypted_data: Node<'_, '_>) -> Result, CommandError> { + let Some(key_info) = singleton_direct_child( + encrypted_data, + XMLDSIG_NS, + "KeyInfo", + "EncryptedData contains more than one direct KeyInfo", + )? + else { + return Ok(None); + }; + optional_direct_child_text( + key_info, + XMLDSIG_NS, + "KeyName", + "KeyInfo contains more than one direct KeyName", + ) +} + +fn optional_direct_child_text( + parent: Node<'_, '_>, + namespace: &str, + name: &str, + duplicate_error: &str, +) -> Result, CommandError> { + let mut children = parent + .children() + .filter(|node| node.has_tag_name((namespace, name))); + let value = children + .next() + .map(|node| direct_simple_text(node, name)) + .transpose()?; + if children.next().is_some() { + return Err(CommandError::Encryption(duplicate_error.into())); + } + Ok(value) +} + +fn encrypted_key_recipient_names( + encrypted_data: Node<'_, '_>, +) -> Result>, CommandError> { + let key_info = singleton_direct_child( + encrypted_data, + XMLDSIG_NS, + "KeyInfo", + "EncryptedData contains more than one direct KeyInfo", + )?; + key_info + .into_iter() + .flat_map(|key_info| key_info.children()) + .filter(|node| node.has_tag_name((XMLENC_NS, "EncryptedKey"))) + .map(|encrypted_key| { + let key_info = singleton_direct_child( + encrypted_key, + XMLDSIG_NS, + "KeyInfo", + "EncryptedKey contains more than one direct KeyInfo", + )?; + key_info + .map(|key_info| { + optional_direct_child_text( + key_info, + XMLDSIG_NS, + "KeyName", + "EncryptedKey KeyInfo contains more than one direct KeyName", + ) + }) + .transpose() + .map(Option::flatten) + }) + .collect() +} + +fn select_recipient_private_keys<'a>( + candidates: &[&'a crate::OptionValue], + recipient_names: &[Option], + lax_key_search: bool, +) -> Result, CommandError> { + if lax_key_search { + return Ok(candidates.to_vec()); + } + if let [candidate] = candidates + && candidate.parameter.is_none() + { + return Ok(vec![*candidate]); + } + if recipient_names.is_empty() { + let requested_names = Vec::new(); + let wrapped = candidates + .iter() + .copied() + .map(|candidate| (candidate, ())) + .collect::>(); + return named_candidate_search(&wrapped, &requested_names, false, true, "RSA private key") + .map(|selected| selected.into_iter().map(|(option, ())| option).collect()); + } + + let matching = candidates + .iter() + .copied() + .filter(|candidate| { + recipient_names + .iter() + .any(|requested| requested.as_deref() == candidate.parameter.as_deref()) + }) + .collect::>(); + if matching.is_empty() { + return Err(CommandError::Usage( + "template requests unknown KeyName for supplied RSA private key".into(), + )); + } + let mut seen = HashSet::new(); + if matching + .iter() + .any(|candidate| !seen.insert(candidate.parameter.as_deref())) + { + return Err(CommandError::Usage( + "multiple RSA private key inputs match the same template recipient identity".into(), + )); + } + Ok(matching) +} + +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>( + document: &'a Document<'a>, + start_node_id: Option<&str>, + id_attributes: &[IdAttributeRegistration], +) -> Result, CommandError> { + let start = if let Some(id) = start_node_id { + 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}")) + })? + } 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()))?; + if matches.next().is_some() { + return Err(CommandError::Encryption( + "multiple matching EncryptedData elements".into(), + )); + } + Ok(selected) +} + +fn keys(invocation: &Invocation, stdout: &mut dyn Write) -> Result<(), CommandError> { + validate_options(invocation, KEYS_OPTIONS)?; + 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 algorithm = option_value_text(generated)?; + 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 key_name = generated + .parameter + .as_deref() + .map_or_else(String::new, |name| { + format!("{}\n", quick_xml::escape::escape(name)) + }); + entries.push_str(&format!( + "\n\ + {key_name}\ + \n\ + {encoded}\n\ + \n\ + \n" + )); + } + let document = format!( + "\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(), + )); + } + if let Some(path) = invocation.positional.first() { + 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], +) -> 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: PathBuf::from("stdout"), + 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_reject_unknown_names() { + 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(); + 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"]), + &mut Vec::new(), + &mut Vec::new(), + ) + .unwrap_err(); + 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(); + execute( + invocation(&["xmlsec1", "help-all"]), + &mut output, + &mut Vec::new(), + ) + .unwrap(); + let help = String::from_utf8(output).unwrap(); + 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 { + 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, + None, + &[], + &EncryptionPolicy::default(), + ) + .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 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, + // 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 + // 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 }) + )); + } + + #[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(""); + + let error = match encryption_template(&xml, None, &[], &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] + 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 }) + )); + } + + #[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/src/key_material.rs b/tools/xmlsec1/src/key_material.rs new file mode 100644 index 0000000..220f92e --- /dev/null +++ b/tools/xmlsec1/src/key_material.rs @@ -0,0 +1,529 @@ +use std::{ + fs::{self, File}, + io::Read as _, + path::{Path, PathBuf}, +}; + +use roxmltree::Document; +use rsa::{ + RsaPrivateKey, RsaPublicKey, + pkcs1::{DecodeRsaPrivateKey as _, DecodeRsaPublicKey as _}, + pkcs8::{ + DecodePrivateKey as _, DecodePublicKey as _, EncodePrivateKey as _, EncodePublicKey as _, + }, +}; +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, +}; + +#[derive(Debug, thiserror::Error)] +pub enum KeyMaterialError { + #[error("failed to read key file {path}: {source}")] + Read { + path: PathBuf, + source: std::io::Error, + }, + #[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("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}")] + SymmetricLength { expected: usize, actual: usize }, + #[error("symmetric key exceeds maximum {maximum} bytes")] + SymmetricTooLarge { maximum: usize }, + #[error("invalid operation policy: {0}")] + Policy(#[from] PolicyViolation), +} + +#[derive(Debug, Eq, PartialEq)] +pub struct SignatureMetadata { + pub algorithm: SignatureAlgorithm, + pub key_names: Vec, +} + +#[derive(Debug, Eq, PartialEq)] +pub struct SigningTemplateMetadata { + pub algorithm: SignatureAlgorithm, + pub key_names: Vec, + 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 { + path: path.to_owned(), + source, + }) +} + +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())) +} + +pub fn verification_signature_metadata( + xml: &str, + start_node_id: Option<&str>, + id_attributes: &[xml_sec::IdAttributeRegistration], + 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::with_id_registrations(&document, id_attributes) + .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.has_tag_name(("http://www.w3.org/2000/09/xmldsig#", "SignedInfo"))) + .ok_or(KeyMaterialError::MissingSignedInfo)?; + let algorithm = parse_signed_info(signed_info) + .map(|info| info.signature_method) + .map_err(|error| KeyMaterialError::Signature(error.to_string()))?; + Ok(SignatureMetadata { + algorithm, + key_names: signature_key_names(signature), + }) +} + +pub fn signing_signature_metadata( + xml: &str, + start_node_id: Option<&str>, + id_attributes: &[xml_sec::IdAttributeRegistration], + policy: &SigningPolicy, +) -> 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::with_id_registrations(&document, id_attributes) + .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)?; + 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(), + }) +} + +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_names(signature: roxmltree::Node<'_, '_>) -> Vec { + signature_key_info(signature) + .into_iter() + .flat_map(|key_info| key_info.children()) + .filter(|node| node.has_tag_name(("http://www.w3.org/2000/09/xmldsig#", "KeyName"))) + .map(|key_name| { + key_name + .children() + .filter(roxmltree::Node::is_text) + .filter_map(|child| child.text()) + .collect() + }) + .collect() +} + +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)?; + 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)); + } + 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: 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) { + 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, + certificate_der: None, + name: None, + }) +} + +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)?; + 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 + }; + 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, path: &Path) -> Result, KeyMaterialError> { + let (rest, pem) = x509_parser::pem::parse_x509_pem(text.as_bytes()) + .map_err(|_| KeyMaterialError::InvalidPem(path.to_owned()))?; + if !rest.iter().all(u8::is_ascii_whitespace) || pem.label != expected_label { + return Err(KeyMaterialError::InvalidPem(path.to_owned())); + } + Ok(pem.contents) +} + +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) { + 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: 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) { + 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_rsa_certificate_public( + path: impl AsRef, +) -> 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, +) -> 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. + const MAX_AES_KEY_BYTES: usize = 32; + + let path = path.as_ref(); + let maximum = expected.unwrap_or(MAX_AES_KEY_BYTES); + let mut key = Vec::with_capacity(maximum.saturating_add(1)); + File::open(path) + .map_err(|source| KeyMaterialError::Read { + path: path.to_owned(), + source, + })? + .take(maximum.saturating_add(1) as u64) + .read_to_end(&mut key) + .map_err(|source| KeyMaterialError::Read { + path: path.to_owned(), + source, + })?; + if key.len() > maximum { + return match expected { + Some(expected) => Err(KeyMaterialError::SymmetricLength { + expected, + actual: key.len(), + }), + None => Err(KeyMaterialError::SymmetricTooLarge { maximum }), + }; + } + if let Some(expected) = expected + && key.len() != expected + { + return Err(KeyMaterialError::SymmetricLength { + expected, + actual: key.len(), + }); + } + Ok(key) +} + +#[cfg(test)] +mod tests { + use rand_chacha::{ChaCha20Rng, rand_core::SeedableRng as _}; + use rsa::pkcs1::{EncodeRsaPrivateKey as _, EncodeRsaPublicKey as _}; + + use super::*; + + #[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. 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"); + 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")); + } + + #[test] + fn symmetric_key_loader_rejects_input_above_the_supported_ceiling() { + // Decryption does not know the exact AES width until it parses the + // ciphertext, but the CLI must still reject data beyond every supported + // AES key size instead of treating an arbitrary file as key material. + let temp = tempfile::tempdir().unwrap(); + let path = temp.path().join("oversized.key"); + fs::write(&path, [0_u8; 33]).unwrap(); + + let error = load_symmetric(&path, None).unwrap_err(); + + assert!(error.to_string().contains("maximum 32 bytes")); + } + + #[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") + ); + + let metadata = verification_signature_metadata( + &xml, + Some("ec"), + &[], + &xml_sec::policy::VerificationPolicy::default(), + ) + .unwrap(); + 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 + // 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/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/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 0000000..7bd9a2c Binary files /dev/null and b/tools/xmlsec1/tests/fixtures/upstream/phaos-xmldsig-three/certs/rsa-ca-cert.der differ 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/import_snapshot.rs b/tools/xmlsec1/tests/import_snapshot.rs new file mode 100644 index 0000000..6642ceb --- /dev/null +++ b/tools/xmlsec1/tests/import_snapshot.rs @@ -0,0 +1,134 @@ +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"); +} + +#[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 new file mode 100644 index 0000000..1f43b93 --- /dev/null +++ b/tools/xmlsec1/tests/process_contract.rs @@ -0,0 +1,4082 @@ +use std::{ + fs, + io::Write as _, + path::Path, + process::{Command, Stdio}, +}; + +use base64::Engine as _; +use rsa::{ + RsaPrivateKey, RsaPublicKey, + pkcs8::{ + DecodePrivateKey as _, DecodePublicKey as _, EncodePrivateKey as _, EncodePublicKey as _, + }, + traits::PublicKeyParts as _, +}; +use xml_sec::{ + c14n::{C14nAlgorithm, C14nMode}, + xmldsig::{ + DigestAlgorithm, ReferenceBuilder, RsaSigningKey, SignContext, SignatureAlgorithm, + SignatureBuilder, Transform, XPathExpression, mutation::append_signature_to_root, + }, + xmlenc::{DataEncryptionAlgorithm, EncryptedDataBuilder, EncryptionRecipient}, +}; + +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() +} + +fn signature_template_without_key_info() -> &'static str { + r##" + + + + + + +payload +"## +} + +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(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()) + ) +} + +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 + // 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 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", "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 + // 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 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")); + 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] +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 signs_and_verifies_libxmlsec_legacy_here_semantics_through_process_api() { + // The expression selects different nodes under specification and libxmlsec + // semantics, so this round trip proves both CLI policies reach transforms. + let temp = tempfile::tempdir().unwrap(); + let template_path = temp.path().join("legacy-here-template.xml"); + let signed_path = temp.path().join("legacy-here.xml"); + let private_key = project_root().join("tests/fixtures/keys/rsa/rsa-2048-key.pem"); + 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 template = append_signature_to_root( + "legacy here", + &builder.build_template().unwrap(), + ) + .unwrap(); + fs::write(&template_path, template).unwrap(); + + let sign = Command::new(binary()) + .args(["sign", "--privkey-pem"]) + .arg(&private_key) + .arg("--output") + .arg(&signed_path) + .arg(&template_path) + .output() + .unwrap(); + assert!( + sign.status.success(), + "{}", + String::from_utf8_lossy(&sign.stderr) + ); + + 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 + // 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 lax_verification_searches_past_incompatible_key_types() { + // Lax lookup relaxes KeyName matching, but it must still search by the + // signature algorithm instead of treating the first registry entry as final. + let temp = tempfile::tempdir().unwrap(); + let template = project_root() + .join("tests/fixtures/xmldsig/aleksey-xmldsig-01/enveloping-sha256-ecdsa-sha256.tmpl"); + let private_key = project_root().join("tests/fixtures/keys/ec/ec-prime256v1-key.pem"); + let rsa_public_key = project_root().join("tests/fixtures/keys/rsa/rsa-2048-pubkey.pem"); + let ec_public_key = project_root().join("tests/fixtures/keys/ec/ec-prime256v1-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(), + "{}", + String::from_utf8_lossy(&sign.stderr) + ); + + let verify = Command::new(binary()) + .args(["verify", "--lax-key-search", "--pubkey-pem:rsa"]) + .arg(&rsa_public_key) + .arg("--pubkey-pem:ec") + .arg(&ec_public_key) + .arg(&signed) + .output() + .unwrap(); + assert!( + verify.status.success(), + "{}", + String::from_utf8_lossy(&verify.stderr) + ); +} + +#[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 + // 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 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; + // 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 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 + // 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 encryption_writes_requested_diagnostics_and_rejects_duplicate_methods() { + // Encryption diagnostics are a separate stdout contract, and malformed + // templates must fail before either diagnostics or ciphertext is emitted. + let temp = tempfile::tempdir().unwrap(); + let plaintext = temp.path().join("plaintext.bin"); + let key = temp.path().join("key.bin"); + fs::write(&plaintext, b"diagnostic payload").unwrap(); + fs::write(&key, b"0123456789abcdef").unwrap(); + + 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()); + + 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, + ), + ( + "key-info-after-cipher-data", + r#"late"#, + 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] +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 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", + "--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; + // 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 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; + // 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"), + "{}", + String::from_utf8_lossy(&result.stderr) + ); +} + +#[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 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(); + 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 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 encryption_rejects_duplicate_encrypted_data_key_info() { + // A generated document must satisfy the same singleton KeyInfo structure + // that the reciprocal parser enforces during decryption. + let temp = tempfile::tempdir().unwrap(); + let template = temp.path().join("duplicate-key-info.xml"); + let plaintext = temp.path().join("plaintext.bin"); + let key = temp.path().join("key.bin"); + fs::write( + &template, + r#""#, + ) + .unwrap(); + fs::write(&plaintext, b"duplicate encrypted data key info").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("EncryptedData contains more than one direct KeyInfo") + ); +} + +#[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 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")); + + // Lax lookup intentionally ignores duplicate identity metadata and searches + // by key kind, so the first compatible entry can complete the round trip. + let lax_encrypted = temp.path().join("lax-encrypted.xml"); + let lax_encrypt = Command::new(binary()) + .args(["encrypt", "--lax-key-search", "--aes-key:selected"]) + .arg(&matching_key) + .args(["--aes-key:selected"]) + .arg(&wrong_key) + .arg("--binary-data") + .arg(&plaintext) + .arg("--output") + .arg(&lax_encrypted) + .arg(&template) + .output() + .unwrap(); + assert!( + lax_encrypt.status.success(), + "{}", + String::from_utf8_lossy(&lax_encrypt.stderr) + ); + let lax_decrypt = Command::new(binary()) + .args(["decrypt", "--lax-key-search", "--aes-key:selected"]) + .arg(&wrong_key) + .args(["--aes-key:selected"]) + .arg(&matching_key) + .arg(&lax_encrypted) + .output() + .unwrap(); + assert!( + lax_decrypt.status.success(), + "{}", + String::from_utf8_lossy(&lax_decrypt.stderr) + ); + assert_eq!(lax_decrypt.stdout, b"selected AES key"); +} + +#[test] +fn encryption_rejects_invalid_content_encryption_method_structure() { + // Encrypt must apply the same EncryptionMethod invariants as decrypt; + // otherwise it can emit ciphertext that its reciprocal parser rejects. + let temp = tempfile::tempdir().unwrap(); + let plaintext = temp.path().join("plaintext.bin"); + let key = temp.path().join("key.bin"); + fs::write(&plaintext, b"method validation").unwrap(); + fs::write(&key, b"0123456789abcdef").unwrap(); + + for (case, children, expected) in [ + ( + "mismatched-key-size", + "192", + "requires KeySize 128", + ), + ( + "duplicate-key-size", + "128128", + "KeySize", + ), + ( + "oaep-on-aes", + "YQ==", + "only valid for RSA-OAEP", + ), + ] { + let template = temp.path().join(format!("{case}.xml")); + fs::write( + &template, + format!( + r#"{children}"# + ), + ) + .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(), "{case} unexpectedly succeeded"); + assert!( + String::from_utf8_lossy(&output.stderr).contains(expected), + "{case}: {}", + String::from_utf8_lossy(&output.stderr) + ); + } +} + +#[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 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 + // 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 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#" + +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) + ); + fs::write(&encrypted, &matching.stdout).unwrap(); + + 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) + ); + + 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] +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_duplicate_recipient_key_info() { + // Each EncryptedKey has one optional KeyInfo identity. Selecting the first + // duplicate would preserve malformed output that decryption rejects. + let temp = tempfile::tempdir().unwrap(); + let template = temp.path().join("duplicate-recipient-key-info.xml"); + let plaintext = temp.path().join("plaintext.bin"); + let public_key = project_root().join("tests/fixtures/keys/rsa/rsa-2048-pubkey.pem"); + fs::write( + &template, + r#""#, + ) + .unwrap(); + fs::write(&plaintext, b"duplicate recipient key info").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("EncryptedKey contains more than one direct KeyInfo") + ); +} + +#[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_with_leading_zeroes(&matching, 1, 2) + ), + ) + .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_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 + // 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 + // 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 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 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 + // 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 decrypt_with_key_ring = Command::new(binary()) + .args(["decrypt", "--privkey-pem:a"]) + .arg(&private_a) + .arg("--privkey-pem:b") + .arg(&private_b) + .arg(&encrypted) + .output() + .unwrap(); + assert!( + decrypt_with_key_ring.status.success(), + "{}", + String::from_utf8_lossy(&decrypt_with_key_ring.stderr) + ); + assert_eq!(decrypt_with_key_ring.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")); + + let lax_encrypted = temp.path().join("lax-encrypted.xml"); + let lax_encrypt = Command::new(binary()) + .args(["encrypt", "--lax-key-search", "--pubkey-pem:ignored-a"]) + .arg(&public_a) + .arg("--pubkey-pem:ignored-b") + .arg(&public_b) + .arg("--binary-data") + .arg(&plaintext) + .arg("--output") + .arg(&lax_encrypted) + .arg(&template) + .output() + .unwrap(); + assert!( + lax_encrypt.status.success(), + "{}", + String::from_utf8_lossy(&lax_encrypt.stderr) + ); + 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(&lax_encrypted) + .output() + .unwrap(); + assert!( + decrypt.status.success(), + "lax recipient {name}: {}", + String::from_utf8_lossy(&decrypt.stderr) + ); + assert_eq!(decrypt.stdout, b"multiple RSA recipients"); + } + + let insufficient_output = temp.path().join("insufficient-lax.xml"); + let insufficient = Command::new(binary()) + .args(["encrypt", "--lax-key-search", "--pubkey-pem:ignored"]) + .arg(&public_a) + .arg("--binary-data") + .arg(&plaintext) + .arg("--output") + .arg(&insufficient_output) + .arg(&template) + .output() + .unwrap(); + assert!(!insufficient.status.success()); + assert!(!insufficient_output.exists()); +} + +#[test] +fn mixed_named_and_unnamed_rsa_recipients_keep_distinct_key_identities() { + // A mixed template must retain the unnamed slot through both encryption and + // decryption selection instead of collapsing all recipients to KeyName. + let temp = tempfile::tempdir().unwrap(); + let template = temp.path().join("mixed-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_b = project_root().join("tests/fixtures/keys/rsa/rsa-4096-key.pem"); + fs::write( + &template, + r#"a"#, + ) + .unwrap(); + fs::write(&plaintext, b"mixed recipient identities").unwrap(); + + let encrypt = Command::new(binary()) + .args(["encrypt", "--pubkey-pem:a"]) + .arg(&public_a) + .arg("--pubkey-pem") + .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 decrypt = Command::new(binary()) + .args(["decrypt", "--privkey-pem:a"]) + .arg(&private_b) + .arg("--privkey-pem") + .arg(&private_b) + .arg(&encrypted) + .output() + .unwrap(); + assert!( + decrypt.status.success(), + "{}", + String::from_utf8_lossy(&decrypt.stderr) + ); + assert_eq!(decrypt.stdout, b"mixed recipient identities"); + + let ambiguous = Command::new(binary()) + .args(["decrypt", "--privkey-pem"]) + .arg(&private_b) + .arg("--privkey-pem") + .arg(&private_b) + .arg(&encrypted) + .output() + .unwrap(); + assert!(!ambiguous.status.success()); + assert!(String::from_utf8_lossy(&ambiguous.stderr).contains("recipient identity")); +} + +#[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 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 + // 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 + // 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 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 + // 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"); + + 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] +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 encryption_rejects_templates_the_decryption_parser_cannot_consume() { + // Encryption preserves template metadata, so it must reject every structure + // that the reciprocal parser would reject instead of emitting dead output. + let temp = tempfile::tempdir().unwrap(); + let plaintext = temp.path().join("plaintext.bin"); + let aes_key = temp.path().join("aes-key.bin"); + let public_key = project_root().join("tests/fixtures/keys/rsa/rsa-4096-pubkey.pem"); + fs::write(&plaintext, b"template validation").unwrap(); + fs::write(&aes_key, b"0123456789abcdef").unwrap(); + + let oversized_id = "x".repeat(4 * 1024 + 1); + let cases = [ + ( + "recipient-child-order", + r#"recipient"#.to_owned(), + true, + ), + ( + "unsupported-recipient-method-child", + r#""#.to_owned(), + true, + ), + ( + "empty-content-key-name", + r#""#.to_owned(), + false, + ), + ( + "empty-recipient-key-name", + r#""#.to_owned(), + true, + ), + ( + "oversized-encrypted-data-id", + format!( + r#""# + ), + false, + ), + ( + "agreement-only-key-info", + r#""#.to_owned(), + false, + ), + ]; + + for (case, xml, uses_rsa_recipient) in cases { + let template = temp.path().join(format!("{case}.xml")); + let output_path = temp.path().join(format!("{case}-output.xml")); + fs::write(&template, xml).unwrap(); + let mut command = Command::new(binary()); + command.arg("encrypt"); + if uses_rsa_recipient { + command.arg("--pubkey-pem").arg(&public_key); + } else { + command.arg("--aes-key").arg(&aes_key); + } + let output = command + .arg("--binary-data") + .arg(&plaintext) + .arg("--output") + .arg(&output_path) + .arg(&template) + .output() + .unwrap(); + assert!(!output.status.success(), "{case} unexpectedly succeeded"); + assert!(!output_path.exists(), "{case} emitted partial output"); + } +} + +#[test] +fn rsa_oaep_label_split_by_comment_round_trips() { + // XML simple content spans every direct text node. Keeping only the first + // side of a comment makes encryption and decryption use different labels. + let temp = tempfile::tempdir().unwrap(); + let template = temp.path().join("split-label.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(&plaintext, b"comment-split OAEP label").unwrap(); + fs::write( + &template, + r#"YWJjZA=="#, + ) + .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 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"comment-split OAEP label"); +} + +#[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 rsa_oaep_accepts_the_xmldsig_more_sha384_digest_uri() { + // libxmlsec1 emits the XMLDSig-more spelling for SHA-384. Encryption and + // decryption must share one alias mapping for this interoperable template. + let temp = tempfile::tempdir().unwrap(); + let template = temp.path().join("sha384-alias.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"SHA-384 OAEP alias").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 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"SHA-384 OAEP alias"); +} + +#[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 + // 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_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 + // 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() { + 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 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(); + 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 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) + ); + + 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] +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 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 + // 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 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("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 + // 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) + ); +} + +#[test] +fn singleton_named_signing_key_reports_missing_template_key_name() { + // A registry-named signing key without a template lookup name is a distinct + // failure from ambiguity among multiple keys and must report that contract. + 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"); + fs::write(&template, signature_template_without_key_info()).unwrap(); + + let result = Command::new(binary()) + .args(["sign", "--privkey-pem:named"]) + .arg(&private_key) + .arg(&template) + .output() + .unwrap(); + assert!(!result.status.success()); + let stderr = String::from_utf8_lossy(&result.stderr); + assert!(stderr.contains("named private key requires a template KeyName")); + assert!(!stderr.contains("multiple private keys")); +} + +#[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", "-g: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"]); + 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 capability_queries_report_supported_and_unsupported_names() { + assert!( + Command::new(binary()) + .args(["check-transforms", "c14n", "rsa-sha256"]) + .status() + .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 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: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("multiple verification key inputs match template KeyNames") + ); +} + +#[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) + .arg(&foreign_template) + .output() + .unwrap(); + assert!(!foreign.status.success()); + assert!(String::from_utf8_lossy(&foreign.stderr).contains("no EncryptedData")); +} + +#[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") + .status() + .unwrap() + .code(), + Some(1) + ); + 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() + ); +} + +#[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!( + 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 new file mode 100644 index 0000000..c12453c --- /dev/null +++ b/tools/xmlsec1/tests/upstream_runner.rs @@ -0,0 +1,73 @@ +use std::{fs, path::Path, process::Command}; + +fn run_upstream(script: &str, selected_test: &str) -> String { + let tests = Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures/upstream"); + 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) + .arg(env!("CARGO_BIN_EXE_xmlsec1")) + .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!( + 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"); + 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 mut run_directories = fs::read_dir(run_root.path()) + .expect("the runner log root must be readable") + .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") +} + +#[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. + 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() { + let _ = run_upstream( + "testEnc.sh", + "xmlenc11-interop-2012/xenc11-example-AES128-GCM", + ); +} + +#[test] +fn unmodified_keys_runner_generates_aes_key_store() { + let _ = run_upstream("testKeys.sh", "test-aes128"); +}