From e101436cd10d530ced918cc145bfd312e517af21 Mon Sep 17 00:00:00 2001 From: ascender1729 Date: Sun, 11 Oct 2026 19:08:13 +0100 Subject: [PATCH 1/2] fix(verifier): reject ambiguous payloads and refresh crypto dependency Fail closed on normalized key collisions, malformed expiry and invalid pinned keys. Upgrade Bouncy Castle to 1.86 to remove advisories in the bundled cryptographic provider. --- build.gradle.kts | 2 +- .../io/vibetensor/attestix/Canonicalizer.java | 7 +++-- .../attestix/CredentialVerifier.java | 8 +++--- .../attestix/SecurityBoundariesTest.java | 28 +++++++++++++++++++ 4 files changed, 38 insertions(+), 7 deletions(-) create mode 100644 src/test/java/io/vibetensor/attestix/SecurityBoundariesTest.java diff --git a/build.gradle.kts b/build.gradle.kts index 0014394..b1b480c 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -31,7 +31,7 @@ repositories { dependencies { // BouncyCastle: trusted, deterministic Ed25519 (RFC 8032) implementation. - api("org.bouncycastle:bcprov-jdk18on:1.78.1") + api("org.bouncycastle:bcprov-jdk18on:1.86") testImplementation(platform("org.junit:junit-bom:5.10.2")) testImplementation("org.junit.jupiter:junit-jupiter") diff --git a/src/main/java/io/vibetensor/attestix/Canonicalizer.java b/src/main/java/io/vibetensor/attestix/Canonicalizer.java index a595686..49524de 100644 --- a/src/main/java/io/vibetensor/attestix/Canonicalizer.java +++ b/src/main/java/io/vibetensor/attestix/Canonicalizer.java @@ -28,7 +28,7 @@ * *

Divergence from strict RFC 8785 that callers MUST honour: RFC 8785 does * not normalize Unicode, but Attestix applies NFC. Non-whole floats are - * formatted via Python's {@code repr}, not ECMAScript Ryū - so signed payloads + * formatted via Python's {@code repr}, not ECMAScript Ryu - so signed payloads * should avoid non-trivial floats. The conformance vectors only use integers and * {@code 1.5}, on which all ports agree. */ @@ -83,6 +83,9 @@ private static void writeObject(StringBuilder sb, Map map) { TreeMap sorted = new TreeMap<>(Canonicalizer::compareByCodePoint); for (Map.Entry e : map.entrySet()) { String key = nfc(String.valueOf(e.getKey())); + if (sorted.containsKey(key)) { + throw new IllegalArgumentException("Duplicate normalized object key"); + } sorted.put(key, e.getValue()); } sb.append('{'); @@ -114,7 +117,7 @@ private static void writeArray(StringBuilder sb, List list) { /** * Write a string with NFC normalization and Python {@code json.dumps}-compatible - * escaping: only {@code "}, {@code \\}, and the control chars U+0000–U+001F are + * escaping: only {@code "}, {@code \\}, and the control chars U+0000-U+001F are * escaped (short escapes for b/t/n/f/r, {@code \\uXXXX} otherwise). With * {@code ensure_ascii=False}, every other character - including all non-ASCII - * is emitted raw (UTF-8 on encode). {@code /} is NOT escaped. diff --git a/src/main/java/io/vibetensor/attestix/CredentialVerifier.java b/src/main/java/io/vibetensor/attestix/CredentialVerifier.java index c799574..7eb908d 100644 --- a/src/main/java/io/vibetensor/attestix/CredentialVerifier.java +++ b/src/main/java/io/vibetensor/attestix/CredentialVerifier.java @@ -69,9 +69,9 @@ public static VerificationResult verifyCredential(Map vc, byte[] } boolean signatureValid = false; - if (proofValue != null) { - byte[] canonical = Canonicalizer.canonicalize(signedPayload); + if (proofValue != null && issuerPublicKey != null && issuerPublicKey.length == 32) { try { + byte[] canonical = Canonicalizer.canonicalize(signedPayload); byte[] sig = Base64Url.decode(proofValue); signatureValid = Ed25519.verify(issuerPublicKey, canonical, sig); } catch (RuntimeException ex) { @@ -79,7 +79,7 @@ public static VerificationResult verifyCredential(Map vc, byte[] } } - boolean notExpired = checkNotExpired(vc.get("expirationDate"), now); + boolean notExpired = !vc.containsKey("expirationDate") || checkNotExpired(vc.get("expirationDate"), now); boolean notRevoked = checkNotRevoked(vc.get("credentialStatus")); return new VerificationResult(signatureValid, notExpired, notRevoked, structureValid); @@ -88,7 +88,7 @@ public static VerificationResult verifyCredential(Map vc, byte[] private static boolean checkNotExpired(Object expirationDate, Instant now) { if (!(expirationDate instanceof String) || ((String) expirationDate).isEmpty()) { // No expiry => never expires. - return true; + return false; } try { Instant exp = OffsetDateTime.parse((String) expirationDate).toInstant(); diff --git a/src/test/java/io/vibetensor/attestix/SecurityBoundariesTest.java b/src/test/java/io/vibetensor/attestix/SecurityBoundariesTest.java new file mode 100644 index 0000000..ad66a91 --- /dev/null +++ b/src/test/java/io/vibetensor/attestix/SecurityBoundariesTest.java @@ -0,0 +1,28 @@ +package io.vibetensor.attestix; + +import org.junit.jupiter.api.Test; +import java.time.Instant; +import java.util.LinkedHashMap; +import java.util.Map; +import static org.junit.jupiter.api.Assertions.*; + +class SecurityBoundariesTest { + @Test void ambiguousNormalizedKeysAreRejected() { + Map value = new LinkedHashMap<>(); + value.put("\u00e9",1); value.put("e\u0301",2); + assertThrows(IllegalArgumentException.class, () -> Attestix.canonicalize(value)); + } + @Test void malformedExpiryFailsClosed() { + for (Object expiry : new Object[]{null,false,123,"","invalid"}) { + Map vc = new LinkedHashMap<>(); + vc.put("proof",Map.of("proofValue","invalid")); vc.put("expirationDate",expiry); + assertFalse(Attestix.verifyCredential(vc,new byte[32],Instant.now()).notExpired()); + } + } + @Test void malformedKeyFailsClosed() { + assertFalse(Attestix.verifyCredential(Map.of("proof",Map.of("proofValue","AQ==")),new byte[]{1},Instant.now()).verify()); + } + @Test void missingExpiryRemainsUnlimited() { + assertTrue(Attestix.verifyCredential(Map.of("proof",Map.of("proofValue","AQ==")),new byte[32],Instant.now()).notExpired()); + } +} From b4d4fa230d02c37a18b923084e3368970e8f27f3 Mon Sep 17 00:00:00 2001 From: ascender1729 Date: Sun, 11 Oct 2026 19:21:36 +0100 Subject: [PATCH 2/2] chore(review): enable validated draft CodeRabbit review --- .coderabbit.yaml | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 .coderabbit.yaml diff --git a/.coderabbit.yaml b/.coderabbit.yaml new file mode 100644 index 0000000..44cb754 --- /dev/null +++ b/.coderabbit.yaml @@ -0,0 +1,18 @@ +# yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json +language: en-US +reviews: + profile: assertive + auto_review: + enabled: true + drafts: true + base_branches: + - main + request_changes_workflow: false + path_filters: + - "!**/target/**" + - "!**/build/**" + - "!**/node_modules/**" +chat: + auto_reply: false +knowledge_base: + opt_out: true