Skip to content

Commit 3ce5e7e

Browse files
authored
docs(verifier): document trusted keys and canonical limits (#4)
1 parent a3c108d commit 3ce5e7e

3 files changed

Lines changed: 158 additions & 173 deletions

File tree

‎CANONICAL.md‎

Lines changed: 49 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -1,53 +1,49 @@
1-
# Attestix canonical form (Java port notes)
2-
3-
The Attestix canonical form is **JCS-*style*, NOT strict RFC 8785.** This port
4-
reproduces, byte-for-byte, the output of the Python reference
5-
`attestix/auth/crypto.py::canonicalize_json` (attestix 0.4.0). The vectors'
6-
`canonical_bytes_hex` is the oracle.
7-
8-
## The rule
9-
10-
```
11-
canonical_bytes = NFC_normalize_recursive(obj)
12-
|> json.dumps(sort_keys=True, separators=(",", ":"), ensure_ascii=False)
13-
|> encode("utf-8")
14-
```
15-
16-
with whole-number floats collapsed to integers.
17-
18-
## Exact behaviours this port implements
19-
20-
1. **NFC normalization**: every string **value** and every object **key** is
21-
`Normalizer.normalize(s, Form.NFC)` before serialization. (`"cafe"+U+0301` →
22-
`café`.) This is the main divergence from RFC 8785, which does *not* normalize.
23-
2. **Key sorting**: object keys sorted by Unicode **code point** ascending,
24-
recursively. NFC is applied to keys *before* sorting.
25-
3. **Separators**: `,` between items, `:` between key and value. No whitespace.
26-
4. **Raw UTF-8, no `\uXXXX`**: non-ASCII characters are emitted as raw UTF-8
27-
bytes (`ensure_ascii=False`). `é` = `c3 a9`, `☃` = `e2 98 83`, `😀` =
28-
`f0 9f 98 80`.
29-
5. **Whole-number floats → integers**: `1.0` → `1`, `2.00` → `2`. A non-whole
30-
float keeps its value: `1.5` → `1.5`. (Implemented via `BigDecimal`:
31-
`stripTrailingZeros().scale() <= 0` means whole.)
32-
6. **Integers stay integers, arbitrary precision**: `9007199254740993` is
33-
serialized exactly. This port parses integral JSON numbers into
34-
`java.math.BigInteger`, so there is no `2^53` precision loss.
35-
7. **Control-char escaping**: only `"`, `\`, and U+0000 to U+001F are escaped,
36-
using `\b \t \n \f \r` where applicable and `\uXXXX` otherwise. `/` is **not**
37-
escaped. This matches Python's `json.dumps`.
38-
39-
## Float caveat
40-
41-
For non-trivial floating-point values, Python `json.dumps` and ECMAScript Ryū can
42-
disagree. The conformance vectors only use integers and `1.5`, on which all ports
43-
agree. **Signed payloads should avoid non-trivial floats** (e.g. `0.1 + 0.2`).
44-
45-
## Signing & encoding
46-
47-
- **VC**: signed payload is the credential minus `proof` and `credentialStatus`,
48-
canonicalized as above, Ed25519-signed; `proof.proofValue` is base64url **with
49-
padding**.
50-
- **UCAN**: EdDSA JWTs (compact form). The signed message is
51-
`base64url(header) + "." + base64url(payload)` (base64url **without** padding,
52-
per the JWT spec). Only `alg=EdDSA` is accepted.
53-
- **did:key**: `did:key:z` + base58btc(`0xed 0x01` || raw-32-byte key).
1+
# Attestix canonical form in Java
2+
3+
The implementation follows the Attestix v1 JCS-style signing form for the
4+
represented conformance cases. It is not strict RFC 8785. The shared
5+
[test vectors](testdata/vectors.json) are the compatibility reference.
6+
7+
## Implemented rules
8+
9+
1. NFC-normalize each string value and object key.
10+
2. Reject distinct object keys that become identical after NFC normalization.
11+
3. Sort the normalized keys recursively by Unicode code point.
12+
4. Use compact separators, with no formatting whitespace.
13+
5. Emit non-ASCII strings as UTF-8; escape quotes, backslashes, and control
14+
characters using JSON escapes.
15+
6. Parse integer tokens into `BigInteger`, preserving values such as
16+
`9007199254740993` without JavaScript's number rounding.
17+
7. Normalize `BigDecimal` values with `stripTrailingZeros`; whole values emit
18+
integers and remaining decimal values use the implementation's decimal form.
19+
20+
The public `Attestix.canonicalize(String)` first parses JSON. Malformed JSON
21+
throws `JsonException`. Unsupported values or normalized-key collisions can
22+
throw during canonicalization. Credential verification catches canonicalization
23+
errors in its signature check and returns `signatureValid=false`.
24+
25+
## Numeric and parsing limits
26+
27+
The vectors exercise integers and `1.5`. The implementation uses `BigDecimal`,
28+
not Python's binary floating-point serializer. Exact equivalence for arbitrary
29+
fractions, exponents, and signed-zero values is not established by those vectors.
30+
Prefer integer or string fields for signed data. Callers passing an already
31+
parsed Map must also preserve numeric precision; conversion to binary floating
32+
point can lose information before canonicalization starts.
33+
34+
Collision rejection applies to distinct keys retained in the parsed object.
35+
It is not a guarantee that identical duplicate names in raw JSON are detected
36+
before the parser constructs its Map.
37+
38+
## Signed fields and encodings
39+
40+
- A VC signature covers the credential minus `proof` and `credentialStatus`.
41+
The verifier uses the caller's trusted issuer key, not unsigned proof metadata.
42+
- A UCAN signature covers the literal compact JWS signing input
43+
`base64url(header) + "." + base64url(payload)`. It is not canonicalized JSON;
44+
only `alg=EdDSA` is accepted.
45+
- Ed25519 public keys contain 32 bytes and signatures contain 64 bytes.
46+
- Attestix VC proofs use padded base64url. Delegation compact-JWT segments use
47+
unpadded base64url.
48+
- An Ed25519 `did:key` is `did:key:z` followed by base58btc of the multicodec
49+
bytes `0xed 0x01` and the raw public key.

‎CHANGELOG.md‎

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
# Changelog
2+
3+
## Unreleased
4+
5+
These changes describe main-branch source. They do not announce a Maven Central
6+
release or imply that configured version 0.4.1 has been republished.
7+
8+
### Verification
9+
10+
- Reject distinct keys that collide after NFC normalization.
11+
- Treat canonicalization failures and malformed pinned keys as invalid
12+
signatures; reject malformed present credential expiry values.
13+
- Update Bouncy Castle bcprov-jdk18on to 1.86.
14+
15+
### Documentation
16+
17+
- Explain caller-pinned issuer trust, input exceptions, offline revocation, and
18+
the explicit pair versus recursive delegation checks.
19+
- Document JDK 17 builds, Java 11 output, and bounded canonical numeric support.

‎README.md‎

Lines changed: 90 additions & 120 deletions
Original file line numberDiff line numberDiff line change
@@ -1,150 +1,120 @@
11
# Attestix Java Verifier
22

3-
Offline verifier for **Attestix**-issued credentials, in pure Java. Verify
4-
Ed25519 [W3C Verifiable Credentials](https://www.w3.org/TR/vc-data-model/) and
5-
[UCAN](https://github.com/ucan-wg/spec) delegation chains that were issued by the
6-
[Attestix](https://github.com/VibeTensor/attestix) Python core (**with no Python
7-
runtime needed**).
8-
9-
This is a language port of the Attestix verifier. It reproduces the Attestix
10-
canonical form **byte-for-byte** and is validated against the shared
11-
cross-language conformance vectors (`spec/verify/v1/vectors.json` in the parent
12-
repo, vendored here under `testdata/` and `src/test/resources/`).
13-
14-
This port is **verifier-only**: it verifies credentials and delegation chains;
15-
it does not issue them. Issuance stays in the Attestix Python core
16-
(`pip install attestix`) or Attestix Cloud.
17-
18-
- Parent project: <https://github.com/VibeTensor/attestix>
19-
- Bundle wire-format spec: <https://attestix.io/spec/bundle/v1>
20-
- Ed25519 via [BouncyCastle](https://www.bouncycastle.org/) (trusted, deterministic)
21-
22-
> **Canonical form note.** Attestix uses a **JCS-*style*** canonical form, **not
23-
> strict RFC 8785.** The load-bearing divergence: Attestix applies Unicode **NFC
24-
> normalization** to every string value and object key (RFC 8785 does not), and
25-
> whole-number floats collapse to integers (`1.0` → `1`). See
26-
> [`CANONICAL.md`](./CANONICAL.md) for the full rules.
27-
28-
## Install
29-
30-
Maven (once published to Maven Central):
31-
32-
```xml
33-
<dependency>
34-
<groupId>com.vibetensor</groupId>
35-
<artifactId>attestix</artifactId>
36-
<version>0.4.1</version>
37-
</dependency>
38-
```
3+
Offline Java verifier for Attestix Ed25519 credentials and UCAN-style
4+
compact JWT delegation chains. It needs no Python runtime and makes no network
5+
calls. It does not issue credentials or provide a presentation-verification API.
6+
See the [Attestix core](https://github.com/VibeTensor/attestix) for issuance.
7+
8+
## Build the current source
399

40-
Gradle (Kotlin DSL):
10+
Build with JDK 17; source and bytecode target Java 11 via `--release 11`.
11+
The checkout uses Bouncy Castle `bcprov-jdk18on` 1.86, as declared in
12+
[build.gradle.kts](build.gradle.kts).
4113

42-
```kotlin
43-
implementation("com.vibetensor:attestix:0.4.1")
14+
```sh
15+
./gradlew test --no-daemon
16+
./gradlew build --no-daemon
4417
```
4518

46-
Requires Java 11+ at runtime. (CI builds and tests on Temurin 17.)
19+
The configured publication coordinates are `com.vibetensor:attestix:0.4.1`.
20+
These coordinates do not establish Maven Central availability or publication of
21+
main-branch fixes. Build a reviewed checkout or confirm the contents of the
22+
release you select before depending on its verification behavior.
4723

48-
## Verify a credential (10 lines)
24+
## Verify a credential with a trusted key
4925

5026
```java
51-
import io.vibetensor.attestix.*;
27+
import io.vibetensor.attestix.Attestix;
28+
import io.vibetensor.attestix.DidKey;
29+
import io.vibetensor.attestix.VerificationResult;
30+
import java.nio.file.Files;
31+
import java.nio.file.Path;
5232
import java.time.Instant;
5333

54-
// The issuer's did:key (published out of band, e.g. in the agent card).
55-
byte[] issuerKey = DidKey.decode("did:key:z6Mko5TBPGKHkCxSgmf3aC6p6SGj2auwCfRmBydXJFEwL4ev");
56-
57-
String vcJson = /* the credential JSON, e.g. read from a file */ "...";
58-
VerificationResult r = Attestix.verifyCredential(vcJson, issuerKey, Instant.now());
59-
60-
System.out.println("signature valid: " + r.signatureValid());
61-
System.out.println("not expired: " + r.notExpired());
62-
System.out.println("not revoked: " + r.notRevoked());
63-
System.out.println("overall verify: " + r.verify()); // AND of all checks
64-
```
65-
66-
## Verify a delegation chain
67-
68-
```java
69-
byte[] serverKey = DidKey.decode("did:key:z6Mko5TBPGKHkCxSgmf3aC6p6SGj2auwCfRmBydXJFEwL4ev");
70-
DelegationResult d = Attestix.verifyDelegationChain(parentJwt, childJwt, serverKey, Instant.now());
71-
// d.verify() == every JWT signature valid AND child att ⊆ parent att AND not expired
34+
public final class VerifyExample {
35+
public static void main(String[] args) throws Exception {
36+
if (args.length != 1) {
37+
throw new IllegalArgumentException("usage: VerifyExample credential.json");
38+
}
39+
// Obtain and authenticate the expected issuer's key out of band.
40+
byte[] issuerKey = DidKey.decode(
41+
"did:key:z6Mko5TBPGKHkCxSgmf3aC6p6SGj2auwCfRmBydXJFEwL4ev");
42+
String vcJson = Files.readString(Path.of(args[0]));
43+
VerificationResult result = Attestix.verifyCredential(vcJson, issuerKey, Instant.now());
44+
System.out.println("valid=" + result.verify());
45+
}
46+
}
7247
```
7348

74-
UCAN delegations are EdDSA JWTs (compact form), **not** JCS-signed. Only
75-
`alg=EdDSA` is accepted (`alg:none` is rejected). Capability attenuation is
76-
enforced: a child's capabilities must be a subset of its parent's.
77-
78-
## Decode a did:key
49+
This sample key is an example, not a trust anchor for arbitrary credentials.
50+
The caller supplies the trusted 32-byte issuer key. Verification does not
51+
resolve a key from `proof.verificationMethod` or independently check that the
52+
pinned key corresponds to the document's issuer identifier. Make that trust
53+
association in the application.
54+
55+
## Credential behavior and errors
56+
57+
- `VerificationResult` exposes `signatureValid()`, `notExpired()`,
58+
`notRevoked()`, `structureValid()`, and their combined `verify()` verdict.
59+
- The signed payload excludes only top-level `proof` and `credentialStatus`.
60+
NFC-normalized key collisions make signature verification fail.
61+
- Null or wrong-length pinned keys, invalid proof encoding, and canonicalization
62+
errors do not pass signature verification.
63+
- Absent `expirationDate` means no expiry. A present null, non-string, empty, or
64+
unparseable value fails the expiry check. Valid expiry requires `now < expiry`.
65+
- Embedded revocation status is read locally. Missing status does not establish
66+
current revocation state; consult a trusted issuer status source separately.
67+
- Malformed JSON can throw `JsonException`; catch it at the input boundary.
68+
Direct canonicalization can also throw for unsupported input. Do not interpret
69+
any exception as a successful verification.
70+
- Structure checks concern the proof shape, not complete W3C schema validation.
71+
This verifier does not perform JSON-LD processing or legal assessment.
72+
73+
## Delegation verification
7974

8075
```java
81-
byte[] rawEd25519 = Attestix.decodeDidKey("did:key:z6Mko5TBPGKHkCxSgmf3aC6p6SGj2auwCfRmBydXJFEwL4ev");
82-
// rawEd25519 is the 32-byte public key (multibase base58btc + 0xed01 multicodec prefix)
76+
DelegationResult result = Attestix.verifyDelegationChain(
77+
parentJwt, childJwt, trustedServerKey, Instant.now());
78+
boolean valid = result.verify();
8379
```
8480

85-
## Canonicalize JSON (the Attestix way)
86-
87-
```java
88-
byte[] canonical = Attestix.canonicalize("{\"b\":2,\"a\":1}");
89-
// -> bytes of {"a":1,"b":2} (keys sorted by code point, no whitespace, raw UTF-8, NFC)
90-
```
81+
Import `io.vibetensor.attestix.DelegationResult` for this fragment. The caller
82+
supplies the trusted server key used for every signature. Verification checks
83+
EdDSA signatures, parent/child expiry, the child's recursive `prf` signatures
84+
and expiry, repeated-`jti` cycles, and capability subset between the explicit
85+
parent and child. It does not independently enforce capability attenuation at
86+
every recursive ancestor, `nbf`, `iat`, audience policy, or live revocation.
87+
Apply the required application policy before authorizing an operation.
9188

9289
## Public API
9390

9491
| Method | Purpose |
9592
|---|---|
96-
| `Attestix.verifyCredential(vcJson, issuerKey, now)` | Verify a W3C VC → `VerificationResult` |
97-
| `Attestix.verifyDelegationChain(parent, child, serverKey, now)` | Verify a UCAN chain → `DelegationResult` |
98-
| `Attestix.canonicalize(json)` | JCS-style canonical UTF-8 bytes |
99-
| `Attestix.decodeDidKey(did)` | `did:key:z...` -> raw 32-byte Ed25519 key |
93+
| `Attestix.verifyCredential(json, issuerKey, now)` | Verify a credential under the caller's trusted key. |
94+
| `Attestix.verifyDelegationChain(parent, child, serverKey, now)` | Verify the explicit pair and the child's proof ancestry. |
95+
| `Attestix.canonicalize(json)` | Produce canonical bytes or throw for invalid/unsupported input. |
96+
| `Attestix.decodeDidKey(did)` | Decode an Ed25519 `did:key` to a 32-byte public key. |
10097

101-
`VerificationResult` exposes `signatureValid()`, `notExpired()`, `notRevoked()`,
102-
`structureValid()`, and `verify()`.
98+
## Canonical form and compatibility
10399

104-
## Build & test
100+
See [CANONICAL.md](CANONICAL.md). Attestix uses NFC normalization, compact UTF-8
101+
JSON, normalized-key sorting, and arbitrary-precision integer handling; it is
102+
not strict RFC 8785. General floating-point interoperability is not guaranteed.
105103

106-
```bash
107-
./gradlew test # runs the conformance vectors
108-
./gradlew build # jar + sources + javadoc
109-
```
110-
111-
CI ([`.github/workflows/test.yml`](.github/workflows/test.yml)) builds and runs
112-
the vectors on Temurin 17 with SHA-pinned actions.
104+
The shared vectors are vendored in [testdata/vectors.json](testdata/vectors.json)
105+
and [src/test/resources/vectors.json](src/test/resources/vectors.json).
106+
[ConformanceVectorsTest.java](src/test/java/io/vibetensor/attestix/ConformanceVectorsTest.java)
107+
checks those represented cases; [SecurityBoundariesTest.java](src/test/java/io/vibetensor/attestix/SecurityBoundariesTest.java)
108+
covers the hardened input boundaries. This is not a guarantee for every JSON
109+
value or every deployment policy.
113110

114-
## Conformance
111+
## Release configuration
115112

116-
Every vector in `testdata/vectors.json` is asserted by
117-
`src/test/java/io/vibetensor/attestix/ConformanceVectorsTest.java`:
118-
119-
| Vector | What it checks |
120-
|---|---|
121-
| `canon-001` | JCS-style canonical bytes (NFC, sorted keys, whole-float→int, big int) |
122-
| `didkey-001` | did:key decode (0xed01 multicodec, 32-byte key) + round-trip |
123-
| `vc-valid-001` | Valid VC → verify true |
124-
| `vc-tampered-001` | Tampered claim → signature invalid → verify false |
125-
| `vc-expired-001` | Expired VC → not_expired false → verify false |
126-
| `ucan-chain-valid-001` | Delegation chain, child att ⊆ parent att → verify true |
127-
| `ucan-chain-escalation-002` | Privilege escalation (`admin` not in parent) → verify false |
128-
129-
## Releasing to Maven Central
130-
131-
This build is **publish-ready** (POM metadata, sources + javadoc jars, GPG
132-
signing wired) via the Sonatype **Central Portal** (`central.sonatype.com`). To
133-
cut a release:
134-
135-
1. Configure Central Portal token credentials
136-
(`mavenCentralUsername`, `mavenCentralPassword`) and an in-memory GPG key
137-
(`signingInMemoryKey`, `signingInMemoryKeyPassword`) as `ORG_GRADLE_PROJECT_*`
138-
environment variables or Gradle properties.
139-
2. Bump `version` in `build.gradle.kts`, commit, then push a matching `vX.Y.Z`
140-
tag. The `publish` GitHub Actions workflow runs the conformance vectors and
141-
then `./gradlew publishToMavenCentral`.
142-
3. With `automaticRelease = true`, Central validates and releases the artifact
143-
automatically (no manual staging step). Flip that flag to stage first.
144-
145-
No tokens are committed; release runs only on a version tag with the credentials
146-
supplied as encrypted GitHub Actions secrets.
113+
The build configures sources/Javadoc artifacts, signing, and the Sonatype
114+
Central Portal. Publication is a separate maintainer operation requiring
115+
credentials and a reviewed version tag. Nothing in this README announces a new
116+
registry release or authorizes publishing artifacts.
147117

148118
## License
149119

150-
[Apache-2.0](./LICENSE). See [`NOTICE`](./NOTICE).
120+
[Apache-2.0](LICENSE). See [NOTICE](NOTICE).

0 commit comments

Comments
 (0)