@adastracomputing/ink 0.1.7 → 0.3.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 +9 -3
- package/README.md +6 -4
- package/dist/crypto/keys.d.ts +14 -1
- package/dist/crypto/keys.js +16 -3
- package/dist/crypto/sign.d.ts +1 -1
- package/dist/crypto/sign.js +38 -6
- package/dist/index.d.ts +4 -4
- package/dist/index.js +3 -3
- package/dist/ink/discovery-gating.d.ts +1 -0
- package/dist/models/agent-card.d.ts +7 -0
- package/dist/models/agent-card.js +21 -0
- package/dist/models/intent.d.ts +17 -1
- package/dist/models/intent.js +10 -1
- package/docs/maturity.md +10 -6
- package/package.json +6 -5
- 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
- package/test-vectors/README.md +4 -3
- package/test-vectors/body-signature.json +201 -0
package/CHANGELOG.md
CHANGED
|
@@ -4,9 +4,15 @@ All notable changes to INK are recorded
|
|
|
4
4
|
here. Pre-1.0 releases follow `0.Y.Z` semantics, see
|
|
5
5
|
[`docs/maturity.md`](docs/maturity.md) for the versioning policy.
|
|
6
6
|
|
|
7
|
-
##
|
|
7
|
+
## 0.2.0, version-keyed body-signature domain
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
Version-keyed body-signature domain. The body message signature is now domain-separated by protocol version. ink/0.1 messages, and any object with no explicit ink/0.2 protocol, keep the legacy `tulpa/sign` domain so every signature produced to date still verifies. ink/0.2 messages are signed and verified under the neutral `ink/sign` domain. The verifier selects exactly one domain from the signed `protocol` field and never tries an alternate, so a signature made under one version's domain cannot be replayed under another.
|
|
10
|
+
|
|
11
|
+
This change is receiver-first and backward compatible. Verifiers accept both versions and `MessageEnvelopeSchema` now accepts ink/0.1 and ink/0.2 as a strict enum, rejecting any unknown version. Senders still emit ink/0.1 by default. The HTTP transport-auth signature is unchanged.
|
|
12
|
+
|
|
13
|
+
New vectors in `test-vectors/body-signature.json` pin the version-keyed domain including the cross-version and tamper cases. The standalone Python interop client verifies them identically.
|
|
14
|
+
|
|
15
|
+
Per the pre-1.0 policy this release publishes under the `next` dist-tag.
|
|
10
16
|
|
|
11
17
|
## 0.1.7, expose per-intent payload schemas and getPayloadSchema from the package root
|
|
12
18
|
|
|
@@ -79,7 +85,7 @@ Fixes the v0.1.1 erratum: the Python `examples/interop-cli/` shipped in v0.1.1 e
|
|
|
79
85
|
|
|
80
86
|
**CLI now builds `connection_request` envelopes.** `ink-interop send/build --intent-type connection_request` (or the alias `connection`) constructs a `ConnectionRequestPayloadSchema`-conformant payload (`method`, `context`, `profileSnapshot`). This is the bootstrap intent for first contact between strangers: receivers that opt in to foreign senders verify the body signature against the inline key extracted from the sender's `did:key` (trust-on-first-use). Other intent types (`intro_request`, `ask`, `follow_up`) presume the sender is already a known contact and remain reserved for established relationships.
|
|
81
87
|
|
|
82
|
-
Verified end-to-end against `https://api.tulpa.network/ink/v1/<agentId>/intent`: a `did:key:` `connection_request` from `ink-interop send` lands as a pending action in the recipient's inbox (`status: 200`, `accepted: true`, `pendingActionId: 01KT…`). Coverage spans schema validation, body + transport signature verification, replay/freshness, identity resolution, routing, the foreign-DID policy gate
|
|
88
|
+
Verified end-to-end against `https://api.tulpa.network/ink/v1/<agentId>/intent`: a `did:key:` `connection_request` from `ink-interop send` lands as a pending action in the recipient's inbox (`status: 200`, `accepted: true`, `pendingActionId: 01KT…`). Coverage spans schema validation, body + transport signature verification, replay/freshness, identity resolution, routing, and the foreign-DID policy gate. Tests pinned to the canonical shape (`tests/test_envelope.py`) prevent regression. The npm library itself is unchanged from v0.1.1.
|
|
83
89
|
|
|
84
90
|
**Example-helper API break.** `examples/interop-cli/`'s Python helper `build_intent_envelope()` now requires `keypair`, replaces `intent_type`/`purpose`/`timestamp` with canonical args (`target`, `reason`, `created_at`, etc.), and removes the `extra=` kwarg. Adopters who imported the old helper directly will need to update their calls — the previous signature emitted invalid wire data so no callable interop existed there to preserve. This is an example-only change; the npm library (`@adastracomputing/ink`) exports are unchanged.
|
|
85
91
|
|
package/README.md
CHANGED
|
@@ -4,7 +4,9 @@
|
|
|
4
4
|
|
|
5
5
|
An open protocol for AI agents that need to send each other typed, signed messages on the public web. Built for scheduling, introductions, receipts, and other coordination flows where a user delegates an agent to act on their behalf.
|
|
6
6
|
|
|
7
|
-
**Status:
|
|
7
|
+
**Status: experimental; current defined wire version `ink/0.2`.** Wire formats, trust semantics, and APIs may change without backward-compatible migration before v1.0. On npm, `latest` is `0.1.2` and `0.2.0` is published on the `next` tag; senders still emit `ink/0.1` by default unless explicitly configured.
|
|
8
|
+
|
|
9
|
+
`ink/0.2` is the recommended target for new receiver implementations. It is a backward-compatible minor over `ink/0.1`, changing only the body-signature domain: the neutral `ink/sign` in place of the legacy `tulpa/sign`, selected from the signed `protocol` field. `ink/0.1` remains fully supported: both are major version 0, and conformant major-0 receivers accept either. There is no plan to drop `ink/0.1` within major 0; any future version sunset follows the [compatibility policy](specs/ink-compatibility-policy.md).
|
|
8
10
|
|
|
9
11
|
| | |
|
|
10
12
|
|---|---|
|
|
@@ -23,7 +25,7 @@ An open protocol for AI agents that need to send each other typed, signed messag
|
|
|
23
25
|
- [Agent-assisted implementation](#agent-assisted-implementation)
|
|
24
26
|
- [Tests](#tests)
|
|
25
27
|
- [Layout](#layout)
|
|
26
|
-
- [What's stable
|
|
28
|
+
- [What's stable](#whats-stable)
|
|
27
29
|
- [Naming](#naming)
|
|
28
30
|
- [Relationship to Tulpa](#relationship-to-tulpa)
|
|
29
31
|
- [Interoperability](#interoperability)
|
|
@@ -130,9 +132,9 @@ test/ vitest unit + integration tests
|
|
|
130
132
|
|
|
131
133
|
The library runs on any runtime providing standard Web Crypto and `fetch`: Node 24+, Deno, Bun, Cloudflare Workers, browsers. The timestamp freshness window is enforced inside `verifyInkAuth`; nonce single-use is enforced when a `NonceStore` is passed (otherwise `checkReplay` must be called separately). Nonce backing storage and its TTL policy are the integrator's choice.
|
|
132
134
|
|
|
133
|
-
## What's stable
|
|
135
|
+
## What's stable
|
|
134
136
|
|
|
135
|
-
Reliable to depend on:
|
|
137
|
+
These hold across major version 0 (both `ink/0.1` and `ink/0.2`). Reliable to depend on:
|
|
136
138
|
|
|
137
139
|
- Envelope structure and signing base
|
|
138
140
|
- Authorization: signed intent plus Agent Card key set
|
package/dist/crypto/keys.d.ts
CHANGED
|
@@ -30,13 +30,26 @@ export declare function decodePublicKeyMultibase(multibase: string): Uint8Array;
|
|
|
30
30
|
* Returns the raw 32-byte public key.
|
|
31
31
|
*/
|
|
32
32
|
export declare function decodeEncryptionKeyMultibase(multibase: string): Uint8Array;
|
|
33
|
+
/**
|
|
34
|
+
* agentId method prefixes that carry the same key-derived identity. Both encode
|
|
35
|
+
* the identical multibase public key, so they denote the same actor. `tulpa:`
|
|
36
|
+
* is canonical for emission (see deriveAgentId); `ink:` is an accepted inbound
|
|
37
|
+
* alias introduced in ink/0.4. Accept both, emit one.
|
|
38
|
+
*/
|
|
39
|
+
export declare const AGENT_ID_KEY_PREFIXES: readonly ["tulpa:", "ink:"];
|
|
33
40
|
/**
|
|
34
41
|
* Derive agent ID from a public key.
|
|
35
|
-
* Format: tulpa:<multibase-encoded-public-key>
|
|
42
|
+
* Format: tulpa:<multibase-encoded-public-key> (canonical emission).
|
|
36
43
|
*/
|
|
37
44
|
export declare function deriveAgentId(publicKey: Uint8Array): string;
|
|
38
45
|
/**
|
|
39
46
|
* Extract the public key from an agent ID.
|
|
40
47
|
* Only used for initial key exchange — after that, always resolve via identity store.
|
|
48
|
+
*
|
|
49
|
+
* Accepts either the canonical `tulpa:` prefix or the `ink:` alias (ink/0.4):
|
|
50
|
+
* both carry the identical multibase key, so a signature made with that key
|
|
51
|
+
* verifies regardless of which accepted prefix carried it. The prefix is
|
|
52
|
+
* identity syntax, not signing authority. The multibase tail is decoded the
|
|
53
|
+
* same way for both, so a malformed tail is rejected identically.
|
|
41
54
|
*/
|
|
42
55
|
export declare function extractPublicKeyFromAgentId(agentId: string): Uint8Array;
|
package/dist/crypto/keys.js
CHANGED
|
@@ -156,9 +156,16 @@ export function decodeEncryptionKeyMultibase(multibase) {
|
|
|
156
156
|
}
|
|
157
157
|
return key;
|
|
158
158
|
}
|
|
159
|
+
/**
|
|
160
|
+
* agentId method prefixes that carry the same key-derived identity. Both encode
|
|
161
|
+
* the identical multibase public key, so they denote the same actor. `tulpa:`
|
|
162
|
+
* is canonical for emission (see deriveAgentId); `ink:` is an accepted inbound
|
|
163
|
+
* alias introduced in ink/0.4. Accept both, emit one.
|
|
164
|
+
*/
|
|
165
|
+
export const AGENT_ID_KEY_PREFIXES = Object.freeze(["tulpa:", "ink:"]);
|
|
159
166
|
/**
|
|
160
167
|
* Derive agent ID from a public key.
|
|
161
|
-
* Format: tulpa:<multibase-encoded-public-key>
|
|
168
|
+
* Format: tulpa:<multibase-encoded-public-key> (canonical emission).
|
|
162
169
|
*/
|
|
163
170
|
export function deriveAgentId(publicKey) {
|
|
164
171
|
return `tulpa:${encodePublicKeyMultibase(publicKey)}`;
|
|
@@ -166,13 +173,19 @@ export function deriveAgentId(publicKey) {
|
|
|
166
173
|
/**
|
|
167
174
|
* Extract the public key from an agent ID.
|
|
168
175
|
* Only used for initial key exchange — after that, always resolve via identity store.
|
|
176
|
+
*
|
|
177
|
+
* Accepts either the canonical `tulpa:` prefix or the `ink:` alias (ink/0.4):
|
|
178
|
+
* both carry the identical multibase key, so a signature made with that key
|
|
179
|
+
* verifies regardless of which accepted prefix carried it. The prefix is
|
|
180
|
+
* identity syntax, not signing authority. The multibase tail is decoded the
|
|
181
|
+
* same way for both, so a malformed tail is rejected identically.
|
|
169
182
|
*/
|
|
170
183
|
export function extractPublicKeyFromAgentId(agentId) {
|
|
171
184
|
if (typeof agentId !== "string" || agentId.length === 0 || agentId.length > 512) {
|
|
172
185
|
throw new Error("Invalid agent ID");
|
|
173
186
|
}
|
|
174
|
-
const prefix =
|
|
175
|
-
if (!
|
|
187
|
+
const prefix = AGENT_ID_KEY_PREFIXES.find((p) => agentId.startsWith(p));
|
|
188
|
+
if (!prefix) {
|
|
176
189
|
throw new Error("Invalid agent ID format");
|
|
177
190
|
}
|
|
178
191
|
return decodePublicKeyMultibase(agentId.slice(prefix.length));
|
package/dist/crypto/sign.d.ts
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*
|
|
4
4
|
* 1. Remove `signature` field if present
|
|
5
5
|
* 2. JCS canonicalize (RFC 8785) via `canonicalize` library
|
|
6
|
-
* 3. Sign canonical bytes directly with Ed25519
|
|
6
|
+
* 3. Sign domain-prefixed canonical bytes directly with Ed25519
|
|
7
7
|
* 4. Return base64url-encoded signature (no padding)
|
|
8
8
|
*/
|
|
9
9
|
export declare function signMessage(message: Record<string, unknown>, privateKey: Uint8Array): Promise<string>;
|
package/dist/crypto/sign.js
CHANGED
|
@@ -54,12 +54,39 @@ function isWithinBounds(value) {
|
|
|
54
54
|
}
|
|
55
55
|
return walk(value, 0);
|
|
56
56
|
}
|
|
57
|
+
/**
|
|
58
|
+
* Body-signature domain-separation prefix, keyed off the message's
|
|
59
|
+
* declared `protocol` version.
|
|
60
|
+
*
|
|
61
|
+
* - `ink/0.2` -> `ink/sign\n` (neutral, current).
|
|
62
|
+
* - everything else (`ink/0.1`, or any object with no explicit
|
|
63
|
+
* `ink/0.2` protocol) -> `tulpa/sign\n`, the legacy domain, kept
|
|
64
|
+
* forever so every signature ever produced still verifies.
|
|
65
|
+
*
|
|
66
|
+
* The prefix is derived from the `protocol` field that is part of the
|
|
67
|
+
* signed body, so a verifier selects exactly one domain and tampering
|
|
68
|
+
* with `protocol` after signing breaks the signature (an `ink/0.2` body
|
|
69
|
+
* re-labelled `ink/0.1` is verified under `tulpa/sign\n` against a
|
|
70
|
+
* signature made over `ink/sign\n`, and fails). Only the exact string
|
|
71
|
+
* `"ink/0.2"` switches domains, so no other value can smuggle one in.
|
|
72
|
+
*
|
|
73
|
+
* This raw signer stays permissive on purpose: it is a general-purpose
|
|
74
|
+
* Ed25519 message signer (receipts, arbitrary objects), not envelope-
|
|
75
|
+
* specific. Strict "reject unknown protocol version" lives at the
|
|
76
|
+
* envelope schema layer, which validates `protocol` against the allowed
|
|
77
|
+
* set before this function is reached.
|
|
78
|
+
*/
|
|
79
|
+
const LEGACY_SIGN_DOMAIN = "tulpa/sign\n";
|
|
80
|
+
const V02_SIGN_DOMAIN = "ink/sign\n";
|
|
81
|
+
function bodySignatureDomain(unsigned) {
|
|
82
|
+
return unsigned.protocol === "ink/0.2" ? V02_SIGN_DOMAIN : LEGACY_SIGN_DOMAIN;
|
|
83
|
+
}
|
|
57
84
|
/**
|
|
58
85
|
* Sign a message object using Ed25519.
|
|
59
86
|
*
|
|
60
87
|
* 1. Remove `signature` field if present
|
|
61
88
|
* 2. JCS canonicalize (RFC 8785) via `canonicalize` library
|
|
62
|
-
* 3. Sign canonical bytes directly with Ed25519
|
|
89
|
+
* 3. Sign domain-prefixed canonical bytes directly with Ed25519
|
|
63
90
|
* 4. Return base64url-encoded signature (no padding)
|
|
64
91
|
*/
|
|
65
92
|
export async function signMessage(message, privateKey) {
|
|
@@ -83,8 +110,10 @@ export async function signMessage(message, privateKey) {
|
|
|
83
110
|
if (canonical.length > MAX_MESSAGE_CANONICAL_BYTES) {
|
|
84
111
|
throw new Error("Canonicalized message exceeds maximum allowed size");
|
|
85
112
|
}
|
|
86
|
-
// Domain-separated signing to prevent cross-protocol signature replay
|
|
87
|
-
|
|
113
|
+
// Domain-separated signing to prevent cross-protocol signature replay.
|
|
114
|
+
// Domain is keyed off the (signed) protocol version; see
|
|
115
|
+
// bodySignatureDomain. Legacy ink/0.1 keeps the tulpa/sign domain.
|
|
116
|
+
const prefixed = `${bodySignatureDomain(unsigned)}${canonical}`;
|
|
88
117
|
const bytes = new TextEncoder().encode(prefixed);
|
|
89
118
|
const sig = await ed.signAsync(bytes, privateKey);
|
|
90
119
|
return base64urlEncode(sig);
|
|
@@ -124,9 +153,12 @@ export async function verifyMessage(message, publicKey) {
|
|
|
124
153
|
if (canonical.length > MAX_MESSAGE_CANONICAL_BYTES) {
|
|
125
154
|
return false;
|
|
126
155
|
}
|
|
127
|
-
// Domain-prefixed verification only
|
|
128
|
-
//
|
|
129
|
-
|
|
156
|
+
// Domain-prefixed verification only. The domain is selected from the
|
|
157
|
+
// signed `protocol` field (see bodySignatureDomain); a verifier never
|
|
158
|
+
// tries an alternate prefix, so a signature made under one version's
|
|
159
|
+
// domain cannot be replayed under another. Legacy unprefixed
|
|
160
|
+
// signatures are not accepted.
|
|
161
|
+
const prefixed = `${bodySignatureDomain(unsigned)}${canonical}`;
|
|
130
162
|
const prefixedBytes = new TextEncoder().encode(prefixed);
|
|
131
163
|
try {
|
|
132
164
|
const sig = base64urlDecode(signature);
|
package/dist/index.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
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, 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";
|
|
@@ -16,12 +16,12 @@ export type { InkAuditEventType, InkAuditEvent, InkAuditInclusion, InkReceipt, I
|
|
|
16
16
|
export { InkChallengeSchema, InkRejectionSchema, InkResolutionSchema, InkTransportSchema, } from "./models/ink-handshake.js";
|
|
17
17
|
export type { AgentCardVisibility, InkChallenge, InkRejection, InkResolution, InkTransport, } from "./models/ink-handshake.js";
|
|
18
18
|
export { AgentCardSchema } from "./models/agent-card.js";
|
|
19
|
-
export { validateMessage, getPayloadSchema, MessageEnvelopeSchema, IntentTypeSchema, ScheduleMeetingPayloadSchema, ScheduleMeetingResponsePayloadSchema, IntroRequestPayloadSchema, IntroResponsePayloadSchema, OpportunityPayloadSchema, OpportunityResponsePayloadSchema, ConnectionRequestPayloadSchema, ConnectionResponsePayloadSchema, FollowUpPayloadSchema, AskPayloadSchema, AskResponsePayloadSchema, PingPayloadSchema, RetractPayloadSchema, ContextSharePayloadSchema, MultiPartySyncPayloadSchema, } from "./models/intent.js";
|
|
20
|
-
export type { MessageEnvelope, IntentType, } from "./models/intent.js";
|
|
19
|
+
export { validateMessage, getPayloadSchema, MessageEnvelopeSchema, ProtocolVersionSchema, INK_PROTOCOL_VERSIONS, IntentTypeSchema, ScheduleMeetingPayloadSchema, ScheduleMeetingResponsePayloadSchema, IntroRequestPayloadSchema, IntroResponsePayloadSchema, OpportunityPayloadSchema, OpportunityResponsePayloadSchema, ConnectionRequestPayloadSchema, ConnectionResponsePayloadSchema, FollowUpPayloadSchema, AskPayloadSchema, AskResponsePayloadSchema, PingPayloadSchema, RetractPayloadSchema, ContextSharePayloadSchema, MultiPartySyncPayloadSchema, } from "./models/intent.js";
|
|
20
|
+
export type { MessageEnvelope, ProtocolVersion, IntentType, } from "./models/intent.js";
|
|
21
21
|
export { KeyStatusSchema, KeyRoleSchema, KeyEntrySchema, } from "./models/key-entry.js";
|
|
22
22
|
export type { KeyStatus, KeyRole, KeyEntry, StoredKey, } from "./models/key-entry.js";
|
|
23
23
|
export type { InkSignInput } from "./crypto/ink.js";
|
|
24
24
|
export type { CandidateKey } from "./models/key-entry.js";
|
|
25
|
-
export { resolveAgentInbox } from "./models/agent-card.js";
|
|
25
|
+
export { resolveAgentInbox, agentSupportedProtocolVersions } from "./models/agent-card.js";
|
|
26
26
|
export type { AgentCard } from "./models/agent-card.js";
|
|
27
27
|
export type { BudgetCheckResult, HandshakeBudgetConfig, } from "./ink/handshake-budget.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, 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
|
|
@@ -32,9 +32,9 @@ export { AgentCardSchema } from "./models/agent-card.js";
|
|
|
32
32
|
// reject malformed envelopes before signature verification; without
|
|
33
33
|
// it they have to re-implement the schema check or import from a
|
|
34
34
|
// non-public path.
|
|
35
|
-
export { validateMessage, getPayloadSchema, MessageEnvelopeSchema, IntentTypeSchema, ScheduleMeetingPayloadSchema, ScheduleMeetingResponsePayloadSchema, IntroRequestPayloadSchema, IntroResponsePayloadSchema, OpportunityPayloadSchema, OpportunityResponsePayloadSchema, ConnectionRequestPayloadSchema, ConnectionResponsePayloadSchema, FollowUpPayloadSchema, AskPayloadSchema, AskResponsePayloadSchema, PingPayloadSchema, RetractPayloadSchema, ContextSharePayloadSchema, MultiPartySyncPayloadSchema, } from "./models/intent.js";
|
|
35
|
+
export { validateMessage, getPayloadSchema, MessageEnvelopeSchema, ProtocolVersionSchema, INK_PROTOCOL_VERSIONS, IntentTypeSchema, ScheduleMeetingPayloadSchema, ScheduleMeetingResponsePayloadSchema, IntroRequestPayloadSchema, IntroResponsePayloadSchema, OpportunityPayloadSchema, OpportunityResponsePayloadSchema, ConnectionRequestPayloadSchema, ConnectionResponsePayloadSchema, FollowUpPayloadSchema, AskPayloadSchema, AskResponsePayloadSchema, PingPayloadSchema, RetractPayloadSchema, ContextSharePayloadSchema, MultiPartySyncPayloadSchema, } from "./models/intent.js";
|
|
36
36
|
// Key-entry types and schemas for adopters wiring their own key-set
|
|
37
37
|
// storage and rotation. `CandidateKey` was already root-exported via
|
|
38
38
|
// the verifier surface; this batch adds the persistence shapes.
|
|
39
39
|
export { KeyStatusSchema, KeyRoleSchema, KeyEntrySchema, } from "./models/key-entry.js";
|
|
40
|
-
export { resolveAgentInbox } from "./models/agent-card.js";
|
|
40
|
+
export { resolveAgentInbox, agentSupportedProtocolVersions } from "./models/agent-card.js";
|
|
@@ -202,6 +202,7 @@ export declare const AgentCardResponseSchema: z.ZodObject<{
|
|
|
202
202
|
currentSigningKeyId: z.ZodOptional<z.ZodString>;
|
|
203
203
|
currentEncryptionKeyId: z.ZodOptional<z.ZodString>;
|
|
204
204
|
keySetVersion: z.ZodOptional<z.ZodNumber>;
|
|
205
|
+
supportedProtocolVersions: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
205
206
|
visibility: z.ZodOptional<z.ZodEnum<{
|
|
206
207
|
public: "public";
|
|
207
208
|
network_only: "network_only";
|
|
@@ -129,6 +129,7 @@ export declare const AgentCardSchema: z.ZodObject<{
|
|
|
129
129
|
currentSigningKeyId: z.ZodOptional<z.ZodString>;
|
|
130
130
|
currentEncryptionKeyId: z.ZodOptional<z.ZodString>;
|
|
131
131
|
keySetVersion: z.ZodOptional<z.ZodNumber>;
|
|
132
|
+
supportedProtocolVersions: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
132
133
|
visibility: z.ZodOptional<z.ZodEnum<{
|
|
133
134
|
public: "public";
|
|
134
135
|
network_only: "network_only";
|
|
@@ -153,6 +154,12 @@ export declare const AgentCardSchema: z.ZodObject<{
|
|
|
153
154
|
}, z.core.$strip>>;
|
|
154
155
|
}, z.core.$strip>;
|
|
155
156
|
export type AgentCard = z.infer<typeof AgentCardSchema>;
|
|
157
|
+
/**
|
|
158
|
+
* The message protocol versions a card's receiver can verify. Falls back
|
|
159
|
+
* to ink/0.1 when the card does not advertise the field, so a sender
|
|
160
|
+
* defaults to the original version for any card that predates it.
|
|
161
|
+
*/
|
|
162
|
+
export declare function agentSupportedProtocolVersions(card: Pick<AgentCard, "supportedProtocolVersions">): string[];
|
|
156
163
|
/**
|
|
157
164
|
* Return the inbound message URL for an Agent Card.
|
|
158
165
|
*
|
|
@@ -58,6 +58,18 @@ export const AgentCardSchema = z.object({
|
|
|
58
58
|
currentSigningKeyId: z.string().optional(),
|
|
59
59
|
currentEncryptionKeyId: z.string().optional(),
|
|
60
60
|
keySetVersion: z.number().int().positive().optional(),
|
|
61
|
+
// Message protocol versions this agent's receiver can verify on the
|
|
62
|
+
// body signature. When absent, assume ink/0.1 only. A sender MUST NOT
|
|
63
|
+
// emit a newer version to a card that does not advertise it; advertising
|
|
64
|
+
// a version here is necessary but not sufficient for a sender to use it.
|
|
65
|
+
//
|
|
66
|
+
// The entries are advisory hints, so they are accepted as bounded
|
|
67
|
+
// strings rather than the strict version enum: a newer peer may
|
|
68
|
+
// advertise a version this build does not know yet, and that must not
|
|
69
|
+
// make its whole card unparseable. A sender intersects this list with
|
|
70
|
+
// the versions it can actually emit. The strict enum lives on the
|
|
71
|
+
// envelope (MessageEnvelopeSchema), where an unknown version is rejected.
|
|
72
|
+
supportedProtocolVersions: z.array(z.string().max(16)).max(8).optional(),
|
|
61
73
|
// Containment extension (Phase 1)
|
|
62
74
|
visibility: AgentCardVisibilitySchema.optional(),
|
|
63
75
|
governance: z.object({
|
|
@@ -83,6 +95,15 @@ export const AgentCardSchema = z.object({
|
|
|
83
95
|
});
|
|
84
96
|
}
|
|
85
97
|
});
|
|
98
|
+
/**
|
|
99
|
+
* The message protocol versions a card's receiver can verify. Falls back
|
|
100
|
+
* to ink/0.1 when the card does not advertise the field, so a sender
|
|
101
|
+
* defaults to the original version for any card that predates it.
|
|
102
|
+
*/
|
|
103
|
+
export function agentSupportedProtocolVersions(card) {
|
|
104
|
+
const advertised = card.supportedProtocolVersions;
|
|
105
|
+
return advertised && advertised.length > 0 ? advertised : ["ink/0.1"];
|
|
106
|
+
}
|
|
86
107
|
/**
|
|
87
108
|
* Return the inbound message URL for an Agent Card.
|
|
88
109
|
*
|
package/dist/models/intent.d.ts
CHANGED
|
@@ -214,8 +214,24 @@ export declare const MessageProvenanceSchema: z.ZodOptional<z.ZodObject<{
|
|
|
214
214
|
extensionId: z.ZodString;
|
|
215
215
|
installationId: z.ZodString;
|
|
216
216
|
}, z.core.$strict>>;
|
|
217
|
+
/**
|
|
218
|
+
* INK protocol versions a receiver accepts. ink/0.1 is the original wire
|
|
219
|
+
* version; ink/0.2 differs only in the body-signature domain (see
|
|
220
|
+
* src/crypto/sign.ts). The enum is strict: an unknown version is rejected
|
|
221
|
+
* at schema validation, never inferred. Senders still emit ink/0.1 by
|
|
222
|
+
* default; emitting ink/0.2 is a later, negotiated step.
|
|
223
|
+
*/
|
|
224
|
+
export declare const INK_PROTOCOL_VERSIONS: readonly ["ink/0.1", "ink/0.2"];
|
|
225
|
+
export declare const ProtocolVersionSchema: z.ZodEnum<{
|
|
226
|
+
"ink/0.1": "ink/0.1";
|
|
227
|
+
"ink/0.2": "ink/0.2";
|
|
228
|
+
}>;
|
|
229
|
+
export type ProtocolVersion = z.infer<typeof ProtocolVersionSchema>;
|
|
217
230
|
export declare const MessageEnvelopeSchema: z.ZodObject<{
|
|
218
|
-
protocol: z.
|
|
231
|
+
protocol: z.ZodEnum<{
|
|
232
|
+
"ink/0.1": "ink/0.1";
|
|
233
|
+
"ink/0.2": "ink/0.2";
|
|
234
|
+
}>;
|
|
219
235
|
id: z.ZodString;
|
|
220
236
|
correlationId: z.ZodString;
|
|
221
237
|
createdAt: z.ZodString;
|
package/dist/models/intent.js
CHANGED
|
@@ -155,8 +155,17 @@ export const MessageProvenanceSchema = z.object({
|
|
|
155
155
|
extensionId: z.string().max(ID_MAX),
|
|
156
156
|
installationId: z.string().uuid(),
|
|
157
157
|
}).strict().optional();
|
|
158
|
+
/**
|
|
159
|
+
* INK protocol versions a receiver accepts. ink/0.1 is the original wire
|
|
160
|
+
* version; ink/0.2 differs only in the body-signature domain (see
|
|
161
|
+
* src/crypto/sign.ts). The enum is strict: an unknown version is rejected
|
|
162
|
+
* at schema validation, never inferred. Senders still emit ink/0.1 by
|
|
163
|
+
* default; emitting ink/0.2 is a later, negotiated step.
|
|
164
|
+
*/
|
|
165
|
+
export const INK_PROTOCOL_VERSIONS = ["ink/0.1", "ink/0.2"];
|
|
166
|
+
export const ProtocolVersionSchema = z.enum(INK_PROTOCOL_VERSIONS);
|
|
158
167
|
export const MessageEnvelopeSchema = z.object({
|
|
159
|
-
protocol:
|
|
168
|
+
protocol: ProtocolVersionSchema,
|
|
160
169
|
id: z.string().max(ID_MAX),
|
|
161
170
|
correlationId: z.string().max(ID_MAX),
|
|
162
171
|
createdAt: z.string().max(TIMESTAMP_MAX),
|
package/docs/maturity.md
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
# Maturity Notice
|
|
2
2
|
|
|
3
|
-
> INK
|
|
4
|
-
>
|
|
5
|
-
>
|
|
3
|
+
> INK is **experimental**. The current defined wire version is `ink/0.2`, a
|
|
4
|
+
> backward-compatible minor over `ink/0.1` (both major version 0). Wire formats,
|
|
5
|
+
> trust semantics and APIs may change without backward-compatible migration
|
|
6
|
+
> before v1.0. Do not use for load-bearing production traffic without your own
|
|
7
|
+
> review.
|
|
6
8
|
|
|
7
9
|
## What "experimental" means here
|
|
8
10
|
|
|
@@ -12,8 +14,8 @@
|
|
|
12
14
|
agent-card fetch, and DoS-amplification surfaces. Internal review is
|
|
13
15
|
not a substitute for a third-party audit, treat the security
|
|
14
16
|
posture accordingly.
|
|
15
|
-
- Interop vectors (`../test-vectors/`) are authoritative for
|
|
16
|
-
be added to or revised between
|
|
17
|
+
- Interop vectors (`../test-vectors/`) are authoritative for the current wire
|
|
18
|
+
version but may be added to or revised between patch releases. Mismatched
|
|
17
19
|
implementations should report discrepancies as issues.
|
|
18
20
|
- The protocol is in use by one production integrator (Tulpa). That is
|
|
19
21
|
one data point, not a guarantee of robustness at scale.
|
|
@@ -22,7 +24,9 @@
|
|
|
22
24
|
Bun, and edge runtimes. Browser use is feasible but not exercised by
|
|
23
25
|
the maintainers.
|
|
24
26
|
|
|
25
|
-
## What is stable
|
|
27
|
+
## What is stable
|
|
28
|
+
|
|
29
|
+
These hold across major version 0 (`ink/0.1` and `ink/0.2`):
|
|
26
30
|
|
|
27
31
|
- Envelope structure (fields, canonicalization with JCS / RFC 8785)
|
|
28
32
|
- Ed25519 signing base: `ink/0.1\nMETHOD\nPATH\nrecipientDid\nJCS(body)\ntimestamp`
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@adastracomputing/ink",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "Library and specification for the INK (Inter-agent Networking Kernel) protocol",
|
|
5
5
|
"license": "MIT OR Apache-2.0",
|
|
6
6
|
"author": "Ad Astra Computing Inc.",
|
|
@@ -49,6 +49,7 @@
|
|
|
49
49
|
"lint": "eslint src/ test/ scripts/",
|
|
50
50
|
"check:surface": "tsx scripts/check-public-surface.ts",
|
|
51
51
|
"check:pack": "./scripts/check-pack.sh",
|
|
52
|
+
"gen:body-vectors": "tsx scripts/gen-body-signature-vectors.ts",
|
|
52
53
|
"prepack": "npm run build",
|
|
53
54
|
"prepublishOnly": "npm run build"
|
|
54
55
|
},
|
|
@@ -60,12 +61,12 @@
|
|
|
60
61
|
"zod": "^4.4.3"
|
|
61
62
|
},
|
|
62
63
|
"devDependencies": {
|
|
63
|
-
"@cloudflare/workers-types": "^4.
|
|
64
|
+
"@cloudflare/workers-types": "^4.20260604.1",
|
|
64
65
|
"@types/node": "^24.12.4",
|
|
65
|
-
"@typescript-eslint/eslint-plugin": "^8.60.
|
|
66
|
+
"@typescript-eslint/eslint-plugin": "^8.60.1",
|
|
66
67
|
"@typescript-eslint/parser": "^8.60.0",
|
|
67
|
-
"eslint": "^10.4.
|
|
68
|
-
"tsx": "^4.22.
|
|
68
|
+
"eslint": "^10.4.1",
|
|
69
|
+
"tsx": "^4.22.4",
|
|
69
70
|
"typescript": "^6.0.3",
|
|
70
71
|
"vitest": "^4.1.7"
|
|
71
72
|
},
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
# INK Compatibility and Versioning Policy
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
3
|
+
**Status:** Draft, v1 stabilization
|
|
4
|
+
**Authors:** Ad Astra Computing
|
|
5
|
+
**Last updated:** 2026-05-24
|
|
5
6
|
|
|
6
7
|
## Purpose
|
|
7
8
|
|
|
@@ -15,7 +16,7 @@ This is the normative compatibility contract. Any change to the INK wire format
|
|
|
15
16
|
|
|
16
17
|
INK uses a single protocol version string in every message envelope, receipt, audit event and handshake message.
|
|
17
18
|
|
|
18
|
-
|
|
19
|
+
Defined versions: `ink/0.1` (default) and `ink/0.2` (negotiated). See [§1.4](#14-defined-wire-versions).
|
|
19
20
|
|
|
20
21
|
The version string appears in the `protocol` field of every top-level INK object and in the first line of every signature base.
|
|
21
22
|
|
|
@@ -48,6 +49,17 @@ prefix (e.g. `network.ink.*`) and define a transition policy. Until then,
|
|
|
48
49
|
conforming implementations MUST emit and accept `network.tulpa.*` types as
|
|
49
50
|
specified.
|
|
50
51
|
|
|
52
|
+
### 1.4 Defined wire versions
|
|
53
|
+
|
|
54
|
+
Two wire versions are defined:
|
|
55
|
+
|
|
56
|
+
- `ink/0.1`, the original version. A sender emits it by default unless it has positively negotiated otherwise.
|
|
57
|
+
- `ink/0.2`, a backward-compatible minor that changes only the body-signature domain separator, from the legacy `tulpa/sign\n` to the neutral `ink/sign\n`. Everything else, the transport-auth signature base, the envelope shape, the encryption and audit sub-protocols and every `network.tulpa.*` type, is identical to `ink/0.1`.
|
|
58
|
+
|
|
59
|
+
`ink/0.2` is receiver-first. A receiver advertises the versions it verifies in its Agent Card `supportedProtocolVersions` array; when that field is absent a sender MUST assume `ink/0.1` only, and a sender MUST NOT emit `ink/0.2` to a receiver that has not advertised it. The negotiation is what keeps the change compatible: an `ink/0.1`-only receiver never receives `ink/0.2` traffic, so it is never asked to verify a domain it does not implement. An `ink/0.2` receiver selects the body-signature domain from the signed `protocol` field and verifies both versions, and because `protocol` is inside the signed body a relabelled message fails verification.
|
|
60
|
+
|
|
61
|
+
This satisfies §1.1. The minor bump adds a capability without breaking deployed `ink/0.1` implementations, because the body-signature domain is negotiated rather than assumed.
|
|
62
|
+
|
|
51
63
|
---
|
|
52
64
|
|
|
53
65
|
## 2. Compatibility Rules
|
package/test-vectors/README.md
CHANGED
|
@@ -7,16 +7,17 @@ Reference test vectors for INK v0.1 signing, encryption, replay protection, hand
|
|
|
7
7
|
| File | Covers | Vector count |
|
|
8
8
|
|------|--------|-------------|
|
|
9
9
|
| `keys.json` | Fixed Ed25519 and X25519 key pairs for Alice and Bob (hex-encoded) |, |
|
|
10
|
-
| `signing.json` |
|
|
10
|
+
| `signing.json` | Transport-auth signature generation and verification (§3.3) | 3 |
|
|
11
|
+
| `body-signature.json` | Version-keyed body message signature: legacy vs ink/0.2 domain, plus cross-version and tamper cases | 9 |
|
|
11
12
|
| `encryption.json` | ECIES encryption/decryption (§3.4) | 2 |
|
|
12
|
-
| `jcs.json` | JCS canonicalization (RFC 8785) |
|
|
13
|
+
| `jcs.json` | JCS canonicalization (RFC 8785) | 2 |
|
|
13
14
|
| `replay.json` | Replay protection acceptance/rejection (§3.5) | 6 |
|
|
14
15
|
| `receipts-and-audit.json` | Receipt signatures, audit query signatures, hash-chained audit events and fork detection (Auditability §1–§3) | 4 |
|
|
15
16
|
| `handshake.json` | Challenge (Stage 2a), rejection (Stage 2b) and resolution (Stage 3), valid signatures, path/recipient/body binding failures, replay protection | 22 |
|
|
16
17
|
| `witness.json` | Audit submit and query with INK transport auth, plus cross-service interop cases | 15 |
|
|
17
18
|
| `key-rotation.json` | Auth header keyId format, rotated-key verification, historical verification, revoked-key rejection, refresh-on-miss, keyId precedence, unknown keyId fallthrough, audit event signingKeyId tracking | 8 |
|
|
18
19
|
|
|
19
|
-
**Total:
|
|
20
|
+
**Total: 71 deterministic vectors across 10 families**
|
|
20
21
|
|
|
21
22
|
## Vector categories
|
|
22
23
|
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
{
|
|
2
|
+
"description": "INK body message signature: domain is keyed off the signed protocol field (ink/0.2 -> ink/sign, else tulpa/sign). Verify each body with the signer public key; signatureVerifies is the expected verifyMessage result.",
|
|
3
|
+
"vectors": [
|
|
4
|
+
{
|
|
5
|
+
"description": "ink/0.1 body signed under the legacy tulpa/sign domain verifies",
|
|
6
|
+
"input": {
|
|
7
|
+
"body": {
|
|
8
|
+
"protocol": "ink/0.1",
|
|
9
|
+
"id": "01HVECTORID0000000000000000",
|
|
10
|
+
"from": "did:key:zAlice",
|
|
11
|
+
"to": "did:key:zBob",
|
|
12
|
+
"intent": "connection_request",
|
|
13
|
+
"payload": {
|
|
14
|
+
"method": "discovery"
|
|
15
|
+
},
|
|
16
|
+
"timestamp": "2026-06-03T00:00:00Z",
|
|
17
|
+
"nonce": "ZmtmaXhlZG5vbmNl",
|
|
18
|
+
"signature": "W44kkGyQFg348NADetWQPq5Ogi7rL_72CwjmK-XVn2hL8sXYuSM7cFaDVidGV5LeK3dmmqW6iu5QiWkm6qboAQ"
|
|
19
|
+
},
|
|
20
|
+
"signerPublicKeyHex": "79b5562e8fe654f94078b112e8a98ba7901f853ae695bed7e0e3910bad049664"
|
|
21
|
+
},
|
|
22
|
+
"expected": {
|
|
23
|
+
"signatureVerifies": true
|
|
24
|
+
}
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
"description": "ink/0.2 body signed under the ink/sign domain verifies",
|
|
28
|
+
"input": {
|
|
29
|
+
"body": {
|
|
30
|
+
"protocol": "ink/0.2",
|
|
31
|
+
"id": "01HVECTORID0000000000000000",
|
|
32
|
+
"from": "did:key:zAlice",
|
|
33
|
+
"to": "did:key:zBob",
|
|
34
|
+
"intent": "connection_request",
|
|
35
|
+
"payload": {
|
|
36
|
+
"method": "discovery"
|
|
37
|
+
},
|
|
38
|
+
"timestamp": "2026-06-03T00:00:00Z",
|
|
39
|
+
"nonce": "ZmtmaXhlZG5vbmNl",
|
|
40
|
+
"signature": "UAITw3Qqcg96s4r5tmxHXGpuHCfzJBgxihVRPdR8FYcQKk2nYcYxrV8fRFUH3NB_-z-006tFWgZfzJbFILw4Bg"
|
|
41
|
+
},
|
|
42
|
+
"signerPublicKeyHex": "79b5562e8fe654f94078b112e8a98ba7901f853ae695bed7e0e3910bad049664"
|
|
43
|
+
},
|
|
44
|
+
"expected": {
|
|
45
|
+
"signatureVerifies": true
|
|
46
|
+
}
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
"description": "protocol-less body signed under the legacy domain verifies",
|
|
50
|
+
"input": {
|
|
51
|
+
"body": {
|
|
52
|
+
"id": "01HVECTORID0000000000000000",
|
|
53
|
+
"from": "did:key:zAlice",
|
|
54
|
+
"to": "did:key:zBob",
|
|
55
|
+
"intent": "connection_request",
|
|
56
|
+
"payload": {
|
|
57
|
+
"method": "discovery"
|
|
58
|
+
},
|
|
59
|
+
"timestamp": "2026-06-03T00:00:00Z",
|
|
60
|
+
"nonce": "ZmtmaXhlZG5vbmNl",
|
|
61
|
+
"signature": "qP1oxHKCEImzkYT8Iz00N4Crqi5prbKRguKj_zg8YlLyCrE4CmElrNWECEBWBe-8XV_rNB4oMc1wReXwHqOFCA"
|
|
62
|
+
},
|
|
63
|
+
"signerPublicKeyHex": "79b5562e8fe654f94078b112e8a98ba7901f853ae695bed7e0e3910bad049664"
|
|
64
|
+
},
|
|
65
|
+
"expected": {
|
|
66
|
+
"signatureVerifies": true
|
|
67
|
+
}
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
"description": "ink/0.2 body relabelled ink/0.1 (domain mismatch) does not verify",
|
|
71
|
+
"input": {
|
|
72
|
+
"body": {
|
|
73
|
+
"protocol": "ink/0.1",
|
|
74
|
+
"id": "01HVECTORID0000000000000000",
|
|
75
|
+
"from": "did:key:zAlice",
|
|
76
|
+
"to": "did:key:zBob",
|
|
77
|
+
"intent": "connection_request",
|
|
78
|
+
"payload": {
|
|
79
|
+
"method": "discovery"
|
|
80
|
+
},
|
|
81
|
+
"timestamp": "2026-06-03T00:00:00Z",
|
|
82
|
+
"nonce": "ZmtmaXhlZG5vbmNl",
|
|
83
|
+
"signature": "UAITw3Qqcg96s4r5tmxHXGpuHCfzJBgxihVRPdR8FYcQKk2nYcYxrV8fRFUH3NB_-z-006tFWgZfzJbFILw4Bg"
|
|
84
|
+
},
|
|
85
|
+
"signerPublicKeyHex": "79b5562e8fe654f94078b112e8a98ba7901f853ae695bed7e0e3910bad049664"
|
|
86
|
+
},
|
|
87
|
+
"expected": {
|
|
88
|
+
"signatureVerifies": false
|
|
89
|
+
}
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
"description": "ink/0.1 body relabelled ink/0.2 (domain mismatch) does not verify",
|
|
93
|
+
"input": {
|
|
94
|
+
"body": {
|
|
95
|
+
"protocol": "ink/0.2",
|
|
96
|
+
"id": "01HVECTORID0000000000000000",
|
|
97
|
+
"from": "did:key:zAlice",
|
|
98
|
+
"to": "did:key:zBob",
|
|
99
|
+
"intent": "connection_request",
|
|
100
|
+
"payload": {
|
|
101
|
+
"method": "discovery"
|
|
102
|
+
},
|
|
103
|
+
"timestamp": "2026-06-03T00:00:00Z",
|
|
104
|
+
"nonce": "ZmtmaXhlZG5vbmNl",
|
|
105
|
+
"signature": "W44kkGyQFg348NADetWQPq5Ogi7rL_72CwjmK-XVn2hL8sXYuSM7cFaDVidGV5LeK3dmmqW6iu5QiWkm6qboAQ"
|
|
106
|
+
},
|
|
107
|
+
"signerPublicKeyHex": "79b5562e8fe654f94078b112e8a98ba7901f853ae695bed7e0e3910bad049664"
|
|
108
|
+
},
|
|
109
|
+
"expected": {
|
|
110
|
+
"signatureVerifies": false
|
|
111
|
+
}
|
|
112
|
+
},
|
|
113
|
+
{
|
|
114
|
+
"description": "ink/0.2 body with protocol removed (falls back to legacy domain) does not verify",
|
|
115
|
+
"input": {
|
|
116
|
+
"body": {
|
|
117
|
+
"id": "01HVECTORID0000000000000000",
|
|
118
|
+
"from": "did:key:zAlice",
|
|
119
|
+
"to": "did:key:zBob",
|
|
120
|
+
"intent": "connection_request",
|
|
121
|
+
"payload": {
|
|
122
|
+
"method": "discovery"
|
|
123
|
+
},
|
|
124
|
+
"timestamp": "2026-06-03T00:00:00Z",
|
|
125
|
+
"nonce": "ZmtmaXhlZG5vbmNl",
|
|
126
|
+
"signature": "UAITw3Qqcg96s4r5tmxHXGpuHCfzJBgxihVRPdR8FYcQKk2nYcYxrV8fRFUH3NB_-z-006tFWgZfzJbFILw4Bg"
|
|
127
|
+
},
|
|
128
|
+
"signerPublicKeyHex": "79b5562e8fe654f94078b112e8a98ba7901f853ae695bed7e0e3910bad049664"
|
|
129
|
+
},
|
|
130
|
+
"expected": {
|
|
131
|
+
"signatureVerifies": false
|
|
132
|
+
}
|
|
133
|
+
},
|
|
134
|
+
{
|
|
135
|
+
"description": "ink/0.1 body with a flipped payload field does not verify",
|
|
136
|
+
"input": {
|
|
137
|
+
"body": {
|
|
138
|
+
"protocol": "ink/0.1",
|
|
139
|
+
"id": "01HVECTORID0000000000000000",
|
|
140
|
+
"from": "did:key:zAlice",
|
|
141
|
+
"to": "did:key:zBob",
|
|
142
|
+
"intent": "connection_request",
|
|
143
|
+
"payload": {
|
|
144
|
+
"method": "qr"
|
|
145
|
+
},
|
|
146
|
+
"timestamp": "2026-06-03T00:00:00Z",
|
|
147
|
+
"nonce": "ZmtmaXhlZG5vbmNl",
|
|
148
|
+
"signature": "W44kkGyQFg348NADetWQPq5Ogi7rL_72CwjmK-XVn2hL8sXYuSM7cFaDVidGV5LeK3dmmqW6iu5QiWkm6qboAQ"
|
|
149
|
+
},
|
|
150
|
+
"signerPublicKeyHex": "79b5562e8fe654f94078b112e8a98ba7901f853ae695bed7e0e3910bad049664"
|
|
151
|
+
},
|
|
152
|
+
"expected": {
|
|
153
|
+
"signatureVerifies": false
|
|
154
|
+
}
|
|
155
|
+
},
|
|
156
|
+
{
|
|
157
|
+
"description": "ink/0.2 body signed under the legacy domain does not verify (a verifier must not try both domains)",
|
|
158
|
+
"input": {
|
|
159
|
+
"body": {
|
|
160
|
+
"protocol": "ink/0.2",
|
|
161
|
+
"id": "01HVECTORID0000000000000000",
|
|
162
|
+
"from": "did:key:zAlice",
|
|
163
|
+
"to": "did:key:zBob",
|
|
164
|
+
"intent": "connection_request",
|
|
165
|
+
"payload": {
|
|
166
|
+
"method": "discovery"
|
|
167
|
+
},
|
|
168
|
+
"timestamp": "2026-06-03T00:00:00Z",
|
|
169
|
+
"nonce": "ZmtmaXhlZG5vbmNl",
|
|
170
|
+
"signature": "axpZvQqF8KPG-Mh1q3hnfZhnaSUHbYfToUz-wA_WD7PV0qKOnVHpuTu8DTaR0OTtGBVHoqpoQFlfMVskELfgDg"
|
|
171
|
+
},
|
|
172
|
+
"signerPublicKeyHex": "79b5562e8fe654f94078b112e8a98ba7901f853ae695bed7e0e3910bad049664"
|
|
173
|
+
},
|
|
174
|
+
"expected": {
|
|
175
|
+
"signatureVerifies": false
|
|
176
|
+
}
|
|
177
|
+
},
|
|
178
|
+
{
|
|
179
|
+
"description": "unknown protocol string uses the legacy body-signature domain and verifies",
|
|
180
|
+
"input": {
|
|
181
|
+
"body": {
|
|
182
|
+
"protocol": "ink/0.3",
|
|
183
|
+
"id": "01HVECTORID0000000000000000",
|
|
184
|
+
"from": "did:key:zAlice",
|
|
185
|
+
"to": "did:key:zBob",
|
|
186
|
+
"intent": "connection_request",
|
|
187
|
+
"payload": {
|
|
188
|
+
"method": "discovery"
|
|
189
|
+
},
|
|
190
|
+
"timestamp": "2026-06-03T00:00:00Z",
|
|
191
|
+
"nonce": "ZmtmaXhlZG5vbmNl",
|
|
192
|
+
"signature": "I3JIzt0kg9zncGvRU9nSZ2ldVrE4L9JnolDJAgW4kNUAqmp6bLAfPptfbYttqp1nTX4OB3oCCOOBPFCA7Fw4Cw"
|
|
193
|
+
},
|
|
194
|
+
"signerPublicKeyHex": "79b5562e8fe654f94078b112e8a98ba7901f853ae695bed7e0e3910bad049664"
|
|
195
|
+
},
|
|
196
|
+
"expected": {
|
|
197
|
+
"signatureVerifies": true
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
]
|
|
201
|
+
}
|