@zanii/mandate-verify-core 0.2.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +37 -1
- package/package.json +1 -1
- package/src/index.js +1 -0
- package/src/passport.js +102 -0
package/README.md
CHANGED
|
@@ -114,6 +114,36 @@ without a full request.
|
|
|
114
114
|
|
|
115
115
|
Supply root-to-nearest-parent entries as `{ mandate, statusList }` with each parent's signed status envelope and independently configured root trust. Every link binds the parent ID, authorized child issuer, validity, depth and exact logical status-list reference. Numeric status claims, per-item key overrides and chains embedded in the action request do not establish authority. Supported references are the logical list ID or its `urn:zanii:status:` encoding; arbitrary URI suffixes do not bind a list. Stable-DID parent/status keys can be resolved through server-owned `resolveMandatePublicKey` and `resolveStatusPublicKey` pin callbacks; request- or artifact-supplied callbacks are not trusted context.
|
|
116
116
|
|
|
117
|
+
## Root passports
|
|
118
|
+
|
|
119
|
+
`verifyPassport` checks an agent's mandate all the way up to a root authority (for example a
|
|
120
|
+
free zone), offline. The root signs one passport mandate to the firm's issuer DID. The firm then
|
|
121
|
+
issues its own tree (company, role, release, instance) with ordinary delegation, so it never
|
|
122
|
+
needs the root to sign anything else.
|
|
123
|
+
|
|
124
|
+
```js
|
|
125
|
+
import { verifyPassport } from '#verify-core';
|
|
126
|
+
|
|
127
|
+
const result = verifyPassport({
|
|
128
|
+
passport, // signed by rootDid, agent = the firm's issuer DID, delegation allowed
|
|
129
|
+
passportStatusList, // the root's signed status list
|
|
130
|
+
rootDid,
|
|
131
|
+
mandate, // the agent instance's mandate
|
|
132
|
+
statusList,
|
|
133
|
+
delegationChain, // the firm's chain, top first: [{ mandate, statusList }, ...]
|
|
134
|
+
request: { agentDid, action, resource },
|
|
135
|
+
});
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
It denies when the passport is not from `rootDid` (`PASSPORT_ROOT_UNTRUSTED`), is not a root
|
|
139
|
+
(`PASSPORT_NOT_ROOT`), names a different issuer (`PASSPORT_ISSUER_MISMATCH`), does not allow
|
|
140
|
+
issuing (`PASSPORT_NOT_DELEGABLE`), or when the firm's top mandate exceeds it
|
|
141
|
+
(`PASSPORT_SCOPE_EXCEEDED`). Then it runs `verifyMandate` twice: the passport against the
|
|
142
|
+
request with the root as the only trusted issuer (reasons tagged `scope: 'passport'`), and the
|
|
143
|
+
firm's chain with the firm's issuer as the only trusted issuer. Revoking any level, from the
|
|
144
|
+
passport down to the instance, denies everything below it. Budgets on the passport cannot be
|
|
145
|
+
counted offline, so they give `review`, not `allow`.
|
|
146
|
+
|
|
117
147
|
## Offline trust bundles
|
|
118
148
|
|
|
119
149
|
`createOfflineTrustBundle` in `#db` reads an initialized control store and exports
|
|
@@ -223,5 +253,11 @@ the connected deployment boundary.
|
|
|
223
253
|
## Test
|
|
224
254
|
|
|
225
255
|
```powershell
|
|
226
|
-
node --test "tests/verify.test.js" "tests/delegation.test.js"
|
|
256
|
+
node --test "tests/verify.test.js" "tests/delegation.test.js" "tests/passport.test.js"
|
|
227
257
|
```
|
|
258
|
+
|
|
259
|
+
## Releases
|
|
260
|
+
|
|
261
|
+
- **0.3.0**: `verifyPassport` checks an agent's mandate offline through the firm's own tree up to a
|
|
262
|
+
root authority (see "Root passports"). Additive; every 0.2.0 export is unchanged.
|
|
263
|
+
- **0.2.0**: first public release.
|
package/package.json
CHANGED
package/src/index.js
CHANGED
|
@@ -26,6 +26,7 @@ import { validityPolicyReason } from './policy.js';
|
|
|
26
26
|
|
|
27
27
|
export { scopeSubsetReasons, inheritParentRestrictions } from './delegation.js';
|
|
28
28
|
export { verifyOfflineTrustBundle } from './offline.js';
|
|
29
|
+
export { verifyPassport } from './passport.js';
|
|
29
30
|
export {
|
|
30
31
|
evaluateConditions,
|
|
31
32
|
verifyMandateAsync,
|
package/src/passport.js
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { scopeSubsetReasons } from './delegation.js';
|
|
2
|
+
import { verifyMandate } from './index.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Verify an agent's passport chain up to a root authority (for example a free zone), offline.
|
|
6
|
+
*
|
|
7
|
+
* The root signs one passport mandate whose agent is the firm's issuer DID: that binds the
|
|
8
|
+
* firm's key under the root. The firm then issues its own tree (company, role, release,
|
|
9
|
+
* instance) with ordinary narrowing delegation, rooted in its issuer DID. This checks both:
|
|
10
|
+
*
|
|
11
|
+
* - the passport: signed by `rootDid`, a root itself, delegable, not revoked or expired in the
|
|
12
|
+
* root's signed status list, and covering the requested action and resource;
|
|
13
|
+
* - the firm's chain and leaf mandate, with the firm's issuer as the only trusted root;
|
|
14
|
+
* - that the firm's top mandate stays within the passport (actions, resources, expiry, limits).
|
|
15
|
+
*
|
|
16
|
+
* Budgets on the passport cannot be checked offline (no usage counts), so they surface as
|
|
17
|
+
* `review`, never as `allow`.
|
|
18
|
+
*/
|
|
19
|
+
export function verifyPassport({
|
|
20
|
+
passport,
|
|
21
|
+
passportStatusList,
|
|
22
|
+
rootDid,
|
|
23
|
+
mandate,
|
|
24
|
+
statusList,
|
|
25
|
+
delegationChain = [],
|
|
26
|
+
request,
|
|
27
|
+
pins = {},
|
|
28
|
+
now = new Date(),
|
|
29
|
+
...rest
|
|
30
|
+
} = {}) {
|
|
31
|
+
const evaluatedAt = new Date(now).toISOString();
|
|
32
|
+
const deny = (code, message) => ({
|
|
33
|
+
verdict: 'deny',
|
|
34
|
+
mandateId: mandate?.id,
|
|
35
|
+
agentDid: request?.agentDid,
|
|
36
|
+
evaluatedAt,
|
|
37
|
+
reasons: [{ code, severity: 'hard', message }],
|
|
38
|
+
checks: [{ code, ok: false, severity: 'hard' }],
|
|
39
|
+
});
|
|
40
|
+
const firmTop = delegationChain[0]?.mandate ?? mandate;
|
|
41
|
+
const firmDid = firmTop?.issuer;
|
|
42
|
+
const subject = passport?.credentialSubject;
|
|
43
|
+
|
|
44
|
+
if (typeof rootDid !== 'string' || !rootDid)
|
|
45
|
+
return deny('PASSPORT_ROOT_MISSING', 'A root DID is required');
|
|
46
|
+
if (!subject || passport.issuer !== rootDid) {
|
|
47
|
+
return deny('PASSPORT_ROOT_UNTRUSTED', 'Passport is not issued by the configured root');
|
|
48
|
+
}
|
|
49
|
+
if (passport.parentMandateId)
|
|
50
|
+
return deny('PASSPORT_NOT_ROOT', 'A passport must be a root mandate');
|
|
51
|
+
if (typeof firmDid !== 'string' || subject.agent?.did !== firmDid) {
|
|
52
|
+
return deny('PASSPORT_ISSUER_MISMATCH', 'Passport does not name the issuer of this chain');
|
|
53
|
+
}
|
|
54
|
+
if (subject.delegation?.allowed !== true) {
|
|
55
|
+
return deny('PASSPORT_NOT_DELEGABLE', 'Passport does not allow its holder to issue');
|
|
56
|
+
}
|
|
57
|
+
const scope = scopeSubsetReasons(firmTop, passport);
|
|
58
|
+
if (scope.length > 0) return deny('PASSPORT_SCOPE_EXCEEDED', scope.join('; '));
|
|
59
|
+
|
|
60
|
+
const authority = verifyMandate({
|
|
61
|
+
mandate: passport,
|
|
62
|
+
statusList: passportStatusList,
|
|
63
|
+
request: { ...request, agentDid: firmDid },
|
|
64
|
+
pins: {
|
|
65
|
+
trustedIssuers: [rootDid],
|
|
66
|
+
requireStatus: true,
|
|
67
|
+
requireApproval: false,
|
|
68
|
+
requireCertification: false,
|
|
69
|
+
requireEvidence: false,
|
|
70
|
+
...(pins.clockSkewSeconds === undefined ? {} : { clockSkewSeconds: pins.clockSkewSeconds }),
|
|
71
|
+
...(pins.maxStatusAgeSeconds === undefined
|
|
72
|
+
? {}
|
|
73
|
+
: { maxStatusAgeSeconds: pins.maxStatusAgeSeconds }),
|
|
74
|
+
},
|
|
75
|
+
now,
|
|
76
|
+
});
|
|
77
|
+
const chain = verifyMandate({
|
|
78
|
+
...rest,
|
|
79
|
+
mandate,
|
|
80
|
+
statusList,
|
|
81
|
+
delegationChain,
|
|
82
|
+
request,
|
|
83
|
+
pins: { ...pins, trustedIssuers: [firmDid] },
|
|
84
|
+
now,
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
const verdicts = [authority.verdict, chain.verdict];
|
|
88
|
+
const verdict = ['error', 'deny', 'review'].find((v) => verdicts.includes(v)) ?? 'allow';
|
|
89
|
+
return {
|
|
90
|
+
...chain,
|
|
91
|
+
verdict,
|
|
92
|
+
reasons: [
|
|
93
|
+
...authority.reasons.map((reason) => ({ ...reason, scope: 'passport' })),
|
|
94
|
+
...chain.reasons,
|
|
95
|
+
],
|
|
96
|
+
checks: [
|
|
97
|
+
...authority.checks.map((check) => ({ ...check, scope: 'passport' })),
|
|
98
|
+
...chain.checks,
|
|
99
|
+
],
|
|
100
|
+
passport: { id: passport.id, root: rootDid, holder: firmDid, verdict: authority.verdict },
|
|
101
|
+
};
|
|
102
|
+
}
|