@orangecheck/agent-core 2.6.0 → 2.7.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -2
- package/dist/index.d.mts +9 -1
- package/dist/index.d.ts +9 -1
- package/dist/index.js +52 -10
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +50 -11
- package/dist/index.mjs.map +1 -1
- package/dist/types.d.mts +9 -1
- package/dist/types.d.ts +9 -1
- package/dist/types.js.map +1 -1
- package/dist/types.mjs.map +1 -1
- package/package.json +1 -1
- package/src/federation.test.ts +3 -3
- package/src/federation.ts +3 -8
- package/src/index.ts +4 -0
- package/src/live-time.test.ts +151 -0
- package/src/revocation-authority.test.ts +32 -28
- package/src/revocation-failclosed.test.ts +1 -1
- package/src/test-vectors.test.ts +43 -8
- package/src/types.ts +9 -0
- package/src/verify.ts +108 -7
package/README.md
CHANGED
|
@@ -114,8 +114,10 @@ if (!result.ok) throw new Error(result.code + ': ' + result.message);
|
|
|
114
114
|
### Verification (§SPEC 8)
|
|
115
115
|
|
|
116
116
|
- `verifyDelegation({ envelope, revocations | skipRevocationCheck, verifyBip322, now?, skipTemporalCheck?, scopeMode? })`
|
|
117
|
-
- `verifyAction({ action, delegation, revocations | skipRevocationCheck, verifyBip322, verifyOtsAnchor?, content?, resolveAnchorBlockHeight?, scopeMode? })`
|
|
118
|
-
- `
|
|
117
|
+
- `verifyAction({ action, delegation, revocations | skipRevocationCheck, verifyBip322, now?, maxSkewMs?, verifyOtsAnchor?, content?, resolveAnchorBlockHeight?, scopeMode? })` — the live check, as of `now` (default: the current time). Every link of the chain must be in force at `now`, a revocation in effect at or before `now` refuses the action (`E_REVOKED`), and `signed_at` must lie within `maxSkewMs` of `now` (default 120 s; else `E_STALE`). The result's `asOf` is `{ basis: 'now', now }`.
|
|
118
|
+
- `verifyActionAsOfSignedAt({ … })` — an audit of a past action, as of its own `signed_at`. The agent chooses that time, so `asOf.anchored` says whether a verified OTS anchor fixes it. Never use it to decide whether to act now.
|
|
119
|
+
- `createReplayGuard(maxSkewMs?)` — `firstUse(action, now?)` is true once per action id. It keeps each id until `signed_at + maxSkewMs`, as long as the action could pass `verifyAction`. One process only; across instances keep the same record in a shared store. Replay is the caller's to stop: `verifyAction` keeps no state.
|
|
120
|
+
- `verifyRevocation({ envelope, delegation, verifyBip322 })` — the principal and the agent may always revoke; `revocation.holders` is informational
|
|
119
121
|
- `verifyOtsAnchor(proofB64, blockHeight, blockHash, envelopeId)` — an anchor orders an action against a revocation only when this returns true
|
|
120
122
|
- `verifyFederationRevocation({ envelope, delegation, verifyBip322 })`
|
|
121
123
|
|
package/dist/index.d.mts
CHANGED
|
@@ -7,6 +7,7 @@ import { SealInput, DeviceRecord, UnsealInput } from '@orangecheck/lock-core';
|
|
|
7
7
|
export { canonicalize, hexEncode } from '@orangecheck/stamp-core/canonical';
|
|
8
8
|
|
|
9
9
|
declare const DEFAULT_MAX_CHAIN_DEPTH = 5;
|
|
10
|
+
declare const DEFAULT_MAX_SKEW_MS = 120000;
|
|
10
11
|
interface VerifyBase {
|
|
11
12
|
verifyBip322?: (msg: string, signatureB64: string, address: string) => Promise<boolean>;
|
|
12
13
|
skipSignatureVerification?: boolean;
|
|
@@ -40,8 +41,15 @@ interface VerifyActionInput extends VerifyBase {
|
|
|
40
41
|
device_id: string;
|
|
41
42
|
secretKey: Uint8Array;
|
|
42
43
|
};
|
|
44
|
+
now?: Date;
|
|
45
|
+
maxSkewMs?: number;
|
|
43
46
|
}
|
|
47
|
+
type VerifyActionAsOfSignedAtInput = Omit<VerifyActionInput, 'now' | 'maxSkewMs'>;
|
|
44
48
|
declare function verifyAction(input: VerifyActionInput): Promise<VerifyActionResult>;
|
|
49
|
+
declare function verifyActionAsOfSignedAt(input: VerifyActionAsOfSignedAtInput): Promise<VerifyActionResult>;
|
|
50
|
+
declare function createReplayGuard(maxSkewMs?: number): {
|
|
51
|
+
firstUse(action: Pick<ActionEnvelope, 'id' | 'signed_at'>, now?: Date): boolean;
|
|
52
|
+
};
|
|
45
53
|
interface VerifyRevocationInput extends VerifyBase {
|
|
46
54
|
envelope: RevocationEnvelope;
|
|
47
55
|
delegation: ChainLink;
|
|
@@ -179,4 +187,4 @@ declare class ScopeNotGrantedError extends Error {
|
|
|
179
187
|
}
|
|
180
188
|
declare function assertScopeGranted(delegation: ScopeBearingDelegation, scopeExercised: string, fnName: string): void;
|
|
181
189
|
|
|
182
|
-
export { ActionEnvelope, ActorRef, AgentError, AgentErrorCode, ChainLink, DEFAULT_MAX_CHAIN_DEPTH, DelegationBond, DelegationEnvelope, DelegationRevocationRef, type FederationDelegationEnvelope, type FederationDescriptor, type FederationGuardian, type FederationPrincipal, type FederationRevocationEnvelope, type FederationSignature, type FederationVerifyResult, RevocationEnvelope, type ScopeBearingDelegation, ScopeNotGrantedError, ScopesEncryptedEnvelope, type SealScopesInput, SubdelegationEnvelope, type UnsealScopesInput, type UnsealedScopes, ValidationOptions, type VerifyActionInput, VerifyActionResult, type VerifyBase, type VerifyDelegationInput, VerifyDelegationResult, type VerifyFederationBase, type VerifyFederationDelegationInput, type VerifyFederationRevocationInput, type VerifyRevocationInput, VerifyRevocationResult, type VerifySubdelegationInput, VerifySubdelegationResult, assertScopeGranted, computeFederationDescriptorId, decodeScopesPayload, encodeScopesPayload, federationDescriptorCanonicalMessage, hasPrivateScopes, sealScopes, unsealScopes, verifyAction, verifyDelegation, verifyFederationDelegation, verifyFederationRevocation, verifyRevocation, verifySubdelegation };
|
|
190
|
+
export { ActionEnvelope, ActorRef, AgentError, AgentErrorCode, ChainLink, DEFAULT_MAX_CHAIN_DEPTH, DEFAULT_MAX_SKEW_MS, DelegationBond, DelegationEnvelope, DelegationRevocationRef, type FederationDelegationEnvelope, type FederationDescriptor, type FederationGuardian, type FederationPrincipal, type FederationRevocationEnvelope, type FederationSignature, type FederationVerifyResult, RevocationEnvelope, type ScopeBearingDelegation, ScopeNotGrantedError, ScopesEncryptedEnvelope, type SealScopesInput, SubdelegationEnvelope, type UnsealScopesInput, type UnsealedScopes, ValidationOptions, type VerifyActionAsOfSignedAtInput, type VerifyActionInput, VerifyActionResult, type VerifyBase, type VerifyDelegationInput, VerifyDelegationResult, type VerifyFederationBase, type VerifyFederationDelegationInput, type VerifyFederationRevocationInput, type VerifyRevocationInput, VerifyRevocationResult, type VerifySubdelegationInput, VerifySubdelegationResult, assertScopeGranted, computeFederationDescriptorId, createReplayGuard, decodeScopesPayload, encodeScopesPayload, federationDescriptorCanonicalMessage, hasPrivateScopes, sealScopes, unsealScopes, verifyAction, verifyActionAsOfSignedAt, verifyDelegation, verifyFederationDelegation, verifyFederationRevocation, verifyRevocation, verifySubdelegation };
|
package/dist/index.d.ts
CHANGED
|
@@ -7,6 +7,7 @@ import { SealInput, DeviceRecord, UnsealInput } from '@orangecheck/lock-core';
|
|
|
7
7
|
export { canonicalize, hexEncode } from '@orangecheck/stamp-core/canonical';
|
|
8
8
|
|
|
9
9
|
declare const DEFAULT_MAX_CHAIN_DEPTH = 5;
|
|
10
|
+
declare const DEFAULT_MAX_SKEW_MS = 120000;
|
|
10
11
|
interface VerifyBase {
|
|
11
12
|
verifyBip322?: (msg: string, signatureB64: string, address: string) => Promise<boolean>;
|
|
12
13
|
skipSignatureVerification?: boolean;
|
|
@@ -40,8 +41,15 @@ interface VerifyActionInput extends VerifyBase {
|
|
|
40
41
|
device_id: string;
|
|
41
42
|
secretKey: Uint8Array;
|
|
42
43
|
};
|
|
44
|
+
now?: Date;
|
|
45
|
+
maxSkewMs?: number;
|
|
43
46
|
}
|
|
47
|
+
type VerifyActionAsOfSignedAtInput = Omit<VerifyActionInput, 'now' | 'maxSkewMs'>;
|
|
44
48
|
declare function verifyAction(input: VerifyActionInput): Promise<VerifyActionResult>;
|
|
49
|
+
declare function verifyActionAsOfSignedAt(input: VerifyActionAsOfSignedAtInput): Promise<VerifyActionResult>;
|
|
50
|
+
declare function createReplayGuard(maxSkewMs?: number): {
|
|
51
|
+
firstUse(action: Pick<ActionEnvelope, 'id' | 'signed_at'>, now?: Date): boolean;
|
|
52
|
+
};
|
|
45
53
|
interface VerifyRevocationInput extends VerifyBase {
|
|
46
54
|
envelope: RevocationEnvelope;
|
|
47
55
|
delegation: ChainLink;
|
|
@@ -179,4 +187,4 @@ declare class ScopeNotGrantedError extends Error {
|
|
|
179
187
|
}
|
|
180
188
|
declare function assertScopeGranted(delegation: ScopeBearingDelegation, scopeExercised: string, fnName: string): void;
|
|
181
189
|
|
|
182
|
-
export { ActionEnvelope, ActorRef, AgentError, AgentErrorCode, ChainLink, DEFAULT_MAX_CHAIN_DEPTH, DelegationBond, DelegationEnvelope, DelegationRevocationRef, type FederationDelegationEnvelope, type FederationDescriptor, type FederationGuardian, type FederationPrincipal, type FederationRevocationEnvelope, type FederationSignature, type FederationVerifyResult, RevocationEnvelope, type ScopeBearingDelegation, ScopeNotGrantedError, ScopesEncryptedEnvelope, type SealScopesInput, SubdelegationEnvelope, type UnsealScopesInput, type UnsealedScopes, ValidationOptions, type VerifyActionInput, VerifyActionResult, type VerifyBase, type VerifyDelegationInput, VerifyDelegationResult, type VerifyFederationBase, type VerifyFederationDelegationInput, type VerifyFederationRevocationInput, type VerifyRevocationInput, VerifyRevocationResult, type VerifySubdelegationInput, VerifySubdelegationResult, assertScopeGranted, computeFederationDescriptorId, decodeScopesPayload, encodeScopesPayload, federationDescriptorCanonicalMessage, hasPrivateScopes, sealScopes, unsealScopes, verifyAction, verifyDelegation, verifyFederationDelegation, verifyFederationRevocation, verifyRevocation, verifySubdelegation };
|
|
190
|
+
export { ActionEnvelope, ActorRef, AgentError, AgentErrorCode, ChainLink, DEFAULT_MAX_CHAIN_DEPTH, DEFAULT_MAX_SKEW_MS, DelegationBond, DelegationEnvelope, DelegationRevocationRef, type FederationDelegationEnvelope, type FederationDescriptor, type FederationGuardian, type FederationPrincipal, type FederationRevocationEnvelope, type FederationSignature, type FederationVerifyResult, RevocationEnvelope, type ScopeBearingDelegation, ScopeNotGrantedError, ScopesEncryptedEnvelope, type SealScopesInput, SubdelegationEnvelope, type UnsealScopesInput, type UnsealedScopes, ValidationOptions, type VerifyActionAsOfSignedAtInput, type VerifyActionInput, VerifyActionResult, type VerifyBase, type VerifyDelegationInput, VerifyDelegationResult, type VerifyFederationBase, type VerifyFederationDelegationInput, type VerifyFederationRevocationInput, type VerifyRevocationInput, VerifyRevocationResult, type VerifySubdelegationInput, VerifySubdelegationResult, assertScopeGranted, computeFederationDescriptorId, createReplayGuard, decodeScopesPayload, encodeScopesPayload, federationDescriptorCanonicalMessage, hasPrivateScopes, sealScopes, unsealScopes, verifyAction, verifyActionAsOfSignedAt, verifyDelegation, verifyFederationDelegation, verifyFederationRevocation, verifyRevocation, verifySubdelegation };
|
package/dist/index.js
CHANGED
|
@@ -411,6 +411,7 @@ function hasPrivateScopes(envelope) {
|
|
|
411
411
|
|
|
412
412
|
// src/verify.ts
|
|
413
413
|
var DEFAULT_MAX_CHAIN_DEPTH = 5;
|
|
414
|
+
var DEFAULT_MAX_SKEW_MS = 12e4;
|
|
414
415
|
var AgentError = class extends Error {
|
|
415
416
|
constructor(code, message) {
|
|
416
417
|
super(message);
|
|
@@ -545,6 +546,12 @@ async function verifyDelegation(input) {
|
|
|
545
546
|
};
|
|
546
547
|
}
|
|
547
548
|
async function verifyAction(input) {
|
|
549
|
+
return verifyActionAt(input, input.now ?? /* @__PURE__ */ new Date());
|
|
550
|
+
}
|
|
551
|
+
async function verifyActionAsOfSignedAt(input) {
|
|
552
|
+
return verifyActionAt(input, null);
|
|
553
|
+
}
|
|
554
|
+
async function verifyActionAt(input, now) {
|
|
548
555
|
const a = input.action;
|
|
549
556
|
const d = input.delegation;
|
|
550
557
|
const chain = input.subdelegationChain ?? [];
|
|
@@ -622,6 +629,17 @@ async function verifyAction(input) {
|
|
|
622
629
|
if (signed < issued || signed >= expires) {
|
|
623
630
|
return err("E_OUT_OF_WINDOW", `action.signed_at ${a.signed_at} is outside leaf window [${leaf.issued_at}, ${leaf.expires_at})`);
|
|
624
631
|
}
|
|
632
|
+
if (now) {
|
|
633
|
+
const t = now.getTime();
|
|
634
|
+
for (const link of [rootHydrated, ...hydratedChain]) {
|
|
635
|
+
if (t < new Date(link.issued_at).getTime()) {
|
|
636
|
+
return err("E_NOT_YET_VALID", `${link.id} is not valid until ${link.issued_at}`);
|
|
637
|
+
}
|
|
638
|
+
if (t >= new Date(link.expires_at).getTime()) {
|
|
639
|
+
return err("E_EXPIRED", `${link.id} expired at ${link.expires_at}`);
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
}
|
|
625
643
|
if (!leaf.scopes) {
|
|
626
644
|
return err("E_SCOPES_NEITHER_PROVIDED", "leaf has no plaintext scopes after hydration");
|
|
627
645
|
}
|
|
@@ -670,9 +688,21 @@ async function verifyAction(input) {
|
|
|
670
688
|
if (revocationPrecedes(rev, revAnchor, a, actionAnchor)) {
|
|
671
689
|
return err("E_REVOKED", `chain link ${link.id} was revoked by ${rev.id} before action was signed`);
|
|
672
690
|
}
|
|
691
|
+
if (now && (revAnchor !== null || new Date(rev.signed_at).getTime() <= now.getTime())) {
|
|
692
|
+
return err("E_REVOKED", `chain link ${link.id} was revoked by ${rev.id}`);
|
|
693
|
+
}
|
|
673
694
|
}
|
|
674
695
|
}
|
|
675
696
|
}
|
|
697
|
+
if (now) {
|
|
698
|
+
const maxSkew = input.maxSkewMs ?? DEFAULT_MAX_SKEW_MS;
|
|
699
|
+
if (Math.abs(now.getTime() - signed) > maxSkew) {
|
|
700
|
+
return err(
|
|
701
|
+
"E_STALE",
|
|
702
|
+
`action.signed_at ${a.signed_at} is more than ${maxSkew} ms from the time of verification ${now.toISOString()}`
|
|
703
|
+
);
|
|
704
|
+
}
|
|
705
|
+
}
|
|
676
706
|
if (input.content) {
|
|
677
707
|
const actualHash = "sha256:" + canonical.hexEncode(sha256.sha256(input.content));
|
|
678
708
|
if (actualHash !== a.content.hash) {
|
|
@@ -700,7 +730,20 @@ async function verifyAction(input) {
|
|
|
700
730
|
delegation: rootHydrated,
|
|
701
731
|
chain: hydratedChain,
|
|
702
732
|
scopeExercised: exercised,
|
|
703
|
-
anchor
|
|
733
|
+
anchor,
|
|
734
|
+
asOf: now ? { basis: "now", now: now.toISOString() } : { basis: "signed_at", signedAt: a.signed_at, anchored: actionAnchor !== null }
|
|
735
|
+
};
|
|
736
|
+
}
|
|
737
|
+
function createReplayGuard(maxSkewMs = DEFAULT_MAX_SKEW_MS) {
|
|
738
|
+
const keepUntil = /* @__PURE__ */ new Map();
|
|
739
|
+
return {
|
|
740
|
+
firstUse(action, now = /* @__PURE__ */ new Date()) {
|
|
741
|
+
const t = now.getTime();
|
|
742
|
+
for (const [id, until] of keepUntil) if (until <= t) keepUntil.delete(id);
|
|
743
|
+
if (keepUntil.has(action.id)) return false;
|
|
744
|
+
keepUntil.set(action.id, new Date(action.signed_at).getTime() + maxSkewMs);
|
|
745
|
+
return true;
|
|
746
|
+
}
|
|
704
747
|
};
|
|
705
748
|
}
|
|
706
749
|
async function hydrateSubdelegationScopes(sub, input) {
|
|
@@ -758,11 +801,11 @@ async function verifyRevocation(input) {
|
|
|
758
801
|
if (env.delegation_id !== d.id) {
|
|
759
802
|
return err("E_DELEGATION_MISMATCH", `revocation.delegation_id (${env.delegation_id}) != delegation.id (${d.id})`);
|
|
760
803
|
}
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
804
|
+
if (env.signer.address !== d.principal.address && env.signer.address !== d.agent.address) {
|
|
805
|
+
return err(
|
|
806
|
+
"E_REVOKER_UNAUTHORIZED",
|
|
807
|
+
`revocation signer ${env.signer.address} is neither the principal nor the agent`
|
|
808
|
+
);
|
|
766
809
|
}
|
|
767
810
|
const canonInput = {
|
|
768
811
|
address: env.signer.address,
|
|
@@ -1145,10 +1188,6 @@ async function verifyRevocationOf(d, rev, input) {
|
|
|
1145
1188
|
});
|
|
1146
1189
|
}
|
|
1147
1190
|
const v1 = rev;
|
|
1148
|
-
const holders = d.revocation?.holders ?? ["principal"];
|
|
1149
|
-
if (!Array.isArray(holders) || !holders.includes("agent")) {
|
|
1150
|
-
return fail("E_REVOKER_UNAUTHORIZED", "only the federation may revoke this delegation");
|
|
1151
|
-
}
|
|
1152
1191
|
if (v1.kind !== "agent-revocation" || v1.signer?.address !== d.agent.address) {
|
|
1153
1192
|
return fail("E_REVOKER_UNAUTHORIZED", "a single-address revocation must be signed by the agent");
|
|
1154
1193
|
}
|
|
@@ -1250,6 +1289,7 @@ Object.defineProperty(exports, "hexEncode", {
|
|
|
1250
1289
|
});
|
|
1251
1290
|
exports.AgentError = AgentError;
|
|
1252
1291
|
exports.DEFAULT_MAX_CHAIN_DEPTH = DEFAULT_MAX_CHAIN_DEPTH;
|
|
1292
|
+
exports.DEFAULT_MAX_SKEW_MS = DEFAULT_MAX_SKEW_MS;
|
|
1253
1293
|
exports.ENVELOPE_VERSION = ENVELOPE_VERSION;
|
|
1254
1294
|
exports.REGISTERED_SCOPES = REGISTERED_SCOPES;
|
|
1255
1295
|
exports.ScopeNotGrantedError = ScopeNotGrantedError;
|
|
@@ -1271,6 +1311,7 @@ exports.computeDelegationId = computeDelegationId;
|
|
|
1271
1311
|
exports.computeFederationDescriptorId = computeFederationDescriptorId;
|
|
1272
1312
|
exports.computeRevocationId = computeRevocationId;
|
|
1273
1313
|
exports.computeSubdelegationId = computeSubdelegationId;
|
|
1314
|
+
exports.createReplayGuard = createReplayGuard;
|
|
1274
1315
|
exports.decodeScopesPayload = decodeScopesPayload;
|
|
1275
1316
|
exports.delegationCanonicalBytes = delegationCanonicalBytes;
|
|
1276
1317
|
exports.delegationCanonicalMessage = delegationCanonicalMessage;
|
|
@@ -1289,6 +1330,7 @@ exports.subdelegationCanonicalMessage = subdelegationCanonicalMessage;
|
|
|
1289
1330
|
exports.unsealScopes = unsealScopes;
|
|
1290
1331
|
exports.validateScope = validateScope;
|
|
1291
1332
|
exports.verifyAction = verifyAction;
|
|
1333
|
+
exports.verifyActionAsOfSignedAt = verifyActionAsOfSignedAt;
|
|
1292
1334
|
exports.verifyDelegation = verifyDelegation;
|
|
1293
1335
|
exports.verifyFederationDelegation = verifyFederationDelegation;
|
|
1294
1336
|
exports.verifyFederationRevocation = verifyFederationRevocation;
|