@adastracomputing/ink 0.2.0 → 0.4.0
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/CHANGELOG.md +52 -0
- package/README.md +16 -5
- package/bin/verify-inclusion-impl.mjs +65 -22
- package/dist/audit/inclusion-receipt.d.ts +18 -8
- package/dist/audit/inclusion-receipt.js +29 -5
- package/dist/crypto/ink.js +12 -4
- package/dist/crypto/keys.d.ts +33 -1
- package/dist/crypto/keys.js +55 -3
- package/dist/crypto/sign.d.ts +21 -0
- package/dist/crypto/sign.js +26 -2
- package/dist/discovery/agent-card.d.ts +9 -7
- package/dist/index.d.ts +3 -3
- package/dist/index.js +5 -5
- package/dist/ink/checkpoint.d.ts +21 -0
- package/dist/ink/checkpoint.js +79 -0
- package/dist/ink/discovery-gating.js +4 -4
- package/dist/ink/receipts.d.ts +33 -1
- package/dist/ink/receipts.js +45 -1
- package/dist/middleware/ink-auth.d.ts +1 -0
- package/dist/middleware/ink-auth.js +7 -4
- package/dist/models/agent-card.js +22 -22
- package/dist/models/ink-audit.js +40 -36
- package/dist/models/ink-handshake.js +13 -13
- package/dist/models/intent.d.ts +2 -2
- package/dist/models/intent.js +9 -0
- package/docs/maturity.md +17 -7
- package/package.json +10 -9
- package/specs/ink-agent-containment-and-governance-extension-spec.md +3 -2
- package/specs/ink-auditability.md +1 -1
- package/specs/ink-authorization-chain.md +1 -1
- package/specs/ink-compatibility-policy.md +15 -3
- package/specs/ink-compliance-checklist.md +3 -2
- package/specs/ink-containment-phase1-implementation-spec.md +3 -2
- package/specs/ink-introduction-receipts-extension.md +3 -5
- package/specs/ink-key-rotation-spec.md +3 -2
|
@@ -24,13 +24,15 @@ export interface FetchAgentCardOptions {
|
|
|
24
24
|
* connect targets (e.g. undici with a custom dispatcher on Node, or
|
|
25
25
|
* `cf: { resolveOverride: validatedIp }` on Cloudflare Workers). */
|
|
26
26
|
fetch?: typeof fetch;
|
|
27
|
-
/** Strict mode: require that the caller supply `options.fetch
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
27
|
+
/** Strict mode: require that the caller supply `options.fetch`, returning
|
|
28
|
+
* null (without fetching) if it is absent. This only guarantees that *some*
|
|
29
|
+
* fetch override was provided; it does NOT and cannot verify that the
|
|
30
|
+
* override pins connect-time IPs, so passing `requireSafeFetch: true` with
|
|
31
|
+
* the plain global `fetch` does not close the DNS-rebinding window. The
|
|
32
|
+
* literal-private-IP allowlist this module applies to `baseUrl` does not stop
|
|
33
|
+
* a public hostname that resolves to a private address at fetch time; only a
|
|
34
|
+
* connect-time-IP-pinning `options.fetch` (for example a custom undici
|
|
35
|
+
* dispatcher) does. Off by default for backwards compatibility. */
|
|
34
36
|
requireSafeFetch?: boolean;
|
|
35
37
|
}
|
|
36
38
|
/**
|
package/dist/index.d.ts
CHANGED
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
export { signInkMessage, verifyInkSignature, buildSignatureBase, buildAuthHeader, computeMessageHash, computeEventHash, computeAuditMerkleLeafHash, signAuditEvent, verifyAuditEventSignature, signAuditResponse, verifyAuditResponseSignature, verifyAuditEventChain, signAuditQueryResponse, verifyAuditQueryResponseSignature, encryptInkPayload, decryptInkPayload, checkReplay, base64urlEncode, base64urlDecode, hexToBytes, bytesToHex, jcsCanonicalize, MAX_TIMESTAMP_AGE_MS, MAX_FUTURE_TIMESTAMP_MS, } from "./crypto/ink.js";
|
|
2
2
|
export { signMessage, verifyMessage } from "./crypto/sign.js";
|
|
3
3
|
export { verifyInkSignatureWithKeys } from "./crypto/multi-key-verify.js";
|
|
4
|
-
export { generateKeypair, generateEncryptionKeypair, deriveAgentId, encodePublicKeyMultibase, encodeEncryptionKeyMultibase, decodePublicKeyMultibase, decodeEncryptionKeyMultibase, extractPublicKeyFromAgentId, } from "./crypto/keys.js";
|
|
4
|
+
export { generateKeypair, generateEncryptionKeypair, deriveAgentId, encodePublicKeyMultibase, encodeEncryptionKeyMultibase, decodePublicKeyMultibase, decodeEncryptionKeyMultibase, extractPublicKeyFromAgentId, canonicalAgentPrincipal, AGENT_ID_KEY_PREFIXES, } from "./crypto/keys.js";
|
|
5
5
|
export { fetchAgentCard, extractCandidateKeys, resolveBaseUrl, } from "./discovery/agent-card.js";
|
|
6
6
|
export { verifyInkAuth, type NonceStore } from "./middleware/ink-auth.js";
|
|
7
7
|
export { verifyInclusionReceipt, verifyAuditQueryResponse, type InclusionReceipt, type InclusionReceiptVerifyResult, type AuditQueryResponse, type AuditQueryResponseVerifyResult, type VerifyStep, } from "./audit/inclusion-receipt.js";
|
|
8
8
|
export { HandshakeBudgetTracker } from "./ink/handshake-budget.js";
|
|
9
|
-
export { buildReceipt, shouldSendReceipt, sendReceiptFireAndForget, } from "./ink/receipts.js";
|
|
9
|
+
export { buildReceipt, verifyReceipt, shouldSendReceipt, sendReceiptFireAndForget, } from "./ink/receipts.js";
|
|
10
10
|
export { resolveEffectiveTransports, checkTransportAllowed, } from "./ink/transport-auth.js";
|
|
11
11
|
export { buildRedactedCard, shouldRedactOnGet, AgentCardQuerySchema, } from "./ink/discovery-gating.js";
|
|
12
|
-
export { parseCheckpoint, formatCheckpoint, } from "./ink/checkpoint.js";
|
|
12
|
+
export { parseCheckpoint, formatCheckpoint, verifyCheckpoint, } from "./ink/checkpoint.js";
|
|
13
13
|
export type { CheckpointData } from "./ink/checkpoint.js";
|
|
14
14
|
export { InkAuditEventTypeSchema, InkAuditEventSchema, InkAuditInclusionSchema, InkReceiptSchema, InkAuditQuerySchema, InkIntroductionReceiptSchema, } from "./models/ink-audit.js";
|
|
15
15
|
export type { InkAuditEventType, InkAuditEvent, InkAuditInclusion, InkReceipt, InkAuditQuery, InkAuditResponse, InkIntroductionReceiptStatus, } from "./models/ink-audit.js";
|
package/dist/index.js
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
export { signInkMessage, verifyInkSignature, buildSignatureBase, buildAuthHeader, computeMessageHash, computeEventHash, computeAuditMerkleLeafHash, signAuditEvent, verifyAuditEventSignature, signAuditResponse, verifyAuditResponseSignature, verifyAuditEventChain, signAuditQueryResponse, verifyAuditQueryResponseSignature, encryptInkPayload, decryptInkPayload, checkReplay, base64urlEncode, base64urlDecode, hexToBytes, bytesToHex, jcsCanonicalize, MAX_TIMESTAMP_AGE_MS, MAX_FUTURE_TIMESTAMP_MS, } from "./crypto/ink.js";
|
|
5
5
|
export { signMessage, verifyMessage } from "./crypto/sign.js";
|
|
6
6
|
export { verifyInkSignatureWithKeys } from "./crypto/multi-key-verify.js";
|
|
7
|
-
export { generateKeypair, generateEncryptionKeypair, deriveAgentId, encodePublicKeyMultibase, encodeEncryptionKeyMultibase, decodePublicKeyMultibase, decodeEncryptionKeyMultibase, extractPublicKeyFromAgentId, } from "./crypto/keys.js";
|
|
7
|
+
export { generateKeypair, generateEncryptionKeypair, deriveAgentId, encodePublicKeyMultibase, encodeEncryptionKeyMultibase, decodePublicKeyMultibase, decodeEncryptionKeyMultibase, extractPublicKeyFromAgentId, canonicalAgentPrincipal, AGENT_ID_KEY_PREFIXES, } from "./crypto/keys.js";
|
|
8
8
|
// Discovery: Agent Card fetch + candidate-key extraction
|
|
9
9
|
export { fetchAgentCard, extractCandidateKeys, resolveBaseUrl, } from "./discovery/agent-card.js";
|
|
10
10
|
// Middleware: transport-level INK auth
|
|
@@ -13,14 +13,14 @@ export { verifyInkAuth } from "./middleware/ink-auth.js";
|
|
|
13
13
|
export { verifyInclusionReceipt, verifyAuditQueryResponse, } from "./audit/inclusion-receipt.js";
|
|
14
14
|
// Optional containment / governance primitives
|
|
15
15
|
export { HandshakeBudgetTracker } from "./ink/handshake-budget.js";
|
|
16
|
-
// Receipts: build and send INK delivery receipts
|
|
17
|
-
export { buildReceipt, shouldSendReceipt, sendReceiptFireAndForget, } from "./ink/receipts.js";
|
|
16
|
+
// Receipts: build, verify, and send INK delivery receipts
|
|
17
|
+
export { buildReceipt, verifyReceipt, shouldSendReceipt, sendReceiptFireAndForget, } from "./ink/receipts.js";
|
|
18
18
|
// Transport-auth: token-level transport allowlist for extension tokens
|
|
19
19
|
export { resolveEffectiveTransports, checkTransportAllowed, } from "./ink/transport-auth.js";
|
|
20
20
|
// Discovery-gating: visibility-aware Agent Card redaction
|
|
21
21
|
export { buildRedactedCard, shouldRedactOnGet, AgentCardQuerySchema, } from "./ink/discovery-gating.js";
|
|
22
|
-
// Checkpoint parsing for transparency-log
|
|
23
|
-
export { parseCheckpoint, formatCheckpoint, } from "./ink/checkpoint.js";
|
|
22
|
+
// Checkpoint parsing and signature verification for transparency-log checkpoints
|
|
23
|
+
export { parseCheckpoint, formatCheckpoint, verifyCheckpoint, } from "./ink/checkpoint.js";
|
|
24
24
|
// Audit event schemas + types for receipts, query, inclusion proofs
|
|
25
25
|
export { InkAuditEventTypeSchema, InkAuditEventSchema, InkAuditInclusionSchema, InkReceiptSchema, InkAuditQuerySchema, InkIntroductionReceiptSchema, } from "./models/ink-audit.js";
|
|
26
26
|
// Handshake message schemas
|
package/dist/ink/checkpoint.d.ts
CHANGED
|
@@ -17,3 +17,24 @@ export interface CheckpointData {
|
|
|
17
17
|
export declare function formatCheckpoint(data: CheckpointData): string;
|
|
18
18
|
/** Parse a checkpoint body. Returns null if invalid. */
|
|
19
19
|
export declare function parseCheckpoint(body: string): CheckpointData | null;
|
|
20
|
+
/**
|
|
21
|
+
* Verify a signed checkpoint and return its parsed body, or `null` if the
|
|
22
|
+
* signature, origin, or format is invalid.
|
|
23
|
+
*
|
|
24
|
+
* The signed form is the C2SP-style note used by the INK witness:
|
|
25
|
+
*
|
|
26
|
+
* <origin>\n<treeSize>\n<rootHash>\n\n-- <origin> <base64url(sig)>\n
|
|
27
|
+
*
|
|
28
|
+
* The Ed25519 signature covers the body bytes `<origin>\n<treeSize>\n<rootHash>`
|
|
29
|
+
* exactly (no trailing newline), so the `origin` first line is the domain
|
|
30
|
+
* separator binding the signed bytes to this log. Verification REQUIRES the
|
|
31
|
+
* caller's `expectedOrigin`: a checkpoint whose body origin, or whose matching
|
|
32
|
+
* signature-line origin, is not `expectedOrigin` is rejected, so a witness that
|
|
33
|
+
* operates several logs (or an attacker replaying another log's signed
|
|
34
|
+
* checkpoint) cannot substitute a different tree. This is the authenticated
|
|
35
|
+
* input that anti-rollback / freshness checks MUST consume; an unverified
|
|
36
|
+
* checkpoint body provides no security.
|
|
37
|
+
*
|
|
38
|
+
* Verification uses RFC 8032 strict mode (small-order keys rejected).
|
|
39
|
+
*/
|
|
40
|
+
export declare function verifyCheckpoint(signed: string, witnessPublicKey: Uint8Array, expectedOrigin: string): Promise<CheckpointData | null>;
|
package/dist/ink/checkpoint.js
CHANGED
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
* INK Checkpoint formatting (C2SP tlog-checkpoint compatible).
|
|
3
3
|
* Used for the public checkpoint endpoint (INK Auditability §7.7).
|
|
4
4
|
*/
|
|
5
|
+
import * as ed from "@noble/ed25519";
|
|
6
|
+
import { base64urlDecode } from "../crypto/ink.js";
|
|
5
7
|
/**
|
|
6
8
|
* Format a checkpoint body per C2SP tlog-checkpoint spec:
|
|
7
9
|
* line 1: origin (log identity)
|
|
@@ -67,3 +69,80 @@ export function parseCheckpoint(body) {
|
|
|
67
69
|
return null;
|
|
68
70
|
return { origin, treeSize, rootHash };
|
|
69
71
|
}
|
|
72
|
+
/** A signed checkpoint is the 3-line body, a blank line, then one or more
|
|
73
|
+
* signature lines, plus a trailing newline. Bound the whole thing so an
|
|
74
|
+
* attacker-supplied blob cannot drive large scans before rejection. */
|
|
75
|
+
const MAX_SIGNED_CHECKPOINT_BODY = 4096;
|
|
76
|
+
/** Cap the number of cosignature lines a verifier will scan. */
|
|
77
|
+
const MAX_CHECKPOINT_SIGNATURES = 8;
|
|
78
|
+
/**
|
|
79
|
+
* Verify a signed checkpoint and return its parsed body, or `null` if the
|
|
80
|
+
* signature, origin, or format is invalid.
|
|
81
|
+
*
|
|
82
|
+
* The signed form is the C2SP-style note used by the INK witness:
|
|
83
|
+
*
|
|
84
|
+
* <origin>\n<treeSize>\n<rootHash>\n\n-- <origin> <base64url(sig)>\n
|
|
85
|
+
*
|
|
86
|
+
* The Ed25519 signature covers the body bytes `<origin>\n<treeSize>\n<rootHash>`
|
|
87
|
+
* exactly (no trailing newline), so the `origin` first line is the domain
|
|
88
|
+
* separator binding the signed bytes to this log. Verification REQUIRES the
|
|
89
|
+
* caller's `expectedOrigin`: a checkpoint whose body origin, or whose matching
|
|
90
|
+
* signature-line origin, is not `expectedOrigin` is rejected, so a witness that
|
|
91
|
+
* operates several logs (or an attacker replaying another log's signed
|
|
92
|
+
* checkpoint) cannot substitute a different tree. This is the authenticated
|
|
93
|
+
* input that anti-rollback / freshness checks MUST consume; an unverified
|
|
94
|
+
* checkpoint body provides no security.
|
|
95
|
+
*
|
|
96
|
+
* Verification uses RFC 8032 strict mode (small-order keys rejected).
|
|
97
|
+
*/
|
|
98
|
+
export async function verifyCheckpoint(signed, witnessPublicKey, expectedOrigin) {
|
|
99
|
+
if (typeof signed !== "string" || signed.length === 0 || signed.length > MAX_SIGNED_CHECKPOINT_BODY) {
|
|
100
|
+
return null;
|
|
101
|
+
}
|
|
102
|
+
if (!(witnessPublicKey instanceof Uint8Array) || witnessPublicKey.length !== 32) {
|
|
103
|
+
return null;
|
|
104
|
+
}
|
|
105
|
+
if (typeof expectedOrigin !== "string" || expectedOrigin.length === 0 || expectedOrigin.length > MAX_CHECKPOINT_LINE) {
|
|
106
|
+
return null;
|
|
107
|
+
}
|
|
108
|
+
const SEP = "\n\n-- ";
|
|
109
|
+
const idx = signed.indexOf(SEP);
|
|
110
|
+
if (idx === -1)
|
|
111
|
+
return null;
|
|
112
|
+
const body = signed.slice(0, idx); // <origin>\n<treeSize>\n<rootHash>
|
|
113
|
+
const data = parseCheckpoint(body + "\n");
|
|
114
|
+
if (!data)
|
|
115
|
+
return null;
|
|
116
|
+
// Bind the body's own origin to the caller's expectation before any crypto.
|
|
117
|
+
if (data.origin !== expectedOrigin)
|
|
118
|
+
return null;
|
|
119
|
+
// Signature block starts at the "-- " that began the separator.
|
|
120
|
+
const sigBlock = signed.slice(idx + 2);
|
|
121
|
+
const sigLines = sigBlock.split("\n").filter((l) => l.length > 0);
|
|
122
|
+
if (sigLines.length === 0 || sigLines.length > MAX_CHECKPOINT_SIGNATURES)
|
|
123
|
+
return null;
|
|
124
|
+
const bodyBytes = new TextEncoder().encode(body);
|
|
125
|
+
for (const line of sigLines) {
|
|
126
|
+
if (!line.startsWith("-- "))
|
|
127
|
+
return null; // any malformed signature line is fatal
|
|
128
|
+
const rest = line.slice(3);
|
|
129
|
+
const sp = rest.indexOf(" ");
|
|
130
|
+
if (sp === -1)
|
|
131
|
+
return null;
|
|
132
|
+
const lineOrigin = rest.slice(0, sp);
|
|
133
|
+
const sigB64 = rest.slice(sp + 1);
|
|
134
|
+
if (lineOrigin !== expectedOrigin)
|
|
135
|
+
continue; // a cosigner whose key we were not given
|
|
136
|
+
try {
|
|
137
|
+
const sig = base64urlDecode(sigB64);
|
|
138
|
+
if (sig.length !== 64)
|
|
139
|
+
return null;
|
|
140
|
+
const ok = await ed.verifyAsync(sig, bodyBytes, witnessPublicKey, { zip215: false });
|
|
141
|
+
return ok ? data : null; // a matching-origin signature that fails verification is fatal
|
|
142
|
+
}
|
|
143
|
+
catch {
|
|
144
|
+
return null;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
return null; // no signature line for the expected origin
|
|
148
|
+
}
|
|
@@ -66,16 +66,16 @@ export function buildRedactedCard(card) {
|
|
|
66
66
|
export const AgentCardQuerySchema = z.object({
|
|
67
67
|
protocol: z.literal("ink/0.1"),
|
|
68
68
|
type: z.literal("network.tulpa.agent_card_query"),
|
|
69
|
-
from: z.string(),
|
|
70
|
-
nonce: z.string(),
|
|
69
|
+
from: z.string().max(512),
|
|
70
|
+
nonce: z.string().max(256),
|
|
71
71
|
timestamp: z.string().datetime(),
|
|
72
|
-
requestedFields: z.array(z.string()).optional(),
|
|
72
|
+
requestedFields: z.array(z.string().max(64)).max(32).optional(),
|
|
73
73
|
});
|
|
74
74
|
export const AgentCardResponseSchema = z.object({
|
|
75
75
|
protocol: z.literal("ink/0.1"),
|
|
76
76
|
type: z.literal("network.tulpa.agent_card_response"),
|
|
77
77
|
card: AgentCardSchema,
|
|
78
|
-
grantedFields: z.array(z.string()),
|
|
78
|
+
grantedFields: z.array(z.string().max(64)).max(32),
|
|
79
79
|
timestamp: z.string().datetime(),
|
|
80
80
|
});
|
|
81
81
|
export const AgentCardDeniedSchema = z.object({
|
package/dist/ink/receipts.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type InkReceipt } from "../models/ink-audit.js";
|
|
2
2
|
export interface BuildReceiptInput {
|
|
3
3
|
from: string;
|
|
4
4
|
to: string;
|
|
@@ -10,6 +10,38 @@ export interface BuildReceiptInput {
|
|
|
10
10
|
}
|
|
11
11
|
/** Build a signed INK receipt envelope. */
|
|
12
12
|
export declare function buildReceipt(input: BuildReceiptInput): Promise<InkReceipt>;
|
|
13
|
+
export interface VerifyReceiptResult {
|
|
14
|
+
valid: boolean;
|
|
15
|
+
/** Machine-readable reason when `valid` is false. */
|
|
16
|
+
reason?: string;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Verify an INK receipt against the message it claims to acknowledge.
|
|
20
|
+
*
|
|
21
|
+
* Checks all the bindings a hand-rolled verifier commonly forgets: the
|
|
22
|
+
* receipt's Ed25519 signature against the issuer's (`from`) key, that `from`,
|
|
23
|
+
* `to` and `messageId` equal the expected values, and that `messageHash`
|
|
24
|
+
* equals the hash of the exact message body that was sent (recomputed here,
|
|
25
|
+
* not trusted from the receipt). A receipt that passes proves the named
|
|
26
|
+
* counterparty acknowledged that specific message; nothing weaker should be
|
|
27
|
+
* treated as proof of delivery.
|
|
28
|
+
*/
|
|
29
|
+
export declare function verifyReceipt(opts: {
|
|
30
|
+
receipt: unknown;
|
|
31
|
+
/** Raw 32-byte Ed25519 public key of the issuer (the receipt's `from`). */
|
|
32
|
+
senderPublicKey: Uint8Array;
|
|
33
|
+
expected: {
|
|
34
|
+
from: string;
|
|
35
|
+
to: string;
|
|
36
|
+
messageId: string;
|
|
37
|
+
messageBody: Record<string, unknown>;
|
|
38
|
+
/** When set, require the receipt to acknowledge this specific disposition
|
|
39
|
+
* (e.g. "delivered"). The disposition is covered by the signature, but
|
|
40
|
+
* without this a signed receipt for a different state would still pass, so
|
|
41
|
+
* callers proving a specific delivery state MUST pin it. */
|
|
42
|
+
disposition?: InkReceipt["disposition"];
|
|
43
|
+
};
|
|
44
|
+
}): Promise<VerifyReceiptResult>;
|
|
13
45
|
export declare function shouldSendReceipt(intentOrType: string): boolean;
|
|
14
46
|
export interface SendReceiptOptions {
|
|
15
47
|
/** Allow endpoints whose hostname is loopback / private / link-local /
|
package/dist/ink/receipts.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { computeMessageHash, signInkMessage, buildAuthHeader } from "../crypto/ink.js";
|
|
2
|
-
import { signMessage } from "../crypto/sign.js";
|
|
2
|
+
import { signMessage, verifyMessage } from "../crypto/sign.js";
|
|
3
3
|
import { isPrivateHostname } from "../discovery/agent-card.js";
|
|
4
|
+
import { InkReceiptSchema } from "../models/ink-audit.js";
|
|
4
5
|
/** Build a signed INK receipt envelope. */
|
|
5
6
|
export async function buildReceipt(input) {
|
|
6
7
|
if (input === null || typeof input !== "object" || Array.isArray(input)) {
|
|
@@ -28,6 +29,49 @@ export async function buildReceipt(input) {
|
|
|
28
29
|
const signature = await signMessage(unsigned, input.privateKey);
|
|
29
30
|
return { ...unsigned, signature };
|
|
30
31
|
}
|
|
32
|
+
/**
|
|
33
|
+
* Verify an INK receipt against the message it claims to acknowledge.
|
|
34
|
+
*
|
|
35
|
+
* Checks all the bindings a hand-rolled verifier commonly forgets: the
|
|
36
|
+
* receipt's Ed25519 signature against the issuer's (`from`) key, that `from`,
|
|
37
|
+
* `to` and `messageId` equal the expected values, and that `messageHash`
|
|
38
|
+
* equals the hash of the exact message body that was sent (recomputed here,
|
|
39
|
+
* not trusted from the receipt). A receipt that passes proves the named
|
|
40
|
+
* counterparty acknowledged that specific message; nothing weaker should be
|
|
41
|
+
* treated as proof of delivery.
|
|
42
|
+
*/
|
|
43
|
+
export async function verifyReceipt(opts) {
|
|
44
|
+
const parsed = InkReceiptSchema.safeParse(opts.receipt);
|
|
45
|
+
if (!parsed.success)
|
|
46
|
+
return { valid: false, reason: "malformed_receipt" };
|
|
47
|
+
const receipt = parsed.data;
|
|
48
|
+
const { senderPublicKey, expected } = opts;
|
|
49
|
+
if (!(senderPublicKey instanceof Uint8Array) || senderPublicKey.length !== 32) {
|
|
50
|
+
return { valid: false, reason: "invalid_public_key" };
|
|
51
|
+
}
|
|
52
|
+
if (receipt.from !== expected.from)
|
|
53
|
+
return { valid: false, reason: "from_mismatch" };
|
|
54
|
+
if (receipt.to !== expected.to)
|
|
55
|
+
return { valid: false, reason: "to_mismatch" };
|
|
56
|
+
if (receipt.messageId !== expected.messageId)
|
|
57
|
+
return { valid: false, reason: "message_id_mismatch" };
|
|
58
|
+
if (expected.disposition !== undefined && receipt.disposition !== expected.disposition) {
|
|
59
|
+
return { valid: false, reason: "disposition_mismatch" };
|
|
60
|
+
}
|
|
61
|
+
let expectedHash;
|
|
62
|
+
try {
|
|
63
|
+
expectedHash = await computeMessageHash(expected.messageBody);
|
|
64
|
+
}
|
|
65
|
+
catch {
|
|
66
|
+
return { valid: false, reason: "message_hash_error" };
|
|
67
|
+
}
|
|
68
|
+
if (receipt.messageHash !== expectedHash)
|
|
69
|
+
return { valid: false, reason: "message_hash_mismatch" };
|
|
70
|
+
const sigOk = await verifyMessage(receipt, senderPublicKey);
|
|
71
|
+
if (!sigOk)
|
|
72
|
+
return { valid: false, reason: "invalid_signature" };
|
|
73
|
+
return { valid: true };
|
|
74
|
+
}
|
|
31
75
|
/** Loop prevention: don't send receipts for receipts or audit messages. */
|
|
32
76
|
const NO_RECEIPT_TYPES = new Set([
|
|
33
77
|
"network.tulpa.receipt",
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { verifyInkSignature, MAX_TIMESTAMP_AGE_MS, MAX_FUTURE_TIMESTAMP_MS } from "../crypto/ink.js";
|
|
2
|
-
import { extractPublicKeyFromAgentId } from "../crypto/keys.js";
|
|
2
|
+
import { extractPublicKeyFromAgentId, canonicalAgentPrincipal } from "../crypto/keys.js";
|
|
3
3
|
import { verifyInkSignatureWithKeys } from "../crypto/multi-key-verify.js";
|
|
4
4
|
/**
|
|
5
5
|
* Parse and verify an INK-Ed25519 Authorization header.
|
|
@@ -33,8 +33,10 @@ export async function verifyInkAuth(opts) {
|
|
|
33
33
|
}
|
|
34
34
|
// Ed25519 signatures are exactly 86 base64url chars — tighten the regex to
|
|
35
35
|
// {86} so clearly-wrong lengths get rejected up front, rather than burning
|
|
36
|
-
// CPU on verifyInkSignature for a malformed value.
|
|
37
|
-
|
|
36
|
+
// CPU on verifyInkSignature for a malformed value. Single literal spaces
|
|
37
|
+
// match the spec grammar and buildAuthHeader's output; `\s` would let
|
|
38
|
+
// CR/LF/TAB into a parsed header value.
|
|
39
|
+
const match = opts.authHeader.match(/^INK-Ed25519 ([A-Za-z0-9_-]{86})(?: keyId=([A-Za-z0-9_:.-]{1,128}))?$/);
|
|
38
40
|
if (!match) {
|
|
39
41
|
return { valid: false, error: "invalid_auth_scheme" };
|
|
40
42
|
}
|
|
@@ -172,6 +174,7 @@ export async function verifyInkAuth(opts) {
|
|
|
172
174
|
return {
|
|
173
175
|
valid: true,
|
|
174
176
|
senderAgentId: senderDid,
|
|
177
|
+
principal: canonicalAgentPrincipal(senderDid),
|
|
175
178
|
keyId: result.keyId,
|
|
176
179
|
keyStatus: result.keyStatus,
|
|
177
180
|
};
|
|
@@ -206,7 +209,7 @@ export async function verifyInkAuth(opts) {
|
|
|
206
209
|
const noncePass = await recordNonce();
|
|
207
210
|
if (!noncePass.ok)
|
|
208
211
|
return { valid: false, error: noncePass.error };
|
|
209
|
-
return { valid: true, senderAgentId: senderDid };
|
|
212
|
+
return { valid: true, senderAgentId: senderDid, principal: canonicalAgentPrincipal(senderDid) };
|
|
210
213
|
}
|
|
211
214
|
catch {
|
|
212
215
|
return { valid: false, error: "signature_verification_failed" };
|
|
@@ -5,17 +5,17 @@ import { ProfileSnapshotSchema } from "./profile.js";
|
|
|
5
5
|
import { KeyEntrySchema } from "./key-entry.js";
|
|
6
6
|
import { InkTransportSchema, AgentCardVisibilitySchema } from "./ink-handshake.js";
|
|
7
7
|
export const ThirdPartyAuditServiceSchema = z.object({
|
|
8
|
-
endpoint: z.string().url(),
|
|
9
|
-
did: z.string(),
|
|
10
|
-
publicKey: z.string(),
|
|
8
|
+
endpoint: z.string().max(2048).url(),
|
|
9
|
+
did: z.string().max(512),
|
|
10
|
+
publicKey: z.string().max(256),
|
|
11
11
|
});
|
|
12
12
|
export const AgentCardSchema = z.object({
|
|
13
13
|
protocol: z.literal("ink/0.1"),
|
|
14
|
-
agentId: z.string(),
|
|
15
|
-
ownerDid: z.string().optional(),
|
|
16
|
-
ownerHandle: z.string().optional(),
|
|
17
|
-
atprotoRecordUri: z.string().optional(),
|
|
18
|
-
handle: z.string(),
|
|
14
|
+
agentId: z.string().max(512),
|
|
15
|
+
ownerDid: z.string().max(512).optional(),
|
|
16
|
+
ownerHandle: z.string().max(256).optional(),
|
|
17
|
+
atprotoRecordUri: z.string().max(2048).optional(),
|
|
18
|
+
handle: z.string().max(256),
|
|
19
19
|
displayName: z.string().max(200),
|
|
20
20
|
/**
|
|
21
21
|
* Inbound message endpoint URL. Required.
|
|
@@ -27,36 +27,36 @@ export const AgentCardSchema = z.object({
|
|
|
27
27
|
* MAY emit `inboxEndpoint` alongside it. The runtime helper
|
|
28
28
|
* `resolveAgentInbox(card)` returns whichever value is present.
|
|
29
29
|
*/
|
|
30
|
-
endpoint: z.string().url(),
|
|
31
|
-
inboxEndpoint: z.string().url().optional(),
|
|
32
|
-
publicKeyMultibase: z.string().startsWith("z"),
|
|
30
|
+
endpoint: z.string().max(2048).url(),
|
|
31
|
+
inboxEndpoint: z.string().max(2048).url().optional(),
|
|
32
|
+
publicKeyMultibase: z.string().startsWith("z").max(128),
|
|
33
33
|
// (other fields below; the `inboxEndpoint === endpoint` invariant
|
|
34
34
|
// is enforced by the .superRefine() at the bottom of this schema.)
|
|
35
35
|
profileSnapshot: ProfileSnapshotSchema.optional(),
|
|
36
36
|
capabilities: z.object({
|
|
37
|
-
intentsAccepted: z.array(IntentTypeSchema),
|
|
38
|
-
intentsSent: z.array(IntentTypeSchema),
|
|
37
|
+
intentsAccepted: z.array(IntentTypeSchema).max(32),
|
|
38
|
+
intentsSent: z.array(IntentTypeSchema).max(32),
|
|
39
39
|
receipts: z.object({
|
|
40
40
|
send: z.boolean(),
|
|
41
|
-
dispositions: z.array(InkReceiptDispositionSchema),
|
|
41
|
+
dispositions: z.array(InkReceiptDispositionSchema).max(16),
|
|
42
42
|
}).optional(),
|
|
43
43
|
auditExchange: z.boolean().optional(),
|
|
44
44
|
thirdPartyAudit: z.object({
|
|
45
|
-
services: z.array(ThirdPartyAuditServiceSchema),
|
|
45
|
+
services: z.array(ThirdPartyAuditServiceSchema).max(16),
|
|
46
46
|
submitPolicy: z.enum(["all", "high_value", "none"]),
|
|
47
47
|
}).optional(),
|
|
48
48
|
}),
|
|
49
49
|
availability: z.object({
|
|
50
|
-
timezone: z.string(),
|
|
51
|
-
meetingHours: z.string().optional(),
|
|
52
|
-
responseSla: z.string().optional(),
|
|
50
|
+
timezone: z.string().max(64),
|
|
51
|
+
meetingHours: z.string().max(200).optional(),
|
|
52
|
+
responseSla: z.string().max(200).optional(),
|
|
53
53
|
}),
|
|
54
54
|
keys: z.object({
|
|
55
|
-
signing: z.array(KeyEntrySchema),
|
|
56
|
-
encryption: z.array(KeyEntrySchema),
|
|
55
|
+
signing: z.array(KeyEntrySchema).max(32),
|
|
56
|
+
encryption: z.array(KeyEntrySchema).max(32),
|
|
57
57
|
}).optional(),
|
|
58
|
-
currentSigningKeyId: z.string().optional(),
|
|
59
|
-
currentEncryptionKeyId: z.string().optional(),
|
|
58
|
+
currentSigningKeyId: z.string().max(128).optional(),
|
|
59
|
+
currentEncryptionKeyId: z.string().max(128).optional(),
|
|
60
60
|
keySetVersion: z.number().int().positive().optional(),
|
|
61
61
|
// Message protocol versions this agent's receiver can verify on the
|
|
62
62
|
// body signature. When absent, assume ink/0.1 only. A sender MUST NOT
|
package/dist/models/ink-audit.js
CHANGED
|
@@ -60,18 +60,18 @@ export const InkAuditEventTypeSchema = z.enum([
|
|
|
60
60
|
]);
|
|
61
61
|
// ── INK Audit Event (hash-chained, signed) ──
|
|
62
62
|
export const InkAuditEventSchema = z.object({
|
|
63
|
-
id: z.string().min(1),
|
|
63
|
+
id: z.string().min(1).max(256),
|
|
64
64
|
version: z.literal("ink-audit/1"),
|
|
65
|
-
agentId: z.string().min(1),
|
|
66
|
-
agentSignature: z.string().min(1),
|
|
65
|
+
agentId: z.string().min(1).max(512),
|
|
66
|
+
agentSignature: z.string().min(1).max(256),
|
|
67
67
|
sequence: z.number().int().positive(),
|
|
68
68
|
previousEventHash: z.string().regex(/^[0-9a-f]{64}$/).nullable(),
|
|
69
69
|
eventType: InkAuditEventTypeSchema,
|
|
70
70
|
timestamp: z.string().datetime(),
|
|
71
|
-
messageId: z.string().min(1).optional(),
|
|
72
|
-
correlationId: z.string().min(1).optional(),
|
|
73
|
-
counterpartyId: z.string().min(1).optional(),
|
|
74
|
-
signingKeyId: z.string().min(1).optional(),
|
|
71
|
+
messageId: z.string().min(1).max(256).optional(),
|
|
72
|
+
correlationId: z.string().min(1).max(256).optional(),
|
|
73
|
+
counterpartyId: z.string().min(1).max(512).optional(),
|
|
74
|
+
signingKeyId: z.string().min(1).max(128).optional(),
|
|
75
75
|
data: z.record(z.string(), z.unknown()).optional(),
|
|
76
76
|
});
|
|
77
77
|
// ── Receipt (INK Auditability §1) ──
|
|
@@ -85,34 +85,38 @@ export const InkReceiptDispositionSchema = z.enum([
|
|
|
85
85
|
export const InkReceiptSchema = z.object({
|
|
86
86
|
protocol: z.literal("ink/0.1"),
|
|
87
87
|
type: z.literal("network.tulpa.receipt"),
|
|
88
|
-
from: z.string(),
|
|
89
|
-
to: z.string(),
|
|
90
|
-
messageId: z.string(),
|
|
88
|
+
from: z.string().max(512),
|
|
89
|
+
to: z.string().max(512),
|
|
90
|
+
messageId: z.string().max(256),
|
|
91
91
|
disposition: InkReceiptDispositionSchema,
|
|
92
92
|
dispositionAt: z.string().datetime(),
|
|
93
93
|
note: z.string().max(500).optional(),
|
|
94
|
-
messageHash: z.string(),
|
|
95
|
-
nonce: z.string(),
|
|
94
|
+
messageHash: z.string().max(256),
|
|
95
|
+
nonce: z.string().max(256),
|
|
96
96
|
timestamp: z.string().datetime(),
|
|
97
|
-
signature: z.string(),
|
|
97
|
+
signature: z.string().max(256),
|
|
98
98
|
});
|
|
99
99
|
// ── Audit Query (INK Auditability §3) ──
|
|
100
100
|
export const InkAuditQuerySchema = z.object({
|
|
101
101
|
protocol: z.literal("ink/0.1"),
|
|
102
102
|
type: z.literal("network.tulpa.audit_query"),
|
|
103
|
-
from: z.string(),
|
|
104
|
-
to: z.string(),
|
|
105
|
-
messageId: z.string(),
|
|
106
|
-
nonce: z.string(),
|
|
103
|
+
from: z.string().max(512),
|
|
104
|
+
to: z.string().max(512),
|
|
105
|
+
messageId: z.string().max(256),
|
|
106
|
+
nonce: z.string().max(256),
|
|
107
107
|
timestamp: z.string().datetime(),
|
|
108
108
|
});
|
|
109
109
|
// ── Audit Response (INK Auditability §3) ──
|
|
110
110
|
export const InkAuditResponseSchema = z.object({
|
|
111
111
|
protocol: z.literal("ink/0.1"),
|
|
112
112
|
type: z.literal("network.tulpa.audit_response"),
|
|
113
|
-
messageId: z.string(),
|
|
114
|
-
|
|
115
|
-
|
|
113
|
+
messageId: z.string().max(256),
|
|
114
|
+
// Bound both the event count and (via InkAuditEventSchema's per-field caps)
|
|
115
|
+
// each event, so an audit response from an untrusted witness cannot force
|
|
116
|
+
// unbounded buffering. A single response that needs more than this should
|
|
117
|
+
// page rather than return one giant array.
|
|
118
|
+
events: z.array(InkAuditEventSchema).max(1000),
|
|
119
|
+
responseSignature: z.string().max(256),
|
|
116
120
|
});
|
|
117
121
|
// ── Third-Party Audit Submit (INK Auditability §7.2) ──
|
|
118
122
|
export const InkAuditSubmitSchema = z.object({
|
|
@@ -128,10 +132,10 @@ export const InkAuditSubmitSchema = z.object({
|
|
|
128
132
|
export const InkAuditInclusionSchema = z.object({
|
|
129
133
|
protocol: z.literal("ink/0.1"),
|
|
130
134
|
type: z.literal("network.tulpa.audit_inclusion"),
|
|
131
|
-
eventId: z.string(),
|
|
135
|
+
eventId: z.string().max(256),
|
|
132
136
|
treeSize: z.number().int().positive(),
|
|
133
137
|
leafIndex: z.number().int().min(0),
|
|
134
|
-
rootHash: z.string(),
|
|
138
|
+
rootHash: z.string().max(128),
|
|
135
139
|
/** Optional Merkle inclusion proof — array of 64-character lowercase hex
|
|
136
140
|
* hash siblings on the path from the leaf to the root. Consumers that
|
|
137
141
|
* verify proofs (third-party auditor clients) read this field; consumers
|
|
@@ -141,7 +145,7 @@ export const InkAuditInclusionSchema = z.object({
|
|
|
141
145
|
* force the parser to allocate megabytes of garbage proof data. */
|
|
142
146
|
inclusionProof: z.array(z.string().regex(/^[0-9a-f]{64}$/)).max(64).optional(),
|
|
143
147
|
timestamp: z.string().datetime(),
|
|
144
|
-
serviceSignature: z.string(),
|
|
148
|
+
serviceSignature: z.string().max(256),
|
|
145
149
|
});
|
|
146
150
|
// ── Introduction Receipt (INK Introduction Receipts Extension §4) ──
|
|
147
151
|
export const InkIntroductionReceiptStatusSchema = z.enum([
|
|
@@ -154,22 +158,22 @@ export const InkIntroductionReceiptStatusSchema = z.enum([
|
|
|
154
158
|
export const InkIntroductionReceiptSchema = z.object({
|
|
155
159
|
protocol: z.literal("ink/0.1"),
|
|
156
160
|
type: z.literal("network.tulpa.introduction_receipt"),
|
|
157
|
-
id: z.string(),
|
|
158
|
-
correlationId: z.string(),
|
|
159
|
-
from: z.string(),
|
|
160
|
-
to: z.string(),
|
|
161
|
-
requesterDid: z.string(),
|
|
162
|
-
introducerDid: z.string(),
|
|
163
|
-
beneficiaryDid: z.string(),
|
|
164
|
-
targetDid: z.string(),
|
|
161
|
+
id: z.string().max(256),
|
|
162
|
+
correlationId: z.string().max(256),
|
|
163
|
+
from: z.string().max(512),
|
|
164
|
+
to: z.string().max(512),
|
|
165
|
+
requesterDid: z.string().max(512),
|
|
166
|
+
introducerDid: z.string().max(512),
|
|
167
|
+
beneficiaryDid: z.string().max(512),
|
|
168
|
+
targetDid: z.string().max(512),
|
|
165
169
|
status: InkIntroductionReceiptStatusSchema,
|
|
166
170
|
purpose: z.string().min(1).max(500),
|
|
167
|
-
nonce: z.string(),
|
|
171
|
+
nonce: z.string().max(256),
|
|
168
172
|
timestamp: z.string().datetime(),
|
|
169
|
-
relatedIntentId: z.string().optional(),
|
|
170
|
-
relatedResolutionId: z.string().optional(),
|
|
173
|
+
relatedIntentId: z.string().max(256).optional(),
|
|
174
|
+
relatedResolutionId: z.string().max(256).optional(),
|
|
171
175
|
note: z.string().max(500).optional(),
|
|
172
|
-
contextHash: z.string().optional(),
|
|
173
|
-
authorizationChainRef: z.string().optional(),
|
|
176
|
+
contextHash: z.string().max(256).optional(),
|
|
177
|
+
authorizationChainRef: z.string().max(512).optional(),
|
|
174
178
|
expiresAt: z.string().datetime().optional(),
|
|
175
179
|
});
|
|
@@ -32,12 +32,12 @@ export const ChallengeTypeSchema = z.enum([
|
|
|
32
32
|
export const InkChallengeSchema = z.object({
|
|
33
33
|
protocol: z.literal("ink/0.1"),
|
|
34
34
|
type: z.literal("network.tulpa.challenge"),
|
|
35
|
-
intentRef: z.string(),
|
|
35
|
+
intentRef: z.string().max(256),
|
|
36
36
|
challengeType: ChallengeTypeSchema,
|
|
37
|
-
fields: z.array(z.string()).optional(),
|
|
38
|
-
availableWindows: z.array(z.string()).optional(),
|
|
39
|
-
contextFields: z.array(z.string()).optional(),
|
|
40
|
-
nonce: z.string(),
|
|
37
|
+
fields: z.array(z.string().max(256)).max(32).optional(),
|
|
38
|
+
availableWindows: z.array(z.string().max(64)).max(32).optional(),
|
|
39
|
+
contextFields: z.array(z.string().max(256)).max(32).optional(),
|
|
40
|
+
nonce: z.string().max(256),
|
|
41
41
|
timestamp: z.string().datetime(),
|
|
42
42
|
});
|
|
43
43
|
// ── Rejection (network.tulpa.rejection) — Stage 2b ──
|
|
@@ -58,12 +58,12 @@ export const RejectionReasonSchema = z.enum([
|
|
|
58
58
|
export const InkRejectionSchema = z.object({
|
|
59
59
|
protocol: z.literal("ink/0.1"),
|
|
60
60
|
type: z.literal("network.tulpa.rejection"),
|
|
61
|
-
intentRef: z.string(),
|
|
61
|
+
intentRef: z.string().max(256),
|
|
62
62
|
reason: RejectionReasonSchema,
|
|
63
63
|
detail: z.string().max(500).optional(),
|
|
64
|
-
retryAfter: z.string().optional(),
|
|
64
|
+
retryAfter: z.string().max(64).optional(),
|
|
65
65
|
backoffHint: InkBackoffHintSchema.optional(),
|
|
66
|
-
nonce: z.string(),
|
|
66
|
+
nonce: z.string().max(256),
|
|
67
67
|
timestamp: z.string().datetime(),
|
|
68
68
|
});
|
|
69
69
|
// ── Resolution (network.tulpa.resolution) — Stage 3 ──
|
|
@@ -74,16 +74,16 @@ export const ResolutionOutcomeSchema = z.enum([
|
|
|
74
74
|
"expired",
|
|
75
75
|
]);
|
|
76
76
|
export const ResolutionDetailsSchema = z.object({
|
|
77
|
-
scheduledAt: z.string().optional(),
|
|
78
|
-
duration: z.string().optional(),
|
|
77
|
+
scheduledAt: z.string().max(64).optional(),
|
|
78
|
+
duration: z.string().max(64).optional(),
|
|
79
79
|
}).passthrough();
|
|
80
80
|
export const InkResolutionSchema = z.object({
|
|
81
81
|
protocol: z.literal("ink/0.1"),
|
|
82
82
|
type: z.literal("network.tulpa.resolution"),
|
|
83
|
-
intentRef: z.string(),
|
|
83
|
+
intentRef: z.string().max(256),
|
|
84
84
|
outcome: ResolutionOutcomeSchema,
|
|
85
85
|
details: ResolutionDetailsSchema.optional(),
|
|
86
|
-
counterpartyDid: z.string().optional(),
|
|
87
|
-
nonce: z.string(),
|
|
86
|
+
counterpartyDid: z.string().max(512).optional(),
|
|
87
|
+
nonce: z.string().max(256),
|
|
88
88
|
timestamp: z.string().datetime(),
|
|
89
89
|
});
|
package/dist/models/intent.d.ts
CHANGED
|
@@ -223,14 +223,14 @@ export declare const MessageProvenanceSchema: z.ZodOptional<z.ZodObject<{
|
|
|
223
223
|
*/
|
|
224
224
|
export declare const INK_PROTOCOL_VERSIONS: readonly ["ink/0.1", "ink/0.2"];
|
|
225
225
|
export declare const ProtocolVersionSchema: z.ZodEnum<{
|
|
226
|
-
"ink/0.1": "ink/0.1";
|
|
227
226
|
"ink/0.2": "ink/0.2";
|
|
227
|
+
"ink/0.1": "ink/0.1";
|
|
228
228
|
}>;
|
|
229
229
|
export type ProtocolVersion = z.infer<typeof ProtocolVersionSchema>;
|
|
230
230
|
export declare const MessageEnvelopeSchema: z.ZodObject<{
|
|
231
231
|
protocol: z.ZodEnum<{
|
|
232
|
-
"ink/0.1": "ink/0.1";
|
|
233
232
|
"ink/0.2": "ink/0.2";
|
|
233
|
+
"ink/0.1": "ink/0.1";
|
|
234
234
|
}>;
|
|
235
235
|
id: z.ZodString;
|
|
236
236
|
correlationId: z.ZodString;
|