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
18 changes: 18 additions & 0 deletions .coderabbit.yaml
Original file line number Diff line number Diff line change
@@ -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
2 changes: 1 addition & 1 deletion build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
7 changes: 5 additions & 2 deletions src/main/java/io/vibetensor/attestix/Canonicalizer.java
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@
*
* <p>Divergence from strict RFC 8785 that callers MUST honour: RFC 8785 does
* <i>not</i> 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.
*/
Expand Down Expand Up @@ -83,6 +83,9 @@ private static void writeObject(StringBuilder sb, Map<?, ?> map) {
TreeMap<String, Object> 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('{');
Expand Down Expand Up @@ -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.
Expand Down
8 changes: 4 additions & 4 deletions src/main/java/io/vibetensor/attestix/CredentialVerifier.java
Original file line number Diff line number Diff line change
Expand Up @@ -69,17 +69,17 @@ public static VerificationResult verifyCredential(Map<String, Object> 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) {
signatureValid = false;
}
}

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);
Expand All @@ -88,7 +88,7 @@ public static VerificationResult verifyCredential(Map<String, Object> 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();
Expand Down
28 changes: 28 additions & 0 deletions src/test/java/io/vibetensor/attestix/SecurityBoundariesTest.java
Original file line number Diff line number Diff line change
@@ -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<String,Object> 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<String,Object> 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());
}
}
Loading