diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 94f0b14..50396d6 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -20,7 +20,7 @@ jobs: - uses: actions/checkout@v6 - name: Install tooling - run: sudo apt-get update -qq && sudo apt-get install -y -qq libxml2-utils xsltproc + run: sudo apt-get update -qq && sudo apt-get install -y -qq libxml2-utils xsltproc xmlsec1 - uses: actions/setup-java@v5 with: @@ -223,6 +223,28 @@ jobs: fi echo "tamper correctly detected" + - name: XML signature - xmlsec1 CLI signs the skeleton template, cross-verifies with Java + run: | + set -e + SH=XML_Signature/cli/sign-verify-xmlsec1.sh + SKEL=FundsXML_Files/4.2.9/signed/Signed_Fund_Skeleton.xml + M="./mvnw -q -B -pl XML_Signature/java exec:java" + # sign the committed template in place (keys from the GenerateTestKey step above) + $SH sign "$SKEL" xs-signed.xml + xmllint --noout --nonet --schema ci-schemas/4.2.9/FundsXML.xsd xs-signed.xml + $SH verify xs-signed.xml + # Java verifies the xmlsec1 output: pinned cert AND embedded certificate + $M -Dexec.mainClass=VerifyFundsXml \ + -Dexec.args="xs-signed.xml XML_Signature/keys/test-signing-cert.pem" + $M -Dexec.mainClass=VerifyFundsXml -Dexec.args="xs-signed.xml" + # xmlsec1 verifies the Java-signed file from the previous step + $SH verify signed.xml + sed 's/125000000125000001 xs-tampered.xml + if $SH verify xs-tampered.xml; then + echo "::error::xmlsec1-signed tampered file unexpectedly verified"; exit 1 + fi + echo "xmlsec1: sign/verify/tamper ok, cross-verified with Java" + - name: DB integration - multi-fund import+export in 4 languages run: | set -e diff --git a/FundsXML_Files/4.2.9/signed/README.md b/FundsXML_Files/4.2.9/signed/README.md index 2d1ac25..e0f9e85 100644 --- a/FundsXML_Files/4.2.9/signed/README.md +++ b/FundsXML_Files/4.2.9/signed/README.md @@ -20,11 +20,15 @@ local `FundsXML.xsd` in any complete copy of the release). > ⚠️ **Placeholder:** `DigestValue` and `SignatureValue` are schema-valid base64 > strings but **not cryptographically verifiable**. Real signing and > verification (Apache Santuario / .NET `SignedXml` / `xmlsec1` / Python -> `signxml`) lives in [`XML_Signature/`](../../../XML_Signature/README.md); -> note that this template uses inclusive C14N and a `ds:KeyName`, whereas the -> Java/.NET examples sign with exclusive C14N and embed the X.509 certificate. - -Algorithms used (enveloped signature): C14N 2001-03-15, RSA-SHA256, SHA-256. +> `signxml`) lives in [`XML_Signature/`](../../../XML_Signature/README.md). +> The `ds:Signature` here doubles as the **signing template** for the +> `xmlsec1` CLI example: it already carries the shared profile, so once +> signed in place it cross-verifies with the Java and .NET verifiers. + +Algorithms (enveloped signature, same profile as every `XML_Signature/` +stack): exclusive C14N (`xml-exc-c14n#`) for both canonicalization and the +second transform, RSA-SHA256, SHA-256 digest, signer certificate embedded as +`ds:X509Certificate` (empty placeholder until signed). ## Validation diff --git a/FundsXML_Files/4.2.9/signed/Signed_Fund_Skeleton.xml b/FundsXML_Files/4.2.9/signed/Signed_Fund_Skeleton.xml index 6f74a97..777d7d5 100644 --- a/FundsXML_Files/4.2.9/signed/Signed_Fund_Skeleton.xml +++ b/FundsXML_Files/4.2.9/signed/Signed_Fund_Skeleton.xml @@ -2,9 +2,13 @@ - + + UExBQ0VIT0xERVItRElHRVNU @@ -60,6 +65,10 @@ UExBQ0VIT0xERVItU0lHTkFUVVJFLVZBTFVFLU5PVC1DUllQVE9HUkFQSElDQUxMWS1WQUxJRA== erste-am-funds-signing-2025 + + + + diff --git a/XML_Signature/README.md b/XML_Signature/README.md index 3993a08..bbef910 100644 --- a/XML_Signature/README.md +++ b/XML_Signature/README.md @@ -35,7 +35,7 @@ Windows too) to write a throwaway self-signed RSA-2048 keystore | Stack | Entry point | Status | |-------|-------------|--------| | Java — Apache Santuario | [`java/SignFundsXml.java`](java/SignFundsXml.java) / [`java/VerifyFundsXml.java`](java/VerifyFundsXml.java) | ✅ verified (sign, verify, tamper-detect) | -| CLI — `xmlsec1` | [`cli/sign-verify-xmlsec1.sh`](cli/sign-verify-xmlsec1.sh) | reference (needs `xmlsec1`) | +| CLI — `xmlsec1` | [`cli/sign-verify-xmlsec1.sh`](cli/sign-verify-xmlsec1.sh) | ✅ verified (xmlsec1 1.2.33, in CI): sign, verify, tamper; cross-verifies with Java and .NET both ways | | Python — `signxml` | [`python/sign_verify_signxml.py`](python/sign_verify_signxml.py) | reference (`pip install -e ".[signature]"` adds `signxml`) | | .NET — `SignedXml` | [`dotnet/SignVerify.cs`](dotnet/SignVerify.cs) | verified (.NET SDK 8): sign, verify, tamper; cross-verifies with Java both ways | @@ -65,13 +65,14 @@ flipping one digit in a signed file → `INVALID`). Santuario verification runs with **secure validation** enabled. > **Note on `xmlsec1`:** it signs an *existing* `ds:Signature` template, so it -> pairs naturally with the committed signed skeleton; the Java/.NET/Python -> examples instead build and append the `ds:Signature` themselves. Because -> the skeleton's template uses **inclusive** C14N (`REC-xml-c14n-20010315`), -> only the enveloped transform and a `ds:KeyName` in `KeyInfo`, the xmlsec1 -> output does **not** follow the exclusive-C14N / embedded-X509 profile above: -> Java and .NET can still verify it against the pinned certificate, but not -> from the embedded `KeyInfo` (which carries no key). +> pairs naturally with the committed signed skeleton +> (`FundsXML_Files/4.2.9/signed/Signed_Fund_Skeleton.xml`); the +> Java/.NET/Python examples instead build and append the `ds:Signature` +> themselves. The skeleton's template carries exactly the profile above +> (exclusive C14N, enveloped + exc-C14N transforms, RSA-SHA256, empty +> `X509Certificate` placeholder that xmlsec1 fills), so the signed result +> verifies in Java and .NET both pinned and from the embedded certificate, +> and xmlsec1 verifies the Java/.NET output. A real signed file is **not committed** — the signature is bound to the throwaway key, which is regenerated per run. CI signs → verifies as a roundtrip. diff --git a/XML_Signature/cli/sign-verify-xmlsec1.sh b/XML_Signature/cli/sign-verify-xmlsec1.sh index 9891c6d..4c28b04 100755 --- a/XML_Signature/cli/sign-verify-xmlsec1.sh +++ b/XML_Signature/cli/sign-verify-xmlsec1.sh @@ -5,20 +5,27 @@ # sign-verify-xmlsec1.sh sign # sign-verify-xmlsec1.sh verify [cert.pem] # -# Reference example — xmlsec1 is a native CLI tool (not bundled): +# xmlsec1 is a native CLI tool (not bundled): # Debian/Ubuntu: sudo apt-get install xmlsec1 # macOS: brew install xmlsec1 -# (The verified, fully cross-platform signing path is the Java example, -# XML_Signature/java — run via the Maven Wrapper.) +# Verified (xmlsec1 1.2.33, also in CI): sign -> verify, tamper detected, and +# the output cross-verifies with the Java (Santuario) and .NET (SignedXml) +# verifiers, both pinned and from the embedded certificate; xmlsec1 in turn +# verifies files signed by Java and .NET. # # Keys: generated cross-platform by the Java GenerateTestKey (no openssl): # ./mvnw -q -pl XML_Signature/java compile exec:java -Dexec.mainClass=GenerateTestKey # It writes test-signing.p12, test-signing-cert.pem AND test-signing-key.pem # (PKCS#8) into XML_Signature/keys/ — the last two are what xmlsec1 needs. # -# Unlike the Java/Python examples, xmlsec1 signs an EXISTING -# template in the document, so it pairs naturally with the committed -# FundsXML_Files/4.2.9/signed/Signed_Fund_Skeleton.xml (placeholder signature). +# Unlike the Java/.NET/Python examples, xmlsec1 signs an EXISTING +# template in the document. The committed +# FundsXML_Files/4.2.9/signed/Signed_Fund_Skeleton.xml is exactly that +# template, already in the shared profile: SignedInfo with exclusive C14N, +# enveloped + exclusive-C14N transforms, RSA-SHA256 / SHA-256, and an empty +# that xmlsec1 fills with the signer cert (that is why +# --privkey-pem gets "key,cert"). To sign any other FundsXML file, append that +# ds:Signature block as the last child of first. # # POSIX sh; the Windows counterpart is sign-verify.ps1 in this directory. diff --git a/XML_Signature/java/VerifyFundsXml.java b/XML_Signature/java/VerifyFundsXml.java index e782095..0916d15 100644 --- a/XML_Signature/java/VerifyFundsXml.java +++ b/XML_Signature/java/VerifyFundsXml.java @@ -49,9 +49,15 @@ public static void main(String[] args) throws Exception { // xmlsec 4.x: secure validation is a constructor argument (blocks // RetrievalMethod loops, dangerous transforms, weak algorithms, etc.). + // Everything from here on is "INVALID, exit 1" territory when Santuario + // throws: a malformed SignatureValue, an empty or unparsable + // X509Certificate (the committed skeleton is an unsigned template with + // exactly those placeholders), a broken transform chain, and so on. + // They are properties of the document under test, not setup errors. + boolean ok; + try { XMLSignature signature = new XMLSignature(sigEl, "", true); - boolean ok; if (args.length >= 2) { try (FileInputStream cf = new FileInputStream(args[1])) { X509Certificate pinned = (X509Certificate) CertificateFactory @@ -80,6 +86,11 @@ public static void main(String[] args) throws Exception { + (embedded != null ? " (cert: " + embedded.getSubjectX500Principal() + ")" : "")); } + } catch (org.apache.xml.security.exceptions.XMLSecurityException e) { + System.out.println("INVALID: " + e.getMessage()); + System.exit(1); + return; + } System.out.println(ok ? "VALID: signature OK" : "INVALID: signature check failed"); System.exit(ok ? 0 : 1);