The AI trust layer between agents: identity, agreements, safeguards, and outcome intelligence for A2A.

Viewing files at commit f1f8fc237d6aCommit detailsView latest

packages/persistence/src/relay.ts 144 lines · 4.7 KB · typescript
1
import {
2
  createCipheriv,
3
  createDecipheriv,
4
  createHash,
5
  createHmac,
6
  createPrivateKey,
7
  createPublicKey,
8
  randomBytes,
9
  sign,
10
  timingSafeEqual,
11
  verify,
12
} from 'node:crypto';
13
import { canonicalHash } from '../../protocol/src/index.js';
14
15
export type GatewayGrant = {
16
  interactionId: string;
17
  senderAgentId: string;
18
  recipientAgentId: string;
19
  expiresAt: number;
20
};
21
22
const keyFromSecret = (secret: string) => createHash('sha256').update(secret).digest();
23
24
export function encryptGatewayPayload(secret: string, payload: unknown) {
25
  const iv = randomBytes(12);
26
  const cipher = createCipheriv('aes-256-gcm', keyFromSecret(secret), iv);
27
  const ciphertext = Buffer.concat([
28
    cipher.update(JSON.stringify(payload), 'utf8'),
29
    cipher.final(),
30
  ]);
31
  return {
32
    ciphertext: ciphertext.toString('base64url'),
33
    iv: iv.toString('base64url'),
34
    authTag: cipher.getAuthTag().toString('base64url'),
35
  };
36
}
37
38
export function decryptGatewayPayload(
39
  secret: string,
40
  encrypted: { ciphertext: string; iv: string; authTag: string },
41
): unknown {
42
  const decipher = createDecipheriv(
43
    'aes-256-gcm',
44
    keyFromSecret(secret),
45
    Buffer.from(encrypted.iv, 'base64url'),
46
  );
47
  decipher.setAuthTag(Buffer.from(encrypted.authTag, 'base64url'));
48
  return JSON.parse(
49
    Buffer.concat([
50
      decipher.update(Buffer.from(encrypted.ciphertext, 'base64url')),
51
      decipher.final(),
52
    ]).toString('utf8'),
53
  );
54
}
55
56
export function issueGatewayGrant(secret: string, grant: GatewayGrant): string {
57
  const payload = Buffer.from(JSON.stringify(grant)).toString('base64url');
58
  const signature = createHmac('sha256', secret).update(payload).digest('base64url');
59
  return `${payload}.${signature}`;
60
}
61
62
export function verifyGatewayGrant(secret: string, token: string): GatewayGrant {
63
  const [payload, provided] = token.split('.');
64
  if (!payload || !provided) throw new Error('Invalid gateway token');
65
  const expected = createHmac('sha256', secret).update(payload).digest();
66
  const actual = Buffer.from(provided, 'base64url');
67
  if (actual.length !== expected.length || !timingSafeEqual(actual, expected))
68
    throw new Error('Invalid gateway token');
69
  const grant = JSON.parse(Buffer.from(payload, 'base64url').toString('utf8')) as GatewayGrant;
70
  if (grant.expiresAt <= Date.now()) throw new Error('Gateway token expired');
71
  if (!grant.interactionId || !grant.senderAgentId || !grant.recipientAgentId)
72
    throw new Error('Invalid gateway token');
73
  return grant;
74
}
75
76
function sessionPrivateKey(secret: string) {
77
  const seed = createHash('sha256').update(`openclasp:live-session:${secret}`).digest();
78
  return createPrivateKey({
79
    key: Buffer.concat([Buffer.from('302e020100300506032b657004220420', 'hex'), seed]),
80
    type: 'pkcs8',
81
    format: 'der',
82
  });
83
}
84
85
export function getSessionVerificationKey(secret: string) {
86
  return createPublicKey(sessionPrivateKey(secret))
87
    .export({ type: 'spki', format: 'der' })
88
    .toString('base64url');
89
}
90
91
export function getSessionKeyId(secret: string) {
92
  return `openclasp:${createHash('sha256')
93
    .update(getSessionVerificationKey(secret))
94
    .digest('base64url')
95
    .slice(0, 16)}`;
96
}
97
98
export function attestSessionRecord(secret: string, value: unknown) {
99
  const digest = canonicalHash(value);
100
  return {
101
    algorithm: 'Ed25519' as const,
102
    keyId: getSessionKeyId(secret),
103
    value: sign(null, Buffer.from(digest), sessionPrivateKey(secret)).toString('base64url'),
104
    digest,
105
  };
106
}
107
108
export function signSessionControl(
109
  secret: string,
110
  requestId: string,
111
  timestamp: string,
112
  body: string,
113
) {
114
  return sign(
115
    null,
116
    Buffer.from(`${timestamp}.${requestId}.${body}`),
117
    sessionPrivateKey(secret),
118
  ).toString('base64url');
119
}
120
121
export function issueSessionGrant(secret: string, grant: GatewayGrant): string {
122
  const payload = Buffer.from(JSON.stringify(grant)).toString('base64url');
123
  const signature = sign(null, Buffer.from(payload), sessionPrivateKey(secret)).toString(
124
    'base64url',
125
  );
126
  return `${payload}.${signature}`;
127
}
128
129
export function verifySessionGrant(secret: string, token: string): GatewayGrant {
130
  const [payload, signature] = token.split('.');
131
  if (!payload || !signature) throw new Error('Invalid live-session credential');
132
  const valid = verify(
133
    null,
134
    Buffer.from(payload),
135
    createPublicKey(sessionPrivateKey(secret)),
136
    Buffer.from(signature, 'base64url'),
137
  );
138
  if (!valid) throw new Error('Invalid live-session credential');
139
  const grant = JSON.parse(Buffer.from(payload, 'base64url').toString('utf8')) as GatewayGrant;
140
  if (grant.expiresAt <= Date.now()) throw new Error('Live-session credential expired');
141
  if (!grant.interactionId || !grant.senderAgentId || !grant.recipientAgentId)
142
    throw new Error('Invalid live-session credential');
143
  return grant;
144
}