@learncard/sss-key-manager 0.2.7 → 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.
@@ -54,7 +54,12 @@ import {
54
54
  derivePinProof,
55
55
  ESCROW_PIN_MAX_ATTEMPTS,
56
56
  } from './escrow-pin';
57
- import { encryptEscrowBlob, generateEscrowKeyPair, openEscrowRelease } from './escrow-crypto';
57
+ import {
58
+ canonicalEscrowEmailBrand,
59
+ encryptEscrowBlob,
60
+ generateEscrowKeyPair,
61
+ openEscrowRelease,
62
+ } from './escrow-crypto';
58
63
  import { verifyEnclaveAttestation } from './escrow-attestation';
59
64
  import {
60
65
  createEscrowEnrollmentIntent,
@@ -69,7 +74,7 @@ import {
69
74
  } from './escrow-authority';
70
75
  import { getEscrowBlobIdentity } from './escrow-enrollment-intent';
71
76
  import type { VerifiedEnclaveIdentity, EscrowAuthorityState } from './escrow-authority.types';
72
- import { EscrowEvidenceError } from './escrow-authority.types';
77
+ import { ESCROW_AUTHORITY_TENANT_PATTERN, EscrowEvidenceError } from './escrow-authority.types';
73
78
  import type { EscrowEnvelope } from './escrow-crypto';
74
79
  import type { EmailRelayBranding } from './email-relay-crypto';
75
80
 
@@ -134,7 +139,36 @@ export interface SSSStorageFunctions {
134
139
 
135
140
  export interface SSSStrategyConfig {
136
141
  /** Enable automatic enrollment under an explicit enclave trust policy. */
137
- escrow?: { enabled: boolean; attestation: EscrowAttestationPolicy };
142
+ escrow?: {
143
+ enabled: boolean;
144
+ attestation: EscrowAttestationPolicy;
145
+ /**
146
+ * The escrow authority's tenant label: the enclave's measured
147
+ * `ESCROW_AUTHORITY_TENANT`, which it signs into every authority record and
148
+ * requires in every signed enrollment intent. Used for everything the
149
+ * enclave verifies (intent tenant, authority state, hold, code-delivery and
150
+ * persisted recovery expectations). Everything else (X-Tenant-Id, email
151
+ * branding, same-tenant guards) keeps using {@link SSSStrategyConfig.tenantId}.
152
+ *
153
+ * Defaults to `tenantId` (then `'learncard'`). Set it explicitly when this app
154
+ * shares another tenant's single-tenant enclave, e.g. VetPass on LearnCard's
155
+ * (`'learncard'`). Must match `/^[A-Za-z0-9_-]{1,128}$/`.
156
+ */
157
+ authorityTenant?: string;
158
+ /**
159
+ * Display name sealed into the escrow blob for enclave-sent emails
160
+ * (D21, `recoveryBrand`), e.g. `'VetPass'`. Sealed only when the
161
+ * recovery email and language are (nitro enroll/replace), never on a
162
+ * carry. Unset: the enclave brands emails from its measured tenant label.
163
+ *
164
+ * Set only after every live enclave host runs the brand-aware image: an
165
+ * older image drops the field whenever it re-encrypts the blob.
166
+ * Must pass {@link canonicalEscrowEmailBrand} (ASCII letters, digits,
167
+ * space, `-`, `'`, `&`; 1-32 characters); `createSSSStrategy` throws
168
+ * otherwise.
169
+ */
170
+ emailBrand?: string;
171
+ };
138
172
 
139
173
  /** Report enrollment failures without logging recovery material. */
140
174
  onEscrowError?: (err: unknown) => void;
@@ -1037,6 +1071,23 @@ export function createSSSStrategy(config: SSSStrategyConfig): SSSKeyDerivationSt
1037
1071
  emailBranding,
1038
1072
  tenantId,
1039
1073
  } = config;
1074
+ // Enclave-verified tenant (see SSSStrategyConfig.escrow.authorityTenant); never
1075
+ // sent as X-Tenant-Id. Defaults keep LearnCard/Scouts behaviour unchanged.
1076
+ const authorityTenant = config.escrow?.authorityTenant ?? tenantId ?? 'learncard';
1077
+ if (config.escrow && !ESCROW_AUTHORITY_TENANT_PATTERN.test(authorityTenant))
1078
+ throw new Error(
1079
+ 'Invalid escrow authority tenant: expected 1-128 letters, digits, "_" or "-".'
1080
+ );
1081
+ // D21: strict on write (the enclave and parsers are lenient on read), and
1082
+ // canonicalized once here so every enrollment path seals the same value.
1083
+ const emailBrand =
1084
+ config.escrow?.emailBrand === undefined
1085
+ ? undefined
1086
+ : canonicalEscrowEmailBrand(config.escrow.emailBrand);
1087
+ if (config.escrow?.emailBrand !== undefined && emailBrand === undefined)
1088
+ throw new Error(
1089
+ 'Invalid escrow email brand: expected 1-32 ASCII letters, digits, spaces, "-", "\'" or "&", starting with a letter.'
1090
+ );
1040
1091
  const storage = config.storage || defaultStorage;
1041
1092
  let storageGeneration = 0;
1042
1093
  const localWrites = new Set<Promise<void>>();
@@ -1289,7 +1340,7 @@ export function createSSSStrategy(config: SSSStrategyConfig): SSSKeyDerivationSt
1289
1340
  if (
1290
1341
  !expected ||
1291
1342
  expected.holdId !== holdId ||
1292
- expected.tenant !== (tenantId ?? 'learncard') ||
1343
+ expected.tenant !== authorityTenant ||
1293
1344
  !expected.did ||
1294
1345
  !expected.clientEphemeralPublicKey ||
1295
1346
  !expected.blobHash ||
@@ -1357,7 +1408,7 @@ export function createSSSStrategy(config: SSSStrategyConfig): SSSKeyDerivationSt
1357
1408
  const result = await verifyEscrowAuthorityState(
1358
1409
  response,
1359
1410
  nonce,
1360
- tenantId ?? 'learncard',
1411
+ authorityTenant,
1361
1412
  did,
1362
1413
  identity
1363
1414
  );
@@ -1427,8 +1478,15 @@ export function createSSSStrategy(config: SSSStrategyConfig): SSSKeyDerivationSt
1427
1478
  did: primaryDid,
1428
1479
  shareVersion,
1429
1480
  ...(pinMaterial ? { pinVerifier: pinMaterial.pinVerifier } : {}),
1481
+ // D21: the brand is sealed exactly when the contact is, so a
1482
+ // carry (whose target the enclave refuses if it claims one)
1483
+ // never sends it; the enclave inherits the source's brand.
1430
1484
  ...(authority && recoveryEmail && bindsRecoveryContact
1431
- ? { recoveryEmail, recoveryLanguage }
1485
+ ? {
1486
+ recoveryEmail,
1487
+ recoveryLanguage,
1488
+ ...(emailBrand ? { recoveryBrand: emailBrand } : {}),
1489
+ }
1432
1490
  : {}),
1433
1491
  },
1434
1492
  publicKey,
@@ -3154,7 +3212,7 @@ export function createSSSStrategy(config: SSSStrategyConfig): SSSKeyDerivationSt
3154
3212
  const verified = await verifyEscrowHoldRecord(
3155
3213
  result.holdRecord,
3156
3214
  {
3157
- tenant: tenantId ?? 'learncard',
3215
+ tenant: authorityTenant,
3158
3216
  did: expectedDid,
3159
3217
  holdId: result.holdId,
3160
3218
  clientEphemeralPublicKey: pair.publicKey,
@@ -3163,7 +3221,7 @@ export function createSSSStrategy(config: SSSStrategyConfig): SSSKeyDerivationSt
3163
3221
  identity
3164
3222
  );
3165
3223
  expectations = {
3166
- tenant: tenantId ?? 'learncard',
3224
+ tenant: authorityTenant,
3167
3225
  did: expectedDid,
3168
3226
  holdId: result.holdId,
3169
3227
  clientEphemeralPublicKey: pair.publicKey,