@xshieldai/aegis-guard 0.2.0 → 0.3.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 +18 -12
- package/package.json +4 -3
- package/src/acc-bus.ts +1 -1
- package/src/approval-token.ts +13 -10
- package/src/envelope.ts +1 -1
- package/src/index.ts +14 -1
- package/src/signing.ts +172 -0
package/README.md
CHANGED
|
@@ -1,4 +1,10 @@
|
|
|
1
|
-
# @
|
|
1
|
+
# @xshieldai/aegis-guard
|
|
2
|
+
|
|
3
|
+
> **🔍 Verification status (2026-10-05 IST — v0.3.1)**
|
|
4
|
+
> - **Tests:** ✅ **70/70 passing** in the repository; the npm package ships `src/` only, so run them from a clone ([tests/aegis-guard.test.ts](https://github.com/rocketlang/aegis/blob/master/packages/aegis-guard/tests/aegis-guard.test.ts) — `bun test`)
|
|
5
|
+
> - **Examples:** ✅ runnable quickstart, in the repository (not in the npm package): [examples/quickstart.ts](https://github.com/rocketlang/aegis/blob/master/packages/aegis-guard/examples/quickstart.ts) — `bun run examples/quickstart.ts` shows all 5 Locks in action with receipts
|
|
6
|
+
> - **Live demo:** ⚠️ planned (Tier 3, see [PROOF-STACK.md](https://github.com/rocketlang/aegis/blob/master/PROOF-STACK.md))
|
|
7
|
+
> - **Phase-1 limits:** documented in the v0.2.0 ACC section below
|
|
2
8
|
|
|
3
9
|
AEGIS Guard SDK — reusable approval-token, nonce, idempotency, SENSE, and quality-evidence primitives for AEGIS-governed services.
|
|
4
10
|
|
|
@@ -19,9 +25,9 @@ The Five Locks were proven across 13 batches (62–74) of carbonx-backend. This
|
|
|
19
25
|
## Install
|
|
20
26
|
|
|
21
27
|
```bash
|
|
22
|
-
bun add @
|
|
28
|
+
bun add @xshieldai/aegis-guard
|
|
23
29
|
# or
|
|
24
|
-
npm install @
|
|
30
|
+
npm install @xshieldai/aegis-guard
|
|
25
31
|
```
|
|
26
32
|
|
|
27
33
|
## Usage
|
|
@@ -29,7 +35,7 @@ npm install @rocketlang/aegis-guard
|
|
|
29
35
|
### LOCK_1 + LOCK_2 — decision + identity
|
|
30
36
|
|
|
31
37
|
```typescript
|
|
32
|
-
import { verifyApprovalToken, verifyScopedApprovalToken } from '@
|
|
38
|
+
import { verifyApprovalToken, verifyScopedApprovalToken } from '@xshieldai/aegis-guard';
|
|
33
39
|
|
|
34
40
|
// LOCK_1 — base token verification (service_id + capability + operation)
|
|
35
41
|
const payload = verifyApprovalToken(token, 'my-service', 'settle', 'record_settle');
|
|
@@ -44,7 +50,7 @@ const payload = verifyScopedApprovalToken(
|
|
|
44
50
|
### LOCK_3 — observability (SENSE)
|
|
45
51
|
|
|
46
52
|
```typescript
|
|
47
|
-
import { emitAegisSenseEvent, digestApprovalToken, configureSenseTransport } from '@
|
|
53
|
+
import { emitAegisSenseEvent, digestApprovalToken, configureSenseTransport } from '@xshieldai/aegis-guard';
|
|
48
54
|
|
|
49
55
|
// Wire your logger (default: process.stdout JSON)
|
|
50
56
|
configureSenseTransport((event) => logger.info(event, `SENSE:${event.event_type}`));
|
|
@@ -67,7 +73,7 @@ emitAegisSenseEvent({
|
|
|
67
73
|
### LOCK_4 — rollback guard (idempotency check)
|
|
68
74
|
|
|
69
75
|
```typescript
|
|
70
|
-
import { checkIdempotency, buildIdempotencyFingerprint } from '@
|
|
76
|
+
import { checkIdempotency, buildIdempotencyFingerprint } from '@xshieldai/aegis-guard';
|
|
71
77
|
|
|
72
78
|
const existing = await db.findByExternalRef(args.externalRef);
|
|
73
79
|
const fp = buildIdempotencyFingerprint({ amount: args.amount, vessel_id: args.vesselId });
|
|
@@ -80,7 +86,7 @@ if (isDuplicate && !safeNoOp) throw new Error('payload mismatch on duplicate ext
|
|
|
80
86
|
### LOCK_5 — nonce replay prevention
|
|
81
87
|
|
|
82
88
|
```typescript
|
|
83
|
-
import { verifyAndConsumeNonce } from '@
|
|
89
|
+
import { verifyAndConsumeNonce } from '@xshieldai/aegis-guard';
|
|
84
90
|
|
|
85
91
|
// Requires nonce in payload; throws IrrNoApprovalError on missing or replayed nonce
|
|
86
92
|
await verifyAndConsumeNonce(payload, redisNonceStore);
|
|
@@ -89,7 +95,7 @@ await verifyAndConsumeNonce(payload, redisNonceStore);
|
|
|
89
95
|
### Quality evidence
|
|
90
96
|
|
|
91
97
|
```typescript
|
|
92
|
-
import { buildQualityMaskAtPromotion, meetsHgQualityRequirement } from '@
|
|
98
|
+
import { buildQualityMaskAtPromotion, meetsHgQualityRequirement } from '@xshieldai/aegis-guard';
|
|
93
99
|
|
|
94
100
|
const mask = buildQualityMaskAtPromotion({
|
|
95
101
|
tests_passed: true,
|
|
@@ -105,7 +111,7 @@ const ready = meetsHgQualityRequirement('HG-2B-financial', mask);
|
|
|
105
111
|
The default `defaultNonceStore` is in-memory (single-process only). Multi-instance deployments must provide a Redis-backed store:
|
|
106
112
|
|
|
107
113
|
```typescript
|
|
108
|
-
import { type NonceStore } from '@
|
|
114
|
+
import { type NonceStore } from '@xshieldai/aegis-guard';
|
|
109
115
|
|
|
110
116
|
const redisNonceStore: NonceStore = {
|
|
111
117
|
async consumeNonce(nonce, ttlMs) {
|
|
@@ -139,7 +145,7 @@ v0.2.0 behaves identically to v0.1.0 — no emission, no state, no side effect.
|
|
|
139
145
|
### Wire it in 3 lines
|
|
140
146
|
|
|
141
147
|
```typescript
|
|
142
|
-
import { setEventBus, type EventBus, type AccReceipt } from '@
|
|
148
|
+
import { setEventBus, type EventBus, type AccReceipt } from '@xshieldai/aegis-guard';
|
|
143
149
|
|
|
144
150
|
const myBus: EventBus = {
|
|
145
151
|
emit: (r: AccReceipt) => console.log(`[ACC] ${r.event_type} verdict=${r.verdict} ${r.summary}`),
|
|
@@ -194,12 +200,12 @@ consumers ingest these events without translation.
|
|
|
194
200
|
etc.) are a consumer choice — implement the `EventBus` interface and
|
|
195
201
|
call `setEventBus(yourBus)`.
|
|
196
202
|
|
|
197
|
-
### Use with `@
|
|
203
|
+
### Use with `@xshieldai/aegis-suite`
|
|
198
204
|
|
|
199
205
|
If you installed the meta-package, you can wire all 6 primitives in one call:
|
|
200
206
|
|
|
201
207
|
```typescript
|
|
202
|
-
import { wireAllToBus } from '@
|
|
208
|
+
import { wireAllToBus } from '@xshieldai/aegis-suite'; // available in suite v0.2.0+
|
|
203
209
|
wireAllToBus(); // default: in-memory bus + SQLite writer to ~/.aegis/acc-events.db
|
|
204
210
|
```
|
|
205
211
|
|
package/package.json
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@xshieldai/aegis-guard",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.1",
|
|
4
4
|
"description": "AEGIS Guard SDK — Five Locks primitives (approval-token, nonce, idempotency, SENSE, quality-evidence) + opt-in Agentic Control Center event bus for AEGIS-governed services",
|
|
5
5
|
"license": "AGPL-3.0-only",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"author": "Capt. Anil Sharma <capt.anil.sharma@powerpbox.org>",
|
|
8
|
-
"homepage": "https://github.com/rocketlang/aegis/tree/
|
|
8
|
+
"homepage": "https://github.com/rocketlang/aegis/tree/master/packages/aegis-guard",
|
|
9
9
|
"repository": {
|
|
10
10
|
"type": "git",
|
|
11
11
|
"url": "git+https://github.com/rocketlang/aegis.git",
|
|
@@ -42,7 +42,8 @@
|
|
|
42
42
|
],
|
|
43
43
|
"scripts": {
|
|
44
44
|
"test": "bun test tests/aegis-guard.test.ts",
|
|
45
|
-
"typecheck": "tsc --noEmit"
|
|
45
|
+
"typecheck": "tsc --noEmit",
|
|
46
|
+
"prepublishOnly": "node ../../scripts/publish-guard.mjs"
|
|
46
47
|
},
|
|
47
48
|
"devDependencies": {
|
|
48
49
|
"typescript": "^5.4.0",
|
package/src/acc-bus.ts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// Copyright (c) 2026 Capt. Anil Sharma (rocketlang). All rights reserved.
|
|
3
3
|
// See LICENSE for details.
|
|
4
4
|
//
|
|
5
|
-
// @
|
|
5
|
+
// @xshieldai/aegis-guard — opt-in ACC event bus integration (v0.2.0)
|
|
6
6
|
// @rule:ACC-003 — Opt-in event bus. emit only when setEventBus() called.
|
|
7
7
|
// @rule:ACC-004 — Lightweight OSS receipt shape (strict subset of EE PRAMANA).
|
|
8
8
|
// @rule:ACC-YK-003 — Stateless-primitive contract preserved. No bus = no emit.
|
package/src/approval-token.ts
CHANGED
|
@@ -6,6 +6,7 @@ import { createHash } from 'crypto';
|
|
|
6
6
|
import { IrrNoApprovalError } from './errors.js';
|
|
7
7
|
import { type NonceStore, defaultNonceStore } from './nonce.js';
|
|
8
8
|
import { emitAccReceipt } from './acc-bus.js';
|
|
9
|
+
import { signApprovalJwt, verifyApprovalJwt } from './signing.js';
|
|
9
10
|
|
|
10
11
|
// Token may arrive up to 60s before local clock (NTP tolerance).
|
|
11
12
|
const CLOCK_SKEW_MS = 60_000;
|
|
@@ -28,14 +29,18 @@ export function digestApprovalToken(token: string): string {
|
|
|
28
29
|
return createHash('sha256').update(token).digest('hex').slice(0, 24);
|
|
29
30
|
}
|
|
30
31
|
|
|
31
|
-
// @rule:AEG-E-016 —
|
|
32
|
-
//
|
|
32
|
+
// @rule:AEG-E-016 @rule:KGT-002 — mint an AEGIS-signed EdDSA JWT (KGT-T1.1).
|
|
33
|
+
// Signing requires the AEGIS private key (~/.aegis/approval-signing.key) — present on
|
|
34
|
+
// the AEGIS box, auto-provisioned by the :4850 dashboard. Throws where the key is absent:
|
|
35
|
+
// a service that cannot sign cannot mint, by design.
|
|
33
36
|
export function mintApprovalToken(payload: ApprovalTokenPayload): string {
|
|
34
|
-
return
|
|
37
|
+
return signApprovalJwt(payload);
|
|
35
38
|
}
|
|
36
39
|
|
|
37
40
|
// @rule:AEG-E-016 — token must match service_id + capability + operation exactly.
|
|
38
|
-
//
|
|
41
|
+
// @rule:KGT-002 — signature verified FIRST, fail-closed: unsigned/tampered/alg:none/
|
|
42
|
+
// wrong-key tokens are rejected before any scope check. Legacy base64url(JSON) tokens
|
|
43
|
+
// (pre-KGT-T1.1, forgeable) are rejected outright.
|
|
39
44
|
export function verifyApprovalToken(
|
|
40
45
|
token: string,
|
|
41
46
|
expectedServiceId: string,
|
|
@@ -45,13 +50,11 @@ export function verifyApprovalToken(
|
|
|
45
50
|
// @rule:ACC-003 @rule:ACC-004 — emit ACC receipt on success OR failure
|
|
46
51
|
const scope = `${expectedServiceId}/${expectedCapability}/${expectedOperation}`;
|
|
47
52
|
try {
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
payload = JSON.parse(decoded) as ApprovalTokenPayload;
|
|
52
|
-
} catch {
|
|
53
|
-
throw new IrrNoApprovalError(expectedCapability, 'token could not be decoded');
|
|
53
|
+
const jwt = verifyApprovalJwt(token);
|
|
54
|
+
if (!jwt.ok) {
|
|
55
|
+
throw new IrrNoApprovalError(expectedCapability, `KGT-002: ${jwt.reason}`);
|
|
54
56
|
}
|
|
57
|
+
const payload = jwt.payload as ApprovalTokenPayload;
|
|
55
58
|
|
|
56
59
|
if (payload.service_id !== expectedServiceId) {
|
|
57
60
|
throw new IrrNoApprovalError(
|
package/src/envelope.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// @
|
|
1
|
+
// @xshieldai/aegis-guard — Agent Session Envelope helpers (ASE-T020)
|
|
2
2
|
//
|
|
3
3
|
// issueEnvelope — POST /api/v1/aegis/session — proxy-native agent frameworks use this at startup
|
|
4
4
|
// verifyEnvelope — GET /api/v1/aegis/sessions/:id/audit — check seal integrity + drift
|
package/src/index.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// @
|
|
1
|
+
// @xshieldai/aegis-guard — AEGIS Guard SDK public API
|
|
2
2
|
// Five Locks proved in carbonx-backend (batches 62-74). Batch 93 makes them reusable.
|
|
3
3
|
// v0.2.0 adds opt-in Agentic Control Center (ACC) event bus integration.
|
|
4
4
|
|
|
@@ -48,6 +48,19 @@ export {
|
|
|
48
48
|
verifyScopedApprovalToken,
|
|
49
49
|
} from './approval-token.js';
|
|
50
50
|
|
|
51
|
+
// @rule:KGT-002 — KGT-T1.1 (v0.3.0): approval tokens are AEGIS-signed EdDSA JWTs.
|
|
52
|
+
// ensureSigningKeypair is called by the AEGIS dashboard at boot (minting authority);
|
|
53
|
+
// verifier-only boxes pin the public key via ~/.aegis/approval-signing.pub or
|
|
54
|
+
// AEGIS_APPROVAL_PUBKEY_PEM (served at :4850 /api/v2/enforcement/signing-key).
|
|
55
|
+
export {
|
|
56
|
+
APPROVAL_JWT_ALG,
|
|
57
|
+
type JwtVerifyResult,
|
|
58
|
+
ensureSigningKeypair,
|
|
59
|
+
getPublicKeyPem,
|
|
60
|
+
signApprovalJwt,
|
|
61
|
+
verifyApprovalJwt,
|
|
62
|
+
} from './signing.js';
|
|
63
|
+
|
|
51
64
|
export {
|
|
52
65
|
type IssueEnvelopeParams,
|
|
53
66
|
type EnvelopeIssueResult,
|
package/src/signing.ts
ADDED
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
// @rule:AEG-E-016 — approval tokens are AEGIS-signed; an unsigned approval is a forgery
|
|
2
|
+
// @rule:KGT-002 — fail-closed: no key material → verification DENIES, never falls open
|
|
3
|
+
//
|
|
4
|
+
// KGT-T1.1 — Ed25519 JWT signing for approval tokens.
|
|
5
|
+
// Before this module, mintApprovalToken emitted base64url(JSON): any client could
|
|
6
|
+
// fabricate an "approval". Now AEGIS (:4850, the minting authority) holds the private
|
|
7
|
+
// key; every verifier needs only the public key. Same key-management pattern as
|
|
8
|
+
// kernel/mudrika.ts: persistent per-machine key material under ~/.aegis, mode 600,
|
|
9
|
+
// generated once. Zero-dep — node:crypto ed25519 only.
|
|
10
|
+
|
|
11
|
+
import {
|
|
12
|
+
generateKeyPairSync,
|
|
13
|
+
createPrivateKey,
|
|
14
|
+
createPublicKey,
|
|
15
|
+
sign as edSign,
|
|
16
|
+
verify as edVerify,
|
|
17
|
+
type KeyObject,
|
|
18
|
+
} from 'crypto';
|
|
19
|
+
import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'fs';
|
|
20
|
+
import { join } from 'path';
|
|
21
|
+
import { homedir } from 'os';
|
|
22
|
+
|
|
23
|
+
export const APPROVAL_JWT_ALG = 'EdDSA';
|
|
24
|
+
|
|
25
|
+
const KEY_FILE = 'approval-signing.key'; // PKCS8 PEM (private) — mode 600
|
|
26
|
+
const PUB_FILE = 'approval-signing.pub'; // SPKI PEM (public) — mode 644
|
|
27
|
+
|
|
28
|
+
function aegisDir(): string {
|
|
29
|
+
return process.env.AEGIS_DIR ?? join(process.env.HOME ?? homedir(), '.aegis');
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Cache keyed by resolved dir so tests (which point AEGIS_DIR at a temp dir) and
|
|
33
|
+
// long-lived services both get one disk read, not one per verify.
|
|
34
|
+
let cache: { dir: string; priv: KeyObject | null; pub: KeyObject | null } | null = null;
|
|
35
|
+
|
|
36
|
+
function loadKeys(): { priv: KeyObject | null; pub: KeyObject | null } {
|
|
37
|
+
const dir = aegisDir();
|
|
38
|
+
if (cache && cache.dir === dir) return cache;
|
|
39
|
+
|
|
40
|
+
let priv: KeyObject | null = null;
|
|
41
|
+
let pub: KeyObject | null = null;
|
|
42
|
+
|
|
43
|
+
const keyPath = join(dir, KEY_FILE);
|
|
44
|
+
if (existsSync(keyPath)) {
|
|
45
|
+
try {
|
|
46
|
+
priv = createPrivateKey(readFileSync(keyPath, 'utf8'));
|
|
47
|
+
pub = createPublicKey(priv);
|
|
48
|
+
} catch { priv = null; pub = null; }
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Verifier-only deployments: public key via env PEM or the .pub file.
|
|
52
|
+
if (!pub) {
|
|
53
|
+
const envPem = process.env.AEGIS_APPROVAL_PUBKEY_PEM;
|
|
54
|
+
const pubPath = join(dir, PUB_FILE);
|
|
55
|
+
try {
|
|
56
|
+
if (envPem) pub = createPublicKey(envPem);
|
|
57
|
+
else if (existsSync(pubPath)) pub = createPublicKey(readFileSync(pubPath, 'utf8'));
|
|
58
|
+
} catch { pub = null; }
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
cache = { dir, priv, pub };
|
|
62
|
+
return cache;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Generate the per-machine keypair if absent. Called lazily by the mint path and by
|
|
66
|
+
// the AEGIS dashboard at boot (the authority guarantees the key exists on its box).
|
|
67
|
+
export function ensureSigningKeypair(): { publicKeyPem: string } {
|
|
68
|
+
const dir = aegisDir();
|
|
69
|
+
const keyPath = join(dir, KEY_FILE);
|
|
70
|
+
const pubPath = join(dir, PUB_FILE);
|
|
71
|
+
|
|
72
|
+
if (!existsSync(keyPath)) {
|
|
73
|
+
const { privateKey, publicKey } = generateKeyPairSync('ed25519');
|
|
74
|
+
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
75
|
+
writeFileSync(keyPath, privateKey.export({ type: 'pkcs8', format: 'pem' }), { mode: 0o600 });
|
|
76
|
+
writeFileSync(pubPath, publicKey.export({ type: 'spki', format: 'pem' }), { mode: 0o644 });
|
|
77
|
+
cache = null;
|
|
78
|
+
} else if (!existsSync(pubPath)) {
|
|
79
|
+
// key exists but pub was lost — re-derive, never regenerate (verifiers may hold the pub)
|
|
80
|
+
const priv = createPrivateKey(readFileSync(keyPath, 'utf8'));
|
|
81
|
+
writeFileSync(pubPath, createPublicKey(priv).export({ type: 'spki', format: 'pem' }), { mode: 0o644 });
|
|
82
|
+
cache = null;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const { pub } = loadKeys();
|
|
86
|
+
return { publicKeyPem: pub!.export({ type: 'spki', format: 'pem' }).toString() };
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export function getPublicKeyPem(): string | null {
|
|
90
|
+
const { pub } = loadKeys();
|
|
91
|
+
return pub ? pub.export({ type: 'spki', format: 'pem' }).toString() : null;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function b64url(buf: Buffer | string): string {
|
|
95
|
+
return Buffer.from(buf).toString('base64url');
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Sign a payload as a compact EdDSA JWT: b64url(header).b64url(payload).b64url(sig).
|
|
99
|
+
// Throws if no private key — only the AEGIS box (or a box it provisioned) can mint.
|
|
100
|
+
export function signApprovalJwt(payload: Record<string, unknown>): string {
|
|
101
|
+
ensureSigningKeypair();
|
|
102
|
+
const { priv } = loadKeys();
|
|
103
|
+
if (!priv) {
|
|
104
|
+
throw new Error(
|
|
105
|
+
'KGT-002: no AEGIS approval signing key — cannot mint (fail-closed). ' +
|
|
106
|
+
`Expected ${join(aegisDir(), KEY_FILE)}; the AEGIS dashboard (:4850) provisions it at boot.`,
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
const header = b64url(JSON.stringify({ alg: APPROVAL_JWT_ALG, typ: 'JWT' }));
|
|
110
|
+
const body = b64url(JSON.stringify(payload));
|
|
111
|
+
const sig = edSign(null, Buffer.from(`${header}.${body}`), priv);
|
|
112
|
+
return `${header}.${body}.${b64url(sig)}`;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export type JwtVerifyResult =
|
|
116
|
+
| { ok: true; payload: Record<string, unknown> }
|
|
117
|
+
| { ok: false; reason: string };
|
|
118
|
+
|
|
119
|
+
// Verify signature + structure ONLY (scope/expiry stay in approval-token.ts).
|
|
120
|
+
// Fail-closed on every path: malformed, wrong alg (alg:none forgery), bad signature,
|
|
121
|
+
// or no public key available at all.
|
|
122
|
+
export function verifyApprovalJwt(token: string): JwtVerifyResult {
|
|
123
|
+
const parts = token.split('.');
|
|
124
|
+
if (parts.length !== 3) {
|
|
125
|
+
return {
|
|
126
|
+
ok: false,
|
|
127
|
+
reason: parts.length === 1
|
|
128
|
+
? 'unsigned legacy token rejected — approval tokens are AEGIS-signed JWTs (KGT-T1.1)'
|
|
129
|
+
: 'malformed token: expected 3 JWT segments',
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
const [header, body, sig] = parts;
|
|
133
|
+
|
|
134
|
+
let alg: unknown;
|
|
135
|
+
try {
|
|
136
|
+
alg = (JSON.parse(Buffer.from(header, 'base64url').toString('utf8')) as { alg?: unknown }).alg;
|
|
137
|
+
} catch {
|
|
138
|
+
return { ok: false, reason: 'malformed token: header is not base64url JSON' };
|
|
139
|
+
}
|
|
140
|
+
if (alg !== APPROVAL_JWT_ALG) {
|
|
141
|
+
return { ok: false, reason: `alg '${String(alg)}' rejected — only ${APPROVAL_JWT_ALG} approvals are trusted` };
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const { pub } = loadKeys();
|
|
145
|
+
if (!pub) {
|
|
146
|
+
return {
|
|
147
|
+
ok: false,
|
|
148
|
+
reason: 'KGT-002: no AEGIS public key available — verification fails closed. ' +
|
|
149
|
+
'Provision ~/.aegis/approval-signing.pub or set AEGIS_APPROVAL_PUBKEY_PEM ' +
|
|
150
|
+
'(GET :4850/api/v2/enforcement/signing-key).',
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
let valid = false;
|
|
155
|
+
try {
|
|
156
|
+
valid = edVerify(null, Buffer.from(`${header}.${body}`), pub, Buffer.from(sig, 'base64url'));
|
|
157
|
+
} catch {
|
|
158
|
+
valid = false;
|
|
159
|
+
}
|
|
160
|
+
if (!valid) return { ok: false, reason: 'signature invalid — token was not minted by AEGIS' };
|
|
161
|
+
|
|
162
|
+
try {
|
|
163
|
+
return { ok: true, payload: JSON.parse(Buffer.from(body, 'base64url').toString('utf8')) };
|
|
164
|
+
} catch {
|
|
165
|
+
return { ok: false, reason: 'malformed token: payload is not base64url JSON' };
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// test-only: drop the key cache so a test can swap AEGIS_DIR mid-suite
|
|
170
|
+
export function __resetSigningCache(): void {
|
|
171
|
+
cache = null;
|
|
172
|
+
}
|