@agentunion/fastaun 0.5.19 → 0.5.21
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 +176 -78
- package/_packed_docs/CHANGELOG.md +176 -78
- package/_packed_docs/INDEX.md +288 -269
- package/_packed_docs/KITE_DOCS_GUIDE.md +101 -95
- package/_packed_docs/agent.md/SCHEMA.md +7 -7
- package/_packed_docs/agent.md/working/AID/347/261/273/345/236/213-owner/345/205/263/347/263/273/344/270/216/346/215/242owner/347/274/226/346/216/222/346/226/271/346/241/210.md +530 -0
- package/_packed_docs/agent.md/working/AID/347/261/273/345/236/213-owner/345/205/263/347/263/273/345/256/214/346/225/264/350/256/276/350/256/241/344/270/216/345/256/236/346/226/275/350/256/241/345/210/222.md +685 -0
- package/_packed_docs/agent.md/working/agent.md-schema/344/270/216/347/255/276/345/217/221/351/252/214/347/255/276/346/265/201/347/250/213/346/276/204/346/270/205.md +349 -0
- package/_packed_docs/agent.md/working//346/234/254/345/234/260Docker/346/265/213/350/257/225Bug/346/270/205/345/215/225.md +40 -0
- package/_packed_docs/agent.md//350/277/234/347/250/213agent.md/347/274/223/345/255/230/344/270/216etag/351/200/217/344/274/240/346/226/271/346/241/210.md +11 -11
- package/_packed_docs/protocol/06-/346/234/215/345/212/241/345/215/217/350/256/256.md +372 -331
- package/_packed_docs/protocol/08-AUN-E2EE.md +17 -12
- package/_packed_docs/protocol/10-Group-/345/255/220/345/215/217/350/256/256.md +676 -676
- package/_packed_docs/protocol/11-Storage-/345/255/220/345/215/217/350/256/256.md +8 -12
- package/_packed_docs/protocol/Ownership/345/215/217/350/256/256/345/256/232/344/271/211.md +445 -0
- package/_packed_docs/protocol/Ownership/345/215/217/350/256/256/345/256/232/344/271/2112.0.md +161 -0
- package/_packed_docs/protocol/Ownership/345/215/217/350/256/256/345/256/232/344/271/2112.1.md +666 -0
- package/_packed_docs/protocol/Ownership/345/215/217/350/256/256/345/256/232/344/271/2113.0.md +233 -0
- package/_packed_docs/protocol/agent.md/SCHEMA.md +1 -1
- package/_packed_docs/sdk/0.5.21-SDK/345/272/224/347/224/250/351/233/206/346/210/220/346/214/207/345/215/227.md +368 -0
- package/_packed_docs/sdk/0.5.21-SDK/345/274/200/345/217/221/344/270/216/347/247/273/346/244/215/346/214/207/345/215/227.md +350 -0
- package/_packed_docs/sdk/01-/345/277/253/351/200/237/345/274/200/345/247/213.md +1 -1
- package/_packed_docs/sdk/02-WebSocket/345/215/217/350/256/256.md +5 -1
- package/_packed_docs/sdk/03-/346/240/270/345/277/203/346/246/202/345/277/265.md +64 -64
- package/_packed_docs/sdk/04-/350/277/236/346/216/245/344/270/216/350/256/244/350/257/201.md +70 -63
- package/_packed_docs/sdk/05-E2EE/345/212/240/345/257/206/351/200/232/344/277/241.md +43 -40
- package/_packed_docs/sdk/06-API/346/211/213/345/206/214.md +179 -19
- package/_packed_docs/sdk/08-/346/234/200/344/275/263/345/256/236/350/267/265.md +50 -49
- package/_packed_docs/sdk/09-group-rpc-manual.md +261 -254
- package/_packed_docs/sdk/09-message-rpc-manual.md +253 -160
- package/_packed_docs/sdk/09-storage-rpc-manual.md +18 -26
- package/_packed_docs/sdk/09-/345/207/255/350/257/201/346/234/215/345/212/241RPC/346/211/213/345/206/214.md +71 -0
- package/_packed_docs/sdk/09-/347/273/204/347/273/207/346/234/215/345/212/241RPC/346/211/213/345/206/214.md +88 -0
- package/_packed_docs/sdk/AUN_DOCS_GUIDE.md +105 -92
- package/_packed_docs/sdk/INDEX.md +197 -167
- package/_packed_docs/sdk/Ownership-SDK/345/256/236/347/216/260/346/214/207/345/215/227.md +169 -0
- package/_packed_docs/sdk/Ownership-SDK/347/274/226/346/216/222/346/214/207/345/215/227.md +349 -0
- package/_packed_docs/sdk/Ownership-SDK/347/274/226/346/216/222/346/214/207/345/215/2272.1.md +930 -0
- package/_packed_docs/sdk/Ownership-SDK/351/233/206/346/210/220/346/214/207/345/215/227.md +270 -0
- package/_packed_docs/sdk/Ownership2.1-SDK/345/272/224/347/224/250/351/233/206/346/210/220/346/214/207/345/215/227.md +363 -0
- package/_packed_docs/sdk/Ownership2.1-SDK/345/274/200/345/217/221/344/270/216/347/247/273/346/244/215/346/214/207/345/215/227.md +553 -0
- package/_packed_docs/sdk/Ownership2.1-TDD/345/210/206/351/230/266/346/256/265/345/256/236/346/226/275/350/256/241/345/210/222.md +708 -0
- package/_packed_docs/sdk/Ownership2.1/350/257/246/347/273/206/350/256/276/350/256/241.md +480 -0
- package/_packed_docs/sdk/Ownership/347/256/241/347/220/206/344/270/223/351/242/230.md +559 -0
- package/_packed_docs/sdk/ProtectedHeaders/347/224/250/346/263/225/344/270/223/351/242/230.md +299 -0
- package/_packed_docs/sdk//345/207/255/350/257/201/344/270/216/347/273/204/347/273/207/346/234/215/345/212/241SDK/345/272/224/347/224/250/351/233/206/346/210/220/346/214/207/345/215/227.md +173 -0
- package/_packed_docs/sdk//345/207/255/350/257/201/344/270/216/347/273/204/347/273/207/346/234/215/345/212/241SDK/345/274/200/345/217/221/344/270/216/347/247/273/346/244/215/346/214/207/345/215/227.md +211 -0
- package/_packed_docs//350/267/250/350/256/276/345/244/207/345/267/262/350/257/273/346/234/252/350/257/273SDK/345/256/236/347/216/260/347/247/273/346/244/215/346/214/207/345/215/227.md +331 -0
- package/_packed_docs//350/267/250/350/256/276/345/244/207/345/267/262/350/257/273/346/234/252/350/257/273SDK/351/233/206/346/210/220/346/214/207/345/215/227.md +207 -0
- package/_packed_docs//350/267/250/350/256/276/345/244/207/345/267/262/350/257/273/346/234/252/350/257/273/344/273/243/347/240/201/345/256/241/346/237/245/346/212/245/345/221/212.md +252 -0
- package/_packed_docs//350/267/250/350/256/276/345/244/207/345/267/262/350/257/273/346/234/252/350/257/273/345/256/236/346/226/275/350/256/241/345/210/222.md +156 -0
- package/_packed_docs//350/267/250/350/256/276/345/244/207/345/267/262/350/257/273/346/234/252/350/257/273/346/226/271/346/241/210.md +273 -0
- package/_packed_docs//350/267/250/350/256/276/345/244/207/345/267/262/350/257/273/346/234/252/350/257/273/350/257/246/347/273/206/350/256/276/350/256/241.md +211 -0
- package/dist/agent-md-schema.d.ts +13 -1
- package/dist/agent-md-schema.js +63 -33
- package/dist/agent-md-schema.js.map +1 -1
- package/dist/agent-md.d.ts +30 -3
- package/dist/agent-md.js +293 -152
- package/dist/agent-md.js.map +1 -1
- package/dist/aid-store.d.ts +42 -0
- package/dist/aid-store.js +224 -44
- package/dist/aid-store.js.map +1 -1
- package/dist/aid.js +18 -13
- package/dist/aid.js.map +1 -1
- package/dist/auth.js +78 -2
- package/dist/auth.js.map +1 -1
- package/dist/cert-utils.d.ts +44 -1
- package/dist/cert-utils.js +157 -56
- package/dist/cert-utils.js.map +1 -1
- package/dist/client/delivery.d.ts +68 -1
- package/dist/client/delivery.js +1196 -135
- package/dist/client/delivery.js.map +1 -1
- package/dist/client/rpc-pipeline.d.ts +5 -1
- package/dist/client/rpc-pipeline.js +240 -111
- package/dist/client/rpc-pipeline.js.map +1 -1
- package/dist/client/v2-e2ee.d.ts +12 -2
- package/dist/client/v2-e2ee.js +842 -379
- package/dist/client/v2-e2ee.js.map +1 -1
- package/dist/client.d.ts +26 -2
- package/dist/client.js +420 -75
- package/dist/client.js.map +1 -1
- package/dist/config.d.ts +4 -0
- package/dist/config.js +1 -0
- package/dist/config.js.map +1 -1
- package/dist/events.d.ts +23 -4
- package/dist/events.js +171 -15
- package/dist/events.js.map +1 -1
- package/dist/facades.d.ts +43 -1
- package/dist/facades.js +368 -12
- package/dist/facades.js.map +1 -1
- package/dist/index.d.ts +7 -4
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/keystore/aid-db.d.ts +7 -2
- package/dist/keystore/aid-db.js +194 -43
- package/dist/keystore/aid-db.js.map +1 -1
- package/dist/keystore/index.d.ts +15 -1
- package/dist/keystore/local-token-store.d.ts +10 -2
- package/dist/keystore/local-token-store.js +81 -16
- package/dist/keystore/local-token-store.js.map +1 -1
- package/dist/org-protocol.d.ts +45 -0
- package/dist/org-protocol.js +829 -22
- package/dist/org-protocol.js.map +1 -1
- package/dist/ownership-v21.d.ts +40 -9
- package/dist/ownership-v21.js +174 -79
- package/dist/ownership-v21.js.map +1 -1
- package/dist/ownership-workflow.d.ts +22 -0
- package/dist/ownership-workflow.js +861 -66
- package/dist/ownership-workflow.js.map +1 -1
- package/dist/secret-store/file-store.js +84 -0
- package/dist/secret-store/file-store.js.map +1 -1
- package/dist/seq-tracker.d.ts +1 -0
- package/dist/seq-tracker.js +2 -2
- package/dist/seq-tracker.js.map +1 -1
- package/dist/tools/cross-sdk-agent.d.ts +67 -3
- package/dist/tools/cross-sdk-agent.js +575 -42
- package/dist/tools/cross-sdk-agent.js.map +1 -1
- package/dist/transport.d.ts +4 -0
- package/dist/transport.js +108 -44
- package/dist/transport.js.map +1 -1
- package/dist/types.d.ts +1 -0
- package/dist/v2/e2ee/encrypt-group.js +1 -0
- package/dist/v2/e2ee/encrypt-group.js.map +1 -1
- package/dist/v2/e2ee/encrypt-p2p.js +1 -0
- package/dist/v2/e2ee/encrypt-p2p.js.map +1 -1
- package/dist/v2/e2ee/types.d.ts +1 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
package/dist/org-protocol.js
CHANGED
|
@@ -28,6 +28,12 @@ const REPRESENTATION_FIELDS = new Set([
|
|
|
28
28
|
]);
|
|
29
29
|
const REPRESENTATION_SIGNATURE_FIELDS = new Set(['cert_fingerprint', 'public_key_fingerprint', 'timestamp', 'signature']);
|
|
30
30
|
const FINGERPRINT_RE = /^sha256:[0-9a-f]{64}$/i;
|
|
31
|
+
const SERVICE_EVENTS = new Set([
|
|
32
|
+
'org.member.join_requested', 'org.member.join_approved', 'org.member.join_rejected',
|
|
33
|
+
'org.member.invite_created', 'org.member.invite_accepted', 'org.member.invite_rejected',
|
|
34
|
+
'org.member.changed', 'org.lifecycle.created', 'org.lifecycle.dissolving',
|
|
35
|
+
'org.lifecycle.dissolved', 'credential.revoked',
|
|
36
|
+
]);
|
|
31
37
|
export class ProtocolError extends Error {
|
|
32
38
|
code;
|
|
33
39
|
retryable;
|
|
@@ -38,6 +44,740 @@ export class ProtocolError extends Error {
|
|
|
38
44
|
this.retryable = protocolErrorRetryable(code);
|
|
39
45
|
}
|
|
40
46
|
}
|
|
47
|
+
export class CredentialRenewal {
|
|
48
|
+
tail = Promise.resolve();
|
|
49
|
+
ports;
|
|
50
|
+
scanGuard;
|
|
51
|
+
constructor(ports) {
|
|
52
|
+
this.ports = {
|
|
53
|
+
...ports,
|
|
54
|
+
currentAid: () => { this.checkScan(); return ports.currentAid(); },
|
|
55
|
+
load: (aid) => this.scanOperation(() => ports.load(aid)),
|
|
56
|
+
save: (aid, records, guard) => {
|
|
57
|
+
const scan = this.scanGuard;
|
|
58
|
+
return this.scanOperation(() => ports.save(aid, records, scan ? { current: () => scan.current() && (!guard || guard.current()), signal: scan.signal } : guard));
|
|
59
|
+
},
|
|
60
|
+
signer: (aid) => this.scanOperation(() => ports.signer(aid, this.scanGuard)),
|
|
61
|
+
certificate: (aid, fingerprint, signedAt) => this.scanOperation(() => ports.certificate(aid, fingerprint, signedAt)),
|
|
62
|
+
call: (method, params, onSent) => this.scanOperation(() => onSent ? ports.call(method, params, onSent) : ports.call(method, params)),
|
|
63
|
+
send: (to, payload) => this.scanOperation(() => ports.send(to, payload)),
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
checkScan() {
|
|
67
|
+
if (this.scanGuard && !this.scanGuard.current())
|
|
68
|
+
throw invalid('credential renewal was superseded');
|
|
69
|
+
}
|
|
70
|
+
async scanOperation(operation) {
|
|
71
|
+
this.checkScan();
|
|
72
|
+
const result = await operation();
|
|
73
|
+
this.checkScan();
|
|
74
|
+
return result;
|
|
75
|
+
}
|
|
76
|
+
serial(operation) {
|
|
77
|
+
const result = this.tail.then(operation);
|
|
78
|
+
this.tail = result.catch(() => { });
|
|
79
|
+
return result;
|
|
80
|
+
}
|
|
81
|
+
remember(credential, material = {}) {
|
|
82
|
+
return this.serial(() => this.rememberNow(credential, material));
|
|
83
|
+
}
|
|
84
|
+
current(input) {
|
|
85
|
+
return this.serial(async () => {
|
|
86
|
+
const records = await this.ports.load(this.ports.currentAid());
|
|
87
|
+
let cid = typeof input === 'string' ? input : String(input.cid);
|
|
88
|
+
const seen = new Set();
|
|
89
|
+
while (records[cid]?.successor_cid && !seen.has(cid)) {
|
|
90
|
+
seen.add(cid);
|
|
91
|
+
cid = String(records[cid].successor_cid);
|
|
92
|
+
}
|
|
93
|
+
return records[cid] ? structuredClone(records[cid]) : null;
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
async rememberNow(credential, material, actor = this.ports.currentAid(), guard) {
|
|
97
|
+
validateCredential(credential);
|
|
98
|
+
if (!material.long_term && !guard)
|
|
99
|
+
return;
|
|
100
|
+
if (guard && !guard.current())
|
|
101
|
+
throw invalid('membership event receiver identity changed');
|
|
102
|
+
const accounts = new Set();
|
|
103
|
+
if (credential.sub === actor)
|
|
104
|
+
accounts.add(actor);
|
|
105
|
+
if (credential.iss === actor || material.issued)
|
|
106
|
+
accounts.add(String(credential.iss));
|
|
107
|
+
for (const aid of accounts) {
|
|
108
|
+
const records = structuredClone(await this.ports.load(aid));
|
|
109
|
+
if (guard && !guard.current())
|
|
110
|
+
throw invalid('membership event receiver identity changed');
|
|
111
|
+
const previous = records[String(credential.cid)] ?? {};
|
|
112
|
+
const defined = Object.fromEntries(Object.entries(material).filter(([, value]) => value !== undefined));
|
|
113
|
+
records[String(credential.cid)] = { ...previous, ...structuredClone(defined), credential: structuredClone(credential), long_term: material.long_term === true };
|
|
114
|
+
if (guard)
|
|
115
|
+
await this.ports.save(aid, records, guard);
|
|
116
|
+
else
|
|
117
|
+
await this.ports.save(aid, records);
|
|
118
|
+
if (guard && !guard.current())
|
|
119
|
+
throw invalid('membership event receiver identity changed');
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
importMembershipPackage(payload, guard) {
|
|
123
|
+
const actor = this.ports.currentAid();
|
|
124
|
+
return this.serial(async () => {
|
|
125
|
+
const check = () => { if (!guard.current())
|
|
126
|
+
throw invalid('membership event receiver identity changed'); };
|
|
127
|
+
check();
|
|
128
|
+
const material = payload.membership_package;
|
|
129
|
+
if (!isRecord(material) || Object.keys(material).length !== 5 || !['credential', 'membership_binding', 'registrationReceipt', 'terms', 'org_signer_aid'].every((key) => key in material)
|
|
130
|
+
|| payload.member_status !== 'active' || !isInt(payload.version) || payload.version < 1)
|
|
131
|
+
throw invalid('membership package is invalid');
|
|
132
|
+
const { credential, membership_binding: binding, registrationReceipt: receipt, terms, org_signer_aid: orgSigner } = material;
|
|
133
|
+
if (!isRecord(credential) || !isRecord(binding) || !isRecord(receipt) || !isRecord(terms) || !isRecord(credential.ext)
|
|
134
|
+
|| credential.iss !== payload.org_id || credential.sub !== payload.member_aid || binding.org_id !== payload.org_id || binding.member_aid !== payload.member_aid
|
|
135
|
+
|| binding.membership_id !== payload.membership_id || credential.cid !== payload.credential_cid || digest(terms) !== binding.terms_hash
|
|
136
|
+
|| credential.ext.credential_type !== 'member' || credential.ext.bound_to_membership !== true || credential.ext.org_id !== payload.org_id
|
|
137
|
+
|| credential.ext.membership_id !== binding.membership_id || credential.ext.binding_version !== binding.binding_version || credential.ext.membership_terms_hash !== binding.terms_hash
|
|
138
|
+
|| credential.parent != null || credential.delegate !== false || !Array.isArray(credential.cap) || credential.cap.length !== 1
|
|
139
|
+
|| credential.cap[0].ns !== 'org' || credential.cap[0].action !== 'role.' + String(binding.role) || credential.cap[0].resource !== payload.org_id)
|
|
140
|
+
throw invalid('membership package does not match the event');
|
|
141
|
+
aid(orgSigner, 'membership organization signer');
|
|
142
|
+
validateCredential(credential);
|
|
143
|
+
validateMembershipBinding(binding);
|
|
144
|
+
const certificates = new Map();
|
|
145
|
+
for (const [signerAid, signed] of [[credential.iss, credential.signature], [binding.member_aid, binding.member_signature], [orgSigner, binding.org_signature], ['credential.' + String(payload.org_id).split('.').slice(1).join('.'), receipt.signature]]) {
|
|
146
|
+
if (!isRecord(signed))
|
|
147
|
+
throw invalid('membership package signature is missing');
|
|
148
|
+
const certificate = await this.ports.certificate(String(signerAid), String(signed.cert_fingerprint));
|
|
149
|
+
check();
|
|
150
|
+
certificates.set(String(signed.cert_fingerprint), certificate);
|
|
151
|
+
}
|
|
152
|
+
const resolve = (fingerprint) => certificates.get(fingerprint);
|
|
153
|
+
const now = Math.trunc(Date.now() / 1000);
|
|
154
|
+
verifyCredentialSignature(credential, resolve, now, String(payload.org_id));
|
|
155
|
+
verifyMembershipBinding(binding, resolve, now, String(orgSigner), true);
|
|
156
|
+
check();
|
|
157
|
+
const service = 'credential.' + String(payload.org_id).split('.').slice(1).join('.');
|
|
158
|
+
if (!isInt(receipt.registered_at) || !isInt(receipt.exp) || receipt.exp > receipt.registered_at + 300 || receipt.registered_at > now)
|
|
159
|
+
throw invalid('membership receipt window is invalid');
|
|
160
|
+
const late = now >= receipt.exp;
|
|
161
|
+
verifyRegistrationReceipt(receipt, credential, { request_id: binding.request_id, nonce: binding.nonce, registrar_aid: payload.org_id }, resolve, service, late ? receipt.registered_at : now);
|
|
162
|
+
check();
|
|
163
|
+
const validUntil = Math.min(Number(credential.exp), late ? await this.status(credential, actor, false, receipt.registered_at) : receipt.exp);
|
|
164
|
+
check();
|
|
165
|
+
const completedAt = Math.trunc(Date.now() / 1000);
|
|
166
|
+
verifyCredentialSignature(credential, resolve, completedAt, String(payload.org_id));
|
|
167
|
+
check();
|
|
168
|
+
if (actor !== payload.member_aid)
|
|
169
|
+
return;
|
|
170
|
+
const issuerCertificate = certificates.get(String(credential.signature.cert_fingerprint));
|
|
171
|
+
await this.rememberNow(credential, { membership_binding: binding, registrationReceipt: receipt, terms, version: payload.version,
|
|
172
|
+
service_aid: 'org.' + String(payload.org_id).split('.').slice(1).join('.'), long_term: credential.exp === Math.floor(Date.parse(new X509Certificate(issuerCertificate).validTo) / 1000) }, actor, { ...guard, current: () => guard.current() && Date.now() / 1000 < validUntil });
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
observe(method, params, result) {
|
|
176
|
+
if (method !== 'credential.register' && !method.startsWith('org.'))
|
|
177
|
+
return Promise.resolve();
|
|
178
|
+
return this.serial(async () => {
|
|
179
|
+
if (!isRecord(result))
|
|
180
|
+
return;
|
|
181
|
+
const values = [params, ...['previous_owner', 'new_owner'].map((key) => params[key]).filter(isRecord),
|
|
182
|
+
...Object.entries(params.credential_templates ?? {}).map(([aid, credential]) => ({ credential_template: credential, membership_binding: params.membership_bindings?.[aid], terms: params.terms }))];
|
|
183
|
+
for (const value of values) {
|
|
184
|
+
const credential = value.credential_template ?? value.credential;
|
|
185
|
+
if (!isRecord(credential))
|
|
186
|
+
continue;
|
|
187
|
+
const member = isRecord(credential.ext) && credential.ext.credential_type === 'member';
|
|
188
|
+
const actor = this.ports.currentAid();
|
|
189
|
+
const existing = (await this.ports.load(actor))[String(credential.cid)];
|
|
190
|
+
let longTerm = existing?.long_term === true;
|
|
191
|
+
if (!longTerm && isRecord(credential.signature)) {
|
|
192
|
+
const cert = await this.ports.certificate(String(credential.iss), String(credential.signature.cert_fingerprint));
|
|
193
|
+
longTerm = credential.exp === Math.min(Math.floor(new Date(new X509Certificate(cert).validTo).getTime() / 1000), Number(credential.proof_chain?.[0]?.exp ?? Infinity));
|
|
194
|
+
}
|
|
195
|
+
await this.rememberNow(credential, {
|
|
196
|
+
long_term: longTerm, issued: true,
|
|
197
|
+
...(member ? { membership_binding: result.membership_binding ?? value.membership_binding,
|
|
198
|
+
terms: value.terms ?? (value.membership_binding?.role === 'owner' ? { org_id: credential.iss, owner_aid: credential.sub } : undefined),
|
|
199
|
+
version: result.member?.version ?? result.version ?? value.expected_version ?? (['org.member.approveJoin', 'org.member.inviteAccept'].includes(method) ? 1 : undefined),
|
|
200
|
+
service_aid: 'org.' + String(credential.iss).split('.').slice(1).join('.') } : {}),
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
resume(guard) {
|
|
206
|
+
return this.serial(async () => {
|
|
207
|
+
this.scanGuard = guard;
|
|
208
|
+
try {
|
|
209
|
+
const actor = this.ports.currentAid();
|
|
210
|
+
const records = await this.ports.load(actor);
|
|
211
|
+
for (const [cid, initial] of Object.entries(records)) {
|
|
212
|
+
if (!isRecord(initial.credential))
|
|
213
|
+
continue;
|
|
214
|
+
try {
|
|
215
|
+
if (initial.response?.registration_request && !initial.response.registrationReceipt && Number(initial.response.registration_request.request_binding?.exp) <= Math.trunc(Date.now() / 1000)) {
|
|
216
|
+
if (!initial.registration_confirmation)
|
|
217
|
+
await this.recoverRegistration(actor, cid, initial, records);
|
|
218
|
+
continue;
|
|
219
|
+
}
|
|
220
|
+
if (initial.submission && initial.received_result && !initial.successor_cid)
|
|
221
|
+
await this.receiveNow(String(initial.credential.iss), initial.received_result, undefined, true);
|
|
222
|
+
else if (initial.incoming?.payload.exp > Math.trunc(Date.now() / 1000))
|
|
223
|
+
await this.receiveNow(initial.incoming.from, initial.incoming.payload);
|
|
224
|
+
const record = (await this.ports.load(actor))[cid];
|
|
225
|
+
if (record.commit_pending)
|
|
226
|
+
await this.sendCommit(record, actor);
|
|
227
|
+
if (record.revoke_pending)
|
|
228
|
+
await this.revokePrevious(record, actor);
|
|
229
|
+
const credential = record.credential;
|
|
230
|
+
const now = Math.trunc(Date.now() / 1000);
|
|
231
|
+
if (record.successor_cid || !record.long_term || credential.sub !== actor || !record.membership_binding && !record.pending && now >= credential.exp || credential.exp - now >= (credential.exp - credential.iat) / 2)
|
|
232
|
+
continue;
|
|
233
|
+
let request = record.pending;
|
|
234
|
+
if (request?.membership_binding && request.membership_binding.exp <= now)
|
|
235
|
+
throw invalid('renewal member consent has expired');
|
|
236
|
+
if (!request || request.exp <= now) {
|
|
237
|
+
const signer = await this.ports.signer(actor);
|
|
238
|
+
if (!signer)
|
|
239
|
+
continue;
|
|
240
|
+
request = request ? { ...request, iat: now, exp: now + 300 } : { type: 'credential.renewal.request', ver: 'aun-credential-renewal/1', action: 'renew', issuer_aid: credential.iss,
|
|
241
|
+
subject_aid: actor, previous_credential_cid: cid, credential_hash: digest(credential), request_id: organizationIdentifier(), nonce: organizationNonce(), iat: now, exp: now + 300 };
|
|
242
|
+
delete request.signature;
|
|
243
|
+
if (record.membership_binding && !request.membership_binding) {
|
|
244
|
+
if (now < credential.exp) {
|
|
245
|
+
const credentials = { [record.membership_binding.role === 'admin' ? 'approval_credential' : 'member_credential']: credential, membership_binding: record.membership_binding };
|
|
246
|
+
const query = prepareOrganizationRequest('org.member.get', { org_id: credential.iss, member_aid: actor, credentials }, record.service_aid, signer);
|
|
247
|
+
const current = await this.ports.call('org.member.get', query);
|
|
248
|
+
const member = current.member ?? current;
|
|
249
|
+
if (!isInt(member.version) || member.credential_cid !== undefined && member.credential_cid !== cid)
|
|
250
|
+
throw invalid('current membership differs from managed credential');
|
|
251
|
+
record.version = member.version;
|
|
252
|
+
}
|
|
253
|
+
if (!isInt(record.version))
|
|
254
|
+
throw invalid('current membership version is unavailable');
|
|
255
|
+
const binding = { ...record.membership_binding, binding_version: Number(record.membership_binding.binding_version) + 1,
|
|
256
|
+
request_id: request.request_id, nonce: request.nonce, iat: now, exp: now + 3600 };
|
|
257
|
+
delete binding.member_signature;
|
|
258
|
+
delete binding.org_signature;
|
|
259
|
+
request.membership_binding = signMembershipBinding(binding, signer, 'member');
|
|
260
|
+
request.expected_version = record.version;
|
|
261
|
+
}
|
|
262
|
+
request.signature = signProtocolPayload(request, signer);
|
|
263
|
+
record.pending = request;
|
|
264
|
+
const latest = await this.ports.load(actor);
|
|
265
|
+
latest[cid] = record;
|
|
266
|
+
await this.ports.save(actor, latest);
|
|
267
|
+
}
|
|
268
|
+
if (this.ports.currentAid() !== actor)
|
|
269
|
+
return;
|
|
270
|
+
const localIssuer = record.membership_binding && await this.ports.signer(String(credential.iss));
|
|
271
|
+
if (localIssuer) {
|
|
272
|
+
await this.receiveNow(actor, request, String(credential.iss));
|
|
273
|
+
const issued = (await this.ports.load(String(credential.iss)))[cid]?.response;
|
|
274
|
+
if (issued)
|
|
275
|
+
await this.receiveNow(String(credential.iss), issued);
|
|
276
|
+
}
|
|
277
|
+
else
|
|
278
|
+
await this.ports.send(String(credential.iss), request);
|
|
279
|
+
}
|
|
280
|
+
catch (error) {
|
|
281
|
+
const latest = await this.ports.load(actor);
|
|
282
|
+
if (latest[cid]) {
|
|
283
|
+
latest[cid].last_error = error instanceof Error ? error.message : String(error);
|
|
284
|
+
await this.ports.save(actor, latest);
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
finally {
|
|
290
|
+
this.scanGuard = undefined;
|
|
291
|
+
}
|
|
292
|
+
});
|
|
293
|
+
}
|
|
294
|
+
receive(from, payload) {
|
|
295
|
+
return this.serial(() => this.receiveNow(from, payload));
|
|
296
|
+
}
|
|
297
|
+
async verifyEnvelope(from, payload, expected, replay = false) {
|
|
298
|
+
const now = Math.trunc(Date.now() / 1000);
|
|
299
|
+
if (from !== expected || payload.ver !== 'aun-credential-renewal/1' || !isInt(payload.iat) || !isInt(payload.exp) || payload.iat > (replay ? payload.iat : now) || payload.exp <= (replay ? payload.iat : now) || payload.exp > payload.iat + 300 || !isRecord(payload.signature))
|
|
300
|
+
throw invalid('renewal message identity or validity is invalid');
|
|
301
|
+
const cert = await this.ports.certificate(expected, String(payload.signature.cert_fingerprint));
|
|
302
|
+
const unsigned = { ...payload };
|
|
303
|
+
delete unsigned.signature;
|
|
304
|
+
verifyAgentSignature(payload.signature, unsigned, expected, () => cert, now);
|
|
305
|
+
}
|
|
306
|
+
async status(credential, actor, allowExpired = false, registrationConfirmedAt, confirmation) {
|
|
307
|
+
const target = 'credential.' + String(credential.iss).split('.').slice(1).join('.');
|
|
308
|
+
const query = { credential_cid: credential.cid, request_id: organizationIdentifier(), nonce: organizationNonce(), include_chain: true };
|
|
309
|
+
const requestedAt = Math.trunc(Date.now() / 1000);
|
|
310
|
+
query.request_binding = buildRequestBinding('credential.status', target, query, String(credential.cid), String(query.nonce), String(query.request_id), requestedAt, requestedAt + 300);
|
|
311
|
+
const proof = await this.ports.call('credential.status', query);
|
|
312
|
+
const now = Math.trunc(Date.now() / 1000);
|
|
313
|
+
if (!isRecord(proof) || proof.ver !== 'aun-credential-status/1' || proof.credentialCid !== credential.cid || proof.credential_hash !== digest(credential)
|
|
314
|
+
|| proof.issuerAid !== credential.iss || proof.subjectAid !== credential.sub || proof.expiresAt !== credential.exp || proof.responder !== target
|
|
315
|
+
|| proof.request_id !== query.request_id || proof.nonce !== query.nonce || proof.target !== actor
|
|
316
|
+
|| !isInt(proof.asOf) || !isInt(proof.proofExpiresAt) || proof.asOf > now + 300 || proof.proofExpiresAt <= proof.asOf || proof.proofExpiresAt <= now || proof.proofExpiresAt > proof.asOf + 300
|
|
317
|
+
|| !isRecord(proof.signature) || proof.signature.timestamp !== proof.asOf)
|
|
318
|
+
throw new ProtocolError('CREDENTIAL_STATUS_UNKNOWN', 'renewal status proof does not match the query');
|
|
319
|
+
const cert = await this.ports.certificate(target, String(proof.signature.cert_fingerprint));
|
|
320
|
+
const unsigned = { ...proof };
|
|
321
|
+
delete unsigned.signature;
|
|
322
|
+
verifyAgentSignature(proof.signature, unsigned, target, () => cert, now, false);
|
|
323
|
+
if (proof.status !== 'active' && !(allowExpired && proof.status === 'expired' && proof.credentialStatus === 'expired' && Array.isArray(proof.chain) && proof.chain.every((item) => item.status === 'active')))
|
|
324
|
+
throw new ProtocolError('CREDENTIAL_STATUS_UNKNOWN', 'renewal credential is not active');
|
|
325
|
+
if (registrationConfirmedAt !== undefined && (proof.asOf < requestedAt || proof.asOf > now || !isInt(proof.registrationConfirmedAt) || proof.registrationConfirmedAt < registrationConfirmedAt || proof.registrationConfirmedAt > proof.asOf || proof.credentialStatus !== 'active' || proof.proofExpiresAt <= Math.trunc(Date.now() / 1000)))
|
|
326
|
+
throw new ProtocolError('CREDENTIAL_STATUS_UNKNOWN', 'membership registration confirmation is invalid');
|
|
327
|
+
if (confirmation)
|
|
328
|
+
Object.assign(confirmation, structuredClone(proof));
|
|
329
|
+
return proof.proofExpiresAt;
|
|
330
|
+
}
|
|
331
|
+
async receiveNow(from, value, localIssuer, replay = false) {
|
|
332
|
+
if (!isRecord(value) || !['credential.renewal.request', 'credential.renewal.result'].includes(String(value.type)))
|
|
333
|
+
return false;
|
|
334
|
+
const payload = value;
|
|
335
|
+
if (!replay && isInt(payload.exp) && payload.exp <= Math.trunc(Date.now() / 1000))
|
|
336
|
+
return true;
|
|
337
|
+
const isRequest = payload.type === 'credential.renewal.request';
|
|
338
|
+
const actor = localIssuer ?? this.ports.currentAid();
|
|
339
|
+
await this.verifyEnvelope(from, payload, String(isRequest ? payload.subject_aid : payload.issuer_aid), replay);
|
|
340
|
+
if (actor !== (isRequest ? payload.issuer_aid : payload.subject_aid))
|
|
341
|
+
throw invalid('renewal message has a different recipient');
|
|
342
|
+
let records = await this.ports.load(actor);
|
|
343
|
+
const cid = String(payload.previous_credential_cid);
|
|
344
|
+
const record = records[cid];
|
|
345
|
+
if (!record || !record.long_term || payload.credential_hash !== digest(record.credential) || payload.issuer_aid !== record.credential.iss || payload.subject_aid !== record.credential.sub)
|
|
346
|
+
throw invalid('renewal does not match a known grant');
|
|
347
|
+
if (replay && (!record.submission || digest(record.received_result) !== digest(payload)))
|
|
348
|
+
throw invalid('renewal replay differs from the saved result');
|
|
349
|
+
record.incoming = { from, payload: structuredClone(payload) };
|
|
350
|
+
await this.ports.save(actor, records);
|
|
351
|
+
if (isRequest && payload.action === 'commit') {
|
|
352
|
+
if (!record.response || record.response.request_id !== payload.request_id || record.response.nonce !== payload.nonce || record.response.credential.cid !== payload.new_credential_cid)
|
|
353
|
+
throw invalid('renewal acknowledgement differs from issued result');
|
|
354
|
+
record.revoke_pending = true;
|
|
355
|
+
await this.ports.save(actor, records);
|
|
356
|
+
await this.revokePrevious(record, actor);
|
|
357
|
+
}
|
|
358
|
+
else if (isRequest && payload.action === 'renew') {
|
|
359
|
+
if (payload.exp > payload.iat + 300)
|
|
360
|
+
throw invalid('renewal request window is too long');
|
|
361
|
+
if (record.successor_cid && record.response?.request_id !== payload.request_id)
|
|
362
|
+
throw invalid('grant was already replaced');
|
|
363
|
+
if (!record.response || record.response.request_id !== payload.request_id) {
|
|
364
|
+
const checkedAt = Math.trunc(Date.now() / 1000);
|
|
365
|
+
if (record.credential.exp - checkedAt >= (record.credential.exp - record.credential.iat) / 2)
|
|
366
|
+
throw invalid('credential renewal threshold is not reached');
|
|
367
|
+
await this.status(record.credential, this.ports.currentAid(), !!record.membership_binding);
|
|
368
|
+
const signer = await this.ports.signer(actor);
|
|
369
|
+
if (!signer)
|
|
370
|
+
throw invalid('issuer signing identity is unavailable');
|
|
371
|
+
const now = Math.trunc(Date.now() / 1000);
|
|
372
|
+
const response = { type: 'credential.renewal.result', ver: payload.ver, action: 'issued', issuer_aid: actor, subject_aid: from,
|
|
373
|
+
previous_credential_cid: cid, credential_hash: payload.credential_hash, request_id: payload.request_id, nonce: payload.nonce, iat: now };
|
|
374
|
+
if (record.membership_binding) {
|
|
375
|
+
const binding = payload.membership_binding;
|
|
376
|
+
const previous = record.membership_binding;
|
|
377
|
+
if (!isRecord(binding) || binding.binding_version !== Number(previous.binding_version) + 1 || binding.request_id !== payload.request_id || binding.nonce !== payload.nonce
|
|
378
|
+
|| ['org_id', 'member_aid', 'membership_id', 'member_type', 'category', 'role', 'ownership', 'visibility', 'terms_hash'].some((key) => binding[key] !== previous[key]))
|
|
379
|
+
throw invalid('renewal changes membership terms');
|
|
380
|
+
const memberSignature = binding.member_signature;
|
|
381
|
+
const memberCert = await this.ports.certificate(from, String(memberSignature?.cert_fingerprint));
|
|
382
|
+
const unsigned = { ...binding };
|
|
383
|
+
delete unsigned.member_signature;
|
|
384
|
+
delete unsigned.org_signature;
|
|
385
|
+
verifyAgentSignature(memberSignature, unsigned, from, () => memberCert, now);
|
|
386
|
+
const credentialOptions = Object.fromEntries(['security_level', 'revocation_endpoint', 'credential_service'].filter((key) => record.credential[key] !== undefined).map((key) => [key, record.credential[key]]));
|
|
387
|
+
const prepared = prepareOrganizationRequest('org.member.renewCredential', { org_id: actor, member_aid: from, previous_credential_cid: cid,
|
|
388
|
+
expected_version: payload.expected_version, terms: record.terms, membership_binding: binding }, record.service_aid, signer, { orgSigner: signer, memberSigner: signer, credentialOptions });
|
|
389
|
+
delete prepared.request_binding;
|
|
390
|
+
response.renewal_params = prepared;
|
|
391
|
+
response.credential = prepared.credential_template;
|
|
392
|
+
response.exp = Math.min(Number(binding.exp), Number(response.credential.exp));
|
|
393
|
+
}
|
|
394
|
+
else {
|
|
395
|
+
if (actor !== this.ports.currentAid())
|
|
396
|
+
throw invalid('generic renewal requires the issuer session');
|
|
397
|
+
let options = { ...record.credential };
|
|
398
|
+
for (const key of ['signature', 'cid', 'iat', 'nbf', 'exp'])
|
|
399
|
+
delete options[key];
|
|
400
|
+
if (options.parent) {
|
|
401
|
+
let parent = records[String(options.parent)];
|
|
402
|
+
const visited = new Set();
|
|
403
|
+
while (parent?.successor_cid && !visited.has(String(parent.credential.cid))) {
|
|
404
|
+
visited.add(String(parent.credential.cid));
|
|
405
|
+
parent = records[String(parent.successor_cid)];
|
|
406
|
+
}
|
|
407
|
+
const successor = parent?.credential;
|
|
408
|
+
if (successor) {
|
|
409
|
+
options.parent = successor.cid;
|
|
410
|
+
options.proof_chain = [successor, ...(successor.proof_chain ?? [])];
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
options.key_version = signer.certFingerprint;
|
|
414
|
+
options.iat = now;
|
|
415
|
+
options.nbf = Math.max(Number(record.credential.nbf), now, Number(options.proof_chain?.[0]?.nbf ?? 0));
|
|
416
|
+
response.credential = createCredential(options, signer);
|
|
417
|
+
response.exp = Math.min(now + 300, response.credential.exp);
|
|
418
|
+
const registration = { request_id: payload.request_id, nonce: payload.nonce, registrar_aid: actor, credential: response.credential };
|
|
419
|
+
registration.request_binding = buildRequestBinding('credential.register', 'credential.' + actor.split('.').slice(1).join('.'), registration, String(response.credential.cid), payload.nonce, payload.request_id, now, now + 300);
|
|
420
|
+
response.registration_request = registration;
|
|
421
|
+
record.response = response;
|
|
422
|
+
await this.ports.save(actor, records);
|
|
423
|
+
}
|
|
424
|
+
record.response = response;
|
|
425
|
+
await this.ports.save(actor, records);
|
|
426
|
+
}
|
|
427
|
+
const response = record.response;
|
|
428
|
+
if (response.registration_request && !response.registrationReceipt) {
|
|
429
|
+
if (Number(response.registration_request.request_binding?.exp) <= Math.trunc(Date.now() / 1000)) {
|
|
430
|
+
if (!record.registration_confirmation)
|
|
431
|
+
await this.recoverRegistration(actor, cid, record, records);
|
|
432
|
+
return true;
|
|
433
|
+
}
|
|
434
|
+
const registered = await this.ports.call('credential.register', response.registration_request, async () => {
|
|
435
|
+
const sentAt = Math.trunc(Date.now() / 1000);
|
|
436
|
+
if (this.ports.currentAid() !== actor || sentAt >= Number(response.registration_request.request_binding.exp))
|
|
437
|
+
throw invalid('registration expired before it was sent');
|
|
438
|
+
if (this.ports.atomicCheckpointSave?.() !== true)
|
|
439
|
+
return;
|
|
440
|
+
const deadline = Number(response.registration_request.request_binding.exp), hash = digest(response.registration_request);
|
|
441
|
+
record.registration_submitted_at ??= sentAt;
|
|
442
|
+
record.registration_submission_hash = hash;
|
|
443
|
+
await this.ports.save(actor, records, { current: () => this.ports.currentAid() === actor && Date.now() / 1000 < deadline && digest(response.registration_request) === hash });
|
|
444
|
+
});
|
|
445
|
+
await this.receipt(response.credential, registered.registrationReceipt, response.registration_request);
|
|
446
|
+
await this.status(response.credential, actor);
|
|
447
|
+
response.registrationReceipt = registered.registrationReceipt;
|
|
448
|
+
}
|
|
449
|
+
const responseNow = Math.trunc(Date.now() / 1000);
|
|
450
|
+
const responseLimit = Math.min(responseNow + 300, Number(response.credential.exp), ...['membership_binding', 'credential_template', 'registration_authorization'].map((key) => Number(response.renewal_params?.[key]?.exp ?? Infinity)));
|
|
451
|
+
if (responseLimit <= responseNow)
|
|
452
|
+
throw invalid('renewal result authorization has expired');
|
|
453
|
+
if (!response.signature || response.exp <= responseNow || response.exp > responseLimit) {
|
|
454
|
+
response.iat = responseNow;
|
|
455
|
+
response.exp = responseLimit;
|
|
456
|
+
delete response.signature;
|
|
457
|
+
response.signature = signProtocolPayload(response, (await this.ports.signer(actor)));
|
|
458
|
+
}
|
|
459
|
+
records[String(response.credential.cid)] = { ...records[String(response.credential.cid)], credential: structuredClone(response.credential), long_term: true,
|
|
460
|
+
...(record.membership_binding ? { membership_binding: structuredClone(response.renewal_params.membership_binding), terms: record.terms, version: Number(payload.expected_version) + 1, service_aid: record.service_aid } : {}) };
|
|
461
|
+
await this.ports.save(actor, records);
|
|
462
|
+
if (!localIssuer)
|
|
463
|
+
await this.ports.send(from, response);
|
|
464
|
+
}
|
|
465
|
+
else if (!isRequest && payload.action === 'issued') {
|
|
466
|
+
if (!record.pending || record.pending.request_id !== payload.request_id || record.pending.nonce !== payload.nonce || record.successor_cid && record.successor_cid !== payload.credential?.cid)
|
|
467
|
+
throw invalid('renewal result does not match pending request');
|
|
468
|
+
if (record.successor_cid) {
|
|
469
|
+
if (record.commit_pending)
|
|
470
|
+
await this.sendCommit(record, actor);
|
|
471
|
+
records = await this.ports.load(actor);
|
|
472
|
+
delete records[cid].incoming;
|
|
473
|
+
await this.ports.save(actor, records);
|
|
474
|
+
return true;
|
|
475
|
+
}
|
|
476
|
+
const credential = payload.credential;
|
|
477
|
+
if (!isRecord(credential) || credential.cid === cid || Number(credential.nbf) < record.credential.nbf || ['iss', 'sub', 'aud', 'cap', 'delegate', 'delegation_depth', 'security_level', 'revocation_endpoint', 'credential_service'].some((key) => canonicalText(credential[key] ?? null) !== canonicalText(record.credential[key] ?? null)))
|
|
478
|
+
throw invalid('renewal result changes the grant');
|
|
479
|
+
const expectedExt = record.membership_binding ? { ...record.credential.ext, binding_version: Number(record.membership_binding.binding_version) + 1 } : record.credential.ext;
|
|
480
|
+
if (canonicalText(credential.ext ?? null) !== canonicalText(expectedExt ?? null))
|
|
481
|
+
throw invalid('renewal result changes grant extensions');
|
|
482
|
+
const credentialSignature = credential.signature;
|
|
483
|
+
const certificate = await this.ports.certificate(from, String(credentialSignature?.cert_fingerprint));
|
|
484
|
+
verifyCredentialSignature(credential, () => certificate, Math.trunc(Date.now() / 1000));
|
|
485
|
+
const previousChain = credentialChainDocuments(record.credential);
|
|
486
|
+
const renewedChain = credentialChainDocuments(credential);
|
|
487
|
+
if (previousChain.length !== renewedChain.length)
|
|
488
|
+
throw invalid('renewal changes the authorization chain');
|
|
489
|
+
for (let index = 1; index < renewedChain.length; index++) {
|
|
490
|
+
const parent = renewedChain[index], previous = previousChain[index];
|
|
491
|
+
if (Number(parent.nbf) < Number(previous.nbf) || ['iss', 'sub', 'aud', 'cap', 'delegate', 'delegation_depth', 'ext', 'security_level', 'revocation_endpoint', 'credential_service'].some((key) => canonicalText(parent[key] ?? null) !== canonicalText(previous[key] ?? null)))
|
|
492
|
+
throw invalid('renewal changes parent authorization');
|
|
493
|
+
const cert = await this.ports.certificate(String(parent.iss), String(parent.signature.cert_fingerprint));
|
|
494
|
+
verifyCredentialSignature(parent, () => cert, Math.trunc(Date.now() / 1000), undefined, renewedChain.slice(index + 1));
|
|
495
|
+
}
|
|
496
|
+
let material = {};
|
|
497
|
+
let recoveryValidUntil = Infinity;
|
|
498
|
+
if (record.membership_binding) {
|
|
499
|
+
const params = payload.renewal_params;
|
|
500
|
+
if (!isRecord(params) || digest(params.credential_template) !== digest(credential) || params.previous_credential_cid !== cid
|
|
501
|
+
|| !isRecord(params.membership_binding) || digest(params.membership_binding.member_signature) !== digest(record.pending.membership_binding.member_signature)
|
|
502
|
+
|| digest(params.terms) !== digest(record.terms))
|
|
503
|
+
throw invalid('renewal membership result is invalid');
|
|
504
|
+
const agreedBinding = { ...params.membership_binding };
|
|
505
|
+
delete agreedBinding.org_signature;
|
|
506
|
+
if (digest(agreedBinding) !== digest(record.pending.membership_binding) || params.org_id !== record.credential.iss || params.member_aid !== actor || params.expected_version !== record.pending.expected_version)
|
|
507
|
+
throw invalid('renewal differs from the signed membership request');
|
|
508
|
+
const signer = await this.ports.signer(actor);
|
|
509
|
+
if (!signer)
|
|
510
|
+
throw invalid('member signing identity is unavailable');
|
|
511
|
+
const consentEnd = Math.min(Number(payload.exp), Number(params.membership_binding.exp), Number(params.registration_authorization?.exp), Number(credential.exp));
|
|
512
|
+
if (replay && consentEnd <= Math.trunc(Date.now() / 1000)) {
|
|
513
|
+
if (this.ports.atomicCheckpointSave?.() !== true)
|
|
514
|
+
throw invalid('atomic checkpoint persistence is required for renewal recovery');
|
|
515
|
+
if (!isInt(record.submitted_at) || record.submitted_at < payload.iat || record.submitted_at >= consentEnd || record.submission_hash !== digest(record.submission))
|
|
516
|
+
throw invalid('renewal operation was not submitted within its authorization window');
|
|
517
|
+
const original = { ...record.submission };
|
|
518
|
+
delete original.request_binding;
|
|
519
|
+
delete original.client_signature;
|
|
520
|
+
const expected = { ...params };
|
|
521
|
+
delete expected.request_binding;
|
|
522
|
+
delete expected.client_signature;
|
|
523
|
+
if (digest(original) !== digest(expected))
|
|
524
|
+
throw invalid('renewal submission differs from the original result');
|
|
525
|
+
const credentialProof = {};
|
|
526
|
+
const proofExpiresAt = await this.status(credential, actor, false, record.submitted_at, credentialProof);
|
|
527
|
+
const query = prepareOrganizationRequest('org.member.get', { org_id: credential.iss, member_aid: actor,
|
|
528
|
+
credentials: { [params.membership_binding.role === 'admin' ? 'approval_credential' : 'member_credential']: credential, membership_binding: params.membership_binding } }, record.service_aid, signer);
|
|
529
|
+
const current = await this.ports.call('org.member.get', query);
|
|
530
|
+
const proof = current.membership_status;
|
|
531
|
+
const now = Math.trunc(Date.now() / 1000);
|
|
532
|
+
if (!isRecord(proof) || proof.ver !== 'aun-membership-status/1' || proof.org_id !== credential.iss || proof.member_aid !== actor || proof.credential_cid !== credential.cid
|
|
533
|
+
|| proof.membership_id !== params.membership_binding.membership_id || proof.role !== params.membership_binding.role || proof.version !== Number(params.expected_version) + 1
|
|
534
|
+
|| proof.status !== 'active' || proof.credential_status !== 'active' || proof.responder !== record.service_aid || proof.target !== actor || proof.request_id !== query.request_id || proof.nonce !== query.nonce
|
|
535
|
+
|| !isInt(proof.as_of) || proof.as_of < Number(query.request_binding.iat) || proof.as_of > now || !isInt(proof.expires_at) || proof.expires_at <= now || proof.expires_at > proof.as_of + 300
|
|
536
|
+
|| !isRecord(proof.signature) || proof.signature.timestamp !== proof.as_of || proofExpiresAt <= now)
|
|
537
|
+
throw invalid('renewal membership confirmation is invalid');
|
|
538
|
+
const cert = await this.ports.certificate(record.service_aid, String(proof.signature.cert_fingerprint));
|
|
539
|
+
const unsigned = { ...proof };
|
|
540
|
+
delete unsigned.signature;
|
|
541
|
+
verifyAgentSignature(proof.signature, unsigned, record.service_aid, () => cert, now, false);
|
|
542
|
+
recoveryValidUntil = Math.min(proofExpiresAt, proof.expires_at, Number(credential.exp));
|
|
543
|
+
material = { membership_binding: params.membership_binding, terms: record.terms, version: proof.version, service_aid: record.service_aid,
|
|
544
|
+
recovery_confirmation: { membership_status: proof, credential_status: credentialProof } };
|
|
545
|
+
}
|
|
546
|
+
else {
|
|
547
|
+
const prepared = record.submission ? structuredClone(record.submission) : structuredClone(params);
|
|
548
|
+
delete prepared.request_binding;
|
|
549
|
+
delete prepared.client_signature;
|
|
550
|
+
const submittedAt = Math.trunc(Date.now() / 1000);
|
|
551
|
+
prepared.request_binding = buildRequestBinding('org.member.renewCredential', record.service_aid, prepared, String(credential.cid), String(prepared.nonce), String(prepared.request_id), submittedAt, Math.min(submittedAt + 300, Number(credential.exp)));
|
|
552
|
+
record.submission = structuredClone(prepared);
|
|
553
|
+
record.received_result = structuredClone(payload);
|
|
554
|
+
records[cid] = record;
|
|
555
|
+
await this.ports.save(actor, records);
|
|
556
|
+
const result = await this.ports.call('org.member.renewCredential', prepared, async () => {
|
|
557
|
+
const sentAt = Math.trunc(Date.now() / 1000);
|
|
558
|
+
if (this.ports.currentAid() !== actor || sentAt >= Math.min(consentEnd, Number(prepared.request_binding.exp)))
|
|
559
|
+
throw invalid('renewal authorization expired before it was sent');
|
|
560
|
+
if (this.ports.atomicCheckpointSave?.() !== true)
|
|
561
|
+
return;
|
|
562
|
+
const deadline = Math.min(consentEnd, Number(prepared.request_binding.exp)), hash = digest(record.submission);
|
|
563
|
+
record.submitted_at ??= sentAt;
|
|
564
|
+
record.submission_hash = hash;
|
|
565
|
+
await this.ports.save(actor, records, { current: () => this.ports.currentAid() === actor && Date.now() / 1000 < deadline && digest(record.submission) === hash });
|
|
566
|
+
});
|
|
567
|
+
await this.receipt(credential, result.registrationReceipt, prepared);
|
|
568
|
+
await this.status(credential, actor);
|
|
569
|
+
material = { membership_binding: result.membership_binding ?? params.membership_binding, terms: record.terms, version: result.version, service_aid: record.service_aid };
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
else {
|
|
573
|
+
await this.receipt(credential, payload.registrationReceipt, payload.registration_request);
|
|
574
|
+
await this.status(credential, actor);
|
|
575
|
+
}
|
|
576
|
+
records = await this.ports.load(actor);
|
|
577
|
+
records[String(credential.cid)] = { credential: structuredClone(credential), long_term: true, ...material };
|
|
578
|
+
records[cid].successor_cid = credential.cid;
|
|
579
|
+
records[cid].response = structuredClone(payload);
|
|
580
|
+
records[cid].commit_pending = true;
|
|
581
|
+
const recoveryGuard = { current: () => Date.now() / 1000 >= Number(credential.nbf) && Date.now() / 1000 < Math.min(recoveryValidUntil, Number(credential.exp)) };
|
|
582
|
+
if (!recoveryGuard.current())
|
|
583
|
+
throw invalid('renewal recovery confirmation has expired');
|
|
584
|
+
await this.ports.save(actor, records, recoveryGuard);
|
|
585
|
+
await this.sendCommit(records[cid], actor);
|
|
586
|
+
}
|
|
587
|
+
else
|
|
588
|
+
throw invalid('renewal message action is invalid');
|
|
589
|
+
records = await this.ports.load(actor);
|
|
590
|
+
delete records[cid].incoming;
|
|
591
|
+
await this.ports.save(actor, records);
|
|
592
|
+
return true;
|
|
593
|
+
}
|
|
594
|
+
async recoverRegistration(actor, cid, record, records) {
|
|
595
|
+
if (this.ports.atomicCheckpointSave?.() !== true)
|
|
596
|
+
throw invalid('atomic checkpoint persistence is required for registration recovery');
|
|
597
|
+
const response = record.response, request = response.registration_request, binding = request.request_binding;
|
|
598
|
+
if (!isInt(record.registration_submitted_at) || record.registration_submitted_at < binding.iat || record.registration_submitted_at >= binding.exp
|
|
599
|
+
|| record.registration_submission_hash !== digest(request) || digest(request.credential) !== digest(response.credential))
|
|
600
|
+
throw invalid('registration was not sent within its authorization window');
|
|
601
|
+
const credential = response.credential;
|
|
602
|
+
const certificate = await this.ports.certificate(String(credential.iss), String(credential.signature?.cert_fingerprint));
|
|
603
|
+
verifyCredentialSignature(credential, () => certificate, Math.trunc(Date.now() / 1000));
|
|
604
|
+
const confirmation = {};
|
|
605
|
+
const validUntil = await this.status(credential, actor, false, record.registration_submitted_at, confirmation);
|
|
606
|
+
records[String(credential.cid)] = { credential: structuredClone(credential), long_term: true, issued: true };
|
|
607
|
+
records[cid].registration_confirmation = confirmation;
|
|
608
|
+
const guard = { current: () => Date.now() / 1000 >= Number(credential.nbf) && Date.now() / 1000 < Math.min(validUntil, Number(credential.exp)) };
|
|
609
|
+
if (!guard.current())
|
|
610
|
+
throw invalid('registration recovery confirmation has expired');
|
|
611
|
+
await this.ports.save(actor, records, guard);
|
|
612
|
+
}
|
|
613
|
+
async receipt(credential, receipt, request) {
|
|
614
|
+
const service = 'credential.' + String(credential.iss).split('.').slice(1).join('.');
|
|
615
|
+
if (!isRecord(receipt) || !isRecord(receipt.signature))
|
|
616
|
+
throw invalid('renewal registration receipt is missing');
|
|
617
|
+
const cert = await this.ports.certificate(service, String(receipt.signature.cert_fingerprint));
|
|
618
|
+
verifyRegistrationReceipt(receipt, credential, request, () => cert, service, Math.trunc(Date.now() / 1000));
|
|
619
|
+
}
|
|
620
|
+
async sendCommit(record, actor) {
|
|
621
|
+
const signer = await this.ports.signer(actor);
|
|
622
|
+
if (!signer)
|
|
623
|
+
throw invalid('renewal holder signing identity is unavailable');
|
|
624
|
+
const now = Math.trunc(Date.now() / 1000);
|
|
625
|
+
const payload = { ...record.pending, action: 'commit', new_credential_cid: record.successor_cid, iat: now, exp: now + 300 };
|
|
626
|
+
delete payload.signature;
|
|
627
|
+
payload.signature = signProtocolPayload(payload, signer);
|
|
628
|
+
if (record.membership_binding && await this.ports.signer(String(record.credential.iss)))
|
|
629
|
+
await this.receiveNow(actor, payload, String(record.credential.iss));
|
|
630
|
+
else
|
|
631
|
+
await this.ports.send(String(record.credential.iss), payload);
|
|
632
|
+
const records = await this.ports.load(actor);
|
|
633
|
+
delete records[String(record.credential.cid)].commit_pending;
|
|
634
|
+
await this.ports.save(actor, records);
|
|
635
|
+
}
|
|
636
|
+
async revokePrevious(record, actor) {
|
|
637
|
+
await this.status(record.response.credential, this.ports.currentAid());
|
|
638
|
+
if (!record.membership_binding && !record.credential.delegate) {
|
|
639
|
+
const now = Math.trunc(Date.now() / 1000);
|
|
640
|
+
const request = { credential_cid: record.credential.cid, registrar_aid: actor,
|
|
641
|
+
request_id: record.response.request_id + '-revoke', nonce: record.response.nonce + '-revoke', reason_code: 'credential_renewed', cascade: false };
|
|
642
|
+
request.request_binding = buildRequestBinding('credential.revoke', 'credential.' + actor.split('.').slice(1).join('.'), request, String(record.credential.cid), String(request.nonce), String(request.request_id), now, now + 300);
|
|
643
|
+
await this.ports.call('credential.revoke', request);
|
|
644
|
+
}
|
|
645
|
+
const records = await this.ports.load(actor);
|
|
646
|
+
records[String(record.credential.cid)].successor_cid = record.response.credential.cid;
|
|
647
|
+
delete records[String(record.credential.cid)].revoke_pending;
|
|
648
|
+
await this.ports.save(actor, records);
|
|
649
|
+
}
|
|
650
|
+
revocationAuthority(document, record) {
|
|
651
|
+
const derived = 'credential.' + String(document.iss).split('.').slice(1).join('.');
|
|
652
|
+
const authority = record?.credential_authority ?? derived;
|
|
653
|
+
if (typeof authority !== 'string' || !/^credential\.[^\s/]+$/.test(authority) || !AID_RE.test(String(document.iss)))
|
|
654
|
+
throw new ProtocolError('CREDENTIAL_STATUS_UNKNOWN', 'credential authority is unavailable');
|
|
655
|
+
return authority;
|
|
656
|
+
}
|
|
657
|
+
revocationState(value) {
|
|
658
|
+
if (value == null || isRecord(value) && Object.keys(value).length === 0)
|
|
659
|
+
return { version: 1, authorities: {} };
|
|
660
|
+
if (!isRecord(value) || value.version !== 1 || !isRecord(value.authorities))
|
|
661
|
+
throw new ProtocolError('CREDENTIAL_STATUS_UNKNOWN', 'credential revocation storage is damaged');
|
|
662
|
+
for (const [authority, partition] of Object.entries(value.authorities)) {
|
|
663
|
+
if (!/^credential\.[^\s/]+$/.test(authority) || !isRecord(partition) || !isRecord(partition.revoked) || !isInt(partition.since_event_seq) || partition.since_event_seq < 0
|
|
664
|
+
|| typeof partition.cursor !== 'string' || partition.cursor !== '' && (!/^(0|[1-9][0-9]*)$/.test(partition.cursor) || !isInt(Number(partition.cursor)) || Number(partition.cursor) !== partition.since_event_seq)
|
|
665
|
+
|| !isInt(partition.as_of) || !isInt(partition.expires_at) || partition.expires_at <= partition.as_of
|
|
666
|
+
|| Object.values(partition.revoked).some((sequence) => !isInt(sequence) || sequence <= 0 || sequence > Number(partition.since_event_seq)))
|
|
667
|
+
throw new ProtocolError('CREDENTIAL_STATUS_UNKNOWN', 'credential revocation storage is damaged');
|
|
668
|
+
}
|
|
669
|
+
return value;
|
|
670
|
+
}
|
|
671
|
+
_assertNotRevoked(credential, proofChain) {
|
|
672
|
+
if (!this.ports.loadRevocations)
|
|
673
|
+
return;
|
|
674
|
+
const actor = this.ports.currentAid();
|
|
675
|
+
const state = this.revocationState(this.ports.loadRevocations(actor));
|
|
676
|
+
const records = this.ports.load(actor);
|
|
677
|
+
if (!isRecord(records))
|
|
678
|
+
throw new ProtocolError('CREDENTIAL_STATUS_UNKNOWN', 'managed credential storage is unavailable');
|
|
679
|
+
const documents = [credential, ...(proofChain ?? [])];
|
|
680
|
+
const seen = new Set();
|
|
681
|
+
for (let index = 0; index < documents.length; index++) {
|
|
682
|
+
const document = documents[index];
|
|
683
|
+
if (!isRecord(document) || seen.has(document))
|
|
684
|
+
continue;
|
|
685
|
+
seen.add(document);
|
|
686
|
+
if (seen.size > 128)
|
|
687
|
+
throw new ProtocolError('CREDENTIAL_CHAIN_INVALID', 'credential chain is too deep');
|
|
688
|
+
const authority = this.revocationAuthority(document, records[String(document.cid)]);
|
|
689
|
+
const revoked = state.authorities[authority]?.revoked;
|
|
690
|
+
if (revoked && Object.hasOwn(revoked, String(document.cid)))
|
|
691
|
+
throw new ProtocolError('CREDENTIAL_REVOKED', 'credential was revoked');
|
|
692
|
+
if (Array.isArray(document.proof_chain))
|
|
693
|
+
documents.push(...document.proof_chain);
|
|
694
|
+
}
|
|
695
|
+
if (this.ports.currentAid() !== actor)
|
|
696
|
+
throw new ProtocolError('CREDENTIAL_STATUS_UNKNOWN', 'credential receiver identity changed');
|
|
697
|
+
}
|
|
698
|
+
_syncRevocations(guard) {
|
|
699
|
+
if (!this.ports.loadRevocations || !this.ports.saveRevocations)
|
|
700
|
+
return Promise.resolve();
|
|
701
|
+
return this.serial(async () => {
|
|
702
|
+
const actor = this.ports.currentAid();
|
|
703
|
+
const check = () => { if (this.ports.currentAid() !== actor || guard && !guard.current())
|
|
704
|
+
throw new ProtocolError('CREDENTIAL_STATUS_UNKNOWN', 'credential revocation synchronization was superseded'); };
|
|
705
|
+
check();
|
|
706
|
+
let state = structuredClone(this.revocationState(await this.ports.loadRevocations(actor)));
|
|
707
|
+
check();
|
|
708
|
+
const records = await this.ports.load(actor);
|
|
709
|
+
check();
|
|
710
|
+
const authorities = new Set();
|
|
711
|
+
if (AID_RE.test(actor))
|
|
712
|
+
authorities.add('credential.' + actor.split('.').slice(1).join('.'));
|
|
713
|
+
for (const record of Object.values(records)) {
|
|
714
|
+
if (!isRecord(record) || !isRecord(record.credential))
|
|
715
|
+
continue;
|
|
716
|
+
validateCredential(record.credential);
|
|
717
|
+
const documents = [record.credential];
|
|
718
|
+
const seen = new Set();
|
|
719
|
+
for (let index = 0; index < documents.length; index++) {
|
|
720
|
+
const document = documents[index];
|
|
721
|
+
if (!isRecord(document) || seen.has(document))
|
|
722
|
+
continue;
|
|
723
|
+
seen.add(document);
|
|
724
|
+
if (seen.size > 128)
|
|
725
|
+
throw new ProtocolError('CREDENTIAL_CHAIN_INVALID', 'credential chain is too deep');
|
|
726
|
+
authorities.add(this.revocationAuthority(document, document === record.credential ? record : undefined));
|
|
727
|
+
if (Array.isArray(document.proof_chain))
|
|
728
|
+
documents.push(...document.proof_chain);
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
for (const authority of authorities) {
|
|
732
|
+
for (let page = 0; page < 32; page++) {
|
|
733
|
+
check();
|
|
734
|
+
const previous = state.authorities[authority] ?? { revoked: {}, since_event_seq: 0, cursor: '' };
|
|
735
|
+
const since = previous.since_event_seq;
|
|
736
|
+
const query = { cursor: previous.cursor || String(since), limit: 1000, request_id: organizationIdentifier(), nonce: organizationNonce() };
|
|
737
|
+
const requestedAt = Math.trunc(Date.now() / 1000);
|
|
738
|
+
query.request_binding = buildRequestBinding('credential.revocationList', authority, query, '', String(query.nonce), String(query.request_id), requestedAt, requestedAt + 300);
|
|
739
|
+
const proof = await this.ports.call('credential.revocationList', query);
|
|
740
|
+
check();
|
|
741
|
+
const now = Math.trunc(Date.now() / 1000);
|
|
742
|
+
if (!isRecord(proof) || proof.responder !== authority || proof.target !== actor || proof.request_id !== query.request_id || proof.nonce !== query.nonce || proof.issuer_aid !== ''
|
|
743
|
+
|| !Array.isArray(proof.events) || proof.events.length > 1000 || typeof proof.next_cursor !== 'string'
|
|
744
|
+
|| !isInt(proof.as_of) || proof.as_of <= 0 || proof.as_of > now + 300 || !isInt(proof.expires_at) || proof.expires_at <= now || proof.expires_at <= proof.as_of || proof.expires_at > proof.as_of + 300
|
|
745
|
+
|| !isRecord(proof.signature) || proof.signature.timestamp !== proof.as_of)
|
|
746
|
+
throw new ProtocolError('CREDENTIAL_STATUS_UNKNOWN', 'revocation list does not match the query');
|
|
747
|
+
let last = since;
|
|
748
|
+
for (const event of proof.events) {
|
|
749
|
+
if (!isRecord(event) || !isInt(event.event_seq) || event.event_seq <= last || typeof event.cid !== 'string' || !event.cid
|
|
750
|
+
|| typeof event.reason_code !== 'string' || !event.reason_code || !isInt(event.revoked_at) || event.revoked_at < 0 || event.revoked_at > proof.as_of
|
|
751
|
+
|| typeof event.revoked_by_aid !== 'string' || !AID_RE.test(event.revoked_by_aid))
|
|
752
|
+
throw new ProtocolError('CREDENTIAL_STATUS_UNKNOWN', 'revocation event sequence is invalid');
|
|
753
|
+
last = event.event_seq;
|
|
754
|
+
}
|
|
755
|
+
if (proof.next_cursor !== '' && (!/^[1-9][0-9]*$/.test(proof.next_cursor) || !isInt(Number(proof.next_cursor)) || Number(proof.next_cursor) <= since || Number(proof.next_cursor) < last))
|
|
756
|
+
throw new ProtocolError('CREDENTIAL_STATUS_UNKNOWN', 'revocation cursor did not advance');
|
|
757
|
+
const certificate = await this.ports.certificate(authority, String(proof.signature.cert_fingerprint));
|
|
758
|
+
check();
|
|
759
|
+
const unsigned = { ...proof };
|
|
760
|
+
delete unsigned.signature;
|
|
761
|
+
verifyAgentSignature(proof.signature, unsigned, authority, () => certificate, Math.trunc(Date.now() / 1000), false);
|
|
762
|
+
const next = structuredClone(state);
|
|
763
|
+
const revoked = { ...previous.revoked };
|
|
764
|
+
for (const event of proof.events)
|
|
765
|
+
Object.defineProperty(revoked, String(event.cid), { value: event.event_seq, enumerable: true, configurable: true, writable: true });
|
|
766
|
+
next.authorities[authority] = { revoked, since_event_seq: proof.next_cursor === '' ? last : Number(proof.next_cursor), cursor: proof.next_cursor, as_of: proof.as_of, expires_at: proof.expires_at };
|
|
767
|
+
const commitGuard = { ...guard, current: () => this.ports.currentAid() === actor && (!guard || guard.current()) && Date.now() / 1000 < Number(proof.expires_at) };
|
|
768
|
+
if (!commitGuard.current())
|
|
769
|
+
throw new ProtocolError('CREDENTIAL_STATUS_UNKNOWN', 'revocation list expired before persistence');
|
|
770
|
+
await this.ports.saveRevocations(actor, next, commitGuard);
|
|
771
|
+
if (!commitGuard.current())
|
|
772
|
+
throw new ProtocolError('CREDENTIAL_STATUS_UNKNOWN', 'revocation synchronization was superseded');
|
|
773
|
+
state = next;
|
|
774
|
+
if (proof.next_cursor === '')
|
|
775
|
+
break;
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
});
|
|
779
|
+
}
|
|
780
|
+
}
|
|
41
781
|
function certificatePem(material) {
|
|
42
782
|
if (!material)
|
|
43
783
|
throw new Error('certificate is missing');
|
|
@@ -50,26 +790,34 @@ function certificatePem(material) {
|
|
|
50
790
|
return new X509Certificate(value).toString();
|
|
51
791
|
}
|
|
52
792
|
export async function verifyServiceEvent(eventName, payload, context) {
|
|
53
|
-
|
|
793
|
+
const event = eventName.startsWith('event/') ? eventName.slice(6) : eventName;
|
|
794
|
+
if (!SERVICE_EVENTS.has(event) || !isRecord(payload) || ['_route', '_federation', '_trace', 'member_aids', 'inline_message'].some((key) => Object.hasOwn(payload, key)) || payload.type !== event || payload.event !== event || typeof payload.event_id !== 'string' || !payload.event_id || typeof payload.responder !== 'string' || !AID_RE.test(payload.responder))
|
|
54
795
|
throw invalid('service event identity is invalid');
|
|
55
796
|
normalizeNfc(payload);
|
|
797
|
+
if (typeof context.targetAid !== 'string' || !AID_RE.test(context.targetAid))
|
|
798
|
+
throw invalid('service event target is invalid');
|
|
56
799
|
const now = context.now ?? Math.trunc(Date.now() / 1000);
|
|
57
800
|
if (!isInt(now) || !isInt(payload.as_of) || !isInt(payload.expires_at) || payload.as_of <= 0 || payload.expires_at <= payload.as_of || payload.expires_at > payload.as_of + 300 || payload.as_of > now + 300 || now >= payload.expires_at)
|
|
58
801
|
throw invalid('service event time window is invalid');
|
|
59
|
-
if (!Array.isArray(payload.target_aids) || !payload.target_aids.length || payload.target_aids.some((target) => typeof target !== 'string' || !AID_RE.test(target))
|
|
802
|
+
if (!Array.isArray(payload.target_aids) || !payload.target_aids.length || payload.target_aids.some((target) => typeof target !== 'string' || !AID_RE.test(target)))
|
|
60
803
|
throw invalid('service event target is invalid');
|
|
61
|
-
|
|
804
|
+
const targetAids = new Set(payload.target_aids.map((target) => aidKey(target)));
|
|
805
|
+
if (targetAids.size !== payload.target_aids.length || !targetAids.has(aidKey(context.targetAid)))
|
|
806
|
+
throw invalid('service event target is invalid');
|
|
807
|
+
const responder = payload.responder;
|
|
808
|
+
const responderKey = aidKey(responder);
|
|
809
|
+
let known = event.startsWith('org.');
|
|
62
810
|
if (known) {
|
|
63
|
-
if (typeof payload.org_id !== 'string' || !AID_RE.test(payload.org_id) ||
|
|
811
|
+
if (typeof payload.org_id !== 'string' || !AID_RE.test(payload.org_id) || responderKey !== aidKey('org.' + payload.org_id.slice(payload.org_id.indexOf('.') + 1)))
|
|
64
812
|
throw invalid('organization event authority is invalid');
|
|
65
813
|
}
|
|
66
|
-
else if (
|
|
814
|
+
else if (event.startsWith('credential.')) {
|
|
67
815
|
const cid = payload.credential_cid ?? payload.credentialCid;
|
|
68
|
-
if (typeof cid !== 'string' || !cid || payload.credential_cid !== undefined && payload.credentialCid !== undefined && payload.credential_cid !== payload.credentialCid || !
|
|
816
|
+
if (typeof cid !== 'string' || !cid || payload.credential_cid !== undefined && payload.credentialCid !== undefined && payload.credential_cid !== payload.credentialCid || !responderKey.startsWith('credential.') || typeof payload.target !== 'string' || !targetAids.has(aidKey(payload.target)))
|
|
69
817
|
throw invalid('credential event authority is invalid');
|
|
70
818
|
const expected = await context.resolveCredentialService?.(cid);
|
|
71
819
|
known = expected != null;
|
|
72
|
-
if (known && expected !==
|
|
820
|
+
if (known && (typeof expected !== 'string' || !AID_RE.test(expected) || aidKey(expected) !== responderKey))
|
|
73
821
|
throw invalid('credential event provider does not match trusted authority');
|
|
74
822
|
}
|
|
75
823
|
else
|
|
@@ -79,7 +827,7 @@ export async function verifyServiceEvent(eventName, payload, context) {
|
|
|
79
827
|
if (signed.timestamp !== payload.as_of)
|
|
80
828
|
throw invalid('service event signature time is invalid');
|
|
81
829
|
try {
|
|
82
|
-
const cert = certificatePem(await context.resolveEventCertificate(
|
|
830
|
+
const cert = certificatePem(await context.resolveEventCertificate(responderKey, String(signed.cert_fingerprint)));
|
|
83
831
|
const value = { ...payload };
|
|
84
832
|
delete value.signature;
|
|
85
833
|
verifyAgentSignature(signed, value, payload.responder, () => cert, context.now ?? Math.trunc(Date.now() / 1000), false);
|
|
@@ -91,6 +839,9 @@ export async function verifyServiceEvent(eventName, payload, context) {
|
|
|
91
839
|
throw invalid('service event time window is invalid');
|
|
92
840
|
return { signature_valid: true, credential_known: known };
|
|
93
841
|
}
|
|
842
|
+
function aidKey(value) {
|
|
843
|
+
return value.trim().toLowerCase();
|
|
844
|
+
}
|
|
94
845
|
export function protocolErrorRetryable(code) {
|
|
95
846
|
return ERROR_RETRYABLE[String(code)] === true;
|
|
96
847
|
}
|
|
@@ -521,6 +1272,8 @@ export function validateCredential(credential, chainDepth = 0, externalProofChai
|
|
|
521
1272
|
if (!isInt(delegationDepth) || delegationDepth < 0)
|
|
522
1273
|
throw invalid('delegation_depth is invalid');
|
|
523
1274
|
const parent = credential.parent;
|
|
1275
|
+
if (credential.proof_chain !== undefined && externalProofChain !== undefined && canonicalText(credential.proof_chain) !== canonicalText(externalProofChain))
|
|
1276
|
+
throw new ProtocolError('CREDENTIAL_CHAIN_INVALID', 'embedded proof_chain conflicts with external proof_chain');
|
|
524
1277
|
if (parent == null && delegationDepth !== 0)
|
|
525
1278
|
throw invalid('root credential must have delegation_depth 0');
|
|
526
1279
|
if (parent != null) {
|
|
@@ -535,12 +1288,12 @@ export function validateCredential(credential, chainDepth = 0, externalProofChai
|
|
|
535
1288
|
throw new ProtocolError('CREDENTIAL_CHAIN_INVALID', 'proof_chain is too deep');
|
|
536
1289
|
for (let index = 0; index < chain.length; index++) {
|
|
537
1290
|
const item = chain[index];
|
|
538
|
-
validateCredential(item, chainDepth + 1);
|
|
539
|
-
if (canonicalText(item.proof_chain
|
|
1291
|
+
validateCredential(item, chainDepth + 1, chain.slice(index + 1));
|
|
1292
|
+
if (item.proof_chain !== undefined && canonicalText(item.proof_chain) !== canonicalText(chain.slice(index + 1)))
|
|
540
1293
|
throw new ProtocolError('CREDENTIAL_CHAIN_INVALID', 'embedded proof_chain conflicts with outer chain');
|
|
541
1294
|
}
|
|
542
1295
|
}
|
|
543
|
-
else if ('proof_chain' in credential && JSON.stringify(credential.proof_chain) !== '[]') {
|
|
1296
|
+
else if ('proof_chain' in credential && JSON.stringify(credential.proof_chain) !== '[]' || parent == null && externalProofChain !== undefined && JSON.stringify(externalProofChain) !== '[]') {
|
|
544
1297
|
throw new ProtocolError('CREDENTIAL_CHAIN_INVALID', 'root proof_chain must be empty');
|
|
545
1298
|
}
|
|
546
1299
|
if (!['low', 'medium', 'high'].includes(Object.hasOwn(credential, 'security_level') ? credential.security_level : 'medium'))
|
|
@@ -579,6 +1332,10 @@ export function createCredential(options, signer) {
|
|
|
579
1332
|
throw invalid('issuer does not match signing AID');
|
|
580
1333
|
const chain = value.proof_chain;
|
|
581
1334
|
const parentDepth = Array.isArray(chain) && isRecord(chain[0]) ? chain[0].delegation_depth ?? 0 : 0;
|
|
1335
|
+
if (!('exp' in value)) {
|
|
1336
|
+
const certExp = Math.floor(new Date(new X509Certificate(signer.certPem).validTo).getTime() / 1000);
|
|
1337
|
+
value.exp = value.parent != null && Array.isArray(chain) && isRecord(chain[0]) && isInt(chain[0].exp) ? Math.min(certExp, chain[0].exp) : certExp;
|
|
1338
|
+
}
|
|
582
1339
|
if (!('delegation_depth' in value))
|
|
583
1340
|
value.delegation_depth = value.parent !== undefined && value.parent !== null && isInt(parentDepth) ? parentDepth + 1 : 0;
|
|
584
1341
|
if (Array.isArray(value.cap))
|
|
@@ -804,7 +1561,7 @@ export function prepareOrganizationRequest(method, params, target, signer, optio
|
|
|
804
1561
|
throw invalid('organization signer or signed member CAP is required');
|
|
805
1562
|
value.credential_template = createMemberCredential(binding, options.orgSigner, { key_version: options.orgSigner.certFingerprint, ...options.credentialOptions });
|
|
806
1563
|
}
|
|
807
|
-
if (
|
|
1564
|
+
if (!['org.member.inviteAccept', 'org.member.inviteReject'].includes(method) && isRecord(value.credential_template) && !('registration_authorization' in value)) {
|
|
808
1565
|
if (!binding)
|
|
809
1566
|
throw invalid('membership binding is required for registration');
|
|
810
1567
|
const issuerSigner = options.orgSigner ?? (signer.aid === value.credential_template.iss ? signer : undefined);
|
|
@@ -837,11 +1594,18 @@ export function prepareOrganizationRequest(method, params, target, signer, optio
|
|
|
837
1594
|
const credentials = isRecord(value.credentials) ? value.credentials : {};
|
|
838
1595
|
if ('credentials' in value && !isRecord(value.credentials))
|
|
839
1596
|
throw invalid('credentials must be an object');
|
|
840
|
-
if (['org.member.join', 'org.member.inviteAccept'].includes(method)) {
|
|
841
|
-
|
|
842
|
-
|
|
1597
|
+
if (['org.member.join', 'org.member.inviteAccept', 'org.member.inviteReject'].includes(method)) {
|
|
1598
|
+
const rejection = method === 'org.member.inviteReject';
|
|
1599
|
+
if (rejection && binding && !('member_aid' in value))
|
|
1600
|
+
value.member_aid = binding.member_aid;
|
|
1601
|
+
if (!binding || ('for' in value ? value.for : signer.aid) !== binding.member_aid || value.member_aid !== binding.member_aid || binding.org_id !== value.org_id || target !== 'org.' + String(binding.org_id).slice(String(binding.org_id).indexOf('.') + 1) || !isRecord(rejection ? binding.org_signature : binding.member_signature))
|
|
1602
|
+
throw representationBindingInvalid('admission requires the original membership signature');
|
|
1603
|
+
if (rejection && (value.request_id === binding.request_id || value.nonce === binding.nonce))
|
|
1604
|
+
throw representationBindingInvalid('invitation rejection requires a distinct action identity');
|
|
843
1605
|
value.credentials = credentials;
|
|
844
1606
|
const now = Math.trunc(Date.now() / 1000);
|
|
1607
|
+
if (now >= Number(binding.exp))
|
|
1608
|
+
throw representationBindingInvalid('membership binding has expired');
|
|
845
1609
|
let exp = Math.min(now + 300, Number(binding.exp));
|
|
846
1610
|
for (const field of ['credential_template', 'registration_authorization'])
|
|
847
1611
|
if (field in value) {
|
|
@@ -906,7 +1670,7 @@ export function createMemberCredential(binding, signer, options) {
|
|
|
906
1670
|
const text = (value) => new TextDecoder().decode(canonicalJson(value));
|
|
907
1671
|
if (!isRecord(options) || Object.entries(fixed).some(([key, value]) => key in options && text(options[key]) !== text(value)) || options.parent !== undefined && options.parent !== null)
|
|
908
1672
|
throw invalid('member credential options conflict with binding');
|
|
909
|
-
return createCredential({
|
|
1673
|
+
return createCredential({ ...options, ...fixed }, signer);
|
|
910
1674
|
}
|
|
911
1675
|
export function verifyCredential(credential, context) {
|
|
912
1676
|
if (!context || !context.audience || !context.subject)
|
|
@@ -920,6 +1684,22 @@ export function verifyCredential(credential, context) {
|
|
|
920
1684
|
throw new ProtocolError('CREDENTIAL_STATUS_UNKNOWN', 'trusted credential status is unavailable');
|
|
921
1685
|
const now = context.now ?? Math.trunc(Date.now() / 1000);
|
|
922
1686
|
const documents = credentialChainDocuments(credential, context.proofChain);
|
|
1687
|
+
const resolvedCertificates = [];
|
|
1688
|
+
const resolveCertificate = (fingerprint) => {
|
|
1689
|
+
const material = context.resolveCertificate(fingerprint);
|
|
1690
|
+
if (material != null)
|
|
1691
|
+
resolvedCertificates.push(material);
|
|
1692
|
+
return material;
|
|
1693
|
+
};
|
|
1694
|
+
const resolveActorCertificate = context.resolveActorCertificate
|
|
1695
|
+
? (fingerprint) => {
|
|
1696
|
+
const material = context.resolveActorCertificate(fingerprint);
|
|
1697
|
+
if (material != null)
|
|
1698
|
+
resolvedCertificates.push(material);
|
|
1699
|
+
return material;
|
|
1700
|
+
}
|
|
1701
|
+
: resolveCertificate;
|
|
1702
|
+
const statusProofs = [];
|
|
923
1703
|
const status = (cid) => {
|
|
924
1704
|
let proof;
|
|
925
1705
|
try {
|
|
@@ -929,6 +1709,8 @@ export function verifyCredential(credential, context) {
|
|
|
929
1709
|
throw new ProtocolError('CREDENTIAL_STATUS_UNKNOWN', 'trusted credential status proof is unavailable');
|
|
930
1710
|
}
|
|
931
1711
|
const document = documents.find((item) => item.cid === cid);
|
|
1712
|
+
if (isRecord(proof))
|
|
1713
|
+
statusProofs.push(proof);
|
|
932
1714
|
if (proof != null || document !== value) {
|
|
933
1715
|
if (!isRecord(proof) || proof.ver !== 'aun-credential-status/1' || typeof proof.credentialCid !== 'string' || !proof.credentialCid || typeof proof.credential_hash !== 'string' || !DIGEST_RE.test(proof.credential_hash) || !isInt(proof.asOf) || !isInt(proof.proofExpiresAt) || proof.asOf > now + 300 || proof.proofExpiresAt <= proof.asOf || proof.proofExpiresAt > proof.asOf + 300 || now >= proof.proofExpiresAt)
|
|
934
1716
|
throw new ProtocolError('CREDENTIAL_STATUS_UNKNOWN', 'trusted credential status proof is invalid');
|
|
@@ -951,7 +1733,7 @@ export function verifyCredential(credential, context) {
|
|
|
951
1733
|
throw new ProtocolError('CREDENTIAL_STATUS_UNKNOWN', 'trusted credential status is unavailable');
|
|
952
1734
|
}
|
|
953
1735
|
};
|
|
954
|
-
verifyCredentialChain(credential,
|
|
1736
|
+
verifyCredentialChain(credential, resolveCertificate, status, now, context.audience, context.proofChain);
|
|
955
1737
|
if (credentialChainDocuments(credential, context.proofChain).at(-1).iss !== (context.rootIssuer ?? context.audience))
|
|
956
1738
|
throw invalid('credential root issuer is not trusted for this resource');
|
|
957
1739
|
const rate = context.checkRateLimit ? (field, unit, limit) => context.checkRateLimit(String(value.cid), String(value.sub), field, unit, limit) : undefined;
|
|
@@ -971,7 +1753,7 @@ export function verifyCredential(credential, context) {
|
|
|
971
1753
|
if (value.iss !== binding.org_id || !value.aud.includes(binding.org_id) || caps.length !== 1 || caps[0].ns !== 'org' || caps[0].action !== 'role.' + String(binding.role) || caps[0].resource !== binding.org_id)
|
|
972
1754
|
throw invalid('member credential capability does not match binding role');
|
|
973
1755
|
}
|
|
974
|
-
verifyMembershipBinding(binding,
|
|
1756
|
+
verifyMembershipBinding(binding, resolveCertificate, now, context.orgSigner, true);
|
|
975
1757
|
if (!context.resolveMembershipStatus || context.resolveMembershipStatus(binding, value) !== true)
|
|
976
1758
|
throw new ProtocolError('MEMBERSHIP_STATE_INVALID', 'trusted current membership status does not permit this operation');
|
|
977
1759
|
}
|
|
@@ -979,6 +1761,7 @@ export function verifyCredential(credential, context) {
|
|
|
979
1761
|
const params = context.params ?? {};
|
|
980
1762
|
const target = context.for ?? params.for;
|
|
981
1763
|
const actor = context.actor ?? context.subject;
|
|
1764
|
+
let verifiedRepresentation;
|
|
982
1765
|
if (typeof actor !== 'string' || !AID_RE.test(actor))
|
|
983
1766
|
throw new ProtocolError('CREDENTIAL_REQUEST_BINDING_INVALID', 'authenticated actor is invalid');
|
|
984
1767
|
if (target !== undefined && (typeof target !== 'string' || !target || !context.actor || Object.hasOwn(params, 'for') && params.for !== target))
|
|
@@ -990,7 +1773,9 @@ export function verifyCredential(credential, context) {
|
|
|
990
1773
|
const proof = context.representationProof ?? packaged;
|
|
991
1774
|
if (packaged !== undefined && canonicalText(packaged) !== canonicalText(proof))
|
|
992
1775
|
throw new ProtocolError('CREDENTIAL_REQUEST_BINDING_INVALID', 'representation proof differs from the request');
|
|
993
|
-
|
|
1776
|
+
const representationRequest = { from: actor, for: target, audience: context.target, resource: target, params, binding: context.requestBinding };
|
|
1777
|
+
verifyRepresentationProof(proof, credential, representationRequest, resolveActorCertificate, now, context.proofChain);
|
|
1778
|
+
verifiedRepresentation = { proof: proof, request: representationRequest };
|
|
994
1779
|
const subjects = [String(target), ...(context.subject !== actor && context.subject !== target ? [context.subject] : [])];
|
|
995
1780
|
let allowed = Boolean(context.resolveOwnership);
|
|
996
1781
|
try {
|
|
@@ -1007,6 +1792,28 @@ export function verifyCredential(credential, context) {
|
|
|
1007
1792
|
throw new ProtocolError('ROLE_ACCESS_DENIED', 'trusted current ownership does not permit representation');
|
|
1008
1793
|
result.representation_checked = true;
|
|
1009
1794
|
}
|
|
1795
|
+
if (context.now === undefined) {
|
|
1796
|
+
const completedAt = Math.trunc(Date.now() / 1000);
|
|
1797
|
+
for (const [index, document] of documents.entries())
|
|
1798
|
+
validateCredentialAt(document, completedAt, context.audience, documents.slice(index + 1));
|
|
1799
|
+
validateCredentialCapability(credential, { ns: context.ns, action: context.action, resource: context.resource, params: context.params, units: context.units, now: completedAt, client_ip: context.clientIp, proof_chain: context.proofChain }, () => true);
|
|
1800
|
+
validateRequestBinding(context.requestBinding, context.method, context.target, context.params ?? {}, String(value.cid), context.nonce ?? '', context.requestId ?? '', completedAt);
|
|
1801
|
+
for (const proof of statusProofs) {
|
|
1802
|
+
if (proof.asOf > completedAt + 300 || completedAt >= proof.proofExpiresAt)
|
|
1803
|
+
throw new ProtocolError('CREDENTIAL_STATUS_UNKNOWN', 'trusted credential status proof is invalid');
|
|
1804
|
+
}
|
|
1805
|
+
for (const material of resolvedCertificates) {
|
|
1806
|
+
const certificate = new X509Certificate(certificatePem(material));
|
|
1807
|
+
if (completedAt < new Date(certificate.validFrom).getTime() / 1000 || completedAt > new Date(certificate.validTo).getTime() / 1000)
|
|
1808
|
+
throw new ProtocolError('CREDENTIAL_SIGNATURE_INVALID', 'certificate is not valid');
|
|
1809
|
+
}
|
|
1810
|
+
if (verifiedRepresentation) {
|
|
1811
|
+
const { proof, request } = verifiedRepresentation;
|
|
1812
|
+
if (completedAt < proof.iat || completedAt >= proof.exp)
|
|
1813
|
+
throw representationBindingInvalid('representation time window is invalid');
|
|
1814
|
+
validateRepresentationContext(proof, value, request, completedAt);
|
|
1815
|
+
}
|
|
1816
|
+
}
|
|
1010
1817
|
return result;
|
|
1011
1818
|
}
|
|
1012
1819
|
export function validateCredentialAt(credential, now, audience, proofChain) {
|
|
@@ -1310,7 +2117,7 @@ export function verifyCredentialChain(credential, resolveCertificate, resolveSta
|
|
|
1310
2117
|
throw new ProtocolError('CREDENTIAL_STATUS_UNKNOWN', 'credential status is unavailable');
|
|
1311
2118
|
}
|
|
1312
2119
|
for (const [index, document] of documents.entries())
|
|
1313
|
-
verifyCredentialSignature(document, resolveCertificate, now, audience, index
|
|
2120
|
+
verifyCredentialSignature(document, resolveCertificate, now, audience, documents.slice(index + 1));
|
|
1314
2121
|
}
|
|
1315
2122
|
function validateMembershipBindingPayload(binding, allowPartial = false) {
|
|
1316
2123
|
if (!isRecord(binding))
|
|
@@ -1457,7 +2264,7 @@ function orgRequestBindingParams(method, params) {
|
|
|
1457
2264
|
export function buildRequestBinding(method, target, params, credentialCid, nonce, requestId, iat, exp) {
|
|
1458
2265
|
if (![method, target, nonce, requestId].every((value) => typeof value === 'string' && value.length > 0) || typeof credentialCid !== 'string' || (!credentialCid !== (method === 'credential.revocationList')))
|
|
1459
2266
|
throw invalid('request binding identity is invalid');
|
|
1460
|
-
if (!isInt(iat) || !isInt(exp) || exp <= iat)
|
|
2267
|
+
if (!isInt(iat) || !isInt(exp) || exp <= iat || exp > iat + 300)
|
|
1461
2268
|
throw invalid('request binding time window is invalid');
|
|
1462
2269
|
return { request_id: requestId, nonce, target, method, params_hash: digest(orgRequestBindingParams(method, params)), iat, exp, credential_cid: credentialCid };
|
|
1463
2270
|
}
|
|
@@ -1468,7 +2275,7 @@ export function validateRequestBinding(binding, method, target, params, credenti
|
|
|
1468
2275
|
throw new ProtocolError('CREDENTIAL_REQUEST_BINDING_INVALID', 'request binding is invalid');
|
|
1469
2276
|
if (binding.request_id !== requestId || binding.nonce !== nonce || binding.target !== target || binding.method !== method || binding.credential_cid !== credentialCid || binding.params_hash !== digest(orgRequestBindingParams(method, params)))
|
|
1470
2277
|
throw new ProtocolError('CREDENTIAL_REQUEST_BINDING_INVALID', 'request binding does not match request');
|
|
1471
|
-
if (!isInt(binding.iat) || !isInt(binding.exp) || binding.exp <= binding.iat || !isInt(now) || now < binding.iat || now >= binding.exp)
|
|
2278
|
+
if (!isInt(binding.iat) || !isInt(binding.exp) || binding.exp <= binding.iat || binding.exp > binding.iat + 300 || !isInt(now) || now < binding.iat || now >= binding.exp)
|
|
1472
2279
|
throw new ProtocolError('CREDENTIAL_REQUEST_BINDING_INVALID', 'request binding is outside its validity window');
|
|
1473
2280
|
}
|
|
1474
2281
|
function representationBindingInvalid(message) {
|