|
1 | 1 | # Attestix Java Verifier |
2 | 2 |
|
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 |
39 | 9 |
|
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). |
41 | 13 |
|
42 | | -```kotlin |
43 | | -implementation("com.vibetensor:attestix:0.4.1") |
| 14 | +```sh |
| 15 | +./gradlew test --no-daemon |
| 16 | +./gradlew build --no-daemon |
44 | 17 | ``` |
45 | 18 |
|
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. |
47 | 23 |
|
48 | | -## Verify a credential (10 lines) |
| 24 | +## Verify a credential with a trusted key |
49 | 25 |
|
50 | 26 | ```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; |
52 | 32 | import java.time.Instant; |
53 | 33 |
|
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 | +} |
72 | 47 | ``` |
73 | 48 |
|
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 |
79 | 74 |
|
80 | 75 | ```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(); |
83 | 79 | ``` |
84 | 80 |
|
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. |
91 | 88 |
|
92 | 89 | ## Public API |
93 | 90 |
|
94 | 91 | | Method | Purpose | |
95 | 92 | |---|---| |
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. | |
100 | 97 |
|
101 | | -`VerificationResult` exposes `signatureValid()`, `notExpired()`, `notRevoked()`, |
102 | | -`structureValid()`, and `verify()`. |
| 98 | +## Canonical form and compatibility |
103 | 99 |
|
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. |
105 | 103 |
|
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. |
113 | 110 |
|
114 | | -## Conformance |
| 111 | +## Release configuration |
115 | 112 |
|
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. |
147 | 117 |
|
148 | 118 | ## License |
149 | 119 |
|
150 | | -[Apache-2.0](./LICENSE). See [`NOTICE`](./NOTICE). |
| 120 | +[Apache-2.0](LICENSE). See [NOTICE](NOTICE). |
0 commit comments