From 0fe1828f5b73d00cdcc100996a015b90bdd8222a Mon Sep 17 00:00:00 2001 From: Aere Network Date: Wed, 30 Sep 2026 12:10:59 +0300 Subject: [PATCH] 1.7.0: identity and compliance verification in all five clients (identityVerify / complianceCheck; Go: IdentityVerify / ComplianceCheck with the presentation as its JSON bytes) POST /v1/identity/verify: an AERE Identity presentation judged by the published command line -> VALID / PARTIAL / INVALID with every row, the claims shown and the subject when no row failed, and the command that reproduces the verdict. POST /v1/compliance/check: your policy judged on a presentation -> COMPLIANT / NOT_COMPLIANT with the reasons and, with record, the decision as an AIP-23 envelope without personal data. In Go the presentation, keys, lists and policy travel as json.RawMessage: a map or float64 would change numbers above 2^53 and the canonical text the signatures cover. The .NET negative control now builds clean for each planted defect (an incremental build could run the tests against the previous planted library). A negative control for the JavaScript and Python routes is added. Tests: JavaScript 13, Python 14, Go ok, Java 10/10 (and the Java 8 API gate); negative controls Go 9/9, Java 10/10, .NET 12/12, JavaScript and Python 4/4. --- README.md | 6 +++ csharp/Aere.Cloud.Tests/Program.cs | 15 +++++- csharp/Aere.Cloud/Aere.Cloud.csproj | 2 +- csharp/Aere.Cloud/AereCloudClient.cs | 12 ++++- csharp/README.md | 4 +- csharp/control-negativ.sh | 6 +++ go/README.md | 5 ++ go/aerecloud.go | 38 ++++++++++++- go/aerecloud_test.go | 25 +++++++++ go/control-negativ.mjs | 4 ++ index.mjs | 17 ++++++ java/README.md | 4 ++ java/control-negativ.mjs | 3 ++ .../java/network/aere/cloud/AereCloud.java | 7 ++- .../network/aere/cloud/TestAereCloud.java | 7 +++ package.json | 2 +- python/README.md | 3 ++ python/aere_cloud/__init__.py | 15 +++++- python/pyproject.toml | 2 +- python/test_aere_cloud.py | 13 +++++ test/control-negativ-js-python.mjs | 53 +++++++++++++++++++ test/sdk.test.mjs | 12 +++++ 22 files changed, 243 insertions(+), 12 deletions(-) create mode 100644 test/control-negativ-js-python.mjs diff --git a/README.md b/README.md index a4b761f..2645aea 100644 --- a/README.md +++ b/README.md @@ -70,6 +70,12 @@ const v = await aere.pqVerify({ scheme: 'ml-dsa-44', interface: 'external', publ // verifier, with the levels it checked and the command that reproduces the verdict. Served by cloud.aere.network since 2026-09-29. const r = await aere.verify(envelope, { chain: 2800 }); // { verdict, levels: [{ level, state, detail }], verifier, reproduce } +// identity and compliance (1.7.0): an AERE Identity presentation (post-quantum credential, selective disclosure, bound to your audience +// and nonce) judged by the published command line; read `verdict` (PARTIAL: some rows could not be judged). No claims come back from +// complianceCheck; with record: true it returns the decision as an AIP-23 envelope without personal data, to notarize. +const id = await aere.identityVerify({ presentation, audience: 'https://your.app/verifier', nonce, trustedIssuers: [issuerId], statusLists: [list] }); +const ok = await aere.complianceCheck({ presentation, policy, audience: 'https://your.app/verifier', nonce, statusLists: [list], record: true }); + // transfer history for an address (token from genesis, native from launch) const tx = await aere.transfers('0xYourAddress', 50); diff --git a/csharp/Aere.Cloud.Tests/Program.cs b/csharp/Aere.Cloud.Tests/Program.cs index 1aa48ab..22041ff 100644 --- a/csharp/Aere.Cloud.Tests/Program.cs +++ b/csharp/Aere.Cloud.Tests/Program.cs @@ -1,4 +1,4 @@ -// Probele clientului .NET (Aere.Cloud 1.6.0), fara framework de probe: fiecare proba tipareste "--- PASS: Nume" sau "--- FAIL: Nume: motiv", +// Probele clientului .NET (Aere.Cloud 1.7.0), fara framework de probe: fiecare proba tipareste "--- PASS: Nume" sau "--- FAIL: Nume: motiv", // iar codul de iesire e numarul de probe picate (0 = toate verzi). Aceleasi garzi ca la clientul Go: cheia obligatorie, cheia trimisa NUMAI // unde trebuie, redirectarile NEurmate (dovedit pe un server local real, fiindca numai handlerul implicit al SDK-ului o poate arata), // exportul jurnalului verificat pe octetii primiti, verificarea unei copii pastrate (tinuta / stearsa / eroare), webhook-ul in timp constant. @@ -49,7 +49,7 @@ await Proba("ReadinessCheiaNumaiLaAttest", async () => await c.PqFinalityAsync(); Cer(!ciot.Ultima.Antete.ContainsKey("x-api-key"), "finalitatea AIP-21 fara cheie"); await c.HealthAsync(); Cer(!ciot.Ultima.Antete.ContainsKey("x-api-key"), "health fara cheie"); await c.AccountAsync(); Cer(ciot.Ultima.Antete.GetValueOrDefault("x-api-key") == Cheie, "account cu cheie"); - Cer(ciot.Ultima.Antete.GetValueOrDefault("user-agent", "").StartsWith("aere-cloud-csharp/1.6.0"), "user-agent-ul clientului"); + Cer(ciot.Ultima.Antete.GetValueOrDefault("user-agent", "").StartsWith("aere-cloud-csharp/1.7.0"), "user-agent-ul clientului"); }); await Proba("PerimetruFinalitateDovada", async () => @@ -77,6 +77,17 @@ await Proba("TrustApiVerify", async () => Cer(ciot.Ultima.Url.EndsWith("/verify"), "fara lant: /verify"); }); +await Proba("IdentityCompliance", async () => +{ + var (c, ciot) = Client(_ => Json(200, "{\"verdict\":\"PARTIAL\"}")); + var r = await c.IdentityVerifyAsync(new Dictionary { ["audience"] = "a", ["nonce"] = "n" }); + Cer(ciot.Ultima.Metoda == "POST" && ciot.Ultima.Url.EndsWith("/v1/identity/verify") && ciot.Ultima.Antete.ContainsKey("x-api-key"), "POST /identity/verify cu cheie: " + ciot.Ultima.Url); + Cer(ciot.Ultima.Corp == "{\"audience\":\"a\",\"nonce\":\"n\"}", "cererea trimisa ca atare: " + ciot.Ultima.Corp); + Cer(r.GetProperty("verdict").GetString() == "PARTIAL", "verdictul intors"); + await c.ComplianceCheckAsync(new Dictionary { ["record"] = true }); + Cer(ciot.Ultima.Url.EndsWith("/v1/compliance/check") && ciot.Ultima.Antete.ContainsKey("x-api-key") && ciot.Ultima.Corp == "{\"record\":true}", "POST /compliance/check: " + ciot.Ultima.Url + " " + ciot.Ultima.Corp); +}); + await Proba("ExportulZilei", async () => { var corp = Encoding.UTF8.GetBytes("{\"a\":1}\n{\"b\":2}\n"); diff --git a/csharp/Aere.Cloud/Aere.Cloud.csproj b/csharp/Aere.Cloud/Aere.Cloud.csproj index 53529f4..6df7bfe 100644 --- a/csharp/Aere.Cloud/Aere.Cloud.csproj +++ b/csharp/Aere.Cloud/Aere.Cloud.csproj @@ -10,7 +10,7 @@ Aere.Cloud Aere.Cloud Aere.Cloud - 1.6.0 + 1.7.0 Aere Network Official .NET client for the Aere Cloud API (Aere Network, chain 2800): RPC, post-quantum verification, the Trust API (AERE Proof Protocol envelopes), chain data, notarization and proofs, quantum readiness, audit log, webhooks and gas sponsorship. MIT diff --git a/csharp/Aere.Cloud/AereCloudClient.cs b/csharp/Aere.Cloud/AereCloudClient.cs index 7212a6e..daa8edd 100644 --- a/csharp/Aere.Cloud/AereCloudClient.cs +++ b/csharp/Aere.Cloud/AereCloudClient.cs @@ -1,6 +1,6 @@ // Aere.Cloud - the official .NET client for the Aere Cloud API (Aere Network, chain 2800). // -// Same routes and semantics as @aere/cloud (JS), aere-cloud (Python), aere-cloud (Java) and the Go client, 1.6.0. No dependency beyond +// Same routes and semantics as @aere/cloud (JS), aere-cloud (Python), aere-cloud (Java) and the Go client, 1.7.0. No dependency beyond // the .NET base library. JSON routes return the server's JSON as a JsonElement, so the integrator binds it to whatever types they // already use; the two routes that return FILES (the audit export and the check of a kept export) return typed results with the // digest computed locally from the bytes received. Non-2xx answers are thrown as AereCloudException (HTTP status, server body, path). @@ -52,7 +52,7 @@ public sealed record KeptCheck(string Day, string State, string Digest, string L public sealed class AereCloudClient : IDisposable { /// Version of this client, sent in the User-Agent (the edge refuses generic library agents on every route). - public const string Version = "1.6.0"; + public const string Version = "1.7.0"; /// The production gateway. public const string DefaultBaseUrl = "https://cloud.aere.network/v1"; @@ -137,6 +137,14 @@ public sealed class AereCloudClient : IDisposable public Task VerifyAsync(object envelope, ulong chain = 0, CancellationToken ct = default) => JsonAsync(HttpMethod.Post, "/verify" + (chain > 0 ? "?chain=" + chain : ""), envelope, true, ct); + /// Identity verification (1.7.0): {presentation, audience, nonce, trustedIssuers?, statusLists?, revocations?, maxAgeSeconds?} -> + /// VALID / PARTIAL / INVALID with every row, judged by the published AERE Identity command line. Read "verdict" (PARTIAL: some rows not judged). + public Task IdentityVerifyAsync(object request, CancellationToken ct = default) => JsonAsync(HttpMethod.Post, "/identity/verify", request, true, ct); + + /// Compliance check (1.7.0): {presentation, policy, audience, nonce, statusLists?, revocations?, record?} -> COMPLIANT / NOT_COMPLIANT + /// with the reasons and, with record, an AIP-23 record without personal data. + public Task ComplianceCheckAsync(object request, CancellationToken ct = default) => JsonAsync(HttpMethod.Post, "/compliance/check", request, true, ct); + /// GET /data/head. public Task DataHeadAsync(CancellationToken ct = default) => JsonAsync(HttpMethod.Get, "/data/head", null, true, ct); /// GET /data/validators. diff --git a/csharp/README.md b/csharp/README.md index b8f45ac..905b4ce 100644 --- a/csharp/README.md +++ b/csharp/README.md @@ -2,8 +2,8 @@ The official .NET client for the [Aere Cloud API](https://aere.network/cloud.html) on Aere Network (chain 2800). **No dependencies** beyond the .NET base library (`System.Net.Http`, `System.Text.Json`, `System.Security.Cryptography`). -Targets .NET 8 (LTS) and runs on later versions. Same routes and semantics as the JS, Python, Go and Java clients 1.6.0, -including `VerifyAsync`, the Trust API. +Targets .NET 8 (LTS) and runs on later versions. Same routes and semantics as the JS, Python, Go and Java clients 1.7.0, +including `VerifyAsync`, the Trust API, and `IdentityVerifyAsync` / `ComplianceCheckAsync`. JSON routes return the server's JSON as a `JsonElement`, so you bind it to the types you already use. The two routes that return files (the audit export and the check of a kept export) return typed results, with the SHA-256 computed diff --git a/csharp/control-negativ.sh b/csharp/control-negativ.sh index 4e5edd1..43ed11e 100644 --- a/csharp/control-negativ.sh +++ b/csharp/control-negativ.sh @@ -24,6 +24,8 @@ P = [ ("webhook acceptat fara semnatura", 'return CryptographicOperations.FixedTimeEquals(want, got);', 'return got.Length >= 0 || CryptographicOperations.FixedTimeEquals(want, got);', "VerifyWebhook"), ("cheia goala acceptata", 'if (string.IsNullOrEmpty(apiKey)) throw', 'if (apiKey == "niciodata-" + "asa") throw', "CheiaEObligatorie"), ("lantul cerut scapat din Trust API", '(chain > 0 ? "?chain=" + chain : "")', '(chain > 0 ? "" : "")', "TrustApiVerify"), + ("conformitatea pe ruta identitatii (1.7.0)", 'JsonAsync(HttpMethod.Post, "/compliance/check", request, true, ct);', 'JsonAsync(HttpMethod.Post, "/identity/verify", request, true, ct);', "IdentityCompliance"), + ("identitatea fara cheie (1.7.0)", 'JsonAsync(HttpMethod.Post, "/identity/verify", request, true, ct);', 'JsonAsync(HttpMethod.Post, "/identity/verify", request, false, ct);', "IdentityCompliance"), ("un rezultat RPC fara forma citit ca zero", ' throw new AereCloudException(200, e.GetRawText(), "/rpc");', ' return 0;', "EroriSiBlockNumber"), ] if sys.argv[1] == "n": print(len(P)); sys.exit(0) @@ -33,6 +35,10 @@ if t.count(vechi) != 1: print("ANCORA %d" % t.count(vechi)); sys.exit(3) open(sys.argv[3], "w").write(t.replace(vechi, nou)); print(nume + "|" + tinta) PY ruleaza() { # -> iesirea probelor in $W/iesire.txt; cod: 0 rulat, 2 nu compileaza + # 2026-09-30: constructie CURATA la fiecare plantare. Cu constructia incrementala, "cheia goala acceptata" a iesit de doua ori din trei + # CONTROL CAZUT: probele rulau cu biblioteca plantarii de DINAINTE (copiata sau nu in bin-ul probelor dupa marcile de timp), deci + # masurau alta plantare decat cea numita. Fara bin/ si obj/ nu ramane nimic de la rularea anterioara. + rm -rf "$W"/Aere.Cloud/bin "$W"/Aere.Cloud/obj "$W"/Aere.Cloud.Tests/bin "$W"/Aere.Cloud.Tests/obj ( cd "$W" && dotnet build Aere.Cloud.Tests -c Release -p:NuGetAudit=false -nodeReuse:false -p:UseSharedCompilation=false > build.log 2>&1 < /dev/null ) || return 2 # exact UN dll de probe in iesirea acestei constructii (un `| head -1` peste mai multe ar alege la intamplare) local dll; dll=$(ls "$W"/Aere.Cloud.Tests/bin/Release/net*/Aere.Cloud.Tests.dll 2>/dev/null); [ "$(printf '%s\n' "$dll" | grep -c .)" = 1 ] || return 2 diff --git a/go/README.md b/go/README.md index b2e957b..90b7169 100644 --- a/go/README.md +++ b/go/README.md @@ -30,6 +30,11 @@ k, err := c.AuditVerifyKept(ctx, "2026-05-01", keptBytes) // Pass the envelope's bytes as received (os.ReadFile): a Go map would sort the keys and change the statement's hash. verdict, err := c.Verify(ctx, json.RawMessage(envelopeBytes), 2800) +// identity and compliance (1.7.0): the presentation, keys, lists and policy travel as their JSON bytes (numbers above 2^53 stay exact) +id, err := c.IdentityVerify(ctx, aerecloud.IdentityVerifyRequest{Presentation: presBytes, Audience: "https://your.app/verifier", Nonce: nonce, + TrustedIssuers: []json.RawMessage{json.RawMessage(`"aere-id:..."`)}, StatusLists: []json.RawMessage{listBytes}}) +ok, err := c.ComplianceCheck(ctx, aerecloud.ComplianceCheckRequest{Presentation: presBytes, Policy: policyBytes, Audience: "https://your.app/verifier", Nonce: nonce, Record: true}) + // verify a webhook delivery (constant-time HMAC-SHA256 over the exact bytes received) ok := aerecloud.VerifyWebhook(rawBody, r.Header.Get("x-aere-signature"), secret) ``` diff --git a/go/aerecloud.go b/go/aerecloud.go index 8194c5a..443f770 100644 --- a/go/aerecloud.go +++ b/go/aerecloud.go @@ -30,7 +30,7 @@ import ( ) // Version of this client, sent in the User-Agent (the edge refuses generic library agents on every route). -const Version = "1.6.0" +const Version = "1.7.0" // DefaultBaseURL is the production gateway. const DefaultBaseURL = "https://cloud.aere.network/v1" @@ -174,6 +174,42 @@ func (c *Client) Verify(ctx context.Context, envelope json.RawMessage, chain uin } return c.jsonReq(ctx, "POST", p, envelope, true) } +// IdentityVerifyRequest is the body of POST /v1/identity/verify (1.7.0). The presentation, keys, status lists and revocations are kept as +// their JSON bytes: a Go map or float64 would change numbers above 2^53 and the canonical text the signatures cover. +type IdentityVerifyRequest struct { + Presentation json.RawMessage `json:"presentation"` + Audience string `json:"audience"` + Nonce string `json:"nonce"` + TrustedIssuers []json.RawMessage `json:"trustedIssuers,omitempty"` + StatusLists []json.RawMessage `json:"statusLists,omitempty"` + Revocations []json.RawMessage `json:"revocations,omitempty"` + MaxAgeSeconds int `json:"maxAgeSeconds,omitempty"` +} + +// ComplianceCheckRequest is the body of POST /v1/compliance/check (1.7.0). +type ComplianceCheckRequest struct { + Presentation json.RawMessage `json:"presentation"` + Policy json.RawMessage `json:"policy"` + Audience string `json:"audience"` + Nonce string `json:"nonce"` + StatusLists []json.RawMessage `json:"statusLists,omitempty"` + Revocations []json.RawMessage `json:"revocations,omitempty"` + Record bool `json:"record,omitempty"` +} + +// IdentityVerify (1.7.0): an AERE Identity presentation judged by the published command line -> VALID / PARTIAL / INVALID with every row, +// the claims shown and the subject when no row failed, and the command (with the moment) that reproduces the verdict. Read "verdict": +// PARTIAL means some rows could not be judged. Needs a gateway that serves POST /v1/identity/verify. +func (c *Client) IdentityVerify(ctx context.Context, req IdentityVerifyRequest) (json.RawMessage, error) { + return c.jsonReq(ctx, "POST", "/identity/verify", req, true) +} + +// ComplianceCheck (1.7.0): your policy judged on an AERE Identity presentation -> COMPLIANT / NOT_COMPLIANT with the reasons, and with +// Record the record as an AIP-23 envelope without personal data. No claims come back. +func (c *Client) ComplianceCheck(ctx context.Context, req ComplianceCheckRequest) (json.RawMessage, error) { + return c.jsonReq(ctx, "POST", "/compliance/check", req, true) +} + func (c *Client) DataHead(ctx context.Context) (json.RawMessage, error) { return c.jsonReq(ctx, "GET", "/data/head", nil, true) } diff --git a/go/aerecloud_test.go b/go/aerecloud_test.go index 47c0933..34c6e0c 100644 --- a/go/aerecloud_test.go +++ b/go/aerecloud_test.go @@ -217,6 +217,31 @@ func TestVerifyPastreazaOrdinea(t *testing.T) { } } +// 1.7.0: cererea de identitate pleaca pe ruta ei, cu cheie, cu prezentarea NESCHIMBATA (un numar peste 2^53 ramane exact) +func TestIdentityVerifyPastreazaOctetii(t *testing.T) { + s, c := porneste(t) + prez := `{"v":1,"kind":"aere-presentation","credential":{"statement":{"claims":{"id":12345678901234567891}}}}` + if _, err := c.IdentityVerify(context.Background(), IdentityVerifyRequest{Presentation: json.RawMessage(prez), Audience: "https://v.example", Nonce: "n-1", TrustedIssuers: []json.RawMessage{json.RawMessage(`"aere-id:00"`)}}); err != nil { + t.Fatal(err) + } + v := s.ultima() + if v.Method != "POST" || v.Path != "/v1/identity/verify" || v.Key == "" || !strings.Contains(v.Body, `"presentation":`+prez) || !strings.Contains(v.Body, `"trustedIssuers":["aere-id:00"]`) || strings.Contains(v.Body, "statusLists") { + t.Fatalf("cererea de identitate: %+v", v) + } + if _, err := c.ComplianceCheck(context.Background(), ComplianceCheckRequest{Presentation: json.RawMessage(prez), Policy: json.RawMessage(`{"id":"p"}`), Audience: "a", Nonce: "n", Record: true}); err != nil { + t.Fatal(err) + } + if v = s.ultima(); v.Path != "/v1/compliance/check" || v.Key == "" || !strings.Contains(v.Body, `"presentation":`+prez) || !strings.Contains(v.Body, `"record":true`) { + t.Fatalf("cererea de conformitate: %+v", v) + } + var m map[string]any + _ = json.Unmarshal([]byte(prez), &m) + prin, _ := json.Marshal(m) + if strings.Contains(string(prin), "12345678901234567891") { + t.Fatal("controlul probei: un map ar fi trebuit sa strice numarul mare") + } +} + func TestVerifyWebhook(t *testing.T) { secret := "AERE-SINTETIC-secret" corp := []byte(`{"event":"pq-anchor","data":{"height":42}}`) diff --git a/go/control-negativ.mjs b/go/control-negativ.mjs index 5675a9e..9cb2cae 100644 --- a/go/control-negativ.mjs +++ b/go/control-negativ.mjs @@ -24,6 +24,10 @@ const PLANTARI = [ // 2026-09-29: plicul trecut printr-un map de Go (cheile sortate = alt text canonic, un plic valid ar iesi INVALID la Trust API) ['Verify trece plicul printr-un map', 'return c.jsonReq(ctx, "POST", p, envelope, true)', 'var m map[string]any\n\t_ = json.Unmarshal(envelope, &m)\n\treturn c.jsonReq(ctx, "POST", p, m, true)', 'TestVerifyPastreazaOrdinea'], + // 1.7.0: prezentarea trecuta printr-un map (numerele mari devin float64) si ruta de conformitate trimisa la identitate + ['IdentityVerify trece prezentarea printr-un map', 'return c.jsonReq(ctx, "POST", "/identity/verify", req, true)', + 'var m map[string]any\n\t_ = json.Unmarshal(req.Presentation, &m)\n\tb, _ := json.Marshal(m)\n\treq.Presentation = b\n\treturn c.jsonReq(ctx, "POST", "/identity/verify", req, true)', 'TestIdentityVerifyPastreazaOctetii'], + ['ComplianceCheck pe ruta de identitate', 'return c.jsonReq(ctx, "POST", "/compliance/check", req, true)', 'return c.jsonReq(ctx, "POST", "/identity/verify", req, true)', 'TestIdentityVerifyPastreazaOctetii'], ]; function ruleaza(src) { diff --git a/index.mjs b/index.mjs index c421c4f..35e951b 100644 --- a/index.mjs +++ b/index.mjs @@ -75,6 +75,23 @@ export class AereCloud { return this._req('POST', '/verify' + (chain ? `?chain=${encodeURIComponent(chain)}` : ''), { body: envelope }); } + /** + * identity verification (1.7.0): an AERE Identity presentation judged by the published command line -> VALID / PARTIAL / INVALID with every + * row, the claims shown and the subject when no row failed, and the command (with the moment) that reproduces the verdict. Read `verdict`: + * PARTIAL means some rows could not be judged (no trusted issuers, no status list). Needs a gateway that serves POST /v1/identity/verify. + * @param {{presentation:object, audience:string, nonce:string, trustedIssuers?:(string|object)[], statusLists?:object[], revocations?:object[], maxAgeSeconds?:number}} request + * @returns {Promise<{verdict:'VALID'|'PARTIAL'|'INVALID', notJudged:number, subject:object|null, claims:object|null, rows:object[], judgedAt:string, verifier:object, reproduce:string}>} + */ + identityVerify(request) { return this._req('POST', '/identity/verify', { body: request }); } + + /** + * compliance check (1.7.0): your policy judged on an AERE Identity presentation -> COMPLIANT / NOT_COMPLIANT with the reasons, and with + * `record: true` the record of the decision as an AIP-23 envelope without personal data (notarize it with notarize()). No claims come back. + * @param {{presentation:object, policy:object, audience:string, nonce:string, statusLists?:object[], revocations?:object[], record?:boolean}} request + * @returns {Promise<{verdict:'COMPLIANT'|'NOT_COMPLIANT', compliant:boolean, reasons:string[], policyId:string, policyHash:string, presentationHash:string, record?:object, judgedAt:string, verifier:object, reproduce:string}>} + */ + complianceCheck(request) { return this._req('POST', '/compliance/check', { body: request }); } + // ---- chain data ---- dataHead() { return this._req('GET', '/data/head'); } validators() { return this._req('GET', '/data/validators'); } diff --git a/java/README.md b/java/README.md index 49d6131..0929bff 100644 --- a/java/README.md +++ b/java/README.md @@ -28,6 +28,10 @@ String acct = aere.sponsorCreatePqAccount("0x09" + falconPkHex, 0); // Trust API (1.6.0): any AERE Proof Protocol (AIP-23) envelope (JSON text) -> VALID / INVALID / PARTIAL with the levels checked String verdict = aere.verify(envelopeJson, 2800); +// identity and compliance (1.7.0): the request as JSON text, sent as given; read "verdict" (PARTIAL: some rows not judged) +String id = aere.identityVerify("{\"presentation\":" + presentationJson + ",\"audience\":\"https://your.app/verifier\",\"nonce\":\"" + nonce + "\"}"); +String ok = aere.complianceCheck(requestWithPolicyJson); + // verify a webhook delivery (constant-time HMAC) — never trust one without this: boolean ok = AereCloud.verifyWebhook(rawBodyBytes, request.header("x-aere-signature"), secret); ``` diff --git a/java/control-negativ.mjs b/java/control-negativ.mjs index fa4d844..c88831b 100644 --- a/java/control-negativ.mjs +++ b/java/control-negativ.mjs @@ -27,6 +27,9 @@ const PLANTARI = [ 'if (r.status < 200 || r.status >= 300) return new KeptCheck(day, "held", null, local, false, null);\n String dig = sha256Hex(r.octeti);', 'auditVerifyKept'], // 1.6.1: HttpURLConnection urmeaza redirectarile implicit; fara oprire, cheia pleaca spre gazda din Location ['redirectarea urmata (cheia pleaca spre alta gazda)', 'c.setInstanceFollowRedirects(false);', 'c.setInstanceFollowRedirects(true);', '302'], + // 1.7.0: conformitatea trimisa pe ruta identitatii; identitatea fara cheie + ['complianceCheck pe ruta identitatii', 'return req("POST", "/compliance/check", requestJson, true);', 'return req("POST", "/identity/verify", requestJson, true);', '1.7.0'], + ['identityVerify fara cheie', 'return req("POST", "/identity/verify", requestJson, true);', 'return req("POST", "/identity/verify", requestJson, false);', '1.7.0'], ]; // 1.6.1: poarta Android = clasa compileaza cu `javac --release 8`. Un API de dupa Java 8 trece probele pe JDK 21 si trebuie prins // numai de poarta; plantarea NU e o proba, e o compilare care TREBUIE sa cada. diff --git a/java/src/main/java/network/aere/cloud/AereCloud.java b/java/src/main/java/network/aere/cloud/AereCloud.java index c7bb962..ccf5cf8 100644 --- a/java/src/main/java/network/aere/cloud/AereCloud.java +++ b/java/src/main/java/network/aere/cloud/AereCloud.java @@ -32,7 +32,8 @@ public final class AereCloud { // 1.6.1 (2026-09-28): transportul pe HttpURLConnection in loc de java.net.http (care NU exista pe Android), fara redirectari // urmate (ca inainte: HttpClient nu le urma implicit); URLEncoder cu numele setului de caractere (varianta cu Charset e // Java 10+). Aceeasi suprafata publica. - public static final String VERSIUNE = "1.6.1"; + // 1.7.0 (2026-09-30): identityVerify() si complianceCheck() (POST /v1/identity/verify, /v1/compliance/check) + public static final String VERSIUNE = "1.7.0"; private static final String BAZA_IMPLICITA = "https://cloud.aere.network/v1"; private static final int TIMP_MS = 30000; @@ -163,6 +164,10 @@ public final class AereCloud { /** the Trust API (1.6.0): one AERE Proof Protocol (AIP-23) envelope of any kind -> VALID / INVALID / PARTIAL with the levels checked and the command that reproduces the verdict; the judgement is the published reference verifier. Needs a gateway that serves POST /v1/verify. chain: 2800 or 28001; 0 = the chain the envelope declares. */ public String verify(String envelopeJson, long chain) { return req("POST", "/verify" + (chain > 0 ? "?chain=" + chain : ""), envelopeJson, true); } public String verify(String envelopeJson) { return verify(envelopeJson, 0); } + /** identity verification (1.7.0): {presentation, audience, nonce, trustedIssuers?, statusLists?, revocations?, maxAgeSeconds?} -> VALID / PARTIAL / INVALID with every row, judged by the published AERE Identity command line; read "verdict" (PARTIAL: some rows not judged). The request is sent as the JSON you give. */ + public String identityVerify(String requestJson) { return req("POST", "/identity/verify", requestJson, true); } + /** compliance check (1.7.0): {presentation, policy, audience, nonce, statusLists?, revocations?, record?} -> COMPLIANT / NOT_COMPLIANT with the reasons and, with record, an AIP-23 record without personal data. */ + public String complianceCheck(String requestJson) { return req("POST", "/compliance/check", requestJson, true); } // ---- chain data ---- public String dataHead() { return req("GET", "/data/head", null, true); } diff --git a/java/src/test/java/network/aere/cloud/TestAereCloud.java b/java/src/test/java/network/aere/cloud/TestAereCloud.java index 0bdc4a1..b2765d6 100644 --- a/java/src/test/java/network/aere/cloud/TestAereCloud.java +++ b/java/src/test/java/network/aere/cloud/TestAereCloud.java @@ -120,6 +120,13 @@ public final class TestAereCloud { lc.verify("{}"); cere(ultima.get()[1].equals("/v1/verify"), ultima.get()[1]); }); + proba("1.7.0: identityVerify si complianceCheck trimit cererea neschimbata pe rutele lor, cu cheie", () -> { + lc.identityVerify("{\"presentation\":{\"v\":1},\"audience\":\"a\",\"nonce\":\"n\"}"); + String[] v = ultima.get(); + cere(v[0].equals("POST") && v[1].equals("/v1/identity/verify") && v[2] != null && v[3].equals("{\"presentation\":{\"v\":1},\"audience\":\"a\",\"nonce\":\"n\"}"), String.join(" | ", v)); + lc.complianceCheck("{\"policy\":{}}"); + cere(ultima.get()[1].equals("/v1/compliance/check") && ultima.get()[2] != null && ultima.get()[3].equals("{\"policy\":{}}"), String.join(" | ", ultima.get())); + }); proba("1.5.0: auditExport: jsonl verified=true pe octetii care dau digestul, FALSE pe alti octeti, null la cef; un 400 se ridica", () -> { raspunsuri.put("/v1/account/audit", new Object[]{200, zi, new String[]{"x-aere-digest", dZi, "x-aere-day", "2026-05-01", "x-aere-entries", "1"}}); AereCloud.AuditExport e = lc.auditExport("2026-05-01", "jsonl"); diff --git a/package.json b/package.json index 507d980..d435cb7 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@aere/cloud", - "version": "1.6.0", + "version": "1.7.0", "description": "Official client for the Aere Cloud API: keyed RPC, post-quantum verification, chain data, notarization, webhooks and gas sponsorship on Aere Network (chain 2800).", "type": "module", "main": "index.mjs", diff --git a/python/README.md b/python/README.md index 52d5d8e..6837e86 100644 --- a/python/README.md +++ b/python/README.md @@ -41,6 +41,9 @@ receipt = aere.notarize("0x" + sha256_hex) # Trust API (1.6.0): any AERE Proof Protocol (AIP-23) envelope -> VALID / INVALID / PARTIAL, with the levels checked r = aere.verify(envelope, chain=2800) # {"verdict", "levels", "verifier", "reproduce"} +# identity and compliance (1.7.0): an AERE Identity presentation judged by the published command line; read "verdict" +idv = aere.identity_verify({"presentation": presentation, "audience": "https://your.app/verifier", "nonce": nonce, "trustedIssuers": [issuer_id]}) +cc = aere.compliance_check({"presentation": presentation, "policy": policy, "audience": "https://your.app/verifier", "nonce": nonce, "record": True}) proof = aere.proof_of("0x" + sha256_hex) # a smart account with a POST-QUANTUM owner (Falcon-512), gas sponsored: diff --git a/python/aere_cloud/__init__.py b/python/aere_cloud/__init__.py index 2a18cae..3f0d8df 100644 --- a/python/aere_cloud/__init__.py +++ b/python/aere_cloud/__init__.py @@ -14,7 +14,7 @@ import urllib.request import urllib.error from typing import Any, Optional -__version__ = "1.6.0" +__version__ = "1.7.0" __all__ = ["AereCloud", "AereCloudError", "verify_webhook"] _BAZA = "https://cloud.aere.network/v1" @@ -111,6 +111,19 @@ class AereCloud: chain: 2800 or 28001; None = the chain the envelope declares.""" return self._req("POST", "/verify" + (f"?chain={int(chain)}" if chain else ""), envelope) + def identity_verify(self, request: dict) -> dict: + """identity verification (1.7.0): an AERE Identity presentation judged by the published command line -> VALID / PARTIAL / INVALID + with every row, the claims shown and the subject when no row failed, and the command (with the moment) that reproduces the verdict. + request: {presentation, audience, nonce, trustedIssuers?, statusLists?, revocations?, maxAgeSeconds?}. Read 'verdict': PARTIAL + means some rows could not be judged. Needs a gateway that serves POST /v1/identity/verify.""" + return self._req("POST", "/identity/verify", request) + + def compliance_check(self, request: dict) -> dict: + """compliance check (1.7.0): your policy judged on an AERE Identity presentation -> COMPLIANT / NOT_COMPLIANT with the reasons, + and with record=True the record as an AIP-23 envelope without personal data. No claims come back. + request: {presentation, policy, audience, nonce, statusLists?, revocations?, record?}.""" + return self._req("POST", "/compliance/check", request) + # ---- chain data ---- def data_head(self) -> dict: return self._req("GET", "/data/head") diff --git a/python/pyproject.toml b/python/pyproject.toml index 7faa2aa..3f88f6e 100644 --- a/python/pyproject.toml +++ b/python/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "aere-cloud" -version = "1.6.0" +version = "1.7.0" description = "Official Python client for the Aere Cloud API (Aere Network, chain 2800): RPC, post-quantum verification, chain data, notarization, webhooks, gas sponsorship." readme = "README.md" requires-python = ">=3.9" diff --git a/python/test_aere_cloud.py b/python/test_aere_cloud.py index a570264..a7fb713 100644 --- a/python/test_aere_cloud.py +++ b/python/test_aere_cloud.py @@ -91,6 +91,18 @@ def t_verify(): assert tr.cereri[0].get_header("X-api-key") == "k", "verify cere cheia" assert json.loads(tr.cereri[0].data.decode()) == {"statement": {}, "statementHash": "0x00"} +def t_identitate(): + tr = Transport([(200, {"verdict": "PARTIAL", "notJudged": 1}), (200, {"verdict": "COMPLIANT", "compliant": True})]) + c = AereCloud("k", opener=tr) + cerere = {"presentation": {"kind": "aere-presentation", "v": 1}, "audience": "https://v.example", "nonce": "n-1"} + assert c.identity_verify(cerere)["verdict"] == "PARTIAL" + assert tr.cereri[0].full_url.endswith("/identity/verify") and tr.cereri[0].get_method() == "POST" + assert tr.cereri[0].get_header("X-api-key") == "k", "identity_verify cere cheia" + assert json.loads(tr.cereri[0].data.decode()) == cerere + cc = {**cerere, "policy": {"id": "p"}, "record": True} + assert c.compliance_check(cc)["verdict"] == "COMPLIANT" + assert tr.cereri[1].full_url.endswith("/compliance/check") and json.loads(tr.cereri[1].data.decode()) == cc + def t_pq_sponsor(): tr = Transport([(200, {"txHash": "0xab", "account": "0xcd", "alreadyDeployed": False})]) c = AereCloud("k", opener=tr) @@ -114,6 +126,7 @@ proba("rpc impacheteaza corect si ridica eroarea RPC", t_rpc) proba("un status de eroare devine AereCloudError cu corpul serverului", t_eroare_http) proba("rutele de date construiesc corect query-ul", t_query) proba("verify (Trust API, 1.6.0): ruta, ?chain, cheia si plicul neschimbat", t_verify) +proba("identity_verify si compliance_check (1.7.0): rutele, cheia si cererea neschimbata", t_identitate) proba("sponsor_create_pq_account: corp si cale corecte", t_pq_sponsor) proba("verify_webhook: buna trece; stricata, prelungita, corp atins pica", t_webhook) diff --git a/test/control-negativ-js-python.mjs b/test/control-negativ-js-python.mjs new file mode 100644 index 0000000..76ed638 --- /dev/null +++ b/test/control-negativ-js-python.mjs @@ -0,0 +1,53 @@ +// Controlul negativ al probelor offline ale clientilor JavaScript si Python pentru rutele 1.7.0 (identityVerify / identity_verify, +// complianceCheck / compliance_check): fiecare paznic se strica intr-o COPIE a SDK-ului (depozitul nu se atinge), proba ruleaza pe copie +// si proba numita TREBUIE sa pice. Trei stari: ROSU cum trebuia, CONTROL CAZUT (proba a ramas verde), STRICAT (ancora nu apare exact o +// data, sau probele nu au ajuns la rezumat). La urma, copia neatinsa trebuie sa fie verde (martorul). +// node sdk/test/control-negativ-js-python.mjs -> 0 toate rosii si martorul verde, 1 altfel +import fs from 'node:fs'; +import os from 'node:os'; +import path from 'node:path'; +import { spawnSync } from 'node:child_process'; +import { fileURLToPath } from 'node:url'; + +const SDK = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..'); +const PLANTARI = [ + // [nume, fisier, vechi, nou, limbaj, proba (fragment din numele ei)] + ['JS: complianceCheck pe ruta identitatii', 'index.mjs', "complianceCheck(request) { return this._req('POST', '/compliance/check', { body: request }); }", "complianceCheck(request) { return this._req('POST', '/identity/verify', { body: request }); }", 'js', '1.7.0: identityVerify si complianceCheck'], + ['JS: identityVerify trimite o cerere schimbata', 'index.mjs', "identityVerify(request) { return this._req('POST', '/identity/verify', { body: request }); }", "identityVerify(request) { return this._req('POST', '/identity/verify', { body: { ...request, maxAgeSeconds: 3600 } }); }", 'js', '1.7.0: identityVerify si complianceCheck'], + ['Python: compliance_check pe ruta identitatii', 'python/aere_cloud/__init__.py', 'return self._req("POST", "/compliance/check", request)', 'return self._req("POST", "/identity/verify", request)', 'py', 'identity_verify si compliance_check'], + ['Python: identity_verify trimite o cerere schimbata', 'python/aere_cloud/__init__.py', 'return self._req("POST", "/identity/verify", request)', 'return self._req("POST", "/identity/verify", {**request, "maxAgeSeconds": 3600})', 'py', 'identity_verify si compliance_check'], +]; +function copie() { + const d = fs.mkdtempSync(path.join(os.tmpdir(), 'aere-sdk-cn-')); + for (const f of ['index.mjs', 'package.json', 'test/sdk.test.mjs', 'python/aere_cloud/__init__.py', 'python/test_aere_cloud.py', 'python/pyproject.toml']) { + fs.mkdirSync(path.dirname(path.join(d, f)), { recursive: true }); fs.copyFileSync(path.join(SDK, f), path.join(d, f)); + } + return d; +} +function ruleaza(d, limbaj) { + const r = limbaj === 'js' + ? spawnSync(process.execPath, ['test/sdk.test.mjs'], { cwd: d, encoding: 'utf8', env: { ...process.env, AERE_CHEIE: '' } }) + : spawnSync(process.platform === 'win32' ? 'python' : 'python3', ['test_aere_cloud.py'], { cwd: path.join(d, 'python'), encoding: 'utf8', env: { ...process.env, AERE_KEY: '' } }); + const out = (r.stdout || '') + (r.stderr || ''); + // JS: ' teste trecute' si randuri ' ESEC '; Python: ' treceri, esecuri' si randuri ' PICA ' + return { out, rulat: /\d+ (teste trecute|treceri)/.test(out), rosii: out.split('\n').filter((l) => /^\s*(ESEC|PICA)\b/.test(l)) }; +} +let rele = 0; +for (const limbaj of ['js', 'py']) { + const d = copie(); const m = ruleaza(d, limbaj); fs.rmSync(d, { recursive: true, force: true }); + if (m.rulat && !m.rosii.length) console.log(` martor ${limbaj}: copia neatinsa verde`); else { rele++; console.log(` STRICAT martorul ${limbaj} nu e verde (${m.rosii.length} rosii, rulat ${m.rulat})`); } +} +for (const [nume, fisier, vechi, nou, limbaj, tinta] of PLANTARI) { + const d = copie(); + try { + const f = path.join(d, fisier); const t = fs.readFileSync(f, 'utf8'); + if (t.split(vechi).length !== 2) { rele++; console.log(` STRICAT ${nume}: ancora apare de ${t.split(vechi).length - 1} ori`); continue; } + fs.writeFileSync(f, t.replace(vechi, nou)); + const r = ruleaza(d, limbaj); + if (!r.rulat) { rele++; console.log(` STRICAT ${nume}: probele nu au ajuns la rezumat`); continue; } + if (r.rosii.some((l) => l.includes(tinta))) console.log(` ROSU cum trebuia ${nume}`); + else { rele++; console.log(` CONTROL CAZUT ${nume}: '${tinta}' a ramas verde`); } + } finally { fs.rmSync(d, { recursive: true, force: true }); } +} +console.log(rele ? `CONTROL NEGATIV: ${rele} probleme` : `CONTROL NEGATIV TRECUT (${PLANTARI.length} din ${PLANTARI.length} rosii, martorii verzi)`); +process.exitCode = rele ? 1 : 0; diff --git a/test/sdk.test.mjs b/test/sdk.test.mjs index 0b00778..e231063 100644 --- a/test/sdk.test.mjs +++ b/test/sdk.test.mjs @@ -61,6 +61,18 @@ await test('un status HTTP de eroare devine AereCloudError cu corpul serverului' (e) => e instanceof AereCloudError && e.status === 400 && e.body.error === 'bad_publicKey'); }); +await test('1.7.0: identityVerify si complianceCheck trimit cererea neschimbata pe rutele lor, cu cheie', async () => { + const rute = { 'POST /identity/verify': { status: 200, corp: { verdict: 'PARTIAL', notJudged: 1 } }, 'POST /compliance/check': { status: 200, corp: { verdict: 'COMPLIANT', compliant: true } } }; + const c = new AereCloud({ apiKey: 'ak2800.0xAAA.AERE-SINTETIC-s', baseUrl: BAZA_TEST, fetch: fetchFals(rute) }); + const cerere = { presentation: { kind: 'aere-presentation', v: 1 }, audience: 'https://v.example', nonce: 'n-1', trustedIssuers: ['aere-id:' + 'ab'.repeat(20)] }; + assert.equal((await c.identityVerify(cerere)).verdict, 'PARTIAL'); + assert.deepEqual(rute['POST /identity/verify']._vazut.body, cerere); + assert.ok(rute['POST /identity/verify']._vazut.headers['x-api-key'], 'identityVerify cere cheia'); + const cc = { presentation: { kind: 'aere-presentation', v: 1 }, policy: { id: 'p', trustedIssuers: ['aere-id:' + 'ab'.repeat(20)], require: [{ claim: 'x', present: true }] }, audience: 'a', nonce: 'n', record: true }; + assert.equal((await c.complianceCheck(cc)).verdict, 'COMPLIANT'); + assert.deepEqual(rute['POST /compliance/check']._vazut.body, cc); +}); + await test('rutele de date construiesc corect query-ul', async () => { const rute = { 'GET /data/anchors?limit=3': { status: 200, corp: { anchors: [] } },