Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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/<TotalPercentage>8.33</<TotalPercentage>9.33</' signed.xml > 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 \
Expand Down
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)_ |

Expand Down
65 changes: 65 additions & 0 deletions XML_Signature/README.md
Original file line number Diff line number Diff line change
@@ -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 `<FundsXML4>`** — 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.
41 changes: 41 additions & 0 deletions XML_Signature/cli/sign-verify-xmlsec1.sh
Original file line number Diff line number Diff line change
@@ -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 <in.xml> <out.xml>
# sign-verify-xmlsec1.sh verify <signed.xml> [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 <ds:Signature>
# 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
91 changes: 91 additions & 0 deletions XML_Signature/dotnet/SignVerify.cs
Original file line number Diff line number Diff line change
@@ -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 <FundsXML4>.
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;
}
}
15 changes: 15 additions & 0 deletions XML_Signature/dotnet/SignVerify.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<Project Sdk="Microsoft.NET.Sdk">
<!-- Reference project. System.Security.Cryptography.Xml is a NuGet package
on .NET (Core): dotnet add package System.Security.Cryptography.Xml
Run: dotnet run -- sign|verify ... -->
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<Nullable>enable</Nullable>
<AssemblyName>SignVerify</AssemblyName>
<RootNamespace>FundsXml.XmlSignature</RootNamespace>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="System.Security.Cryptography.Xml" Version="8.0.0" />
</ItemGroup>
</Project>
30 changes: 30 additions & 0 deletions XML_Signature/generate-test-key.sh
Original file line number Diff line number Diff line change
@@ -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"
90 changes: 90 additions & 0 deletions XML_Signature/java/SignFundsXml.java
Original file line number Diff line number Diff line change
@@ -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 <in.xml> <out.xml> \
// XML_Signature/keys/test-signing.p12 changeit fundsxml
//
// Produces an enveloped signature whose ds:Signature is appended as the last
// child of <FundsXML4> — 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 <in.xml> <out.xml> "
+ "<keystore.p12> <storepass> <alias>");
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 <FundsXML4>.
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)");
}
}
Loading
Loading