From 020961ed6f9ae9d4d2df80addf07f1b7da372a60 Mon Sep 17 00:00:00 2001 From: Karl Kauc Date: Fri, 15 May 2026 18:58:40 +0200 Subject: [PATCH] Phase 3: XML signature (Apache Santuario sign/verify) + references XML_Signature/: enveloped XML-DSig, RSA-SHA256 / exclusive-C14N, ds:Signature appended as last child of (schema-allowed). Java/Apache Santuario (xmlsec 4.0.4) sign + verify verified locally incl. tamper detection and that the signed file stays XSD-valid; secure validation enabled on verify. generate-test-key.sh makes a throwaway RSA keypair (keys/ gitignored). xmlsec1 CLI, Python signxml and .NET SignedXml provided as references with the same profile so files cross-verify. tools/fetch-tools.sh also fetches Santuario (xmlsec + commons-codec + slf4j). CI gains a sign->verify->tamper roundtrip step (verified locally). Top-level index row -> done. Co-Authored-By: Claude Opus 4.7 (1M context) --- .github/workflows/ci.yml | 20 +++++ .gitignore | 4 + README.md | 2 +- XML_Signature/README.md | 65 +++++++++++++++ XML_Signature/cli/sign-verify-xmlsec1.sh | 41 ++++++++++ XML_Signature/dotnet/SignVerify.cs | 91 +++++++++++++++++++++ XML_Signature/dotnet/SignVerify.csproj | 15 ++++ XML_Signature/generate-test-key.sh | 30 +++++++ XML_Signature/java/SignFundsXml.java | 90 ++++++++++++++++++++ XML_Signature/java/VerifyFundsXml.java | 75 +++++++++++++++++ XML_Signature/python/sign_verify_signxml.py | 72 ++++++++++++++++ tools/fetch-tools.sh | 9 ++ 12 files changed, 513 insertions(+), 1 deletion(-) create mode 100644 XML_Signature/README.md create mode 100644 XML_Signature/cli/sign-verify-xmlsec1.sh create mode 100644 XML_Signature/dotnet/SignVerify.cs create mode 100644 XML_Signature/dotnet/SignVerify.csproj create mode 100755 XML_Signature/generate-test-key.sh create mode 100644 XML_Signature/java/SignFundsXml.java create mode 100644 XML_Signature/java/VerifyFundsXml.java create mode 100644 XML_Signature/python/sign_verify_signxml.py diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a4b087b..ee3a575 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -100,6 +100,26 @@ jobs: done test "$(xmllint --xpath 'count(//Holding)' out_top.xml)" = "5" + - name: XML signature - Apache Santuario sign/verify/tamper roundtrip + run: | + set -e + XML_Signature/generate-test-key.sh + CP=.lib/xmlsec-4.0.4.jar:.lib/commons-codec-1.18.0.jar:.lib/slf4j-api-2.0.17.jar:.lib/slf4j-nop-2.0.17.jar + javac -cp "$CP" -d /tmp/sig \ + XML_Signature/java/SignFundsXml.java XML_Signature/java/VerifyFundsXml.java + SRC=FundsXML_Files/4.2.9/positions/Mixed-Fund_Positions.xml + java -cp "$CP:/tmp/sig" SignFundsXml "$SRC" signed.xml \ + XML_Signature/keys/test-signing.p12 changeit fundsxml + java -cp "$CP:/tmp/sig" VerifyFundsXml signed.xml \ + XML_Signature/keys/test-signing-cert.pem + xmllint --noout --nonet --schema .schema-cache/4.2.9/FundsXML.xsd signed.xml + sed 's/8.339.33 tampered.xml + if java -cp "$CP:/tmp/sig" VerifyFundsXml tampered.xml \ + XML_Signature/keys/test-signing-cert.pem; then + echo "::error::tampered file unexpectedly verified"; exit 1 + fi + echo "tamper correctly detected" + - name: Regression - legacy XSLT 1.0 report still runs run: | xsltproc XSLT_DataQuality_Checks/Enhanced_Check/FundsXML_CompleteDQReport_HTML.xsl \ diff --git a/.gitignore b/.gitignore index e863448..d83ce56 100644 --- a/.gitignore +++ b/.gitignore @@ -8,6 +8,10 @@ CLAUDE.md # dependencies from Maven Central, not repo source. .lib/ +# Throwaway signing keys (XML_Signature/generate-test-key.sh) — never commit +# private keys, even demo ones. +XML_Signature/keys/ + # Generated reports / outputs *.report.html report.html diff --git a/README.md b/README.md index ecb15bb..e726056 100644 --- a/README.md +++ b/README.md @@ -36,7 +36,7 @@ locations. Items marked _(planned)_ are on the roadmap (see | Schema fetch (proxy-aware) | Bash | [tools/fetch-schema.sh](./tools/fetch-schema.sh) | ✅ | | CI (validate all samples) | GitHub Actions | [.github/workflows/ci.yml](./.github/workflows/) | ✅ | | XQuery analytics (aggregation, top-holdings, look-through) | Saxon CLI/Java, Python, BaseX | [XQuery_Examples/](./XQuery_Examples/) | ✅ | -| XML signature sign/verify | Apache Santuario, .NET, xmlsec1, signxml | `XML_Signature/` | _(planned)_ | +| XML signature sign/verify | Apache Santuario (Java), .NET, xmlsec1, signxml | [XML_Signature/](./XML_Signature/) | ✅ | | Database load ↔ generate | Oracle/SQL Server/Postgres (code only) | `Database_Integration/` | _(planned)_ | | Large-file/stream processing | StAX/SAX/lxml iterparse | `Large_File_Processing/` | _(planned)_ | diff --git a/XML_Signature/README.md b/XML_Signature/README.md new file mode 100644 index 0000000..ba00ae3 --- /dev/null +++ b/XML_Signature/README.md @@ -0,0 +1,65 @@ +# XML Signature (XML-DSig) + +![status](https://img.shields.io/badge/Java%20(Santuario)-verified-brightgreen) ![profile](https://img.shields.io/badge/RSA--SHA256%20%2F%20exc--C14N%20%2F%20enveloped-blue) + +Sign and verify FundsXML with **enveloped XML Digital Signatures**. All stacks +use the same profile so signed files **cross-verify** between them: + +| Property | Value | +|----------|-------| +| Signature method | RSA-SHA256 | +| Digest | SHA-256 | +| Canonicalization | Exclusive C14N (`xml-exc-c14n#`) | +| Transform | enveloped-signature + exclusive C14N | +| Reference | `URI=""` (whole document) | +| KeyInfo | signer X.509 certificate embedded | +| Placement | `ds:Signature` is the **last child of ``** — exactly where the 4.2.9 schema allows it (`xmldsig-core-schema.xsd` import) | + +A signed file **stays XSD-valid** (verified) and matches the structure of the +committed [`FundsXML_Files/4.2.9/signed/Signed_Fund_Skeleton.xml`](../FundsXML_Files/4.2.9/signed/) +placeholder. + +## Keys + +```bash +XML_Signature/generate-test-key.sh # -> XML_Signature/keys/ (gitignored) +``` +Throwaway self-signed RSA-2048 (`test-signing.p12` alias `fundsxml`, pass +`changeit`; plus PEM key/cert). **Demo only — never commit private keys.** + +## Stacks + +| 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`) | +| Python — `signxml` | [`python/sign_verify_signxml.py`](python/sign_verify_signxml.py) | reference (`pip install signxml`) | +| .NET — `SignedXml` | [`dotnet/SignVerify.cs`](dotnet/SignVerify.cs) | reference (needs .NET SDK) | + +## Run (Java / Apache Santuario — verified) + +```bash +tools/fetch-tools.sh +XML_Signature/generate-test-key.sh +CP=.lib/xmlsec-4.0.4.jar:.lib/commons-codec-1.18.0.jar:.lib/slf4j-api-2.0.17.jar:.lib/slf4j-nop-2.0.17.jar +javac -cp "$CP" -d /tmp/sig XML_Signature/java/SignFundsXml.java XML_Signature/java/VerifyFundsXml.java + +# sign +java -cp "$CP:/tmp/sig" SignFundsXml \ + FundsXML_Files/4.2.9/positions/Mixed-Fund_Positions.xml signed.xml \ + XML_Signature/keys/test-signing.p12 changeit fundsxml + +# verify — pin the signer cert (don't trust only the embedded key) +java -cp "$CP:/tmp/sig" VerifyFundsXml signed.xml XML_Signature/keys/test-signing-cert.pem +``` + +`VerifyFundsXml` exits 0 on a valid signature, 1 on tamper/failure (verified: +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. + +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 new file mode 100644 index 0000000..cc0a9f8 --- /dev/null +++ b/XML_Signature/cli/sign-verify-xmlsec1.sh @@ -0,0 +1,41 @@ +#!/usr/bin/env bash +# Enveloped XML-DSig sign / verify on the command line with xmlsec1 +# (the xmlsec C library CLI, package: libxmlsec1 / xmlsec1). +# +# sign-verify-xmlsec1.sh sign +# sign-verify-xmlsec1.sh verify [cert.pem] +# +# Reference implementation (xmlsec1 not installed in the dev environment: +# Debian/Ubuntu: sudo apt-get install xmlsec1 +# macOS: brew install xmlsec1 +# +# 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 (which already carries a +# placeholder ds:Signature). Keys: XML_Signature/generate-test-key.sh. +set -euo pipefail + +MODE="${1:?usage: sign-verify-xmlsec1.sh sign|verify ...}" +KEYS="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)/keys" + +case "$MODE" in + sign) + IN="${2:?in.xml}"; OUT="${3:?out.xml}" + # The template's ds:Signature/SignedInfo must already exist (it does in the + # signed skeleton). xmlsec1 fills DigestValue + SignatureValue + KeyInfo. + xmlsec1 --sign \ + --privkey-pem "${KEYS}/test-signing-key.pem,${KEYS}/test-signing-cert.pem" \ + --output "$OUT" "$IN" + echo "signed -> $OUT" + ;; + verify) + SIGNED="${2:?signed.xml}" + CERT="${3:-${KEYS}/test-signing-cert.pem}" + # --trusted-pem pins the signer cert (do NOT trust only the embedded key). + xmlsec1 --verify --trusted-pem "$CERT" "$SIGNED" \ + && echo "VALID: signature OK" \ + || { echo "INVALID: signature check failed"; exit 1; } + ;; + *) + echo "unknown mode: $MODE" >&2; exit 2 ;; +esac diff --git a/XML_Signature/dotnet/SignVerify.cs b/XML_Signature/dotnet/SignVerify.cs new file mode 100644 index 0000000..fe42201 --- /dev/null +++ b/XML_Signature/dotnet/SignVerify.cs @@ -0,0 +1,91 @@ +// Enveloped XML-DSig sign / verify in .NET via System.Security.Cryptography.Xml. +// +// dotnet run --project XML_Signature/dotnet -- sign in.xml out.xml +// dotnet run --project XML_Signature/dotnet -- verify signed.xml [cert.pem] +// Exit: 0 ok, 1 invalid, 2 setup error. +// +// Reference implementation — not executed in the dev environment (no .NET SDK). +// Same profile as the Apache Santuario (Java) example: RSA-SHA256, exclusive +// C14N, enveloped, signer cert embedded in KeyInfo, so files cross-verify. +// Keys: XML_Signature/generate-test-key.sh (PKCS#12 test-signing.p12). + +using System; +using System.IO; +using System.Security.Cryptography.X509Certificates; +using System.Security.Cryptography.Xml; +using System.Xml; + +internal static class SignVerify +{ + private static int Main(string[] args) + { + if (args.Length < 2) { Console.Error.WriteLine("usage: sign|verify ..."); return 2; } + string keysDir = Path.Combine(AppContext.BaseDirectory, + "..", "..", "..", "..", "keys"); + + var doc = new XmlDocument { PreserveWhitespace = true }; + // XXE-hardened load. + using (var r = XmlReader.Create(args[1], + new XmlReaderSettings { DtdProcessing = DtdProcessing.Prohibit, + XmlResolver = null })) + doc.Load(r); + + if (args[0] == "sign") + { + var cert = new X509Certificate2( + Path.Combine(keysDir, "test-signing.p12"), "changeit", + X509KeyStorageFlags.Exportable); + var rsa = cert.GetRSAPrivateKey(); + + var signedXml = new SignedXml(doc) { SigningKey = rsa }; + signedXml.SignedInfo.CanonicalizationMethod = + SignedXml.XmlDsigExcC14NTransformUrl; + signedXml.SignedInfo.SignatureMethod = + "http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"; + + var reference = new Reference(""); + reference.DigestMethod = "http://www.w3.org/2001/04/xmlenc#sha256"; + reference.AddTransform(new XmlDsigEnvelopedSignatureTransform()); + reference.AddTransform(new XmlDsigExcC14NTransform()); + signedXml.AddReference(reference); + + var ki = new KeyInfo(); + ki.AddClause(new KeyInfoX509Data(cert)); + signedXml.KeyInfo = ki; + + signedXml.ComputeSignature(); + // ds:Signature must be the LAST child of . + doc.DocumentElement!.AppendChild( + doc.ImportNode(signedXml.GetXml(), true)); + doc.Save(args[2]); + Console.WriteLine($"signed -> {args[2]}"); + return 0; + } + + if (args[0] == "verify") + { + var signedXml = new SignedXml(doc); + var sig = (XmlElement)doc.GetElementsByTagName( + "Signature", SignedXml.XmlDsigNamespaceUrl)[0]!; + signedXml.LoadXml(sig); + + bool ok; + if (args.Length >= 3) + { + var pinned = new X509Certificate2(args[2]); + ok = signedXml.CheckSignature(pinned, true); + Console.WriteLine($"pinned cert: {pinned.Subject}"); + } + else + { + ok = signedXml.CheckSignature(); // trusts embedded key (demo) + } + Console.WriteLine(ok ? "VALID: signature OK" + : "INVALID: signature check failed"); + return ok ? 0 : 1; + } + + Console.Error.WriteLine($"unknown mode {args[0]}"); + return 2; + } +} diff --git a/XML_Signature/dotnet/SignVerify.csproj b/XML_Signature/dotnet/SignVerify.csproj new file mode 100644 index 0000000..f23f5af --- /dev/null +++ b/XML_Signature/dotnet/SignVerify.csproj @@ -0,0 +1,15 @@ + + + + Exe + net8.0 + enable + SignVerify + FundsXml.XmlSignature + + + + + diff --git a/XML_Signature/generate-test-key.sh b/XML_Signature/generate-test-key.sh new file mode 100755 index 0000000..77dd220 --- /dev/null +++ b/XML_Signature/generate-test-key.sh @@ -0,0 +1,30 @@ +#!/usr/bin/env bash +# generate-test-key.sh — create a THROWAWAY self-signed RSA key + cert for the +# XML-signature examples. +# +# Output (into XML_Signature/keys/, gitignored — never commit private keys): +# test-signing.p12 PKCS#12 keystore (alias: fundsxml, pass: changeit) +# test-signing-cert.pem public certificate (for verification / xmlsec1) +# test-signing-key.pem private key in PEM (for xmlsec1 / signxml) +# +# FOR DEMO USE ONLY — 2048-bit, 10-year self-signed, hard-coded password. +set -euo pipefail + +DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/keys" +mkdir -p "$DIR" +PASS="changeit" + +openssl req -x509 -newkey rsa:2048 -sha256 -days 3650 -nodes \ + -keyout "$DIR/test-signing-key.pem" \ + -out "$DIR/test-signing-cert.pem" \ + -subj "/C=AT/O=FundsXML Examples/OU=Demo/CN=fundsxml-test-signer" 2>/dev/null + +openssl pkcs12 -export \ + -inkey "$DIR/test-signing-key.pem" \ + -in "$DIR/test-signing-cert.pem" \ + -name fundsxml \ + -out "$DIR/test-signing.p12" \ + -passout "pass:${PASS}" + +echo "wrote: $DIR/test-signing.p12 (alias=fundsxml pass=${PASS})" +echo " $DIR/test-signing-cert.pem $DIR/test-signing-key.pem" diff --git a/XML_Signature/java/SignFundsXml.java b/XML_Signature/java/SignFundsXml.java new file mode 100644 index 0000000..4196cf3 --- /dev/null +++ b/XML_Signature/java/SignFundsXml.java @@ -0,0 +1,90 @@ +// SignFundsXml — enveloped XML-DSig signing with Apache Santuario (xmlsec 4.x). +// +// tools/fetch-tools.sh +// XML_Signature/generate-test-key.sh +// CP=.lib/xmlsec-4.0.4.jar:.lib/commons-codec-1.18.0.jar:\ +// .lib/slf4j-api-2.0.17.jar:.lib/slf4j-nop-2.0.17.jar +// javac -cp "$CP" -d /tmp/sig XML_Signature/java/SignFundsXml.java +// java -cp "$CP:/tmp/sig" SignFundsXml \ +// XML_Signature/keys/test-signing.p12 changeit fundsxml +// +// Produces an enveloped signature whose ds:Signature is appended as the last +// child of — exactly where the FundsXML 4.2.9 schema allows it +// (xmldsig-core-schema.xsd import). The signer certificate is embedded in +// KeyInfo/X509Data so the signed file is self-verifiable. +// +// Security: DocumentBuilderFactory is namespace-aware and XXE-hardened. + +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.security.Key; +import java.security.KeyStore; +import java.security.PrivateKey; +import java.security.cert.X509Certificate; +import javax.xml.parsers.DocumentBuilderFactory; +import javax.xml.transform.Transformer; +import javax.xml.transform.TransformerFactory; +import javax.xml.transform.dom.DOMSource; +import javax.xml.transform.stream.StreamResult; +import org.apache.xml.security.Init; +import org.apache.xml.security.algorithms.MessageDigestAlgorithm; +import org.apache.xml.security.c14n.Canonicalizer; +import org.apache.xml.security.signature.XMLSignature; +import org.apache.xml.security.transforms.Transforms; +import org.w3c.dom.Document; +import org.w3c.dom.Element; + +public class SignFundsXml { + public static void main(String[] args) throws Exception { + if (args.length != 5) { + System.err.println("usage: SignFundsXml " + + " "); + System.exit(2); + } + String in = args[0], out = args[1], ks = args[2], + pass = args[3], alias = args[4]; + + Init.init(); + + DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance(); + dbf.setNamespaceAware(true); + dbf.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true); + dbf.setFeature("http://xml.org/sax/features/external-general-entities", false); + dbf.setFeature("http://xml.org/sax/features/external-parameter-entities", false); + Document doc; + try (FileInputStream fis = new FileInputStream(in)) { + doc = dbf.newDocumentBuilder().parse(fis); + } + + KeyStore keyStore = KeyStore.getInstance("PKCS12"); + try (FileInputStream kfis = new FileInputStream(ks)) { + keyStore.load(kfis, pass.toCharArray()); + } + Key key = keyStore.getKey(alias, pass.toCharArray()); + X509Certificate cert = (X509Certificate) keyStore.getCertificate(alias); + + Element root = doc.getDocumentElement(); + XMLSignature sig = new XMLSignature(doc, "", + XMLSignature.ALGO_ID_SIGNATURE_RSA_SHA256, + Canonicalizer.ALGO_ID_C14N_EXCL_OMIT_COMMENTS); + + // ds:Signature is the LAST allowed child of . + root.appendChild(sig.getElement()); + + Transforms tr = new Transforms(doc); + tr.addTransform(Transforms.TRANSFORM_ENVELOPED_SIGNATURE); + tr.addTransform(Canonicalizer.ALGO_ID_C14N_EXCL_OMIT_COMMENTS); + sig.addDocument("", tr, MessageDigestAlgorithm.ALGO_ID_DIGEST_SHA256); + + sig.addKeyInfo(cert); + sig.addKeyInfo(cert.getPublicKey()); + sig.sign((PrivateKey) key); + + Transformer t = TransformerFactory.newInstance().newTransformer(); + try (FileOutputStream fos = new FileOutputStream(out)) { + t.transform(new DOMSource(doc), new StreamResult(fos)); + } + System.out.println("signed -> " + out + + " (RSA-SHA256, exclusive C14N, enveloped)"); + } +} diff --git a/XML_Signature/java/VerifyFundsXml.java b/XML_Signature/java/VerifyFundsXml.java new file mode 100644 index 0000000..0141a83 --- /dev/null +++ b/XML_Signature/java/VerifyFundsXml.java @@ -0,0 +1,75 @@ +// VerifyFundsXml — verify an enveloped XML-DSig signature with Apache Santuario. +// +// java -cp "$CP:/tmp/sig" VerifyFundsXml [cert.pem] +// +// With no cert argument the certificate embedded in KeyInfo/X509Data is used +// (self-verifiable file). Pass a PEM cert to pin verification to a known key +// (recommended in production — never trust the key shipped inside the document +// alone). Exit: 0 = signature valid, 1 = invalid, 2 = setup error. +// +// Security: namespace-aware, XXE-hardened parser; "secure validation" mode on. + +import java.io.FileInputStream; +import java.security.PublicKey; +import java.security.cert.CertificateFactory; +import java.security.cert.X509Certificate; +import javax.xml.parsers.DocumentBuilderFactory; +import org.apache.xml.security.Init; +import org.apache.xml.security.signature.XMLSignature; +import org.apache.xml.security.utils.Constants; +import org.w3c.dom.Document; +import org.w3c.dom.Element; + +public class VerifyFundsXml { + public static void main(String[] args) throws Exception { + if (args.length < 1) { + System.err.println("usage: VerifyFundsXml [cert.pem]"); + System.exit(2); + } + Init.init(); + + DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance(); + dbf.setNamespaceAware(true); + dbf.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true); + dbf.setFeature("http://xml.org/sax/features/external-general-entities", false); + dbf.setFeature("http://xml.org/sax/features/external-parameter-entities", false); + Document doc; + try (FileInputStream fis = new FileInputStream(args[0])) { + doc = dbf.newDocumentBuilder().parse(fis); + } + + Element sigEl = (Element) doc.getElementsByTagNameNS( + Constants.SignatureSpecNS, "Signature").item(0); + if (sigEl == null) { + System.err.println("INVALID: no ds:Signature element found"); + System.exit(1); + } + + // xmlsec 4.x: secure validation is a constructor argument (blocks + // RetrievalMethod loops, dangerous transforms, weak algorithms, etc.). + XMLSignature signature = new XMLSignature(sigEl, "", true); + + boolean ok; + if (args.length >= 2) { + try (FileInputStream cf = new FileInputStream(args[1])) { + X509Certificate pinned = (X509Certificate) CertificateFactory + .getInstance("X.509").generateCertificate(cf); + ok = signature.checkSignatureValue(pinned.getPublicKey()); + System.out.println("verifying against pinned cert: " + + pinned.getSubjectX500Principal()); + } + } else { + X509Certificate embedded = + signature.getKeyInfo().getX509Certificate(); + PublicKey pk = embedded != null ? embedded.getPublicKey() + : signature.getKeyInfo().getPublicKey(); + ok = signature.checkSignatureValue(pk); + System.out.println("verifying against KeyInfo-embedded key" + + (embedded != null ? " (cert: " + + embedded.getSubjectX500Principal() + ")" : "")); + } + + System.out.println(ok ? "VALID: signature OK" : "INVALID: signature check failed"); + System.exit(ok ? 0 : 1); + } +} diff --git a/XML_Signature/python/sign_verify_signxml.py b/XML_Signature/python/sign_verify_signxml.py new file mode 100644 index 0000000..7c064f4 --- /dev/null +++ b/XML_Signature/python/sign_verify_signxml.py @@ -0,0 +1,72 @@ +#!/usr/bin/env python3 +"""Enveloped XML-DSig sign / verify in Python via `signxml`. + +Usage: + python sign_verify_signxml.py sign + python sign_verify_signxml.py verify [cert.pem] + +Exit: 0 ok, 1 invalid signature, 2 setup error. + +Reference implementation (signxml not installed in the dev environment): + pip install signxml +Keys come from XML_Signature/generate-test-key.sh (key/cert PEM in +XML_Signature/keys/). RSA-SHA256, exclusive C14N, enveloped — same profile as +the Apache Santuario (Java) example, so files cross-verify between stacks. +""" +import sys +from pathlib import Path + +KEYS = Path(__file__).resolve().parents[1] / "keys" + + +def main() -> int: + if len(sys.argv) < 3: + print(__doc__, file=sys.stderr) + return 2 + mode = sys.argv[1] + + try: + from lxml import etree + from signxml import XMLSigner, XMLVerifier, methods + except ImportError: + print("signxml not installed. Run: pip install signxml", + file=sys.stderr) + return 2 + + if mode == "sign": + src, out = sys.argv[2], sys.argv[3] + data = etree.parse(src).getroot() + key = (KEYS / "test-signing-key.pem").read_bytes() + cert = (KEYS / "test-signing-cert.pem").read_bytes() + signed = XMLSigner( + method=methods.enveloped, + signature_algorithm="rsa-sha256", + digest_algorithm="sha256", + c14n_algorithm="http://www.w3.org/2001/10/xml-exc-c14n#", + ).sign(data, key=key, cert=cert) + Path(out).write_bytes(etree.tostring(signed)) + print(f"signed -> {out}") + return 0 + + if mode == "verify": + signed = etree.parse(sys.argv[2]).getroot() + try: + if len(sys.argv) >= 4: + XMLVerifier().verify( + signed, x509_cert=Path(sys.argv[3]).read_text()) + else: + # Trust the embedded cert ONLY for the demo; in production pin + # x509_cert / ca_pem_file to a known signer. + XMLVerifier().verify(signed) + print("VALID: signature OK") + return 0 + except Exception as e: # signxml raises on any failure + print(f"INVALID: {e}") + return 1 + + print(f"unknown mode {mode!r}", file=sys.stderr) + return 2 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/tools/fetch-tools.sh b/tools/fetch-tools.sh index 9eaa120..2e589b9 100755 --- a/tools/fetch-tools.sh +++ b/tools/fetch-tools.sh @@ -44,5 +44,14 @@ fetch "${M2}/commons-cli/commons-cli/1.5.0/commons-cli-1.5.0.jar" "${LIB}/common fetch "${M2}/org/slf4j/slf4j-api/1.7.32/slf4j-api-1.7.32.jar" "${LIB}/slf4j-api-1.7.32.jar" fetch "${M2}/org/slf4j/slf4j-nop/1.7.32/slf4j-nop-1.7.32.jar" "${LIB}/slf4j-nop-1.7.32.jar" +# --- Apache Santuario (XML Signature, Phase 3) ------------------------------ +# xmlsec 4.0.4 runtime needs commons-codec; slf4j-api for logging (+ a no-op +# binding to silence "no SLF4J providers" on the console). +fetch "${M2}/org/apache/santuario/xmlsec/4.0.4/xmlsec-4.0.4.jar" "${LIB}/xmlsec-4.0.4.jar" +fetch "${M2}/commons-codec/commons-codec/1.18.0/commons-codec-1.18.0.jar" "${LIB}/commons-codec-1.18.0.jar" +fetch "${M2}/org/slf4j/slf4j-api/2.0.17/slf4j-api-2.0.17.jar" "${LIB}/slf4j-api-2.0.17.jar" +fetch "${M2}/org/slf4j/slf4j-nop/2.0.17/slf4j-nop-2.0.17.jar" "${LIB}/slf4j-nop-2.0.17.jar" + echo "SAXON_CP=${LIB}/Saxon-HE-12.5.jar:${LIB}/xmlresolver-5.2.2.jar:${LIB}/xmlresolver-5.2.2-data.jar" echo "SCHXSLT_CP=${LIB}/schxslt-cli-1.10.1.jar:${LIB}/commons-cli-1.5.0.jar:${LIB}/slf4j-api-1.7.32.jar:${LIB}/slf4j-nop-1.7.32.jar" +echo "SANTUARIO_CP=${LIB}/xmlsec-4.0.4.jar:${LIB}/commons-codec-1.18.0.jar:${LIB}/slf4j-api-2.0.17.jar:${LIB}/slf4j-nop-2.0.17.jar"