aqta-verify-receipt 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +17 -0
- package/README.md +63 -0
- package/dist/index.d.ts +70 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +175 -0
- package/dist/index.js.map +1 -0
- package/dist/index.test.d.ts +6 -0
- package/dist/index.test.d.ts.map +1 -0
- package/dist/index.test.js +85 -0
- package/dist/index.test.js.map +1 -0
- package/package.json +53 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
Apache License
|
|
2
|
+
Version 2.0, January 2004
|
|
3
|
+
http://www.apache.org/licenses/
|
|
4
|
+
|
|
5
|
+
Copyright 2026 Aqta Technologies Ltd
|
|
6
|
+
|
|
7
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
8
|
+
you may not use this file except in compliance with the License.
|
|
9
|
+
You may obtain a copy of the License at
|
|
10
|
+
|
|
11
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
12
|
+
|
|
13
|
+
Unless required by applicable law or agreed to in writing, software
|
|
14
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
15
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
16
|
+
See the License for the specific language governing permissions and
|
|
17
|
+
limitations under the License.
|
package/README.md
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# aqta-verify-receipt
|
|
2
|
+
|
|
3
|
+
Independent verifier for [AqtaCore](https://app.aqta.ai) attestation receipts.
|
|
4
|
+
Checks the Ed25519 signature on an enforcement-attestation receipt using only
|
|
5
|
+
the published public key: **no dependency on Aqta's servers**.
|
|
6
|
+
|
|
7
|
+
## Why this exists
|
|
8
|
+
|
|
9
|
+
AqtaCore returns a signed receipt with every AI enforcement decision.
|
|
10
|
+
Regulators, auditors, and internal compliance teams need to verify those
|
|
11
|
+
receipts independently, without trusting the issuer. This package is the
|
|
12
|
+
reference implementation of that verifier, maintained by Aqta under the open
|
|
13
|
+
[ATTESTATION-v1](https://github.com/Aqta-ai/attestation-spec/blob/main/spec/ATTESTATION-v1.md)
|
|
14
|
+
format specification.
|
|
15
|
+
|
|
16
|
+
## Install
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
npm install aqta-verify-receipt
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## Usage
|
|
23
|
+
|
|
24
|
+
```ts
|
|
25
|
+
import { verifyReceipt, fetchPublishedPublicKey } from 'aqta-verify-receipt';
|
|
26
|
+
|
|
27
|
+
// One-time: fetch and pin the issuer's public key.
|
|
28
|
+
const trustedPublicKey = await fetchPublishedPublicKey();
|
|
29
|
+
|
|
30
|
+
// Per receipt:
|
|
31
|
+
const result = verifyReceipt(receipt, { trustedPublicKey });
|
|
32
|
+
|
|
33
|
+
if (!result.valid) {
|
|
34
|
+
throw new Error(`Receipt invalid: ${result.reason}`);
|
|
35
|
+
}
|
|
36
|
+
console.log('Receipt verified without contacting Aqta.');
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## API
|
|
40
|
+
|
|
41
|
+
### `verifyReceipt(receipt, options?) → { valid, reason? }`
|
|
42
|
+
|
|
43
|
+
Verifies an attestation receipt against the declared (or pinned) public key.
|
|
44
|
+
|
|
45
|
+
Options:
|
|
46
|
+
- `trustedPublicKey`: base64url public key. If set, the receipt's `public_key`
|
|
47
|
+
field must match byte for byte. Strongly recommended for production.
|
|
48
|
+
- `strictFields`: if `true` (default), any unknown top-level field causes
|
|
49
|
+
rejection, per ATTESTATION-v1 §4.
|
|
50
|
+
|
|
51
|
+
### `fetchPublishedPublicKey(url?) → Promise<string>`
|
|
52
|
+
|
|
53
|
+
Fetches the AqtaCore public key from
|
|
54
|
+
`https://app.aqta.ai/security/pubkey.txt`. Pass a custom URL for self-hosted
|
|
55
|
+
issuers.
|
|
56
|
+
|
|
57
|
+
## Receipt format
|
|
58
|
+
|
|
59
|
+
See [ATTESTATION-v1](https://github.com/Aqta-ai/attestation-spec/blob/main/spec/ATTESTATION-v1.md).
|
|
60
|
+
|
|
61
|
+
## Licence
|
|
62
|
+
|
|
63
|
+
Apache-2.0. See `LICENSE`.
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* aqta-verify-receipt
|
|
3
|
+
*
|
|
4
|
+
* Independent verifier for AqtaCore attestation receipts (ATTESTATION-v1).
|
|
5
|
+
* Verifies the Ed25519 signature on a receipt using only the published
|
|
6
|
+
* public key: no dependency on any third-party server.
|
|
7
|
+
*
|
|
8
|
+
* Spec: https://github.com/Aqta-ai/attestation-spec/blob/main/spec/ATTESTATION-v1.md
|
|
9
|
+
*/
|
|
10
|
+
/** Required top-level fields of an ATTESTATION-v1 receipt. */
|
|
11
|
+
export interface AttestationReceipt {
|
|
12
|
+
v: number;
|
|
13
|
+
attestation_id: string;
|
|
14
|
+
trace_id: string;
|
|
15
|
+
org_id: string;
|
|
16
|
+
request_hash: string;
|
|
17
|
+
model: string;
|
|
18
|
+
outcome: 'ALLOWED' | 'BLOCKED' | 'SUPPRESSED' | 'PASSED';
|
|
19
|
+
policy_applied: string[];
|
|
20
|
+
cost_prevented_eur: number;
|
|
21
|
+
timestamp: string;
|
|
22
|
+
public_key: string;
|
|
23
|
+
signature: string;
|
|
24
|
+
}
|
|
25
|
+
export interface VerifyOptions {
|
|
26
|
+
/**
|
|
27
|
+
* The trusted public key (base64url, no padding). If supplied, the receipt
|
|
28
|
+
* is rejected unless its `public_key` field matches byte-for-byte. This
|
|
29
|
+
* prevents an attacker from substituting a different key.
|
|
30
|
+
*
|
|
31
|
+
* If omitted, the receipt is verified against whatever public key it
|
|
32
|
+
* declares. This is lower trust but useful for offline / one-shot checks.
|
|
33
|
+
*/
|
|
34
|
+
trustedPublicKey?: string;
|
|
35
|
+
/**
|
|
36
|
+
* If true, unknown top-level fields in the receipt cause rejection. Default
|
|
37
|
+
* true per spec §4: "Verifiers MUST reject receipts containing unknown
|
|
38
|
+
* top-level fields."
|
|
39
|
+
*/
|
|
40
|
+
strictFields?: boolean;
|
|
41
|
+
}
|
|
42
|
+
export interface VerifyResult {
|
|
43
|
+
valid: boolean;
|
|
44
|
+
reason?: string;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Verify an AqtaCore attestation receipt.
|
|
48
|
+
*
|
|
49
|
+
* @param receipt The full receipt object (including `signature`).
|
|
50
|
+
* @param options Optional verification constraints.
|
|
51
|
+
* @returns `{ valid: true }` if the signature is valid under the
|
|
52
|
+
* declared (and optionally pinned) public key; otherwise
|
|
53
|
+
* `{ valid: false, reason: string }`.
|
|
54
|
+
*
|
|
55
|
+
* @example
|
|
56
|
+
* const result = verifyReceipt(receipt, {
|
|
57
|
+
* trustedPublicKey: 'gUoUhIvptKAoLTnry3VrDtOQEWggGQveLrHFVrfNqmE',
|
|
58
|
+
* });
|
|
59
|
+
* if (!result.valid) throw new Error(`Receipt invalid: ${result.reason}`);
|
|
60
|
+
*/
|
|
61
|
+
export declare function verifyReceipt(receipt: unknown, options?: VerifyOptions): VerifyResult;
|
|
62
|
+
/**
|
|
63
|
+
* Fetch the AqtaCore public key from the published URL and return it as a
|
|
64
|
+
* base64url string ready to pass to `verifyReceipt` as `trustedPublicKey`.
|
|
65
|
+
*
|
|
66
|
+
* Callers SHOULD pin this value once retrieved, rather than re-fetching on
|
|
67
|
+
* every verification.
|
|
68
|
+
*/
|
|
69
|
+
export declare function fetchPublishedPublicKey(url?: string): Promise<string>;
|
|
70
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAKH,8DAA8D;AAC9D,MAAM,WAAW,kBAAkB;IACjC,CAAC,EAAE,MAAM,CAAC;IACV,cAAc,EAAE,MAAM,CAAC;IACvB,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;IACf,YAAY,EAAE,MAAM,CAAC;IACrB,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,SAAS,GAAG,SAAS,GAAG,YAAY,GAAG,QAAQ,CAAC;IACzD,cAAc,EAAE,MAAM,EAAE,CAAC;IACzB,kBAAkB,EAAE,MAAM,CAAC;IAC3B,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,aAAa;IAC5B;;;;;;;OAOG;IACH,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAE1B;;;;OAIG;IACH,YAAY,CAAC,EAAE,OAAO,CAAC;CACxB;AAED,MAAM,WAAW,YAAY;IAC3B,KAAK,EAAE,OAAO,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAoED;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,aAAa,CAC3B,OAAO,EAAE,OAAO,EAChB,OAAO,GAAE,aAAkB,GAC1B,YAAY,CAuEd;AAED;;;;;;GAMG;AACH,wBAAsB,uBAAuB,CAC3C,GAAG,SAA4C,GAC9C,OAAO,CAAC,MAAM,CAAC,CAMjB"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* aqta-verify-receipt
|
|
4
|
+
*
|
|
5
|
+
* Independent verifier for AqtaCore attestation receipts (ATTESTATION-v1).
|
|
6
|
+
* Verifies the Ed25519 signature on a receipt using only the published
|
|
7
|
+
* public key: no dependency on any third-party server.
|
|
8
|
+
*
|
|
9
|
+
* Spec: https://github.com/Aqta-ai/attestation-spec/blob/main/spec/ATTESTATION-v1.md
|
|
10
|
+
*/
|
|
11
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
12
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
13
|
+
};
|
|
14
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
15
|
+
exports.verifyReceipt = verifyReceipt;
|
|
16
|
+
exports.fetchPublishedPublicKey = fetchPublishedPublicKey;
|
|
17
|
+
const tweetnacl_1 = __importDefault(require("tweetnacl"));
|
|
18
|
+
const tweetnacl_util_1 = __importDefault(require("tweetnacl-util"));
|
|
19
|
+
const ALLOWED_OUTCOMES = new Set([
|
|
20
|
+
'ALLOWED',
|
|
21
|
+
'BLOCKED',
|
|
22
|
+
'SUPPRESSED',
|
|
23
|
+
'PASSED',
|
|
24
|
+
]);
|
|
25
|
+
const REQUIRED_FIELDS = new Set([
|
|
26
|
+
'v',
|
|
27
|
+
'attestation_id',
|
|
28
|
+
'trace_id',
|
|
29
|
+
'org_id',
|
|
30
|
+
'request_hash',
|
|
31
|
+
'model',
|
|
32
|
+
'outcome',
|
|
33
|
+
'policy_applied',
|
|
34
|
+
'cost_prevented_eur',
|
|
35
|
+
'timestamp',
|
|
36
|
+
'public_key',
|
|
37
|
+
'signature',
|
|
38
|
+
]);
|
|
39
|
+
/** Decode a base64url string (no padding) into a Uint8Array. */
|
|
40
|
+
function base64urlDecode(s) {
|
|
41
|
+
const padded = s + '='.repeat((4 - (s.length % 4)) % 4);
|
|
42
|
+
const b64 = padded.replace(/-/g, '+').replace(/_/g, '/');
|
|
43
|
+
return tweetnacl_util_1.default.decodeBase64(b64);
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Canonical JSON serialisation per ATTESTATION-v1 §6.
|
|
47
|
+
*
|
|
48
|
+
* Equivalent to Python's:
|
|
49
|
+
* json.dumps(payload, sort_keys=True, separators=(",", ":")).encode("utf-8")
|
|
50
|
+
*
|
|
51
|
+
* This function handles only the receipt shape (flat object with scalar +
|
|
52
|
+
* array values). It is NOT a general-purpose canonical-JSON library.
|
|
53
|
+
*/
|
|
54
|
+
function canonicalise(payload) {
|
|
55
|
+
const keys = Object.keys(payload).sort();
|
|
56
|
+
const parts = [];
|
|
57
|
+
for (const k of keys) {
|
|
58
|
+
parts.push(`${JSON.stringify(k)}:${canonicalValue(payload[k])}`);
|
|
59
|
+
}
|
|
60
|
+
return new TextEncoder().encode('{' + parts.join(',') + '}');
|
|
61
|
+
}
|
|
62
|
+
function canonicalValue(v) {
|
|
63
|
+
if (v === null || v === undefined) {
|
|
64
|
+
return 'null';
|
|
65
|
+
}
|
|
66
|
+
if (Array.isArray(v)) {
|
|
67
|
+
return '[' + v.map(canonicalValue).join(',') + ']';
|
|
68
|
+
}
|
|
69
|
+
if (typeof v === 'number') {
|
|
70
|
+
// Preserving the serialisation of floats such as 0.0 as "0.0" is NOT
|
|
71
|
+
// spec. JSON.stringify(0) yields "0" and that is what Python json.dumps
|
|
72
|
+
// yields for float 0.0 when sort_keys=True on a rounded value. For the
|
|
73
|
+
// one float field on the receipt (cost_prevented_eur) the issuer rounds
|
|
74
|
+
// to 6 dp and integers serialise as integers.
|
|
75
|
+
return JSON.stringify(v);
|
|
76
|
+
}
|
|
77
|
+
// Strings, booleans
|
|
78
|
+
return JSON.stringify(v);
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Verify an AqtaCore attestation receipt.
|
|
82
|
+
*
|
|
83
|
+
* @param receipt The full receipt object (including `signature`).
|
|
84
|
+
* @param options Optional verification constraints.
|
|
85
|
+
* @returns `{ valid: true }` if the signature is valid under the
|
|
86
|
+
* declared (and optionally pinned) public key; otherwise
|
|
87
|
+
* `{ valid: false, reason: string }`.
|
|
88
|
+
*
|
|
89
|
+
* @example
|
|
90
|
+
* const result = verifyReceipt(receipt, {
|
|
91
|
+
* trustedPublicKey: 'gUoUhIvptKAoLTnry3VrDtOQEWggGQveLrHFVrfNqmE',
|
|
92
|
+
* });
|
|
93
|
+
* if (!result.valid) throw new Error(`Receipt invalid: ${result.reason}`);
|
|
94
|
+
*/
|
|
95
|
+
function verifyReceipt(receipt, options = {}) {
|
|
96
|
+
if (typeof receipt !== 'object' || receipt === null) {
|
|
97
|
+
return { valid: false, reason: 'receipt is not an object' };
|
|
98
|
+
}
|
|
99
|
+
const r = receipt;
|
|
100
|
+
// Structural checks
|
|
101
|
+
for (const field of REQUIRED_FIELDS) {
|
|
102
|
+
if (!(field in r)) {
|
|
103
|
+
return { valid: false, reason: `missing required field: ${field}` };
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
if (options.strictFields !== false) {
|
|
107
|
+
for (const k of Object.keys(r)) {
|
|
108
|
+
if (!REQUIRED_FIELDS.has(k)) {
|
|
109
|
+
return { valid: false, reason: `unknown top-level field: ${k}` };
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
// Semantic sanity
|
|
114
|
+
if (r.v !== 1) {
|
|
115
|
+
return { valid: false, reason: `unsupported version: ${r.v}` };
|
|
116
|
+
}
|
|
117
|
+
if (typeof r.outcome !== 'string' || !ALLOWED_OUTCOMES.has(r.outcome)) {
|
|
118
|
+
return { valid: false, reason: `invalid outcome: ${String(r.outcome)}` };
|
|
119
|
+
}
|
|
120
|
+
if (!Array.isArray(r.policy_applied)) {
|
|
121
|
+
return { valid: false, reason: 'policy_applied must be an array' };
|
|
122
|
+
}
|
|
123
|
+
if (typeof r.request_hash !== 'string' || !/^[0-9a-f]{64}$/.test(r.request_hash)) {
|
|
124
|
+
return { valid: false, reason: 'request_hash must be 64 lowercase hex chars' };
|
|
125
|
+
}
|
|
126
|
+
// Public-key pinning
|
|
127
|
+
if (options.trustedPublicKey !== undefined &&
|
|
128
|
+
options.trustedPublicKey !== r.public_key) {
|
|
129
|
+
return {
|
|
130
|
+
valid: false,
|
|
131
|
+
reason: 'public_key does not match trusted key',
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
// Canonical payload (all fields except signature)
|
|
135
|
+
const payload = { ...r };
|
|
136
|
+
delete payload.signature;
|
|
137
|
+
let canonical;
|
|
138
|
+
try {
|
|
139
|
+
canonical = canonicalise(payload);
|
|
140
|
+
}
|
|
141
|
+
catch (e) {
|
|
142
|
+
return { valid: false, reason: `failed to canonicalise: ${String(e)}` };
|
|
143
|
+
}
|
|
144
|
+
// Ed25519 verification
|
|
145
|
+
try {
|
|
146
|
+
const sig = base64urlDecode(r.signature);
|
|
147
|
+
const pub = base64urlDecode(r.public_key);
|
|
148
|
+
if (sig.length !== 64) {
|
|
149
|
+
return { valid: false, reason: 'signature length != 64 bytes' };
|
|
150
|
+
}
|
|
151
|
+
if (pub.length !== 32) {
|
|
152
|
+
return { valid: false, reason: 'public key length != 32 bytes' };
|
|
153
|
+
}
|
|
154
|
+
const ok = tweetnacl_1.default.sign.detached.verify(canonical, sig, pub);
|
|
155
|
+
return ok ? { valid: true } : { valid: false, reason: 'signature check failed' };
|
|
156
|
+
}
|
|
157
|
+
catch (e) {
|
|
158
|
+
return { valid: false, reason: `signature decode error: ${String(e)}` };
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* Fetch the AqtaCore public key from the published URL and return it as a
|
|
163
|
+
* base64url string ready to pass to `verifyReceipt` as `trustedPublicKey`.
|
|
164
|
+
*
|
|
165
|
+
* Callers SHOULD pin this value once retrieved, rather than re-fetching on
|
|
166
|
+
* every verification.
|
|
167
|
+
*/
|
|
168
|
+
async function fetchPublishedPublicKey(url = 'https://app.aqta.ai/security/pubkey.txt') {
|
|
169
|
+
const res = await fetch(url);
|
|
170
|
+
if (!res.ok) {
|
|
171
|
+
throw new Error(`failed to fetch public key: HTTP ${res.status}`);
|
|
172
|
+
}
|
|
173
|
+
return (await res.text()).trim();
|
|
174
|
+
}
|
|
175
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.test.d.ts","sourceRoot":"","sources":["../src/index.test.ts"],"names":[],"mappings":"AAAA;;;GAGG"}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Interop tests: verify a receipt produced by the Python reference implementation
|
|
4
|
+
* against this TypeScript verifier. Run: `npm run build && node --test dist/index.test.js`.
|
|
5
|
+
*/
|
|
6
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
7
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
8
|
+
};
|
|
9
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
10
|
+
const node_test_1 = require("node:test");
|
|
11
|
+
const strict_1 = __importDefault(require("node:assert/strict"));
|
|
12
|
+
const index_js_1 = require("./index.js");
|
|
13
|
+
/**
|
|
14
|
+
* Fixture receipt shape, used here to exercise the structural checks.
|
|
15
|
+
* Full signature-level interop is covered by the cross-implementation
|
|
16
|
+
* runner at `scripts/make-interop-fixture.mjs`, which generates a receipt
|
|
17
|
+
* with the reference issuer and verifies it with this library.
|
|
18
|
+
*/
|
|
19
|
+
const FIXTURE_RECEIPT = {
|
|
20
|
+
v: 1,
|
|
21
|
+
attestation_id: 'a3f2b1c4-9d87-4e6f-b012-34567890abcd',
|
|
22
|
+
trace_id: 'trace-fixture-v1',
|
|
23
|
+
org_id: 'org-test',
|
|
24
|
+
request_hash: '8f3a7e2b9c4d5f6a1b0c9d8e7f6a5b4c3d2e1f0a9b8c7d6e5f4a3b2c1d0e9f8a',
|
|
25
|
+
model: 'gpt-4o',
|
|
26
|
+
outcome: 'ALLOWED',
|
|
27
|
+
policy_applied: ['budget_guard', 'loop_guard'],
|
|
28
|
+
cost_prevented_eur: 0.0,
|
|
29
|
+
timestamp: '2026-04-23T10:15:30.123456+00:00',
|
|
30
|
+
public_key: '', // populated by make-test-fixture.py
|
|
31
|
+
signature: '', // populated by make-test-fixture.py
|
|
32
|
+
};
|
|
33
|
+
(0, node_test_1.test)('rejects empty object', () => {
|
|
34
|
+
const res = (0, index_js_1.verifyReceipt)({});
|
|
35
|
+
strict_1.default.equal(res.valid, false);
|
|
36
|
+
});
|
|
37
|
+
(0, node_test_1.test)('rejects missing required field', () => {
|
|
38
|
+
const bad = { ...FIXTURE_RECEIPT };
|
|
39
|
+
delete bad.outcome;
|
|
40
|
+
const res = (0, index_js_1.verifyReceipt)(bad);
|
|
41
|
+
strict_1.default.equal(res.valid, false);
|
|
42
|
+
strict_1.default.match(res.reason ?? '', /missing required field/);
|
|
43
|
+
});
|
|
44
|
+
(0, node_test_1.test)('rejects unknown top-level field (strict mode)', () => {
|
|
45
|
+
const bad = { ...FIXTURE_RECEIPT, extra: 'foo' };
|
|
46
|
+
const res = (0, index_js_1.verifyReceipt)(bad);
|
|
47
|
+
strict_1.default.equal(res.valid, false);
|
|
48
|
+
strict_1.default.match(res.reason ?? '', /unknown top-level field/);
|
|
49
|
+
});
|
|
50
|
+
(0, node_test_1.test)('rejects unsupported version', () => {
|
|
51
|
+
const bad = { ...FIXTURE_RECEIPT, v: 2 };
|
|
52
|
+
const res = (0, index_js_1.verifyReceipt)(bad);
|
|
53
|
+
strict_1.default.equal(res.valid, false);
|
|
54
|
+
strict_1.default.match(res.reason ?? '', /unsupported version/);
|
|
55
|
+
});
|
|
56
|
+
(0, node_test_1.test)('rejects invalid outcome', () => {
|
|
57
|
+
const bad = { ...FIXTURE_RECEIPT, outcome: 'YOLO' };
|
|
58
|
+
const res = (0, index_js_1.verifyReceipt)(bad);
|
|
59
|
+
strict_1.default.equal(res.valid, false);
|
|
60
|
+
strict_1.default.match(res.reason ?? '', /invalid outcome/);
|
|
61
|
+
});
|
|
62
|
+
(0, node_test_1.test)('rejects malformed request_hash', () => {
|
|
63
|
+
const bad = { ...FIXTURE_RECEIPT, request_hash: 'not-a-hash' };
|
|
64
|
+
const res = (0, index_js_1.verifyReceipt)(bad);
|
|
65
|
+
strict_1.default.equal(res.valid, false);
|
|
66
|
+
strict_1.default.match(res.reason ?? '', /request_hash/);
|
|
67
|
+
});
|
|
68
|
+
(0, node_test_1.test)('rejects pinned-key mismatch', () => {
|
|
69
|
+
const res = (0, index_js_1.verifyReceipt)(FIXTURE_RECEIPT, {
|
|
70
|
+
trustedPublicKey: 'different_key_000000000000000000000000000000',
|
|
71
|
+
});
|
|
72
|
+
strict_1.default.equal(res.valid, false);
|
|
73
|
+
strict_1.default.match(res.reason ?? '', /does not match trusted key/);
|
|
74
|
+
});
|
|
75
|
+
// Note: full signature-verification interop test requires the fixture to be
|
|
76
|
+
// populated by `scripts/make-test-fixture.py` before running this suite.
|
|
77
|
+
// When FIXTURE_RECEIPT.signature is empty, the signature check is skipped.
|
|
78
|
+
(0, node_test_1.test)('signature decoder rejects empty signature', () => {
|
|
79
|
+
const res = (0, index_js_1.verifyReceipt)(FIXTURE_RECEIPT);
|
|
80
|
+
strict_1.default.equal(res.valid, false);
|
|
81
|
+
// Either the empty signature fails decode or fails length check
|
|
82
|
+
strict_1.default.ok(/signature/.test(res.reason ?? '') ||
|
|
83
|
+
/signature length/.test(res.reason ?? ''));
|
|
84
|
+
});
|
|
85
|
+
//# sourceMappingURL=index.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.test.js","sourceRoot":"","sources":["../src/index.test.ts"],"names":[],"mappings":"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"}
|
package/package.json
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "aqta-verify-receipt",
|
|
3
|
+
"version": "1.0.1",
|
|
4
|
+
"description": "Independent verifier for AqtaCore attestation receipts. Verifies the Ed25519 signature on an attestation receipt without trusting any third-party server. Reference implementation of ATTESTATION-v1.",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"types": "dist/index.d.ts",
|
|
7
|
+
"files": [
|
|
8
|
+
"dist/",
|
|
9
|
+
"README.md",
|
|
10
|
+
"LICENSE"
|
|
11
|
+
],
|
|
12
|
+
"scripts": {
|
|
13
|
+
"build": "tsc",
|
|
14
|
+
"test": "node --test dist/index.test.js",
|
|
15
|
+
"prepublishOnly": "npm run build"
|
|
16
|
+
},
|
|
17
|
+
"keywords": [
|
|
18
|
+
"aqta",
|
|
19
|
+
"ai-governance",
|
|
20
|
+
"attestation",
|
|
21
|
+
"ed25519",
|
|
22
|
+
"zero-trust",
|
|
23
|
+
"eu-ai-act",
|
|
24
|
+
"dora",
|
|
25
|
+
"compliance",
|
|
26
|
+
"receipt-verification"
|
|
27
|
+
],
|
|
28
|
+
"author": "Aqta Technologies Ltd",
|
|
29
|
+
"license": "Apache-2.0",
|
|
30
|
+
"repository": {
|
|
31
|
+
"type": "git",
|
|
32
|
+
"url": "https://github.com/Aqta-ai/attestation-spec.git",
|
|
33
|
+
"directory": "packages/verify-receipt"
|
|
34
|
+
},
|
|
35
|
+
"homepage": "https://github.com/Aqta-ai/attestation-spec",
|
|
36
|
+
"bugs": {
|
|
37
|
+
"url": "https://github.com/Aqta-ai/attestation-spec/issues"
|
|
38
|
+
},
|
|
39
|
+
"dependencies": {
|
|
40
|
+
"tweetnacl": "^1.0.3",
|
|
41
|
+
"tweetnacl-util": "^0.15.1"
|
|
42
|
+
},
|
|
43
|
+
"devDependencies": {
|
|
44
|
+
"@types/node": "^20.0.0",
|
|
45
|
+
"typescript": "^5.3.0"
|
|
46
|
+
},
|
|
47
|
+
"engines": {
|
|
48
|
+
"node": ">=18"
|
|
49
|
+
},
|
|
50
|
+
"publishConfig": {
|
|
51
|
+
"access": "public"
|
|
52
|
+
}
|
|
53
|
+
}
|