Skip to content

Commit 78f2f75

Browse files
joewizclaude
andcommitted
[feature] EXPath Cryptographic Module for eXist-db
Standalone XAR implementing the EXPath Cryptographic Module spec (http://expath.org/ns/crypto) using Java's built-in JCE — zero external dependencies. Functions: - crypto:hash (MD5, SHA-1, SHA-256, SHA-384, SHA-512) - crypto:hmac (HMAC authentication, base64/hex output) - crypto:encrypt / crypto:decrypt (AES-128/192/256, DES) - crypto:generate-signature / crypto:validate-signature (XML DSIG) Includes pre-install.xq to auto-remove the legacy expath-crypto-module, GitHub Actions CI with Docker-based smoke tests, and 46 integration tests. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
0 parents  commit 78f2f75

File tree

17 files changed

+2289
-0
lines changed

17 files changed

+2289
-0
lines changed

.github/workflows/exist.yml

Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
# This workflow builds a xar archive, deploys it into exist and runs a smoke test.
2+
3+
name: exist-db CI
4+
5+
on: [push, pull_request]
6+
7+
jobs:
8+
build:
9+
runs-on: ubuntu-latest
10+
strategy:
11+
fail-fast: false
12+
matrix:
13+
exist-version: [latest]
14+
15+
steps:
16+
- uses: actions/checkout@v6
17+
18+
# Build with Maven
19+
- name: Set up JDK 21
20+
uses: actions/setup-java@v5
21+
with:
22+
java-version: '21'
23+
distribution: 'temurin'
24+
cache: maven
25+
26+
# Extract exist-core JAR from Docker image for compilation,
27+
# since the published 7.0.0-SNAPSHOT in the Maven repo is stale.
28+
- name: Pull eXist-db image
29+
run: docker pull existdb/existdb:${{ matrix.exist-version }}
30+
31+
- name: Install exist-core from Docker image into local Maven repo
32+
run: |
33+
id=$(docker create existdb/existdb:${{ matrix.exist-version }})
34+
docker cp "$id:/exist/lib/exist.uber.jar" /tmp/exist.uber.jar
35+
docker rm "$id"
36+
mvn install:install-file -Dfile=/tmp/exist.uber.jar \
37+
-DgroupId=org.exist-db -DartifactId=exist-core \
38+
-Dversion=7.0.0-SNAPSHOT -Dpackaging=jar -q
39+
40+
- name: Build with Maven
41+
run: mvn clean package -DskipTests -q
42+
43+
# Deploy XAR in Container
44+
- name: Start eXist-db container
45+
run: |
46+
docker run -dit -p 8080:8080 \
47+
-v ${{ github.workspace }}/target:/exist/autodeploy \
48+
--name exist --rm --health-interval=1s --health-start-period=1s \
49+
existdb/existdb:${{ matrix.exist-version }}
50+
51+
- name: Wait for eXist-db to start and deploy packages
52+
timeout-minutes: 5
53+
run: |
54+
while ! docker logs exist | grep -q "Server has started"; \
55+
do sleep 6s; \
56+
done
57+
sleep 5
58+
59+
# Smoke test: verify module loads and functions work
60+
- name: Test crypto:hash with known SHA-256 vector
61+
run: |
62+
result=$(curl -sf -u admin: -H "Content-Type: application/xml" --data '
63+
<query xmlns="http://exist.sourceforge.net/NS/exist">
64+
<text><![CDATA[
65+
import module namespace crypto = "http://expath.org/ns/crypto";
66+
crypto:hash("test", "SHA-256", "hex")
67+
]]></text>
68+
</query>' "http://localhost:8080/exist/rest/db")
69+
echo "$result"
70+
echo "$result" | grep -q "9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08" || (echo "FAIL: SHA-256 hash mismatch" && exit 1)
71+
72+
- name: Test crypto:hmac with SHA256 hex output
73+
run: |
74+
result=$(curl -sf -u admin: -H "Content-Type: application/xml" --data '
75+
<query xmlns="http://exist.sourceforge.net/NS/exist">
76+
<text><![CDATA[
77+
import module namespace crypto = "http://expath.org/ns/crypto";
78+
string-length(crypto:hmac("test", "key", "SHA256", "hex"))
79+
]]></text>
80+
</query>' "http://localhost:8080/exist/rest/db")
81+
echo "$result"
82+
echo "$result" | grep -q ">64<" || (echo "FAIL: expected 64 hex chars" && exit 1)
83+
84+
- name: Test crypto:hmac with base64 output
85+
run: |
86+
result=$(curl -sf -u admin: -H "Content-Type: application/xml" --data '
87+
<query xmlns="http://exist.sourceforge.net/NS/exist">
88+
<text><![CDATA[
89+
import module namespace crypto = "http://expath.org/ns/crypto";
90+
string-length(crypto:hmac("test", "key", "SHA256")) > 0
91+
]]></text>
92+
</query>' "http://localhost:8080/exist/rest/db")
93+
echo "$result"
94+
echo "$result" | grep -q "true" || (echo "FAIL: expected non-empty base64" && exit 1)
95+
96+
- name: Test crypto:encrypt and crypto:decrypt round-trip
97+
run: |
98+
result=$(curl -sf -u admin: -H "Content-Type: application/xml" --data '
99+
<query xmlns="http://exist.sourceforge.net/NS/exist">
100+
<text><![CDATA[
101+
import module namespace crypto = "http://expath.org/ns/crypto";
102+
let $key := "0123456789abcdef"
103+
let $enc := crypto:encrypt("secret message", "symmetric", $key, "AES")
104+
return crypto:decrypt($enc, "symmetric", $key, "AES")
105+
]]></text>
106+
</query>' "http://localhost:8080/exist/rest/db")
107+
echo "$result"
108+
echo "$result" | grep -q "secret message" || (echo "FAIL: decrypt did not return original" && exit 1)
109+
110+
- name: Test crypto:generate-signature produces Signature element
111+
run: |
112+
result=$(curl -sf -u admin: -H "Content-Type: application/xml" --data '
113+
<query xmlns="http://exist.sourceforge.net/NS/exist">
114+
<text><![CDATA[
115+
import module namespace crypto = "http://expath.org/ns/crypto";
116+
let $doc := <root><data>test</data></root>
117+
let $signed := crypto:generate-signature($doc, "inclusive", "SHA256", "RSA_SHA256", "dsig", "enveloped")
118+
return exists($signed//*[local-name() = "Signature"])
119+
]]></text>
120+
</query>' "http://localhost:8080/exist/rest/db")
121+
echo "$result"
122+
echo "$result" | grep -q "true" || (echo "FAIL: expected Signature element" && exit 1)
123+
124+
- name: Test multiple HMAC algorithms
125+
run: |
126+
result=$(curl -sf -u admin: -H "Content-Type: application/xml" --data '
127+
<query xmlns="http://exist.sourceforge.net/NS/exist">
128+
<text><![CDATA[
129+
import module namespace crypto = "http://expath.org/ns/crypto";
130+
string-join(
131+
for $alg in ("MD5", "SHA1", "SHA256", "SHA384", "SHA512")
132+
return string-length(crypto:hmac("test", "key", $alg, "hex")),
133+
","
134+
)
135+
]]></text>
136+
</query>' "http://localhost:8080/exist/rest/db")
137+
echo "$result"
138+
echo "$result" | grep -q "32,40,64,96,128" || (echo "FAIL: expected 32,40,64,96,128" && exit 1)

.gitignore

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
target/
2+
.mvn/
3+
.m2-repo/
4+
.env
5+
*.iml
6+
.idea/
7+
.classpath
8+
.project
9+
.settings/

README.md

Lines changed: 201 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,201 @@
1+
# EXPath Crypto Module for eXist-db
2+
3+
A standalone XAR package implementing the [EXPath Cryptographic Module](http://expath.org/spec/crypto) for eXist-db 7.0+. Provides cryptographic hashing, HMAC authentication, symmetric encryption/decryption, and XML digital signatures using Java's built-in JCE (Java Cryptography Extension) — no external dependencies.
4+
5+
## Install
6+
7+
Download the `.xar` from [Releases](https://github.com/joewiz/exist-crypto/releases) and install with the eXist-db Package Manager or the `xst` CLI:
8+
9+
```bash
10+
xst package install exist-crypto-0.9.0-SNAPSHOT.xar
11+
```
12+
13+
## Functions
14+
15+
| Function | Description |
16+
|----------|-------------|
17+
| `crypto:hash($data, $algorithm)` | Cryptographic hash with Base64 output |
18+
| `crypto:hash($data, $algorithm, $encoding)` | Cryptographic hash with specified encoding (`base64` or `hex`) |
19+
| `crypto:hmac($data, $key, $algorithm)` | HMAC with Base64 output |
20+
| `crypto:hmac($data, $key, $algorithm, $encoding)` | HMAC with specified encoding (`base64` or `hex`) |
21+
| `crypto:encrypt($data, $type, $key, $algorithm)` | Symmetric encryption (AES or DES) |
22+
| `crypto:decrypt($data, $type, $key, $algorithm)` | Symmetric decryption |
23+
| `crypto:generate-signature($data, $c14n, $digest, $sig, $prefix, $type)` | Generate XML digital signature |
24+
| `crypto:validate-signature($data)` | Validate XML digital signature |
25+
26+
**Module namespace:** `http://expath.org/ns/crypto`
27+
28+
### Hash
29+
30+
```xquery
31+
import module namespace crypto = "http://expath.org/ns/crypto";
32+
33+
(: Base64 output (default) :)
34+
crypto:hash("data", "SHA-256")
35+
36+
(: Hex output :)
37+
crypto:hash("data", "SHA-256", "hex")
38+
```
39+
40+
**Algorithms:** MD5, SHA-1, SHA-256, SHA-384, SHA-512
41+
42+
For new code, prefer `fn:hash()` (XQuery 4.0) or `util:hash()`. `crypto:hash` is provided for backward compatibility with the EXPath spec and cross-engine portability (BaseX also implements it).
43+
44+
### HMAC
45+
46+
```xquery
47+
import module namespace crypto = "http://expath.org/ns/crypto";
48+
49+
(: Base64 output (default) :)
50+
crypto:hmac("data", "secret-key", "SHA256")
51+
52+
(: Hex output :)
53+
crypto:hmac("data", "secret-key", "SHA256", "hex")
54+
```
55+
56+
**Algorithms:** MD5, SHA1, SHA256, SHA384, SHA512
57+
58+
### Symmetric Encryption
59+
60+
```xquery
61+
import module namespace crypto = "http://expath.org/ns/crypto";
62+
63+
let $key := "0123456789abcdef" (: 16 bytes = AES-128 :)
64+
let $encrypted := crypto:encrypt("secret message", "symmetric", $key, "AES")
65+
return crypto:decrypt($encrypted, "symmetric", $key, "AES")
66+
```
67+
68+
**Algorithms:** AES (16/24/32-byte key), DES (8-byte key)
69+
70+
A random initialization vector (IV) is automatically generated and prepended to the ciphertext.
71+
72+
### XML Digital Signatures
73+
74+
```xquery
75+
import module namespace crypto = "http://expath.org/ns/crypto";
76+
77+
let $doc := <order id="123"><total>99.99</total></order>
78+
let $signed := crypto:generate-signature(
79+
$doc, "inclusive", "SHA256", "RSA_SHA256", "dsig", "enveloped")
80+
return crypto:validate-signature($signed)
81+
```
82+
83+
## Upgrading from expath-crypto-module
84+
85+
This package is a clean-room replacement for the legacy [`expath-crypto-module`](https://github.com/eXist-db/expath-crypto-module) (maintained by Claudius Teodorescu, last updated 2020). Both packages implement the same EXPath Cryptographic Module specification and use the same module namespace (`http://expath.org/ns/crypto`), so they **cannot be installed simultaneously**.
86+
87+
### Why upgrade?
88+
89+
| | Legacy (`expath-crypto-module`) | This package (`exist-crypto`) |
90+
|---|---|---|
91+
| **eXist-db compatibility** | 5.x–6.x (broken on 7.x due to Type constant renumbering) | 7.0+ |
92+
| **Java version** | Java 8+ | Java 21+ |
93+
| **Dependencies** | External `crypto-java` library (ro.kuberam) | Zero — uses Java's built-in JCE |
94+
| **Parent POM** | `exist-apps-parent` 1.11.0 | `exist-apps-parent` 2.0.0 |
95+
| **`crypto:hash`** | Yes (2–3 arity) | Yes (2–3 arity, compatible) |
96+
97+
### Upgrade steps
98+
99+
1. **Install the new package** — a `pre-install.xq` script automatically detects and removes the old `expath-crypto-module` if present:
100+
101+
```bash
102+
xst package install exist-crypto-0.9.0-SNAPSHOT.xar
103+
```
104+
105+
To remove the old package manually (e.g., before downloading the new one):
106+
107+
```bash
108+
xst package remove http://expath.org/ns/crypto
109+
```
110+
111+
2. **Update your XQuery code** — see the migration guide below.
112+
113+
### Migration guide
114+
115+
#### No changes needed
116+
117+
The module namespace is identical, so **import statements require no changes**:
118+
119+
```xquery
120+
import module namespace crypto = "http://expath.org/ns/crypto";
121+
```
122+
123+
Calls to `crypto:hash`, `crypto:hmac`, and `crypto:validate-signature` are compatible as-is for the most common usage patterns.
124+
125+
#### `crypto:hmac` — return type changed
126+
127+
| | Legacy | New |
128+
|---|---|---|
129+
| **Parameter types** | `$data as xs:atomic*`, `$key as xs:atomic*` | `$data as xs:string`, `$key as xs:string` |
130+
| **Return type** | `xs:byte*` | `xs:string` |
131+
132+
The legacy module accepted binary types and returned a byte sequence. The new module accepts strings and returns a string (Base64 or hex). If your code passes `xs:string` arguments (the common case), no changes are needed.
133+
134+
**If you passed binary data:** Convert to string first with `util:binary-to-string()`.
135+
136+
#### `crypto:encrypt` / `crypto:decrypt` — IV handling changed
137+
138+
| | Legacy | New |
139+
|---|---|---|
140+
| **IV parameter** | Optional 5th argument (`xs:base64Binary`) | Automatic (random IV prepended to ciphertext) |
141+
| **Provider parameter** | Optional 6th argument | Not supported |
142+
| **Arity** | 4–6 | 4 |
143+
144+
The new module always generates a random IV and prepends it to the ciphertext. The decrypt function extracts the IV automatically. This is more secure than reusing IVs but means **ciphertext from the old module cannot be decrypted by the new module** (and vice versa) unless the IV handling is compatible.
145+
146+
**If you stored encrypted data:** You will need to re-encrypt data during migration. Decrypt with the old module, then re-encrypt with the new one.
147+
148+
**If you used a custom provider:** Remove the provider argument. Java's default JCE provider is used.
149+
150+
#### `crypto:generate-signature` — fewer arities
151+
152+
| | Legacy | New |
153+
|---|---|---|
154+
| **Arities** | 6, 7 (XPath), 8 (certificate), 3 (private key) | 6 |
155+
| **Signature algorithms** | RSA\_SHA1, DSA\_SHA1 | RSA\_SHA1, DSA\_SHA1, RSA\_SHA256 |
156+
157+
The 6-argument form is compatible. The XPath subsetting (7-arg), certificate (8-arg), and private key (3-arg) variants are not yet implemented.
158+
159+
**If you used the 6-argument form:** No changes needed.
160+
161+
**If you used XPath subsetting or certificates:** These features are not yet available. File an issue if you need them.
162+
163+
#### `crypto:hash` — return type changed
164+
165+
| | Legacy | New |
166+
|---|---|---|
167+
| **Parameter types** | `$data as item()*` | `$data as item()` |
168+
| **Return type** | `xs:byte*` | `xs:string` |
169+
170+
The legacy module returned a byte sequence. The new module returns a string (Base64 or hex encoded). The function signatures and algorithm names are otherwise compatible.
171+
172+
For new code, prefer `fn:hash()` (XQuery 4.0) or `util:hash()`. `crypto:hash` is provided for backward compatibility with the EXPath Cryptographic Module specification and cross-engine portability (BaseX also implements it).
173+
174+
### Package identity comparison
175+
176+
| Property | Legacy | New |
177+
|---|---|---|
178+
| Package name (URI) | `http://expath.org/ns/crypto` | `http://exist-db.org/pkg/crypto` |
179+
| Package abbreviation | `crypto` | `exist-crypto` |
180+
| Module namespace | `http://expath.org/ns/crypto` | `http://expath.org/ns/crypto` |
181+
| Maven groupId | `org.exist-db.xquery.extensions.expath` | `org.exist-db` |
182+
| Maven artifactId | `expath-crypto-module` | `exist-crypto` |
183+
| Java package | `org.expath.exist.crypto` | `org.exist.xquery.modules.crypto` |
184+
185+
The package abbreviations now differ (`crypto` vs `exist-crypto`), but both register the same module namespace. A `pre-install.xq` script automatically removes the old package if present.
186+
187+
## Build
188+
189+
```bash
190+
JAVA_HOME=/path/to/java-21 mvn clean package -DskipTests
191+
```
192+
193+
Run integration tests (requires exist-core 7.0.0-SNAPSHOT in your local Maven repo):
194+
195+
```bash
196+
mvn test -Pintegration-tests
197+
```
198+
199+
## License
200+
201+
[GNU Lesser General Public License v2.1](https://opensource.org/licenses/LGPL-2.1)

0 commit comments

Comments
 (0)