@learncard/lca-api-service 1.3.2 → 1.5.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/.env.example +99 -2
- package/AGENTS.md +155 -44
- package/CHANGELOG.md +84 -0
- package/bootstrapLambda.test.ts +76 -0
- package/compose.env +5 -0
- package/config/config.dev.json +23 -0
- package/config/config.example.json +22 -0
- package/config/config.json +1 -0
- package/config/config.production.json +12 -0
- package/config/config.scouts.dev.json +11 -0
- package/config/config.scouts.production.json +11 -0
- package/dist/accesslayer/signing-authority/index.d.ts +8 -2
- package/dist/app.d.ts +361 -8
- package/dist/cache/challenges.d.ts +2 -0
- package/dist/cache/getDel.d.ts +2 -0
- package/dist/cache/getDel.test.d.ts +1 -0
- package/dist/cache/login-tickets.d.ts +12 -0
- package/dist/cache/recoverySessions.d.ts +42 -0
- package/dist/config/applyDockerStageConfig.d.ts +1 -0
- package/dist/config/cacheEnvironment.d.ts +16 -0
- package/dist/config/environment.d.ts +253 -2
- package/dist/config/environment.test.d.ts +1 -0
- package/dist/config/escrowBundleMode.test.d.ts +1 -0
- package/dist/config/escrowDeployment.test.d.ts +1 -0
- package/dist/config/oidcEnvironment.d.ts +74 -0
- package/dist/config/serverlessFunctionEnv.test.d.ts +1 -0
- package/dist/config/stageConfig.d.ts +62 -0
- package/dist/config/stageConfig.test.d.ts +1 -0
- package/dist/didkit_wasm_bg.wasm +0 -0
- package/dist/docker-entry.d.ts +1 -1
- package/dist/helpers/auth.helpers.d.ts +1 -1
- package/dist/helpers/escrowCancelLink.d.ts +7 -0
- package/dist/helpers/escrowCancelLink.test.d.ts +1 -0
- package/dist/helpers/escrowCancelToken.d.ts +4 -0
- package/dist/helpers/escrowCancelToken.test.d.ts +1 -0
- package/dist/helpers/keycloak.helpers.d.ts +23 -0
- package/dist/helpers/learnCard.signingAuthority.test.d.ts +1 -0
- package/dist/helpers/login-ticket.helpers.d.ts +31 -0
- package/dist/helpers/login-ticket.helpers.test.d.ts +1 -0
- package/dist/helpers/oidc.helpers.d.ts +47 -0
- package/dist/helpers/rate-limit.helpers.d.ts +6 -0
- package/dist/helpers/rate-limit.helpers.test.d.ts +1 -0
- package/dist/helpers/rateLimit.helpers.d.ts +4 -0
- package/dist/helpers/seedEncryption.helpers.d.ts +23 -0
- package/dist/helpers/seedEncryption.helpers.test.d.ts +1 -0
- package/dist/helpers/social-token.helpers.d.ts +19 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +2 -2
- package/dist/jobs/escrowBlobRewrap.d.ts +9 -0
- package/dist/jobs/escrowHoldReminders.d.ts +12 -0
- package/dist/migrations/signingAuthoritySeeds.d.ts +13 -0
- package/dist/migrations/signingAuthoritySeedsCli.test.d.ts +1 -0
- package/dist/models/AuthSubject.d.ts +20 -0
- package/dist/models/EscrowHold.d.ts +153 -0
- package/dist/models/Notification.d.ts +5 -0
- package/dist/models/SigningAuthority.d.ts +7 -1
- package/dist/models/UserKey.d.ts +326 -3
- package/dist/models/UserKey.escrowPin.test.d.ts +1 -0
- package/dist/models/index.d.ts +2 -0
- package/dist/oidc.d.ts +5 -0
- package/dist/routes/auth.d.ts +31 -0
- package/dist/routes/escrow.d.ts +164 -0
- package/dist/routes/index.d.ts +6 -0
- package/dist/routes/keys.d.ts +182 -7
- package/dist/routes/notifications.d.ts +5 -0
- package/dist/routes/qr-login.d.ts +1 -1
- package/dist/routes/signing-authority.d.ts +14 -0
- package/dist/services/escrow-enclave/blobIdentity.d.ts +10 -0
- package/dist/services/escrow-enclave/blobIdentity.test.d.ts +1 -0
- package/dist/services/escrow-enclave/cancellation.d.ts +3 -0
- package/dist/services/escrow-enclave/enclaveTrustAnchors.d.ts +1 -0
- package/dist/services/escrow-enclave/index.d.ts +25 -0
- package/dist/services/escrow-enclave/notifications.d.ts +24 -0
- package/dist/services/escrow-enclave/notifications.test.d.ts +1 -0
- package/dist/services/escrow-enclave/production.test.d.ts +1 -0
- package/dist/services/escrow-enclave/remoteEnclave.contract.test.d.ts +1 -0
- package/dist/services/escrow-enclave/remoteEnclave.d.ts +20 -0
- package/dist/services/escrow-enclave/remoteEnclave.test.d.ts +1 -0
- package/dist/services/escrow-enclave/remoteEnclave.tls.test.d.ts +1 -0
- package/dist/services/escrow-enclave/softwareEnclave.d.ts +27 -0
- package/dist/services/escrow-enclave/softwareEnclave.test.d.ts +1 -0
- package/dist/services/escrow-enclave/types.d.ts +111 -0
- package/dist/types/notifications.d.ts +5 -0
- package/dist/types/seed-encryption.d.ts +31 -0
- package/dist/types/seed-migration.d.ts +49 -0
- package/esbuildPlugins.cjs +1 -0
- package/lambda.ts +27 -112
- package/lambdaApp.ts +146 -0
- package/oidcLambda.test.ts +38 -0
- package/oidcLambda.ts +18 -0
- package/oidcLambdaApp.ts +11 -0
- package/package.json +30 -14
- package/scripts/README.md +88 -0
- package/scripts/check-seed-encryption-infrastructure.mjs +139 -0
- package/scripts/check-signing-authority-redis.mjs +93 -0
- package/scripts/escrow-blob-mode-report.test.ts +227 -0
- package/scripts/escrow-blob-mode-report.ts +465 -0
- package/scripts/keycloak-admin.ts +108 -0
- package/scripts/keycloak-provider-links.ts +147 -0
- package/scripts/migrate-signing-authority-seeds-local.sh +51 -0
- package/scripts/migrate-signing-authority-seeds.mjs +105 -0
- package/scripts/print-seed-rollout-flags.ts +21 -0
- package/scripts/provision-keycloak-users.ts +419 -0
- package/seedMigrationApp.test.ts +145 -0
- package/seedMigrationApp.ts +50 -0
- package/seedMigrationLambda.test.ts +40 -0
- package/seedMigrationLambda.ts +19 -0
- package/serverless-local.yml +28 -1
- package/serverless.function-env.cjs +58 -0
- package/serverless.yml +294 -26
- package/src/accesslayer/notifications/index.ts +9 -2
- package/src/accesslayer/signing-authority/create.ts +11 -7
- package/src/accesslayer/signing-authority/index.ts +4 -1
- package/src/app.ts +4 -0
- package/src/cache/challenges.ts +12 -2
- package/src/cache/getDel.test.ts +34 -0
- package/src/cache/getDel.ts +32 -0
- package/src/cache/in-memory-lru.ts +1 -0
- package/src/cache/index.ts +1 -1
- package/src/cache/login-tickets.ts +26 -0
- package/src/cache/recoverySessions.ts +252 -0
- package/src/config/applyDockerStageConfig.ts +6 -0
- package/src/config/cacheEnvironment.ts +34 -0
- package/src/config/environment.test.ts +159 -0
- package/src/config/environment.ts +164 -0
- package/src/config/escrowBundleMode.test.ts +52 -0
- package/src/config/escrowDeployment.test.ts +114 -0
- package/src/config/oidcEnvironment.ts +59 -0
- package/src/config/serverlessFunctionEnv.test.ts +98 -0
- package/src/config/stageConfig.test.ts +26 -0
- package/src/config/stageConfig.ts +24 -0
- package/src/docker-entry.ts +13 -0
- package/src/helpers/auth.helpers.ts +68 -30
- package/src/helpers/escrowCancelLink.test.ts +60 -0
- package/src/helpers/escrowCancelLink.ts +38 -0
- package/src/helpers/escrowCancelToken.test.ts +105 -0
- package/src/helpers/escrowCancelToken.ts +47 -0
- package/src/helpers/keycloak.helpers.ts +120 -0
- package/src/helpers/learnCard.helpers.test.ts +11 -1
- package/src/helpers/learnCard.helpers.ts +57 -45
- package/src/helpers/learnCard.signingAuthority.test.ts +194 -0
- package/src/helpers/login-ticket.helpers.test.ts +45 -0
- package/src/helpers/login-ticket.helpers.ts +91 -0
- package/src/helpers/oidc.helpers.ts +354 -0
- package/src/helpers/rate-limit.helpers.test.ts +61 -0
- package/src/helpers/rate-limit.helpers.ts +73 -0
- package/src/helpers/rateLimit.helpers.ts +112 -0
- package/src/helpers/seedEncryption.helpers.test.ts +427 -0
- package/src/helpers/seedEncryption.helpers.ts +372 -0
- package/src/helpers/signingAuthority.helpers.ts +4 -1
- package/src/helpers/social-token.helpers.ts +144 -0
- package/src/index.ts +1 -0
- package/src/jobs/escrowBlobRewrap.ts +121 -0
- package/src/jobs/escrowHoldReminders.ts +177 -0
- package/src/migrations/signingAuthoritySeeds.ts +430 -0
- package/src/migrations/signingAuthoritySeedsCli.test.ts +70 -0
- package/src/models/AuthSubject.ts +147 -0
- package/src/models/EscrowHold.ts +541 -0
- package/src/models/SigningAuthority.ts +14 -3
- package/src/models/UserKey.escrowPin.test.ts +49 -0
- package/src/models/UserKey.ts +1109 -40
- package/src/models/index.ts +2 -0
- package/src/models/pruneOrphanedRecoveryMethods.ts +3 -1
- package/src/oidc.ts +371 -0
- package/src/routes/auth.ts +173 -0
- package/src/routes/credentials.ts +8 -0
- package/src/routes/escrow.ts +1073 -0
- package/src/routes/firebase.ts +90 -48
- package/src/routes/index.ts +98 -5
- package/src/routes/keys.ts +1022 -174
- package/src/routes/qr-login.ts +51 -53
- package/src/routes/signing-authority.ts +39 -1
- package/src/services/delivery/adapters/postmark.adapter.ts +48 -1
- package/src/services/escrow-enclave/README.md +122 -0
- package/src/services/escrow-enclave/__fixtures__/tls/pinned-ca.pem +11 -0
- package/src/services/escrow-enclave/__fixtures__/tls/pinned-leaf.key +5 -0
- package/src/services/escrow-enclave/__fixtures__/tls/pinned-leaf.pem +12 -0
- package/src/services/escrow-enclave/__fixtures__/tls/rogue-ca.pem +11 -0
- package/src/services/escrow-enclave/__fixtures__/tls/rogue-leaf.key +5 -0
- package/src/services/escrow-enclave/__fixtures__/tls/rogue-leaf.pem +12 -0
- package/src/services/escrow-enclave/blobIdentity.test.ts +42 -0
- package/src/services/escrow-enclave/blobIdentity.ts +55 -0
- package/src/services/escrow-enclave/cancellation.ts +59 -0
- package/src/services/escrow-enclave/enclaveTrustAnchors.ts +34 -0
- package/src/services/escrow-enclave/index.ts +174 -0
- package/src/services/escrow-enclave/notifications.test.ts +377 -0
- package/src/services/escrow-enclave/notifications.ts +348 -0
- package/src/services/escrow-enclave/production.test.ts +34 -0
- package/src/services/escrow-enclave/remoteEnclave.contract.test.ts +344 -0
- package/src/services/escrow-enclave/remoteEnclave.test.ts +490 -0
- package/src/services/escrow-enclave/remoteEnclave.tls.test.ts +62 -0
- package/src/services/escrow-enclave/remoteEnclave.ts +263 -0
- package/src/services/escrow-enclave/softwareEnclave.test.ts +450 -0
- package/src/services/escrow-enclave/softwareEnclave.ts +254 -0
- package/src/services/escrow-enclave/types.ts +122 -0
- package/src/types/seed-encryption.ts +51 -0
- package/src/types/seed-migration.ts +61 -0
- package/test/auth-tickets.spec.ts +480 -0
- package/test/credentials.spec.ts +2 -0
- package/test/helpers/keycloak-broker.ts +181 -0
- package/test/helpers/live-mongo.ts +9 -0
- package/test/jobs/escrowBlobRewrap.spec.ts +262 -0
- package/test/jobs/escrowHoldReminders.spec.ts +319 -0
- package/test/keycloak-admin.spec.ts +58 -0
- package/test/keycloak-broker-roundtrip.integration.spec.ts +40 -0
- package/test/keycloak-migration.integration.spec.ts +266 -0
- package/test/keycloak-provider-links.spec.ts +201 -0
- package/test/keycloak-social-broker.integration.spec.ts +228 -0
- package/test/keycloak-verify.integration.spec.ts +100 -0
- package/test/keycloak-verify.spec.ts +307 -0
- package/test/keys-escrow.spec.ts +2328 -0
- package/test/keys-identity-rebind.spec.ts +456 -0
- package/test/keys-lifecycle-invariants.spec.ts +785 -0
- package/test/keys-recovery-confirmation.spec.ts +586 -0
- package/test/keys-recovery-rate-limit.spec.ts +123 -0
- package/test/keys-secret-leakage.spec.ts +265 -0
- package/test/models/authSubjectIndexes.spec.ts +132 -0
- package/test/models/escrow.spec.ts +572 -0
- package/test/notifications.spec.ts +4 -0
- package/test/oidc-e2e/broker.spec.ts +116 -0
- package/test/oidc-e2e/compose.yaml +33 -0
- package/test/oidc-e2e/fixtures.ts +245 -0
- package/test/oidc-e2e/lock.test.mjs +73 -0
- package/test/oidc-e2e/playwright.config.ts +21 -0
- package/test/oidc-e2e/run.mjs +312 -0
- package/test/oidc-e2e/tsconfig.json +10 -0
- package/test/oidc-e2e/types.ts +21 -0
- package/test/oidc.integration.spec.ts +54 -0
- package/test/oidc.spec.ts +697 -0
- package/test/oidcLambdaApp.boot.spec.ts +105 -0
- package/test/provision-keycloak-users.spec.ts +299 -0
- package/test/signing-authority-http.spec.ts +161 -0
- package/test/signing-authority-seed-migration.spec.ts +414 -0
- package/test/signing-authority.spec.ts +90 -0
- package/tsconfig.build.json +1 -1
- package/tsconfig.json +1 -0
- package/vitest.config.ts +24 -1
- package/vitest.integration.config.ts +25 -3
- package/dist/cache/signing-authority.d.ts +0 -6
- package/src/cache/signing-authority.ts +0 -37
|
@@ -0,0 +1,450 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import {
|
|
3
|
+
encryptEscrowBlob,
|
|
4
|
+
generateEscrowKeyPair,
|
|
5
|
+
openEscrowRelease,
|
|
6
|
+
} from '@learncard/sss-key-manager';
|
|
7
|
+
import { SoftwareEnclave } from './softwareEnclave';
|
|
8
|
+
import {
|
|
9
|
+
EscrowBlobError,
|
|
10
|
+
EscrowPolicyError,
|
|
11
|
+
EscrowPinMismatchError,
|
|
12
|
+
type EscrowHoldRecord,
|
|
13
|
+
} from './types';
|
|
14
|
+
|
|
15
|
+
describe('software enclave', () => {
|
|
16
|
+
it.each([
|
|
17
|
+
'success',
|
|
18
|
+
'source DID',
|
|
19
|
+
'target DID',
|
|
20
|
+
'expected DID',
|
|
21
|
+
'equal version',
|
|
22
|
+
'lower version',
|
|
23
|
+
'source version',
|
|
24
|
+
'target version',
|
|
25
|
+
'missing PIN',
|
|
26
|
+
'target PIN',
|
|
27
|
+
])('carries the sealed verifier with strict binding: %s', async scenario => {
|
|
28
|
+
const keys = await generateEscrowKeyPair();
|
|
29
|
+
const client = await generateEscrowKeyPair();
|
|
30
|
+
const enclave = new SoftwareEnclave({
|
|
31
|
+
privateKeys: { test: keys.privateKey },
|
|
32
|
+
activeKeyId: 'test',
|
|
33
|
+
});
|
|
34
|
+
const did = 'did:key:test';
|
|
35
|
+
const pinVerifier = 'ab'.repeat(32);
|
|
36
|
+
const targetVersion =
|
|
37
|
+
scenario === 'equal version' ? 2 : scenario === 'lower version' ? 1 : 3;
|
|
38
|
+
const sourceEnvelope = await encryptEscrowBlob(
|
|
39
|
+
{
|
|
40
|
+
recoveryShare: 'ab'.repeat(33),
|
|
41
|
+
did: scenario === 'source DID' ? 'did:key:other' : did,
|
|
42
|
+
shareVersion: 2,
|
|
43
|
+
...(scenario === 'missing PIN' ? {} : { pinVerifier }),
|
|
44
|
+
},
|
|
45
|
+
keys.publicKey,
|
|
46
|
+
'test'
|
|
47
|
+
);
|
|
48
|
+
const target = {
|
|
49
|
+
recoveryShare: 'cd'.repeat(33),
|
|
50
|
+
did: scenario === 'target DID' ? 'did:key:other' : did,
|
|
51
|
+
shareVersion: targetVersion,
|
|
52
|
+
...(scenario === 'target PIN' ? { pinVerifier } : {}),
|
|
53
|
+
};
|
|
54
|
+
const targetEnvelope = await encryptEscrowBlob(target, keys.publicKey, 'test');
|
|
55
|
+
const result = enclave.carryPinVerifier({
|
|
56
|
+
sourceEnvelope,
|
|
57
|
+
targetEnvelope,
|
|
58
|
+
expectedDid: scenario === 'expected DID' ? 'did:key:other' : did,
|
|
59
|
+
sourceShareVersion: scenario === 'source version' ? 1 : 2,
|
|
60
|
+
targetShareVersion: scenario === 'target version' ? 4 : targetVersion,
|
|
61
|
+
targetEnrollmentEpoch: 2,
|
|
62
|
+
// Unused in software mode (see softwareEnclave.ts); any value is fine here.
|
|
63
|
+
sourceEnrollmentEpoch: 1,
|
|
64
|
+
});
|
|
65
|
+
if (scenario !== 'success') {
|
|
66
|
+
await expect(result).rejects.toBeInstanceOf(EscrowBlobError);
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
const { envelope } = await result;
|
|
70
|
+
expect(envelope.ciphertext).not.toBe(targetEnvelope.ciphertext);
|
|
71
|
+
expect(
|
|
72
|
+
await enclave.verifyEscrowBlob({ envelope, expectedDid: did, expectedShareVersion: 3 })
|
|
73
|
+
).toEqual({ ok: true, hasPin: true });
|
|
74
|
+
const { sealed } = await enclave.releaseEscrow({
|
|
75
|
+
envelope,
|
|
76
|
+
expectedDid: did,
|
|
77
|
+
pinProof: pinVerifier,
|
|
78
|
+
clientEphemeralPublicKey: client.publicKey,
|
|
79
|
+
hold: {
|
|
80
|
+
hold: {
|
|
81
|
+
holdId: 'carry',
|
|
82
|
+
did,
|
|
83
|
+
shareVersion: 3,
|
|
84
|
+
blobHash: 'ab'.repeat(32),
|
|
85
|
+
enrollmentEpoch: 1,
|
|
86
|
+
releasePolicy: 'pin',
|
|
87
|
+
clientEphemeralPublicKey: client.publicKey,
|
|
88
|
+
createdLo: 0,
|
|
89
|
+
createdHi: 0,
|
|
90
|
+
policyVersion: 1,
|
|
91
|
+
signature: 'test-signature',
|
|
92
|
+
},
|
|
93
|
+
holdDurationMs: 0,
|
|
94
|
+
ledgerSeq: 0,
|
|
95
|
+
},
|
|
96
|
+
});
|
|
97
|
+
expect(await openEscrowRelease(sealed, client.privateKey)).toEqual({
|
|
98
|
+
...target,
|
|
99
|
+
version: 1,
|
|
100
|
+
holdId: 'carry',
|
|
101
|
+
});
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it.each(['success', 'current key', 'unknown key', 'wrong DID', 'wrong version'])(
|
|
105
|
+
'rewraps an escrow blob sealed under a previous key (P9.3): %s',
|
|
106
|
+
async scenario => {
|
|
107
|
+
const current = await generateEscrowKeyPair();
|
|
108
|
+
const previous = await generateEscrowKeyPair();
|
|
109
|
+
const client = await generateEscrowKeyPair();
|
|
110
|
+
const enclave = new SoftwareEnclave({
|
|
111
|
+
privateKeys: { current: current.privateKey, previous: previous.privateKey },
|
|
112
|
+
activeKeyId: 'current',
|
|
113
|
+
});
|
|
114
|
+
const did = 'did:key:test';
|
|
115
|
+
const pinVerifier = 'ab'.repeat(32);
|
|
116
|
+
const target = { recoveryShare: 'ef'.repeat(33), did, shareVersion: 1 };
|
|
117
|
+
const sourceKeys = scenario === 'current key' ? current : previous;
|
|
118
|
+
const sourceKeyId = scenario === 'current key' ? 'current' : 'previous';
|
|
119
|
+
const sourceEnvelope = await encryptEscrowBlob(
|
|
120
|
+
{ ...target, pinVerifier },
|
|
121
|
+
sourceKeys.publicKey,
|
|
122
|
+
sourceKeyId
|
|
123
|
+
);
|
|
124
|
+
const unknown =
|
|
125
|
+
scenario === 'unknown key'
|
|
126
|
+
? await encryptEscrowBlob(
|
|
127
|
+
target,
|
|
128
|
+
(await generateEscrowKeyPair()).publicKey,
|
|
129
|
+
'retired-and-removed'
|
|
130
|
+
)
|
|
131
|
+
: undefined;
|
|
132
|
+
const result = enclave.rewrapEscrowBlob({
|
|
133
|
+
envelope: unknown ?? sourceEnvelope,
|
|
134
|
+
expectedDid: scenario === 'wrong DID' ? 'did:key:other' : did,
|
|
135
|
+
expectedShareVersion: scenario === 'wrong version' ? 2 : 1,
|
|
136
|
+
sourceEnrollmentEpoch: 1,
|
|
137
|
+
});
|
|
138
|
+
if (scenario !== 'success') {
|
|
139
|
+
await expect(result).rejects.toBeInstanceOf(EscrowBlobError);
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
const { envelope } = await result;
|
|
143
|
+
expect(envelope.ciphertext).not.toBe(sourceEnvelope.ciphertext);
|
|
144
|
+
expect(
|
|
145
|
+
await enclave.verifyEscrowBlob({
|
|
146
|
+
envelope,
|
|
147
|
+
expectedDid: did,
|
|
148
|
+
expectedShareVersion: 1,
|
|
149
|
+
})
|
|
150
|
+
).toEqual({ ok: true, hasPin: true });
|
|
151
|
+
const { sealed } = await enclave.releaseEscrow({
|
|
152
|
+
envelope,
|
|
153
|
+
expectedDid: did,
|
|
154
|
+
pinProof: pinVerifier,
|
|
155
|
+
clientEphemeralPublicKey: client.publicKey,
|
|
156
|
+
hold: {
|
|
157
|
+
hold: {
|
|
158
|
+
holdId: 'rewrap',
|
|
159
|
+
did,
|
|
160
|
+
shareVersion: 1,
|
|
161
|
+
blobHash: 'ab'.repeat(32),
|
|
162
|
+
enrollmentEpoch: 1,
|
|
163
|
+
releasePolicy: 'pin',
|
|
164
|
+
clientEphemeralPublicKey: client.publicKey,
|
|
165
|
+
createdLo: 0,
|
|
166
|
+
createdHi: 0,
|
|
167
|
+
policyVersion: 1,
|
|
168
|
+
signature: 'test-signature',
|
|
169
|
+
},
|
|
170
|
+
holdDurationMs: 0,
|
|
171
|
+
ledgerSeq: 0,
|
|
172
|
+
},
|
|
173
|
+
});
|
|
174
|
+
expect(await openEscrowRelease(sealed, client.privateKey)).toEqual({
|
|
175
|
+
...target,
|
|
176
|
+
version: 1,
|
|
177
|
+
holdId: 'rewrap',
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
);
|
|
181
|
+
|
|
182
|
+
it('derives the attestation key, verifies enrollment, and enforces release policy', async () => {
|
|
183
|
+
const keys = await generateEscrowKeyPair();
|
|
184
|
+
const client = await generateEscrowKeyPair();
|
|
185
|
+
const enclave = new SoftwareEnclave({
|
|
186
|
+
privateKeys: { test: keys.privateKey },
|
|
187
|
+
activeKeyId: 'test',
|
|
188
|
+
});
|
|
189
|
+
const attestation = await enclave.getAttestation();
|
|
190
|
+
expect(attestation.publicKey).toBe(keys.publicKey);
|
|
191
|
+
expect(JSON.parse(Buffer.from(attestation.document, 'base64').toString())).toEqual({
|
|
192
|
+
mode: 'software',
|
|
193
|
+
keyId: 'test',
|
|
194
|
+
previousKeyIds: [],
|
|
195
|
+
publicKey: keys.publicKey,
|
|
196
|
+
issuedAt: attestation.issuedAt,
|
|
197
|
+
});
|
|
198
|
+
const plaintext = { recoveryShare: 'ab'.repeat(33), did: 'did:key:test', shareVersion: 1 };
|
|
199
|
+
const envelope = await encryptEscrowBlob(plaintext, attestation.publicKey, 'test');
|
|
200
|
+
expect(
|
|
201
|
+
await enclave.verifyEscrowBlob({
|
|
202
|
+
envelope,
|
|
203
|
+
expectedDid: plaintext.did,
|
|
204
|
+
expectedShareVersion: 1,
|
|
205
|
+
})
|
|
206
|
+
).toEqual({ ok: true, hasPin: false });
|
|
207
|
+
expect(
|
|
208
|
+
await enclave.verifyEscrowBlob({
|
|
209
|
+
envelope,
|
|
210
|
+
expectedDid: 'did:key:wrong',
|
|
211
|
+
expectedShareVersion: 1,
|
|
212
|
+
})
|
|
213
|
+
).toMatchObject({ ok: false });
|
|
214
|
+
const { holdRecord: created } = await enclave.createHold({
|
|
215
|
+
envelope,
|
|
216
|
+
holdId: 'test-hold',
|
|
217
|
+
expectedDid: plaintext.did,
|
|
218
|
+
expectedShareVersion: 1,
|
|
219
|
+
enrollmentEpoch: 1,
|
|
220
|
+
releasePolicy: 'hold',
|
|
221
|
+
clientEphemeralPublicKey: client.publicKey,
|
|
222
|
+
});
|
|
223
|
+
const hold: EscrowHoldRecord = {
|
|
224
|
+
...created,
|
|
225
|
+
hold: { ...created.hold, createdLo: 0, createdHi: 0 },
|
|
226
|
+
holdDurationMs: 1000,
|
|
227
|
+
};
|
|
228
|
+
expect(created).toMatchObject({
|
|
229
|
+
hold: {
|
|
230
|
+
holdId: 'test-hold',
|
|
231
|
+
did: plaintext.did,
|
|
232
|
+
shareVersion: 1,
|
|
233
|
+
enrollmentEpoch: 1,
|
|
234
|
+
releasePolicy: 'hold',
|
|
235
|
+
clientEphemeralPublicKey: client.publicKey,
|
|
236
|
+
createdLo: expect.any(Number),
|
|
237
|
+
createdHi: expect.any(Number),
|
|
238
|
+
policyVersion: 1,
|
|
239
|
+
signature: 'software-mode-unsigned',
|
|
240
|
+
blobHash: expect.stringMatching(/^[0-9a-f]{64}$/),
|
|
241
|
+
},
|
|
242
|
+
holdDurationMs: 604800000,
|
|
243
|
+
ledgerSeq: 0,
|
|
244
|
+
});
|
|
245
|
+
const createInput = {
|
|
246
|
+
envelope,
|
|
247
|
+
holdId: 'test-hold',
|
|
248
|
+
expectedDid: plaintext.did,
|
|
249
|
+
expectedShareVersion: 1,
|
|
250
|
+
enrollmentEpoch: 1,
|
|
251
|
+
releasePolicy: 'hold' as const,
|
|
252
|
+
clientEphemeralPublicKey: client.publicKey,
|
|
253
|
+
};
|
|
254
|
+
for (const change of [
|
|
255
|
+
{ expectedDid: 'did:key:other' },
|
|
256
|
+
{ expectedShareVersion: 2 },
|
|
257
|
+
{ enrollmentEpoch: 0 },
|
|
258
|
+
{ releasePolicy: 'pin' as const },
|
|
259
|
+
]) {
|
|
260
|
+
await expect(enclave.createHold({ ...createInput, ...change })).rejects.toBeInstanceOf(
|
|
261
|
+
EscrowPolicyError
|
|
262
|
+
);
|
|
263
|
+
}
|
|
264
|
+
await expect(
|
|
265
|
+
enclave.createHold({ ...createInput, envelope: { ...envelope, ciphertext: 'bad' } })
|
|
266
|
+
).rejects.toBeInstanceOf(EscrowBlobError);
|
|
267
|
+
const request = {
|
|
268
|
+
envelope,
|
|
269
|
+
hold,
|
|
270
|
+
clientEphemeralPublicKey: client.publicKey,
|
|
271
|
+
expectedDid: plaintext.did,
|
|
272
|
+
};
|
|
273
|
+
await expect(enclave.cancelHold(request)).resolves.toBeUndefined();
|
|
274
|
+
for (const change of [
|
|
275
|
+
{ expectedDid: 'did:key:other' },
|
|
276
|
+
{ clientEphemeralPublicKey: keys.publicKey },
|
|
277
|
+
{ hold: { ...hold, hold: { ...hold.hold, did: 'did:key:other' } } },
|
|
278
|
+
]) {
|
|
279
|
+
await expect(enclave.cancelHold({ ...request, ...change })).rejects.toBeInstanceOf(
|
|
280
|
+
EscrowPolicyError
|
|
281
|
+
);
|
|
282
|
+
}
|
|
283
|
+
await expect(
|
|
284
|
+
enclave.cancelHold({ ...request, envelope: { ...envelope, ciphertext: 'bad' } })
|
|
285
|
+
).rejects.toBeInstanceOf(EscrowBlobError);
|
|
286
|
+
await expect(
|
|
287
|
+
enclave.releaseEscrow({ ...request, now: new Date(999) })
|
|
288
|
+
).rejects.toBeInstanceOf(EscrowPolicyError);
|
|
289
|
+
for (const changed of [{ shareVersion: 2 }, { did: 'did:key:wrong' }]) {
|
|
290
|
+
await expect(
|
|
291
|
+
enclave.releaseEscrow({
|
|
292
|
+
...request,
|
|
293
|
+
hold: { ...hold, hold: { ...hold.hold, ...changed } },
|
|
294
|
+
now: new Date(1000),
|
|
295
|
+
})
|
|
296
|
+
).rejects.toBeInstanceOf(EscrowPolicyError);
|
|
297
|
+
}
|
|
298
|
+
await expect(
|
|
299
|
+
enclave.releaseEscrow({ ...request, expectedDid: 'did:key:wrong', now: new Date(1000) })
|
|
300
|
+
).rejects.toBeInstanceOf(EscrowPolicyError);
|
|
301
|
+
await expect(
|
|
302
|
+
enclave.releaseEscrow({
|
|
303
|
+
...request,
|
|
304
|
+
clientEphemeralPublicKey: keys.publicKey,
|
|
305
|
+
now: new Date(1000),
|
|
306
|
+
})
|
|
307
|
+
).rejects.toBeInstanceOf(EscrowPolicyError);
|
|
308
|
+
const released = await enclave.releaseEscrow({ ...request, now: new Date(1000) });
|
|
309
|
+
expect(await openEscrowRelease(released.sealed, client.privateKey)).toEqual({
|
|
310
|
+
...plaintext,
|
|
311
|
+
version: 1,
|
|
312
|
+
holdId: hold.hold.holdId,
|
|
313
|
+
});
|
|
314
|
+
await expect(
|
|
315
|
+
enclave.verifyEscrowBlob({
|
|
316
|
+
envelope: { ...envelope, keyId: 'unknown' },
|
|
317
|
+
expectedDid: plaintext.did,
|
|
318
|
+
expectedShareVersion: 1,
|
|
319
|
+
})
|
|
320
|
+
).rejects.toBeInstanceOf(EscrowBlobError);
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
it.each([
|
|
324
|
+
{ policy: 'pin' as const, verifier: true, proof: 'ab'.repeat(32), error: undefined },
|
|
325
|
+
{
|
|
326
|
+
policy: 'pin' as const,
|
|
327
|
+
verifier: true,
|
|
328
|
+
proof: 'cd'.repeat(32),
|
|
329
|
+
error: EscrowPinMismatchError,
|
|
330
|
+
},
|
|
331
|
+
{ policy: 'pin' as const, verifier: true, proof: 'ab', error: EscrowPinMismatchError },
|
|
332
|
+
{ policy: 'pin' as const, verifier: true, proof: undefined, error: EscrowPolicyError },
|
|
333
|
+
{
|
|
334
|
+
policy: 'pin' as const,
|
|
335
|
+
verifier: false,
|
|
336
|
+
proof: 'ab'.repeat(32),
|
|
337
|
+
error: EscrowPolicyError,
|
|
338
|
+
},
|
|
339
|
+
{ policy: 'hold' as const, verifier: true, proof: 'cd'.repeat(32), error: undefined },
|
|
340
|
+
])(
|
|
341
|
+
'checks policy/proof case %# without exposing the verifier',
|
|
342
|
+
async ({ policy, verifier, proof, error }) => {
|
|
343
|
+
const keys = await generateEscrowKeyPair();
|
|
344
|
+
const client = await generateEscrowKeyPair();
|
|
345
|
+
const enclave = new SoftwareEnclave({
|
|
346
|
+
privateKeys: { test: keys.privateKey },
|
|
347
|
+
activeKeyId: 'test',
|
|
348
|
+
});
|
|
349
|
+
const plaintext = {
|
|
350
|
+
recoveryShare: 'ab'.repeat(33),
|
|
351
|
+
did: 'did:key:test',
|
|
352
|
+
shareVersion: 1,
|
|
353
|
+
};
|
|
354
|
+
const envelope = await encryptEscrowBlob(
|
|
355
|
+
{ ...plaintext, ...(verifier ? { pinVerifier: 'ab'.repeat(32) } : {}) },
|
|
356
|
+
keys.publicKey,
|
|
357
|
+
'test'
|
|
358
|
+
);
|
|
359
|
+
expect(
|
|
360
|
+
await enclave.verifyEscrowBlob({
|
|
361
|
+
envelope,
|
|
362
|
+
expectedDid: plaintext.did,
|
|
363
|
+
expectedShareVersion: 1,
|
|
364
|
+
})
|
|
365
|
+
).toEqual({ ok: true, hasPin: verifier });
|
|
366
|
+
const createInput = {
|
|
367
|
+
envelope,
|
|
368
|
+
holdId: 'pin-hold',
|
|
369
|
+
expectedDid: plaintext.did,
|
|
370
|
+
expectedShareVersion: 1,
|
|
371
|
+
enrollmentEpoch: 1,
|
|
372
|
+
releasePolicy: policy,
|
|
373
|
+
clientEphemeralPublicKey: client.publicKey,
|
|
374
|
+
};
|
|
375
|
+
if (policy === 'pin' && !verifier) {
|
|
376
|
+
await expect(enclave.createHold(createInput)).rejects.toBeInstanceOf(
|
|
377
|
+
EscrowPolicyError
|
|
378
|
+
);
|
|
379
|
+
return;
|
|
380
|
+
}
|
|
381
|
+
const { holdRecord } = await enclave.createHold(createInput);
|
|
382
|
+
const request = {
|
|
383
|
+
envelope,
|
|
384
|
+
expectedDid: plaintext.did,
|
|
385
|
+
clientEphemeralPublicKey: client.publicKey,
|
|
386
|
+
pinProof: proof,
|
|
387
|
+
hold: holdRecord,
|
|
388
|
+
now: new Date(holdRecord.hold.createdHi + holdRecord.holdDurationMs),
|
|
389
|
+
};
|
|
390
|
+
if (error) await expect(enclave.releaseEscrow(request)).rejects.toBeInstanceOf(error);
|
|
391
|
+
else {
|
|
392
|
+
const result = await enclave.releaseEscrow(request);
|
|
393
|
+
expect(await openEscrowRelease(result.sealed, client.privateKey)).toEqual({
|
|
394
|
+
...plaintext,
|
|
395
|
+
version: 1,
|
|
396
|
+
holdId: 'pin-hold',
|
|
397
|
+
});
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
);
|
|
401
|
+
|
|
402
|
+
// P9.1 software-enclave equivalent: every configured key other than
|
|
403
|
+
// activeKeyId is already usable for decrypt (verifyEscrowBlob/createHold/
|
|
404
|
+
// releaseEscrow key the plaintext lookup by the envelope's own keyId, not
|
|
405
|
+
// by activeKeyId) — this test asserts the attestation now ADVERTISES that
|
|
406
|
+
// existing capability, and that a blob still sealed under a retired key
|
|
407
|
+
// decrypts/creates a hold/releases exactly like a current-key blob.
|
|
408
|
+
it('advertises other configured keys as previousKeyIds and still releases a blob sealed under one', async () => {
|
|
409
|
+
const active = await generateEscrowKeyPair();
|
|
410
|
+
const retired = await generateEscrowKeyPair();
|
|
411
|
+
const client = await generateEscrowKeyPair();
|
|
412
|
+
const enclave = new SoftwareEnclave({
|
|
413
|
+
privateKeys: { active: active.privateKey, retired: retired.privateKey },
|
|
414
|
+
activeKeyId: 'active',
|
|
415
|
+
});
|
|
416
|
+
const attestation = await enclave.getAttestation();
|
|
417
|
+
expect(attestation.keyId).toBe('active');
|
|
418
|
+
expect(attestation.previousKeyIds).toEqual(['retired']);
|
|
419
|
+
const plaintext = { recoveryShare: 'cd'.repeat(33), did: 'did:key:test', shareVersion: 1 };
|
|
420
|
+
const envelope = await encryptEscrowBlob(plaintext, retired.publicKey, 'retired');
|
|
421
|
+
expect(
|
|
422
|
+
await enclave.verifyEscrowBlob({
|
|
423
|
+
envelope,
|
|
424
|
+
expectedDid: plaintext.did,
|
|
425
|
+
expectedShareVersion: 1,
|
|
426
|
+
})
|
|
427
|
+
).toEqual({ ok: true, hasPin: false });
|
|
428
|
+
const { holdRecord } = await enclave.createHold({
|
|
429
|
+
envelope,
|
|
430
|
+
holdId: 'retired-key-hold',
|
|
431
|
+
expectedDid: plaintext.did,
|
|
432
|
+
expectedShareVersion: 1,
|
|
433
|
+
enrollmentEpoch: 1,
|
|
434
|
+
releasePolicy: 'hold',
|
|
435
|
+
clientEphemeralPublicKey: client.publicKey,
|
|
436
|
+
});
|
|
437
|
+
const result = await enclave.releaseEscrow({
|
|
438
|
+
envelope,
|
|
439
|
+
hold: holdRecord,
|
|
440
|
+
expectedDid: plaintext.did,
|
|
441
|
+
clientEphemeralPublicKey: client.publicKey,
|
|
442
|
+
now: new Date(holdRecord.hold.createdHi + holdRecord.holdDurationMs),
|
|
443
|
+
});
|
|
444
|
+
expect(await openEscrowRelease(result.sealed, client.privateKey)).toEqual({
|
|
445
|
+
...plaintext,
|
|
446
|
+
version: 1,
|
|
447
|
+
holdId: 'retired-key-hold',
|
|
448
|
+
});
|
|
449
|
+
});
|
|
450
|
+
});
|