@robota-sdk/agent-keyring 0.0.0-stage → 3.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.
Files changed (62) hide show
  1. package/CHANGELOG.md +33 -0
  2. package/LICENSE +661 -0
  3. package/README.md +91 -2
  4. package/dist/node/index.cjs +1 -0
  5. package/dist/node/index.d.cts +470 -0
  6. package/dist/node/index.d.cts.map +1 -0
  7. package/dist/node/index.d.ts +470 -0
  8. package/dist/node/index.d.ts.map +1 -0
  9. package/dist/node/index.js +2 -0
  10. package/dist/node/index.js.map +1 -0
  11. package/package.json +74 -3
  12. package/src/__tests__/authorship-constant-time.test.ts +46 -0
  13. package/src/__tests__/authorship.test.ts +65 -0
  14. package/src/__tests__/crypto-failures.test.ts +89 -0
  15. package/src/__tests__/fixtures/keyring-v1.json +25 -0
  16. package/src/__tests__/fixtures/sealed-v1.json +8 -0
  17. package/src/__tests__/helpers.ts +130 -0
  18. package/src/__tests__/kdf-bounds.test.ts +154 -0
  19. package/src/__tests__/keyring-document.test.ts +251 -0
  20. package/src/__tests__/keyring.test.ts +329 -0
  21. package/src/__tests__/keyslot.test.ts +98 -0
  22. package/src/__tests__/known-answers.test.ts +110 -0
  23. package/src/__tests__/link-helpers.ts +228 -0
  24. package/src/__tests__/owner-handover-derivation.test.ts +50 -0
  25. package/src/__tests__/owner-handover-dispose.test.ts +64 -0
  26. package/src/__tests__/owner-handover.test.ts +145 -0
  27. package/src/__tests__/owner-link-adversarial.test.ts +712 -0
  28. package/src/__tests__/owner-link-grant-wipe.test.ts +75 -0
  29. package/src/__tests__/owner-link-known-answers.test.ts +117 -0
  30. package/src/__tests__/owner-link-secret-wipe.test.ts +116 -0
  31. package/src/__tests__/owner-link.test.ts +229 -0
  32. package/src/__tests__/read-keys.test.ts +67 -0
  33. package/src/__tests__/recovery-key.test.ts +76 -0
  34. package/src/__tests__/runtime-identity.test.ts +171 -0
  35. package/src/__tests__/sealed-format-vectors.test.ts +91 -0
  36. package/src/__tests__/sealing.test.ts +376 -0
  37. package/src/__tests__/spawner-exchange.test.ts +127 -0
  38. package/src/__tests__/transient-key-pair.test.ts +36 -0
  39. package/src/aead.ts +62 -0
  40. package/src/authorship.ts +61 -0
  41. package/src/bytes.ts +67 -0
  42. package/src/errors.ts +106 -0
  43. package/src/fingerprint.ts +32 -0
  44. package/src/index.ts +77 -0
  45. package/src/kdf.ts +111 -0
  46. package/src/key-pairs.ts +148 -0
  47. package/src/keyring-document.ts +168 -0
  48. package/src/keyring-types.ts +51 -0
  49. package/src/keyring.ts +307 -0
  50. package/src/keyslot.ts +66 -0
  51. package/src/link-entropy.ts +24 -0
  52. package/src/link-frames.ts +271 -0
  53. package/src/link-primitives.ts +77 -0
  54. package/src/owner-handover.ts +155 -0
  55. package/src/owner-link.ts +508 -0
  56. package/src/read-keys.ts +78 -0
  57. package/src/recovery-key.ts +79 -0
  58. package/src/runtime-certificate.ts +129 -0
  59. package/src/runtime-identity.ts +166 -0
  60. package/src/sealing.ts +281 -0
  61. package/src/signatures.ts +45 -0
  62. package/src/spawner-exchange.ts +155 -0
@@ -0,0 +1,155 @@
1
+ /**
2
+ * The spawner exchange: how a runtime and the process that started it come to share a key that no
3
+ * other program holds.
4
+ *
5
+ * It runs once, over the socket pair the runtime inherited from its spawner and before the runtime
6
+ * starts any child, so the two ends are known by construction. The spawner writes its X25519
7
+ * ephemeral first; the runtime answers with its own ephemeral and its instance public key; each
8
+ * side derives the spawner key from the shared secret, salted with a hash of all three public values
9
+ * so the key is bound to this runtime's instance key. The spawner learns the instance key here, over
10
+ * the one channel no other program reaches, and later certifies exactly that key.
11
+ *
12
+ * The spawner key has no lifetime of its own. The runtime forgets it when its trust root is
13
+ * installed, and the spawner wipes it once it has certified the runtime, so it is never an owner
14
+ * credential: it admits only the process that started this runtime, and only until certification.
15
+ */
16
+ import { Buffer } from 'node:buffer';
17
+
18
+ import { isBytes, utf8, wipe } from './bytes.js';
19
+ import { KeyringError, LinkRefusedError } from './errors.js';
20
+ import type { IKeyPair } from './key-pairs.js';
21
+ import { KEY_BYTES } from './kdf.js';
22
+ import { SYSTEM_LINK_ENTROPY, type ILinkEntropy } from './link-entropy.js';
23
+ import { agree, ephemeralKeyPair, hashParts, hkdf } from './link-primitives.js';
24
+
25
+ /** Bound into the salt, so a key from this exchange is never one of any other derivation. */
26
+ export const SPAWNER_EXCHANGE_LABEL = 'robota/link/v1/spawner-exchange';
27
+ /** The HKDF `info` of the spawner key. */
28
+ export const SPAWNER_KEY_LABEL = 'robota/link/v1/spawner';
29
+ /** The spawner's message: its ephemeral public key. */
30
+ export const SPAWNER_EXCHANGE_MESSAGE_BYTES = KEY_BYTES;
31
+ /** The runtime's reply: its ephemeral public key, then its instance public key. */
32
+ export const SPAWNER_EXCHANGE_REPLY_BYTES = 2 * KEY_BYTES;
33
+
34
+ const SPAWNER_KEYS = new WeakMap<SpawnerKey, Buffer>();
35
+ /** Only this module holds it, so only a completed exchange can make a spawner key. */
36
+ const ISSUED = Symbol('spawner-key');
37
+
38
+ /**
39
+ * The key a spawner shares with the runtime it started. Opaque: it offers only `wipe()`, and the link
40
+ * initiator reads it through an accessor this package does not export. It has no expiry; it is
41
+ * wiped by whoever holds it once it is no longer needed.
42
+ */
43
+ export class SpawnerKey {
44
+ constructor(issued: typeof ISSUED, bytes: Buffer) {
45
+ if (issued !== ISSUED) {
46
+ throw new KeyringError('invalid-argument', 'A spawner key comes only from an exchange.');
47
+ }
48
+ SPAWNER_KEYS.set(this, bytes);
49
+ }
50
+
51
+ get wiped(): boolean {
52
+ return !SPAWNER_KEYS.has(this);
53
+ }
54
+
55
+ /** Zero-fills the key and forgets it. Idempotent. */
56
+ wipe(): void {
57
+ SPAWNER_KEYS.get(this)?.fill(0);
58
+ SPAWNER_KEYS.delete(this);
59
+ }
60
+ }
61
+
62
+ /** Package-private: the bytes of a spawner key, for the link initiator alone. */
63
+ export function spawnerKeyBytes(key: SpawnerKey): Buffer {
64
+ const bytes = SPAWNER_KEYS.get(key);
65
+ if (bytes === undefined) throw new KeyringError('keys-locked', 'The spawner key was wiped.');
66
+ return bytes;
67
+ }
68
+
69
+ export interface ISpawnerExchangeResult {
70
+ readonly spawnerKey: SpawnerKey;
71
+ /** The runtime's instance public key, as it sent it over the inherited pair. */
72
+ readonly instancePublicKey: Uint8Array;
73
+ }
74
+
75
+ /** The spawner's side of one exchange. */
76
+ export interface ISpawnerExchange {
77
+ /** The first message to write to the runtime: the spawner's ephemeral public key. */
78
+ readonly message: Uint8Array;
79
+ /** Derives the spawner key from the runtime's reply. Once: the ephemeral is dropped by the first call. */
80
+ complete(reply: Uint8Array): ISpawnerExchangeResult;
81
+ }
82
+
83
+ /** The spawner key both sides derive from the shared secret and the three public values. */
84
+ export function deriveSpawnerKey(
85
+ sharedSecret: Uint8Array,
86
+ spawnerEphemeral: Uint8Array,
87
+ runtimeEphemeral: Uint8Array,
88
+ instancePublicKey: Uint8Array,
89
+ ): Buffer {
90
+ const salt = hashParts([
91
+ utf8(SPAWNER_EXCHANGE_LABEL),
92
+ spawnerEphemeral,
93
+ runtimeEphemeral,
94
+ instancePublicKey,
95
+ ]);
96
+ return hkdf(sharedSecret, salt, SPAWNER_KEY_LABEL, KEY_BYTES);
97
+ }
98
+
99
+ /** {@link startSpawnerExchange} drawing from `entropy`; the package's own tests pass fixed values. */
100
+ export function startSpawnerExchangeWith(entropy: ILinkEntropy): ISpawnerExchange {
101
+ let ephemeral: IKeyPair | undefined = ephemeralKeyPair(entropy);
102
+ const message = Uint8Array.from(ephemeral.publicKey);
103
+ return {
104
+ message,
105
+ complete(reply: Uint8Array): ISpawnerExchangeResult {
106
+ const own = ephemeral;
107
+ ephemeral = undefined;
108
+ if (own === undefined) throw new LinkRefusedError('out-of-order');
109
+ if (!isBytes(reply) || reply.length !== SPAWNER_EXCHANGE_REPLY_BYTES) {
110
+ throw new LinkRefusedError('malformed');
111
+ }
112
+ const runtimeEphemeral = reply.subarray(0, KEY_BYTES);
113
+ const instancePublicKey = Uint8Array.from(reply.subarray(KEY_BYTES));
114
+ const shared = agree(own.privateKey, runtimeEphemeral);
115
+ try {
116
+ const key = deriveSpawnerKey(shared, own.publicKey, runtimeEphemeral, instancePublicKey);
117
+ return { spawnerKey: new SpawnerKey(ISSUED, key), instancePublicKey };
118
+ } finally {
119
+ wipe(shared);
120
+ }
121
+ },
122
+ };
123
+ }
124
+
125
+ /**
126
+ * Starts the spawner's side of the exchange with a fresh ephemeral. Write `message` to the runtime
127
+ * over the inherited pair, read its 64-byte reply, and `complete` it.
128
+ */
129
+ export function startSpawnerExchange(): ISpawnerExchange {
130
+ return startSpawnerExchangeWith(SYSTEM_LINK_ENTROPY);
131
+ }
132
+
133
+ /**
134
+ * The runtime's side: its reply to the spawner's `message` and the spawner key, for its instance key.
135
+ * The caller keeps the key; the ephemeral exists only inside this call.
136
+ */
137
+ export function answerSpawnerExchange(
138
+ message: Uint8Array,
139
+ instancePublicKey: Uint8Array,
140
+ entropy: ILinkEntropy,
141
+ ): { readonly reply: Buffer; readonly spawnerKey: Buffer } {
142
+ if (!isBytes(message) || message.length !== SPAWNER_EXCHANGE_MESSAGE_BYTES) {
143
+ throw new LinkRefusedError('malformed');
144
+ }
145
+ const ephemeral = ephemeralKeyPair(entropy);
146
+ const shared = agree(ephemeral.privateKey, message);
147
+ try {
148
+ return {
149
+ reply: Buffer.concat([ephemeral.publicKey, instancePublicKey]),
150
+ spawnerKey: deriveSpawnerKey(shared, message, ephemeral.publicKey, instancePublicKey),
151
+ };
152
+ } finally {
153
+ wipe(shared);
154
+ }
155
+ }