@calimero-network/mero-js 25.0.7 → 25.1.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/README.md +25 -0
- package/dist/agent-gate/chacha20poly1305.d.ts +20 -0
- package/dist/agent-gate/chacha20poly1305.js +148 -0
- package/dist/agent-gate/gate.d.ts +115 -0
- package/dist/agent-gate/gate.js +232 -0
- package/dist/agent-gate/hpke.d.ts +23 -0
- package/dist/agent-gate/hpke.js +84 -0
- package/dist/agent-gate/index.d.ts +4 -0
- package/dist/agent-gate/index.js +2 -0
- package/dist/agent-gate/policy.d.ts +65 -0
- package/dist/agent-gate/policy.js +103 -0
- package/dist/index.browser.mjs +2 -2
- package/dist/index.browser.mjs.map +4 -4
- package/dist/index.cjs +551 -102
- package/dist/index.cjs.map +4 -4
- package/dist/index.d.ts +1 -0
- package/dist/index.js +2 -0
- package/dist/index.mjs +551 -102
- package/dist/index.mjs.map +4 -4
- package/dist/namespace-op/namespace-op.d.ts +3 -1
- package/dist/namespace-op/namespace-op.js +3 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -249,6 +249,31 @@ with no node of their own can be admitted by one of them. The `read` client is
|
|
|
249
249
|
`null` until the relay's node key is attested, which is why the session is
|
|
250
250
|
built in that order.
|
|
251
251
|
|
|
252
|
+
## Private agents
|
|
253
|
+
|
|
254
|
+
A private agent runs in a TDX VM (mero-tee's agent image) that several owners' agents share, each named by an agent id. Its gate proves which image runs and takes each agent's secrets only sealed to it, from its owner. These functions are that owner's side; whoever relays the requests (mdma included) sees only quotes and ciphertext.
|
|
255
|
+
|
|
256
|
+
```ts
|
|
257
|
+
import { verify as dcapVerify, getCollateral } from '@phala/dcap-qvl';
|
|
258
|
+
import {
|
|
259
|
+
agentQuoteVerifier, attestAgent, generateOwnerKey, provisionAgent, releaseAgent, verifySignedAgentPolicy,
|
|
260
|
+
} from '@calimero-network/mero-js';
|
|
261
|
+
|
|
262
|
+
// The mero-kms release's agent allowlist, checked against its Sigstore signature.
|
|
263
|
+
const policy = await verifySignedAgentPolicy(policyJson, bundleJson, { profile: 'locked-read-only' });
|
|
264
|
+
const verifier = agentQuoteVerifier(policy, { dcapVerify, fetchCollateral: (quote) => getCollateral(PCCS_URL, quote) });
|
|
265
|
+
|
|
266
|
+
const owner = await generateOwnerKey(); // keep owner.secret: only it can change or release the agent
|
|
267
|
+
const agent = await attestAgent({ gateUrl, agentId: 'a-1f2e', verifier });
|
|
268
|
+
await provisionAgent({ gateUrl, agent, ownerSecret: owner.secret, verifier, secrets: { NEAR_AI_API_KEY: key } });
|
|
269
|
+
// later: await releaseAgent({ gateUrl, agent: await attestAgent({ gateUrl, agentId, verifier }), ownerSecret, verifier });
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
- `attestAgent` returns the agent's keys only once its quote verifies: genuine, from an image in the policy, not a debug TD, for your nonce, and committing to the agent id asked about and to the keys the gate names.
|
|
273
|
+
- `provisionAgent` refuses an agent another key claimed before sending anything. The first bundle for an unclaimed agent claims it for your key, and a fresh quote must then name your key as owner.
|
|
274
|
+
- Secrets are sealed with RFC 9180 HPKE in Auth mode (X25519, HKDF-SHA256, ChaCha20-Poly1305) to the gate's in-memory key, for that agent id only.
|
|
275
|
+
- `releaseAgent` stops the agent and erases its slot; a fresh quote must name no owner.
|
|
276
|
+
|
|
252
277
|
## Lower-level HTTP client
|
|
253
278
|
|
|
254
279
|
If you need raw HTTP against a node (without the `MeroJs` facade), the SDK
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ChaCha20-Poly1305 (RFC 8439), the AEAD mero-agent-gate's HPKE suite uses.
|
|
3
|
+
* WebCrypto has no ChaCha20, and the SDK takes no runtime dependencies, so it
|
|
4
|
+
* is written here from the RFC. Its tests check it against the RFC's vectors
|
|
5
|
+
* and against Node's own implementation.
|
|
6
|
+
*
|
|
7
|
+
* Only for sealing a provisioning bundle once: short messages, one key per
|
|
8
|
+
* message. Not constant-time in Poly1305's arithmetic (BigInt), which leaks
|
|
9
|
+
* nothing here: the one-time Poly1305 key is never reused.
|
|
10
|
+
*/
|
|
11
|
+
/** One 64-byte ChaCha20 block (RFC 8439 §2.3). */
|
|
12
|
+
export declare function chacha20Block(key: Uint8Array, counter: number, nonce: Uint8Array): Uint8Array;
|
|
13
|
+
/** ChaCha20 encryption from block `counter` on (RFC 8439 §2.4). */
|
|
14
|
+
export declare function chacha20(key: Uint8Array, counter: number, nonce: Uint8Array, data: Uint8Array): Uint8Array;
|
|
15
|
+
/** Poly1305 over `message` with the one-time `key` (RFC 8439 §2.5). */
|
|
16
|
+
export declare function poly1305(key: Uint8Array, message: Uint8Array): Uint8Array;
|
|
17
|
+
/** Seal: the ciphertext followed by its 16-byte tag (RFC 8439 §2.8). */
|
|
18
|
+
export declare function chacha20Poly1305Seal(key: Uint8Array, nonce: Uint8Array, plaintext: Uint8Array, aad?: Uint8Array): Uint8Array;
|
|
19
|
+
/** Open what {@link chacha20Poly1305Seal} made; throws if it was altered. */
|
|
20
|
+
export declare function chacha20Poly1305Open(key: Uint8Array, nonce: Uint8Array, sealed: Uint8Array, aad?: Uint8Array): Uint8Array;
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ChaCha20-Poly1305 (RFC 8439), the AEAD mero-agent-gate's HPKE suite uses.
|
|
3
|
+
* WebCrypto has no ChaCha20, and the SDK takes no runtime dependencies, so it
|
|
4
|
+
* is written here from the RFC. Its tests check it against the RFC's vectors
|
|
5
|
+
* and against Node's own implementation.
|
|
6
|
+
*
|
|
7
|
+
* Only for sealing a provisioning bundle once: short messages, one key per
|
|
8
|
+
* message. Not constant-time in Poly1305's arithmetic (BigInt), which leaks
|
|
9
|
+
* nothing here: the one-time Poly1305 key is never reused.
|
|
10
|
+
*/
|
|
11
|
+
const KEY_LEN = 32;
|
|
12
|
+
const NONCE_LEN = 12;
|
|
13
|
+
const TAG_LEN = 16;
|
|
14
|
+
function rotl(value, shift) {
|
|
15
|
+
return ((value << shift) | (value >>> (32 - shift))) >>> 0;
|
|
16
|
+
}
|
|
17
|
+
function quarterRound(s, a, b, c, d) {
|
|
18
|
+
s[a] = (s[a] + s[b]) >>> 0;
|
|
19
|
+
s[d] = rotl(s[d] ^ s[a], 16);
|
|
20
|
+
s[c] = (s[c] + s[d]) >>> 0;
|
|
21
|
+
s[b] = rotl(s[b] ^ s[c], 12);
|
|
22
|
+
s[a] = (s[a] + s[b]) >>> 0;
|
|
23
|
+
s[d] = rotl(s[d] ^ s[a], 8);
|
|
24
|
+
s[c] = (s[c] + s[d]) >>> 0;
|
|
25
|
+
s[b] = rotl(s[b] ^ s[c], 7);
|
|
26
|
+
}
|
|
27
|
+
function u32le(bytes, offset) {
|
|
28
|
+
return (bytes[offset] | (bytes[offset + 1] << 8) | (bytes[offset + 2] << 16) | (bytes[offset + 3] << 24)) >>> 0;
|
|
29
|
+
}
|
|
30
|
+
/** One 64-byte ChaCha20 block (RFC 8439 §2.3). */
|
|
31
|
+
export function chacha20Block(key, counter, nonce) {
|
|
32
|
+
const state = new Uint32Array(16);
|
|
33
|
+
state[0] = 0x61707865;
|
|
34
|
+
state[1] = 0x3320646e;
|
|
35
|
+
state[2] = 0x79622d32;
|
|
36
|
+
state[3] = 0x6b206574;
|
|
37
|
+
for (let i = 0; i < 8; i += 1)
|
|
38
|
+
state[4 + i] = u32le(key, i * 4);
|
|
39
|
+
state[12] = counter >>> 0;
|
|
40
|
+
for (let i = 0; i < 3; i += 1)
|
|
41
|
+
state[13 + i] = u32le(nonce, i * 4);
|
|
42
|
+
const working = state.slice();
|
|
43
|
+
for (let round = 0; round < 10; round += 1) {
|
|
44
|
+
quarterRound(working, 0, 4, 8, 12);
|
|
45
|
+
quarterRound(working, 1, 5, 9, 13);
|
|
46
|
+
quarterRound(working, 2, 6, 10, 14);
|
|
47
|
+
quarterRound(working, 3, 7, 11, 15);
|
|
48
|
+
quarterRound(working, 0, 5, 10, 15);
|
|
49
|
+
quarterRound(working, 1, 6, 11, 12);
|
|
50
|
+
quarterRound(working, 2, 7, 8, 13);
|
|
51
|
+
quarterRound(working, 3, 4, 9, 14);
|
|
52
|
+
}
|
|
53
|
+
const out = new Uint8Array(64);
|
|
54
|
+
for (let i = 0; i < 16; i += 1) {
|
|
55
|
+
const word = (working[i] + state[i]) >>> 0;
|
|
56
|
+
out[i * 4] = word & 0xff;
|
|
57
|
+
out[i * 4 + 1] = (word >>> 8) & 0xff;
|
|
58
|
+
out[i * 4 + 2] = (word >>> 16) & 0xff;
|
|
59
|
+
out[i * 4 + 3] = (word >>> 24) & 0xff;
|
|
60
|
+
}
|
|
61
|
+
return out;
|
|
62
|
+
}
|
|
63
|
+
/** ChaCha20 encryption from block `counter` on (RFC 8439 §2.4). */
|
|
64
|
+
export function chacha20(key, counter, nonce, data) {
|
|
65
|
+
const out = new Uint8Array(data.length);
|
|
66
|
+
for (let offset = 0, block = counter; offset < data.length; offset += 64, block += 1) {
|
|
67
|
+
const stream = chacha20Block(key, block, nonce);
|
|
68
|
+
const end = Math.min(offset + 64, data.length);
|
|
69
|
+
for (let i = offset; i < end; i += 1)
|
|
70
|
+
out[i] = data[i] ^ stream[i - offset];
|
|
71
|
+
}
|
|
72
|
+
return out;
|
|
73
|
+
}
|
|
74
|
+
function leToBigInt(bytes) {
|
|
75
|
+
let value = 0n;
|
|
76
|
+
for (let i = bytes.length - 1; i >= 0; i -= 1)
|
|
77
|
+
value = (value << 8n) | BigInt(bytes[i]);
|
|
78
|
+
return value;
|
|
79
|
+
}
|
|
80
|
+
const P1305 = (1n << 130n) - 5n;
|
|
81
|
+
/** Poly1305 over `message` with the one-time `key` (RFC 8439 §2.5). */
|
|
82
|
+
export function poly1305(key, message) {
|
|
83
|
+
const r = leToBigInt(key.subarray(0, 16)) & 0x0ffffffc0ffffffc0ffffffc0fffffffn;
|
|
84
|
+
const s = leToBigInt(key.subarray(16, 32));
|
|
85
|
+
let acc = 0n;
|
|
86
|
+
for (let offset = 0; offset < message.length; offset += 16) {
|
|
87
|
+
const block = message.subarray(offset, Math.min(offset + 16, message.length));
|
|
88
|
+
const n = leToBigInt(block) + (1n << BigInt(8 * block.length));
|
|
89
|
+
acc = ((acc + n) * r) % P1305;
|
|
90
|
+
}
|
|
91
|
+
let tag = (acc + s) & ((1n << 128n) - 1n);
|
|
92
|
+
const out = new Uint8Array(TAG_LEN);
|
|
93
|
+
for (let i = 0; i < TAG_LEN; i += 1) {
|
|
94
|
+
out[i] = Number(tag & 0xffn);
|
|
95
|
+
tag >>= 8n;
|
|
96
|
+
}
|
|
97
|
+
return out;
|
|
98
|
+
}
|
|
99
|
+
function pad16(length) {
|
|
100
|
+
return new Uint8Array((16 - (length % 16)) % 16);
|
|
101
|
+
}
|
|
102
|
+
function u64le(value) {
|
|
103
|
+
const out = new Uint8Array(8);
|
|
104
|
+
new DataView(out.buffer).setBigUint64(0, BigInt(value), true);
|
|
105
|
+
return out;
|
|
106
|
+
}
|
|
107
|
+
function macData(aad, ciphertext) {
|
|
108
|
+
const parts = [aad, pad16(aad.length), ciphertext, pad16(ciphertext.length), u64le(aad.length), u64le(ciphertext.length)];
|
|
109
|
+
const out = new Uint8Array(parts.reduce((n, p) => n + p.length, 0));
|
|
110
|
+
let offset = 0;
|
|
111
|
+
for (const part of parts) {
|
|
112
|
+
out.set(part, offset);
|
|
113
|
+
offset += part.length;
|
|
114
|
+
}
|
|
115
|
+
return out;
|
|
116
|
+
}
|
|
117
|
+
function check(key, nonce) {
|
|
118
|
+
if (key.length !== KEY_LEN)
|
|
119
|
+
throw new Error('ChaCha20-Poly1305 needs a 32-byte key');
|
|
120
|
+
if (nonce.length !== NONCE_LEN)
|
|
121
|
+
throw new Error('ChaCha20-Poly1305 needs a 12-byte nonce');
|
|
122
|
+
}
|
|
123
|
+
/** Seal: the ciphertext followed by its 16-byte tag (RFC 8439 §2.8). */
|
|
124
|
+
export function chacha20Poly1305Seal(key, nonce, plaintext, aad = new Uint8Array(0)) {
|
|
125
|
+
check(key, nonce);
|
|
126
|
+
const otk = chacha20Block(key, 0, nonce).subarray(0, 32);
|
|
127
|
+
const ciphertext = chacha20(key, 1, nonce, plaintext);
|
|
128
|
+
const tag = poly1305(otk, macData(aad, ciphertext));
|
|
129
|
+
const out = new Uint8Array(ciphertext.length + TAG_LEN);
|
|
130
|
+
out.set(ciphertext);
|
|
131
|
+
out.set(tag, ciphertext.length);
|
|
132
|
+
return out;
|
|
133
|
+
}
|
|
134
|
+
/** Open what {@link chacha20Poly1305Seal} made; throws if it was altered. */
|
|
135
|
+
export function chacha20Poly1305Open(key, nonce, sealed, aad = new Uint8Array(0)) {
|
|
136
|
+
check(key, nonce);
|
|
137
|
+
if (sealed.length < TAG_LEN)
|
|
138
|
+
throw new Error('The sealed message is shorter than its tag');
|
|
139
|
+
const ciphertext = sealed.subarray(0, sealed.length - TAG_LEN);
|
|
140
|
+
const otk = chacha20Block(key, 0, nonce).subarray(0, 32);
|
|
141
|
+
const expected = poly1305(otk, macData(aad, ciphertext));
|
|
142
|
+
let diff = 0;
|
|
143
|
+
for (let i = 0; i < TAG_LEN; i += 1)
|
|
144
|
+
diff |= expected[i] ^ sealed[ciphertext.length + i];
|
|
145
|
+
if (diff !== 0)
|
|
146
|
+
throw new Error('The sealed message did not authenticate');
|
|
147
|
+
return chacha20(key, 1, nonce, ciphertext);
|
|
148
|
+
}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A client for mero-agent-gate, the gate in a private agent TD (mero-tee,
|
|
3
|
+
* `mero-agent-gate/`): attest an agent, claim it with your key, give it its
|
|
4
|
+
* secrets sealed to the TD, and release it. It runs where the owner is (a
|
|
5
|
+
* browser, a CLI), so whoever carries the requests, mdma included, sees only
|
|
6
|
+
* quotes and ciphertext.
|
|
7
|
+
*
|
|
8
|
+
* One agent TD hosts several owners' agents, each named by an agent id. For
|
|
9
|
+
* an agent the gate's quote binds, in its report data:
|
|
10
|
+
*
|
|
11
|
+
* `nonce ‖ SHA-256("mero-agent-gate/attest/v3" ‖ provisioning_pub ‖ len(id) ‖ id ‖ signing_pub ‖ owner_pub)`
|
|
12
|
+
*
|
|
13
|
+
* (signing and owner keys 32 zero bytes while unclaimed). {@link attestAgent}
|
|
14
|
+
* checks that and the quote itself before returning anything; every function
|
|
15
|
+
* that sends refuses an agent whose quote names another owner.
|
|
16
|
+
*
|
|
17
|
+
* ```ts
|
|
18
|
+
* import { verify as dcapVerify, getCollateral } from '@phala/dcap-qvl';
|
|
19
|
+
*
|
|
20
|
+
* const policy = await verifySignedAgentPolicy(policyJson, bundleJson, { profile: 'locked-read-only' });
|
|
21
|
+
* const verifier = agentQuoteVerifier(policy, { dcapVerify, fetchCollateral: (q) => getCollateral(PCCS, q) });
|
|
22
|
+
* const owner = await generateOwnerKey(); // keep owner.secret: it is the only key that can change the agent
|
|
23
|
+
* const agent = await attestAgent({ gateUrl, agentId: 'a-1f2e', verifier });
|
|
24
|
+
* await provisionAgent({ gateUrl, agent, ownerSecret: owner.secret, verifier, secrets: { NEAR_AI_API_KEY: key } });
|
|
25
|
+
* ```
|
|
26
|
+
*/
|
|
27
|
+
import type { VerifyTransportQuote } from '../sealed/sealed.js';
|
|
28
|
+
export declare const AGENT_ATTEST_DOMAIN = "mero-agent-gate/attest/v3";
|
|
29
|
+
export declare const AGENT_PROVISION_INFO_DOMAIN = "mero-agent-gate/provision/v2";
|
|
30
|
+
/** A gate's refusal, with its `error` code (`not_the_owner`, `no_free_slot`, …) when it gave one. */
|
|
31
|
+
export declare class AgentGateError extends Error {
|
|
32
|
+
readonly status?: number;
|
|
33
|
+
readonly code?: string;
|
|
34
|
+
constructor(message: string, status?: number, code?: string);
|
|
35
|
+
}
|
|
36
|
+
/** An agent's keys, once its quote passed {@link attestAgent}'s checks. */
|
|
37
|
+
export interface VerifiedAgent {
|
|
38
|
+
agentId: string;
|
|
39
|
+
/** Base64; secrets are sealed to it. Valid until the gate restarts. */
|
|
40
|
+
provisioningPublicKey: string;
|
|
41
|
+
/** Base64 Ed25519 key the agent signs with; absent while unclaimed. */
|
|
42
|
+
signingPublicKey?: string;
|
|
43
|
+
/** Base64 X25519 key of the owner that claimed it; absent while unclaimed. */
|
|
44
|
+
ownerPublicKey?: string;
|
|
45
|
+
}
|
|
46
|
+
export interface OwnerKey {
|
|
47
|
+
/** 32 raw X25519 bytes. The only key that can provision or release the agent: keep it. */
|
|
48
|
+
secret: Uint8Array;
|
|
49
|
+
/** Base64 of the public half: what the gate's quote names as owner. */
|
|
50
|
+
publicKey: string;
|
|
51
|
+
}
|
|
52
|
+
type Fetch = typeof fetch;
|
|
53
|
+
export declare function validateAgentId(agentId: string): void;
|
|
54
|
+
/** The 32 bytes after the nonce in a gate's quote for `agentId` (empty: the TD alone). */
|
|
55
|
+
export declare function agentKeyBinding(provisioningPublic: Uint8Array, agentId: string, signingPublic?: Uint8Array, ownerPublic?: Uint8Array): Promise<Uint8Array>;
|
|
56
|
+
/** HPKE `info` for a message to `agentId` on the gate whose provisioning key is `recipientPublic`. */
|
|
57
|
+
export declare function provisionInfo(recipientPublic: Uint8Array, agentId: string): Uint8Array;
|
|
58
|
+
/** A fresh owner key, made here; it never needs to leave the owner. */
|
|
59
|
+
export declare function generateOwnerKey(): Promise<OwnerKey>;
|
|
60
|
+
/** The base64 public half of an owner's X25519 secret. */
|
|
61
|
+
export declare function ownerPublicKey(secret: Uint8Array): Promise<string>;
|
|
62
|
+
export interface AttestAgentOptions {
|
|
63
|
+
/** The gate's base URL, or a relay of it (mdma's): only verified quotes are trusted. */
|
|
64
|
+
gateUrl: string;
|
|
65
|
+
/** The agent to attest; omit for the TD alone (no keys, nothing to send to). */
|
|
66
|
+
agentId?: string;
|
|
67
|
+
/** Checks the quote's signature, TCB, image and report data: {@link agentQuoteVerifier}. */
|
|
68
|
+
verifier: VerifyTransportQuote;
|
|
69
|
+
fetch?: Fetch;
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Ask the gate for a quote for `agentId` over a fresh nonce and verify it:
|
|
73
|
+
* a genuine quote from a measured agent image, for this nonce, committing to
|
|
74
|
+
* the agent id asked about and to the keys the gate names. Returns those
|
|
75
|
+
* keys only once every check has passed.
|
|
76
|
+
*/
|
|
77
|
+
export declare function attestAgent(options: AttestAgentOptions): Promise<VerifiedAgent>;
|
|
78
|
+
/** The owner key `ownerSecret` may act on `agent`: it is unclaimed, or claimed by that key. */
|
|
79
|
+
export declare function checkOwner(agent: VerifiedAgent, ownerSecret: Uint8Array): Promise<string>;
|
|
80
|
+
export interface ProvisionAgentOptions {
|
|
81
|
+
gateUrl: string;
|
|
82
|
+
/** What {@link attestAgent} returned for this agent, just now. */
|
|
83
|
+
agent: VerifiedAgent;
|
|
84
|
+
ownerSecret: Uint8Array;
|
|
85
|
+
/** Name to value; a name replaces what was there, others stay. */
|
|
86
|
+
secrets: Record<string, string>;
|
|
87
|
+
/** The verifier `agent` was attested with: the claim is checked by a fresh quote. */
|
|
88
|
+
verifier: VerifyTransportQuote;
|
|
89
|
+
fetch?: Fetch;
|
|
90
|
+
}
|
|
91
|
+
export interface ProvisionedAgent extends Required<VerifiedAgent> {
|
|
92
|
+
written: string[];
|
|
93
|
+
/** Whether this claimed the agent for your key. */
|
|
94
|
+
claimed: boolean;
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Seal `secrets` to the agent as its owner and send them. The first bundle for
|
|
98
|
+
* an unclaimed agent claims it for `ownerSecret`'s key. The gate's answer is
|
|
99
|
+
* not taken as proof: a fresh quote must name your key as owner, and the
|
|
100
|
+
* agent's signing key must not have changed.
|
|
101
|
+
*/
|
|
102
|
+
export declare function provisionAgent(options: ProvisionAgentOptions): Promise<ProvisionedAgent>;
|
|
103
|
+
export interface ReleaseAgentOptions {
|
|
104
|
+
gateUrl: string;
|
|
105
|
+
agent: VerifiedAgent;
|
|
106
|
+
ownerSecret: Uint8Array;
|
|
107
|
+
verifier: VerifyTransportQuote;
|
|
108
|
+
fetch?: Fetch;
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Release the agent as its owner: the gate stops it and erases its claim,
|
|
112
|
+
* keys, secrets and state. Checked by a fresh quote that names no owner.
|
|
113
|
+
*/
|
|
114
|
+
export declare function releaseAgent(options: ReleaseAgentOptions): Promise<void>;
|
|
115
|
+
export {};
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A client for mero-agent-gate, the gate in a private agent TD (mero-tee,
|
|
3
|
+
* `mero-agent-gate/`): attest an agent, claim it with your key, give it its
|
|
4
|
+
* secrets sealed to the TD, and release it. It runs where the owner is (a
|
|
5
|
+
* browser, a CLI), so whoever carries the requests, mdma included, sees only
|
|
6
|
+
* quotes and ciphertext.
|
|
7
|
+
*
|
|
8
|
+
* One agent TD hosts several owners' agents, each named by an agent id. For
|
|
9
|
+
* an agent the gate's quote binds, in its report data:
|
|
10
|
+
*
|
|
11
|
+
* `nonce ‖ SHA-256("mero-agent-gate/attest/v3" ‖ provisioning_pub ‖ len(id) ‖ id ‖ signing_pub ‖ owner_pub)`
|
|
12
|
+
*
|
|
13
|
+
* (signing and owner keys 32 zero bytes while unclaimed). {@link attestAgent}
|
|
14
|
+
* checks that and the quote itself before returning anything; every function
|
|
15
|
+
* that sends refuses an agent whose quote names another owner.
|
|
16
|
+
*
|
|
17
|
+
* ```ts
|
|
18
|
+
* import { verify as dcapVerify, getCollateral } from '@phala/dcap-qvl';
|
|
19
|
+
*
|
|
20
|
+
* const policy = await verifySignedAgentPolicy(policyJson, bundleJson, { profile: 'locked-read-only' });
|
|
21
|
+
* const verifier = agentQuoteVerifier(policy, { dcapVerify, fetchCollateral: (q) => getCollateral(PCCS, q) });
|
|
22
|
+
* const owner = await generateOwnerKey(); // keep owner.secret: it is the only key that can change the agent
|
|
23
|
+
* const agent = await attestAgent({ gateUrl, agentId: 'a-1f2e', verifier });
|
|
24
|
+
* await provisionAgent({ gateUrl, agent, ownerSecret: owner.secret, verifier, secrets: { NEAR_AI_API_KEY: key } });
|
|
25
|
+
* ```
|
|
26
|
+
*/
|
|
27
|
+
import { concat, hex } from '../crypto/internal.js';
|
|
28
|
+
import { x25519KeyPair } from '../sealed/noise.js';
|
|
29
|
+
import { hpkeSealAuth } from './hpke.js';
|
|
30
|
+
export const AGENT_ATTEST_DOMAIN = 'mero-agent-gate/attest/v3';
|
|
31
|
+
export const AGENT_PROVISION_INFO_DOMAIN = 'mero-agent-gate/provision/v2';
|
|
32
|
+
const AGENT_ID = /^[a-z0-9][a-z0-9-]{0,62}$/;
|
|
33
|
+
const SECRET_NAME = /^[A-Za-z0-9][A-Za-z0-9_.-]{0,63}$/;
|
|
34
|
+
const MAX_SECRETS = 64;
|
|
35
|
+
const MAX_SECRET_LEN = 64 * 1024;
|
|
36
|
+
/** A gate's refusal, with its `error` code (`not_the_owner`, `no_free_slot`, …) when it gave one. */
|
|
37
|
+
export class AgentGateError extends Error {
|
|
38
|
+
constructor(message, status, code) {
|
|
39
|
+
super(message);
|
|
40
|
+
this.status = status;
|
|
41
|
+
this.code = code;
|
|
42
|
+
this.name = 'AgentGateError';
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
export function validateAgentId(agentId) {
|
|
46
|
+
if (!AGENT_ID.test(agentId)) {
|
|
47
|
+
throw new Error(`Agent id ${JSON.stringify(agentId)} must match [a-z0-9][a-z0-9-]{0,62}`);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
function utf8(value) {
|
|
51
|
+
return new TextEncoder().encode(value);
|
|
52
|
+
}
|
|
53
|
+
function lengthPrefixed(agentId) {
|
|
54
|
+
const id = utf8(agentId);
|
|
55
|
+
return concat(new Uint8Array([id.length]), id);
|
|
56
|
+
}
|
|
57
|
+
function toBase64(bytes) {
|
|
58
|
+
let binary = '';
|
|
59
|
+
for (const byte of bytes)
|
|
60
|
+
binary += String.fromCharCode(byte);
|
|
61
|
+
return btoa(binary);
|
|
62
|
+
}
|
|
63
|
+
function fromBase64(value, label, length) {
|
|
64
|
+
let bytes;
|
|
65
|
+
try {
|
|
66
|
+
bytes = Uint8Array.from(atob(value.trim()), (c) => c.charCodeAt(0));
|
|
67
|
+
}
|
|
68
|
+
catch {
|
|
69
|
+
throw new Error(`${label} is not base64`);
|
|
70
|
+
}
|
|
71
|
+
if (length !== undefined && bytes.length !== length)
|
|
72
|
+
throw new Error(`${label} is not ${length} bytes`);
|
|
73
|
+
return bytes;
|
|
74
|
+
}
|
|
75
|
+
/** The 32 bytes after the nonce in a gate's quote for `agentId` (empty: the TD alone). */
|
|
76
|
+
export async function agentKeyBinding(provisioningPublic, agentId, signingPublic, ownerPublic) {
|
|
77
|
+
const zero = new Uint8Array(32);
|
|
78
|
+
const data = concat(utf8(AGENT_ATTEST_DOMAIN), provisioningPublic, lengthPrefixed(agentId), signingPublic ?? zero, ownerPublic ?? zero);
|
|
79
|
+
return new Uint8Array(await crypto.subtle.digest('SHA-256', data));
|
|
80
|
+
}
|
|
81
|
+
/** HPKE `info` for a message to `agentId` on the gate whose provisioning key is `recipientPublic`. */
|
|
82
|
+
export function provisionInfo(recipientPublic, agentId) {
|
|
83
|
+
return concat(utf8(AGENT_PROVISION_INFO_DOMAIN), recipientPublic, lengthPrefixed(agentId));
|
|
84
|
+
}
|
|
85
|
+
/** A fresh owner key, made here; it never needs to leave the owner. */
|
|
86
|
+
export async function generateOwnerKey() {
|
|
87
|
+
const secret = crypto.getRandomValues(new Uint8Array(32));
|
|
88
|
+
return { secret, publicKey: await ownerPublicKey(secret) };
|
|
89
|
+
}
|
|
90
|
+
/** The base64 public half of an owner's X25519 secret. */
|
|
91
|
+
export async function ownerPublicKey(secret) {
|
|
92
|
+
if (secret.length !== 32)
|
|
93
|
+
throw new Error('An owner key is 32 bytes');
|
|
94
|
+
return toBase64((await x25519KeyPair(secret)).publicKey);
|
|
95
|
+
}
|
|
96
|
+
function url(gateUrl, path) {
|
|
97
|
+
return `${gateUrl.replace(/\/+$/, '')}${path}`;
|
|
98
|
+
}
|
|
99
|
+
async function postJson(fetchImpl, target, body) {
|
|
100
|
+
const response = await fetchImpl(target, {
|
|
101
|
+
method: 'POST',
|
|
102
|
+
headers: { 'content-type': 'application/json' },
|
|
103
|
+
body: JSON.stringify(body),
|
|
104
|
+
});
|
|
105
|
+
const text = await response.text();
|
|
106
|
+
let parsed = undefined;
|
|
107
|
+
try {
|
|
108
|
+
parsed = text ? JSON.parse(text) : undefined;
|
|
109
|
+
}
|
|
110
|
+
catch {
|
|
111
|
+
// Not JSON: reported below with the status.
|
|
112
|
+
}
|
|
113
|
+
if (!response.ok) {
|
|
114
|
+
const error = parsed;
|
|
115
|
+
throw new AgentGateError(`The gate answered ${response.status}${error?.error ? ` ${error.error}` : ''}: ${error?.details ?? text.slice(0, 200)}`, response.status, error?.error);
|
|
116
|
+
}
|
|
117
|
+
return parsed;
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Ask the gate for a quote for `agentId` over a fresh nonce and verify it:
|
|
121
|
+
* a genuine quote from a measured agent image, for this nonce, committing to
|
|
122
|
+
* the agent id asked about and to the keys the gate names. Returns those
|
|
123
|
+
* keys only once every check has passed.
|
|
124
|
+
*/
|
|
125
|
+
export async function attestAgent(options) {
|
|
126
|
+
const agentId = options.agentId ?? '';
|
|
127
|
+
if (agentId)
|
|
128
|
+
validateAgentId(agentId);
|
|
129
|
+
const nonce = crypto.getRandomValues(new Uint8Array(32));
|
|
130
|
+
const body = (await postJson(options.fetch ?? fetch, url(options.gateUrl, '/attest'), {
|
|
131
|
+
nonceB64: toBase64(nonce),
|
|
132
|
+
...(agentId ? { agentId } : {}),
|
|
133
|
+
}));
|
|
134
|
+
if ((body?.agentId ?? '') !== agentId) {
|
|
135
|
+
throw new Error(`Asked about agent ${JSON.stringify(agentId)}, the gate answered for ${JSON.stringify(body?.agentId)}`);
|
|
136
|
+
}
|
|
137
|
+
const provisioning = fromBase64(body.provisioningPublicKeyB64 ?? '', 'provisioningPublicKeyB64', 32);
|
|
138
|
+
const signing = body.signingPublicKeyB64 ? fromBase64(body.signingPublicKeyB64, 'signingPublicKeyB64', 32) : undefined;
|
|
139
|
+
const owner = body.ownerPublicKeyB64 ? fromBase64(body.ownerPublicKeyB64, 'ownerPublicKeyB64', 32) : undefined;
|
|
140
|
+
if (!signing !== !owner) {
|
|
141
|
+
throw new Error('A claimed agent has both a signing key and an owner; the gate named one');
|
|
142
|
+
}
|
|
143
|
+
// The id asked about, not the one answered: a quote for another agent fails here.
|
|
144
|
+
const binding = await agentKeyBinding(provisioning, agentId, signing, owner);
|
|
145
|
+
await options.verifier({ quoteB64: body.quoteB64, nonce: hex(nonce), reportDataSuffix: hex(binding) });
|
|
146
|
+
return {
|
|
147
|
+
agentId,
|
|
148
|
+
provisioningPublicKey: toBase64(provisioning),
|
|
149
|
+
...(signing ? { signingPublicKey: toBase64(signing) } : {}),
|
|
150
|
+
...(owner ? { ownerPublicKey: toBase64(owner) } : {}),
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
/** The owner key `ownerSecret` may act on `agent`: it is unclaimed, or claimed by that key. */
|
|
154
|
+
export async function checkOwner(agent, ownerSecret) {
|
|
155
|
+
const mine = await ownerPublicKey(ownerSecret);
|
|
156
|
+
if (agent.ownerPublicKey && agent.ownerPublicKey !== mine) {
|
|
157
|
+
throw new AgentGateError(`This agent is claimed by another key (${agent.ownerPublicKey}); it is not yours. Send it nothing and do not authorize its signing key`, undefined, 'not_yours');
|
|
158
|
+
}
|
|
159
|
+
return mine;
|
|
160
|
+
}
|
|
161
|
+
function validateSecrets(secrets) {
|
|
162
|
+
const names = Object.keys(secrets);
|
|
163
|
+
if (names.length === 0)
|
|
164
|
+
throw new Error('There are no secrets to provision');
|
|
165
|
+
if (names.length > MAX_SECRETS)
|
|
166
|
+
throw new Error(`At most ${MAX_SECRETS} secrets at once`);
|
|
167
|
+
for (const name of names) {
|
|
168
|
+
if (!SECRET_NAME.test(name))
|
|
169
|
+
throw new Error(`Secret name ${JSON.stringify(name)} must match [A-Za-z0-9][A-Za-z0-9_.-]{0,63}`);
|
|
170
|
+
if (typeof secrets[name] !== 'string')
|
|
171
|
+
throw new Error(`Secret ${name} is not a string`);
|
|
172
|
+
if (utf8(secrets[name]).length > MAX_SECRET_LEN)
|
|
173
|
+
throw new Error(`Secret ${name} is over ${MAX_SECRET_LEN} bytes`);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
async function sendSealed(fetchImpl, gateUrl, path, agent, ownerSecret, message) {
|
|
177
|
+
if (!agent.agentId)
|
|
178
|
+
throw new Error('This quote names no agent; attest with the agent id');
|
|
179
|
+
const mine = await checkOwner(agent, ownerSecret);
|
|
180
|
+
const recipient = fromBase64(agent.provisioningPublicKey, 'provisioningPublicKey', 32);
|
|
181
|
+
const sealed = await hpkeSealAuth(recipient, ownerSecret, provisionInfo(recipient, agent.agentId), utf8(JSON.stringify(message)));
|
|
182
|
+
return postJson(fetchImpl, url(gateUrl, path), {
|
|
183
|
+
agentId: agent.agentId,
|
|
184
|
+
provisioningPublicKeyB64: agent.provisioningPublicKey,
|
|
185
|
+
senderPublicKeyB64: mine,
|
|
186
|
+
encappedKeyB64: toBase64(sealed.enc),
|
|
187
|
+
ciphertextB64: toBase64(sealed.ciphertext),
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
/**
|
|
191
|
+
* Seal `secrets` to the agent as its owner and send them. The first bundle for
|
|
192
|
+
* an unclaimed agent claims it for `ownerSecret`'s key. The gate's answer is
|
|
193
|
+
* not taken as proof: a fresh quote must name your key as owner, and the
|
|
194
|
+
* agent's signing key must not have changed.
|
|
195
|
+
*/
|
|
196
|
+
export async function provisionAgent(options) {
|
|
197
|
+
validateSecrets(options.secrets);
|
|
198
|
+
const fetchImpl = options.fetch ?? fetch;
|
|
199
|
+
const answer = (await sendSealed(fetchImpl, options.gateUrl, '/provision', options.agent, options.ownerSecret, {
|
|
200
|
+
secrets: options.secrets,
|
|
201
|
+
}));
|
|
202
|
+
const after = await attestAgent({ gateUrl: options.gateUrl, agentId: options.agent.agentId, verifier: options.verifier, fetch: fetchImpl });
|
|
203
|
+
const mine = await ownerPublicKey(options.ownerSecret);
|
|
204
|
+
if (after.ownerPublicKey !== mine || !after.signingPublicKey) {
|
|
205
|
+
throw new AgentGateError('The gate accepted the secrets, but its quote does not name your key as owner');
|
|
206
|
+
}
|
|
207
|
+
if (options.agent.signingPublicKey && options.agent.signingPublicKey !== after.signingPublicKey) {
|
|
208
|
+
throw new AgentGateError("The agent's signing key changed while provisioning");
|
|
209
|
+
}
|
|
210
|
+
return {
|
|
211
|
+
agentId: after.agentId,
|
|
212
|
+
provisioningPublicKey: after.provisioningPublicKey,
|
|
213
|
+
signingPublicKey: after.signingPublicKey,
|
|
214
|
+
ownerPublicKey: mine,
|
|
215
|
+
written: answer?.written ?? [],
|
|
216
|
+
claimed: answer?.claimed === true,
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
/**
|
|
220
|
+
* Release the agent as its owner: the gate stops it and erases its claim,
|
|
221
|
+
* keys, secrets and state. Checked by a fresh quote that names no owner.
|
|
222
|
+
*/
|
|
223
|
+
export async function releaseAgent(options) {
|
|
224
|
+
if (!options.agent.ownerPublicKey)
|
|
225
|
+
throw new Error('This agent is not claimed; there is nothing to release');
|
|
226
|
+
const fetchImpl = options.fetch ?? fetch;
|
|
227
|
+
await sendSealed(fetchImpl, options.gateUrl, '/release', options.agent, options.ownerSecret, { release: true });
|
|
228
|
+
const after = await attestAgent({ gateUrl: options.gateUrl, agentId: options.agent.agentId, verifier: options.verifier, fetch: fetchImpl });
|
|
229
|
+
if (after.ownerPublicKey) {
|
|
230
|
+
throw new AgentGateError('The gate says it released the agent, but its quote still names an owner');
|
|
231
|
+
}
|
|
232
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RFC 9180 HPKE, the one suite and mode mero-agent-gate takes: Auth mode,
|
|
3
|
+
* DHKEM(X25519, HKDF-SHA256), HKDF-SHA256, ChaCha20-Poly1305, single-shot
|
|
4
|
+
* (one message per context, sequence number 0). Auth mode proves the sender:
|
|
5
|
+
* the gate opens a bundle only under the X25519 key it names, so nobody can
|
|
6
|
+
* seal in an owner's name.
|
|
7
|
+
*
|
|
8
|
+
* X25519 and HMAC are WebCrypto; ChaCha20-Poly1305 is ./chacha20poly1305.
|
|
9
|
+
* The tests open what mero-agent-gate (Rust, the `hpke` crate) sealed, and the
|
|
10
|
+
* gate opens what this seals.
|
|
11
|
+
*/
|
|
12
|
+
export interface HpkeSealed {
|
|
13
|
+
/** The encapsulated key: the sender's ephemeral X25519 public key. */
|
|
14
|
+
enc: Uint8Array;
|
|
15
|
+
ciphertext: Uint8Array;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Seal `plaintext` to `recipientPublic` in Auth mode as the holder of
|
|
19
|
+
* `senderSecret` (32 raw X25519 bytes). `ephemeralSecret` is for tests.
|
|
20
|
+
*/
|
|
21
|
+
export declare function hpkeSealAuth(recipientPublic: Uint8Array, senderSecret: Uint8Array, info: Uint8Array, plaintext: Uint8Array, aad?: Uint8Array, ephemeralSecret?: Uint8Array): Promise<HpkeSealed>;
|
|
22
|
+
/** Open what {@link hpkeSealAuth} (or the gate's own sealer) made, as the recipient. */
|
|
23
|
+
export declare function hpkeOpenAuth(recipientSecret: Uint8Array, senderPublic: Uint8Array, info: Uint8Array, sealed: HpkeSealed, aad?: Uint8Array): Promise<Uint8Array>;
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RFC 9180 HPKE, the one suite and mode mero-agent-gate takes: Auth mode,
|
|
3
|
+
* DHKEM(X25519, HKDF-SHA256), HKDF-SHA256, ChaCha20-Poly1305, single-shot
|
|
4
|
+
* (one message per context, sequence number 0). Auth mode proves the sender:
|
|
5
|
+
* the gate opens a bundle only under the X25519 key it names, so nobody can
|
|
6
|
+
* seal in an owner's name.
|
|
7
|
+
*
|
|
8
|
+
* X25519 and HMAC are WebCrypto; ChaCha20-Poly1305 is ./chacha20poly1305.
|
|
9
|
+
* The tests open what mero-agent-gate (Rust, the `hpke` crate) sealed, and the
|
|
10
|
+
* gate opens what this seals.
|
|
11
|
+
*/
|
|
12
|
+
import { concat } from '../crypto/internal.js';
|
|
13
|
+
import { dh, x25519KeyPair } from '../sealed/noise.js';
|
|
14
|
+
import { chacha20Poly1305Open, chacha20Poly1305Seal } from './chacha20poly1305.js';
|
|
15
|
+
const KEM_ID = 0x0020; // DHKEM(X25519, HKDF-SHA256)
|
|
16
|
+
const KDF_ID = 0x0001; // HKDF-SHA256
|
|
17
|
+
const AEAD_ID = 0x0003; // ChaCha20-Poly1305
|
|
18
|
+
const MODE_AUTH = 0x02;
|
|
19
|
+
const N_SECRET = 32;
|
|
20
|
+
const N_KEY = 32;
|
|
21
|
+
const N_NONCE = 12;
|
|
22
|
+
const utf8 = (s) => new TextEncoder().encode(s);
|
|
23
|
+
const i2osp2 = (n) => new Uint8Array([(n >> 8) & 0xff, n & 0xff]);
|
|
24
|
+
const KEM_SUITE = concat(utf8('KEM'), i2osp2(KEM_ID));
|
|
25
|
+
const HPKE_SUITE = concat(utf8('HPKE'), i2osp2(KEM_ID), i2osp2(KDF_ID), i2osp2(AEAD_ID));
|
|
26
|
+
async function hmac(key, data) {
|
|
27
|
+
// An empty salt is HashLen zeros (RFC 5869); WebCrypto refuses a zero-length HMAC key.
|
|
28
|
+
const raw = key.length === 0 ? new Uint8Array(32) : key;
|
|
29
|
+
const k = await crypto.subtle.importKey('raw', raw, { name: 'HMAC', hash: 'SHA-256' }, false, ['sign']);
|
|
30
|
+
return new Uint8Array(await crypto.subtle.sign('HMAC', k, data));
|
|
31
|
+
}
|
|
32
|
+
async function expand(prk, info, length) {
|
|
33
|
+
const out = new Uint8Array(length);
|
|
34
|
+
let previous = new Uint8Array(0);
|
|
35
|
+
for (let i = 1, offset = 0; offset < length; i += 1) {
|
|
36
|
+
previous = await hmac(prk, concat(previous, info, new Uint8Array([i])));
|
|
37
|
+
out.set(previous.subarray(0, Math.min(previous.length, length - offset)), offset);
|
|
38
|
+
offset += previous.length;
|
|
39
|
+
}
|
|
40
|
+
return out;
|
|
41
|
+
}
|
|
42
|
+
function labeledExtract(suite, salt, label, ikm) {
|
|
43
|
+
return hmac(salt, concat(utf8('HPKE-v1'), suite, utf8(label), ikm));
|
|
44
|
+
}
|
|
45
|
+
function labeledExpand(suite, prk, label, info, length) {
|
|
46
|
+
return expand(prk, concat(i2osp2(length), utf8('HPKE-v1'), suite, utf8(label), info), length);
|
|
47
|
+
}
|
|
48
|
+
async function extractAndExpand(dhValue, kemContext) {
|
|
49
|
+
const prk = await labeledExtract(KEM_SUITE, new Uint8Array(0), 'eae_prk', dhValue);
|
|
50
|
+
return labeledExpand(KEM_SUITE, prk, 'shared_secret', kemContext, N_SECRET);
|
|
51
|
+
}
|
|
52
|
+
async function keySchedule(sharedSecret, info) {
|
|
53
|
+
const empty = new Uint8Array(0);
|
|
54
|
+
const pskIdHash = await labeledExtract(HPKE_SUITE, empty, 'psk_id_hash', empty);
|
|
55
|
+
const infoHash = await labeledExtract(HPKE_SUITE, empty, 'info_hash', info);
|
|
56
|
+
const context = concat(new Uint8Array([MODE_AUTH]), pskIdHash, infoHash);
|
|
57
|
+
const secret = await labeledExtract(HPKE_SUITE, sharedSecret, 'secret', empty);
|
|
58
|
+
return {
|
|
59
|
+
key: await labeledExpand(HPKE_SUITE, secret, 'key', context, N_KEY),
|
|
60
|
+
nonce: await labeledExpand(HPKE_SUITE, secret, 'base_nonce', context, N_NONCE),
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Seal `plaintext` to `recipientPublic` in Auth mode as the holder of
|
|
65
|
+
* `senderSecret` (32 raw X25519 bytes). `ephemeralSecret` is for tests.
|
|
66
|
+
*/
|
|
67
|
+
export async function hpkeSealAuth(recipientPublic, senderSecret, info, plaintext, aad = new Uint8Array(0), ephemeralSecret) {
|
|
68
|
+
if (recipientPublic.length !== 32)
|
|
69
|
+
throw new Error('The recipient key is not 32 bytes of X25519');
|
|
70
|
+
const ephemeral = await x25519KeyPair(ephemeralSecret);
|
|
71
|
+
const sender = await x25519KeyPair(senderSecret);
|
|
72
|
+
const dhValue = concat(await dh(ephemeral.privateKey, recipientPublic), await dh(sender.privateKey, recipientPublic));
|
|
73
|
+
const shared = await extractAndExpand(dhValue, concat(ephemeral.publicKey, recipientPublic, sender.publicKey));
|
|
74
|
+
const { key, nonce } = await keySchedule(shared, info);
|
|
75
|
+
return { enc: ephemeral.publicKey, ciphertext: chacha20Poly1305Seal(key, nonce, plaintext, aad) };
|
|
76
|
+
}
|
|
77
|
+
/** Open what {@link hpkeSealAuth} (or the gate's own sealer) made, as the recipient. */
|
|
78
|
+
export async function hpkeOpenAuth(recipientSecret, senderPublic, info, sealed, aad = new Uint8Array(0)) {
|
|
79
|
+
const recipient = await x25519KeyPair(recipientSecret);
|
|
80
|
+
const dhValue = concat(await dh(recipient.privateKey, sealed.enc), await dh(recipient.privateKey, senderPublic));
|
|
81
|
+
const shared = await extractAndExpand(dhValue, concat(sealed.enc, recipient.publicKey, senderPublic));
|
|
82
|
+
const { key, nonce } = await keySchedule(shared, info);
|
|
83
|
+
return chacha20Poly1305Open(key, nonce, sealed.ciphertext, aad);
|
|
84
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export { AGENT_ATTEST_DOMAIN, AGENT_PROVISION_INFO_DOMAIN, AgentGateError, agentKeyBinding, attestAgent, checkOwner, generateOwnerKey, ownerPublicKey, provisionAgent, provisionInfo, releaseAgent, validateAgentId, } from './gate.js';
|
|
2
|
+
export type { AttestAgentOptions, OwnerKey, ProvisionAgentOptions, ProvisionedAgent, ReleaseAgentOptions, VerifiedAgent, } from './gate.js';
|
|
3
|
+
export { KMS_RELEASE_SIGNER, agentQuoteVerifier, verifySignedAgentPolicy } from './policy.js';
|
|
4
|
+
export type { AgentPolicy, AgentQuoteVerifierOptions, VerifySignedAgentPolicyOptions } from './policy.js';
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
export { AGENT_ATTEST_DOMAIN, AGENT_PROVISION_INFO_DOMAIN, AgentGateError, agentKeyBinding, attestAgent, checkOwner, generateOwnerKey, ownerPublicKey, provisionAgent, provisionInfo, releaseAgent, validateAgentId, } from './gate.js';
|
|
2
|
+
export { KMS_RELEASE_SIGNER, agentQuoteVerifier, verifySignedAgentPolicy } from './policy.js';
|