@openlfcp/client 0.3.1 → 0.4.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/dist/identity/devices.d.ts +18 -5
- package/dist/identity/index.d.ts +1 -1
- package/dist/identity/pairing.js +4 -0
- package/dist/identity/reconcile.js +2 -1
- package/dist/identity/revocation.js +3 -1
- package/dist/identity/rotation.d.ts +11 -6
- package/dist/identity/rotation.js +27 -5
- package/package.json +5 -5
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { type PrincipalId, type ResourceId } from "@openlfcp/core";
|
|
2
2
|
import type { SecretStore } from "@openlfcp/storage";
|
|
3
|
-
import { type CertificateChain, type ControlState, type
|
|
3
|
+
import { type CertificateChain, type ControlState, type DeviceCertificate, type DeviceCertificatePayload, type DeviceKind, type PrincipalDescriptor, type Signer } from "@openlfcp/wire";
|
|
4
|
+
import type { DataProfileHandler } from "../apply.js";
|
|
4
5
|
import { type IdentityStorage, type Installation, type KeyDelivery } from "./context.js";
|
|
5
6
|
import { type IdentityNetwork } from "./network.js";
|
|
6
7
|
import { type LinearChain } from "./phases.js";
|
|
@@ -32,12 +33,24 @@ export interface FlowOptions {
|
|
|
32
33
|
readonly network: IdentityNetwork;
|
|
33
34
|
readonly now: () => number;
|
|
34
35
|
/**
|
|
35
|
-
* The application's Data
|
|
36
|
-
*
|
|
37
|
-
*
|
|
36
|
+
* The application's Data Profiles, each as its receiver checks one unit:
|
|
37
|
+
* `codecFor({resourceId, actor})`, the codec bound to the unit's Resource
|
|
38
|
+
* and signer (the DataProfileHandler hook; e.g. shared-objects'
|
|
39
|
+
* sharedObjectsUnitProfile, sectionsUnitProfile, identityUnitProfile). A
|
|
40
|
+
* rotation judges each unit with its Resource's profile, as recipients do
|
|
41
|
+
* (W §26.3 rules 6 and 7, including the change's actor); without one, only
|
|
42
|
+
* decryption and recorded refusals are checked. Replaces 0.3.1's
|
|
43
|
+
* `dataProfiles` (bare codecs, which cannot bind the actor): a flow given
|
|
44
|
+
* `dataProfiles` throws instead of checking less.
|
|
38
45
|
*/
|
|
39
|
-
readonly
|
|
46
|
+
readonly profiles?: readonly UnitProfile[];
|
|
40
47
|
}
|
|
48
|
+
/**
|
|
49
|
+
* One Data Profile's per-unit check (the receiver's DataProfileHandler
|
|
50
|
+
* `dataProfile` and `codecFor`): `decode` of the codec it returns throws to
|
|
51
|
+
* reject the unit's plaintext.
|
|
52
|
+
*/
|
|
53
|
+
export type UnitProfile = Pick<DataProfileHandler<unknown>, "dataProfile" | "codecFor">;
|
|
41
54
|
/**
|
|
42
55
|
* The certificates above a leaf that `issuer` signs for `root` in the
|
|
43
56
|
* state: none when the issuer is the root, else the issuer's own current
|
package/dist/identity/index.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/** Identity flows (IDENTITY-PROFILE-01 §§5–9, LFCP-04-410). */
|
|
2
2
|
export { acceptInvitationAsDevice, claimForRootRecord, createResourceAsDevice, type DeviceAcceptResult, type DeviceCreateResult, type DeviceResourceResult, type IncompleteDeviceResource, selfRegistrationRecord, } from "./accept.js";
|
|
3
3
|
export { type DocumentWrite, deliverKeys, extendChain, fetchDeks, genesisOwner, heldDeks, IdentityConflict, type IdentityDocumentLoad, type IdentityStorage, type Installation, type KeyDelivery, loadIdentityDocument, type PhaseOutcome, readersAt, runControlPhase, runUnitPhase, sealIdentityUnit, storeDek, syncChain, } from "./context.js";
|
|
4
|
-
export { checkIssuance, type Feasibility, type FlowOptions, issuerCertificates, leafPayload, MASK_PRESETS, type MaskPreset, mayManageDevices, type RegisterTarget, type RegistrationResult, type RegistrationStatus, registerDevice, signChain, } from "./devices.js";
|
|
4
|
+
export { checkIssuance, type Feasibility, type FlowOptions, issuerCertificates, leafPayload, MASK_PRESETS, type MaskPreset, mayManageDevices, type RegisterTarget, type RegistrationResult, type RegistrationStatus, registerDevice, signChain, type UnitProfile, } from "./devices.js";
|
|
5
5
|
export { type DeviceEntryValue, IDENTITY_PROFILE, type IdentityAuthor, type IdentityDocumentPort, type IdentityIntent, type IdentityWriterClass, identityPlaintextCodec, identityWriterClass, type PairingRequestValue, principalOfRef, principalRefOf, type RosterEntryValue, resourceRef, } from "./document.js";
|
|
6
6
|
export { type JournalOperation, type JournalPhase, type LedgerEntry, OperationJournal, type OperationKind, type OperationStatus, PackageLedger, pairingSecretRef, pendingDekRef, } from "./journal.js";
|
|
7
7
|
export { ChannelPool, type ConnectionNetworkOptions, connectionNetwork, IDENTITY_EXTENSION, type IdentityAnswer, type IdentityChannel, type IdentityNetwork, IdentityRefused, IdentityUnavailable, type OpenAnswer, } from "./network.js";
|
package/dist/identity/pairing.js
CHANGED
|
@@ -9,6 +9,7 @@ import { ChannelPool, IdentityUnavailable } from "./network.js";
|
|
|
9
9
|
import { classifyCancel, classifyConsume, classifyRegister } from "./phases.js";
|
|
10
10
|
import { identityTarget } from "./resource.js";
|
|
11
11
|
import { rotateIfExposed } from "./revocation.js";
|
|
12
|
+
import { checkFlowProfiles } from "./rotation.js";
|
|
12
13
|
/**
|
|
13
14
|
* Pairing a new installation, the managing side `A` (IDENTITY-PROFILE-01
|
|
14
15
|
* §5.3 steps 1, 4–7, 9 and 10): a pairing device `J` registered in the
|
|
@@ -323,6 +324,7 @@ export async function pairingRequests(o) {
|
|
|
323
324
|
* phase 1; phase 2 signs exactly that payload.
|
|
324
325
|
*/
|
|
325
326
|
export async function approvePairing(o) {
|
|
327
|
+
checkFlowProfiles(o);
|
|
326
328
|
const { I, R, J, url } = await readSession(o, o.review.session);
|
|
327
329
|
const pool = new ChannelPool(o.network);
|
|
328
330
|
const me = o.installation.signer;
|
|
@@ -413,6 +415,7 @@ export const approvedGrantRecord = (chain, signer, root, device, pairing) => sig
|
|
|
413
415
|
export const cancelRecord = (chain, signer, root, pairing) => signControlRecord(next(chain), { type: "DEVICE_REVOKE", root, device: pairing, reason: 0n }, signer).bytes;
|
|
414
416
|
/** Resumes a journaled approval from its next phase (§9.1: "If A crashes after step 5 …"). */
|
|
415
417
|
export async function resumeApproval(o, operation) {
|
|
418
|
+
checkFlowProfiles(o);
|
|
416
419
|
const journal = new OperationJournal(o.storage);
|
|
417
420
|
const pool = new ChannelPool(o.network);
|
|
418
421
|
const me = o.installation.signer;
|
|
@@ -565,6 +568,7 @@ export async function resumeApproval(o, operation) {
|
|
|
565
568
|
* human does not confirm is rejected).
|
|
566
569
|
*/
|
|
567
570
|
export async function cancelPairing(o) {
|
|
571
|
+
checkFlowProfiles(o);
|
|
568
572
|
const { I, R, J, url } = await readSession(o, o.session);
|
|
569
573
|
const journal = new OperationJournal(o.storage);
|
|
570
574
|
const pool = new ChannelPool(o.network);
|
|
@@ -8,7 +8,7 @@ import { ChannelPool, IDENTITY_EXTENSION, IdentityRefused, IdentityUnavailable,
|
|
|
8
8
|
import { classifyRegister, classifyRevoke } from "./phases.js";
|
|
9
9
|
import { identityTarget } from "./resource.js";
|
|
10
10
|
import { rotateIfExposed } from "./revocation.js";
|
|
11
|
-
import { resumeRotations } from "./rotation.js";
|
|
11
|
+
import { checkFlowProfiles, resumeRotations } from "./rotation.js";
|
|
12
12
|
/**
|
|
13
13
|
* §7.1 steps 1 and 2 for one target, in their normative order. For the
|
|
14
14
|
* target `I` itself both steps are vacuous. Registrations are planned on
|
|
@@ -98,6 +98,7 @@ export function reconcileRecord(target, step, signer) {
|
|
|
98
98
|
* opened and this installation is the rotator, it rotates (§8.4).
|
|
99
99
|
*/
|
|
100
100
|
export async function reconcileDevices(o) {
|
|
101
|
+
checkFlowProfiles(o);
|
|
101
102
|
const target = await identityTarget(o);
|
|
102
103
|
const me = o.installation.signer;
|
|
103
104
|
const journal = new OperationJournal(o.storage);
|
|
@@ -7,7 +7,7 @@ import { newOperationId, OperationJournal, phaseOf } from "./journal.js";
|
|
|
7
7
|
import { ChannelPool, IDENTITY_EXTENSION, IdentityRefused, IdentityUnavailable, } from "./network.js";
|
|
8
8
|
import { classifyRevoke } from "./phases.js";
|
|
9
9
|
import { identityTarget } from "./resource.js";
|
|
10
|
-
import { exposingRevocationsSince, resumeRotations, rotateAfter, rotatorTest, } from "./rotation.js";
|
|
10
|
+
import { checkFlowProfiles, exposingRevocationsSince, resumeRotations, rotateAfter, rotatorTest, } from "./rotation.js";
|
|
11
11
|
const toKeep = (state, root) => [...state.devices.entries()]
|
|
12
12
|
.filter(([, d]) => bytesEqual(d.root, root))
|
|
13
13
|
.map(([hex]) => principalId(fromHex(hex)))
|
|
@@ -131,6 +131,7 @@ class SelfDependent extends Error {
|
|
|
131
131
|
* another reason is `complete` and the rotation test still runs.
|
|
132
132
|
*/
|
|
133
133
|
export async function revokeDevice(o) {
|
|
134
|
+
checkFlowProfiles(o);
|
|
134
135
|
const target = await identityTarget(o);
|
|
135
136
|
const me = o.installation.signer.descriptor.principalId;
|
|
136
137
|
const selfRetire = bytesEqual(me, o.device);
|
|
@@ -387,6 +388,7 @@ async function markRevoked(o, channel, journal, operation, I, device) {
|
|
|
387
388
|
* nothing exposing happened since the last rotation.
|
|
388
389
|
*/
|
|
389
390
|
export async function rotateIfExposed(o) {
|
|
391
|
+
checkFlowProfiles(o);
|
|
390
392
|
// A rotation this operation already started resumes from its journaled phase: its
|
|
391
393
|
// KEY_EPOCH may be committed while its DEK still waits in the pending slot (§9.1).
|
|
392
394
|
const started = phaseOf(await new OperationJournal(o.storage).load(o.operation), `rotate:${toHex(o.resource)}`);
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { type ControlRecordId, type DataEpoch, type PrincipalId, type ResourceId } from "@openlfcp/core";
|
|
2
2
|
import { type ResourceDEK } from "@openlfcp/crypto";
|
|
3
|
-
import { type ControlState, type
|
|
3
|
+
import { type ControlState, type HaveVector, type PrincipalDescriptor } from "@openlfcp/wire";
|
|
4
4
|
import { type KeyDelivery } from "./context.js";
|
|
5
|
-
import type { FlowOptions } from "./devices.js";
|
|
5
|
+
import type { FlowOptions, UnitProfile } from "./devices.js";
|
|
6
6
|
import { type ChannelPool, type IdentityChannel } from "./network.js";
|
|
7
7
|
import { type LinearChain } from "./phases.js";
|
|
8
8
|
/**
|
|
@@ -73,12 +73,15 @@ export interface FrontierData {
|
|
|
73
73
|
/** The DEKs held for the Resource's epochs, by epoch number. */
|
|
74
74
|
readonly deks: ReadonlyMap<string, ResourceDEK>;
|
|
75
75
|
/**
|
|
76
|
-
* The Resource's Data Profile
|
|
77
|
-
*
|
|
76
|
+
* The Resource's Data Profile, when the application gives it, as its
|
|
77
|
+
* receiver checks one unit: `codecFor({resourceId, actor})` is bound to the
|
|
78
|
+
* unit's Resource and signer, and the codec's `decode` is the profile's
|
|
79
|
+
* acceptance (the change's actor included). Without it only decryption is
|
|
78
80
|
* checked here, and a profile refusal this installation recorded still
|
|
79
|
-
* counts (LocalAccepted.refused).
|
|
81
|
+
* counts (LocalAccepted.refused). 0.3.1 took a bare codec, which cannot
|
|
82
|
+
* bind the actor: one without `codecFor` is refused (TypeError).
|
|
80
83
|
*/
|
|
81
|
-
readonly profile?:
|
|
84
|
+
readonly profile?: UnitProfile;
|
|
82
85
|
}
|
|
83
86
|
export type RotationStatus =
|
|
84
87
|
/** This installation committed the KEY_EPOCH and sealed the new DEK to every reader. */
|
|
@@ -117,6 +120,8 @@ export declare function rotateAfter(o: FlowOptions & {
|
|
|
117
120
|
readonly resource: ResourceId;
|
|
118
121
|
readonly revokedAt: bigint;
|
|
119
122
|
}): Promise<RotationOutcome>;
|
|
123
|
+
/** The flow options of a rotation: 0.3.1's bare `dataProfiles` is refused, never checked less. */
|
|
124
|
+
export declare function checkFlowProfiles(o: FlowOptions): void;
|
|
120
125
|
/**
|
|
121
126
|
* Resumes every rotation a journaled operation started and did not adopt:
|
|
122
127
|
* a KEY_EPOCH whose ACK was lost leaves its DEK in the operation's pending
|
|
@@ -58,6 +58,8 @@ export function exposingRevocationsSince(chain) {
|
|
|
58
58
|
* signature, authorization and the actor chain are checked (0.3.0).
|
|
59
59
|
*/
|
|
60
60
|
export async function verifiedFrontier(channel, chain, local = { units: [], snapshot: [] }, data) {
|
|
61
|
+
if (data?.profile !== undefined)
|
|
62
|
+
checkUnitProfile(data.profile, "FrontierData.profile");
|
|
61
63
|
const R = chain.state.resourceId;
|
|
62
64
|
const opened = await channel.open(R);
|
|
63
65
|
const ranges = opened.kind === "nack"
|
|
@@ -103,7 +105,7 @@ export async function verifiedFrontier(channel, chain, local = { units: [], snap
|
|
|
103
105
|
// covers are attested by it (W §29.3).
|
|
104
106
|
if (!covered &&
|
|
105
107
|
data !== undefined &&
|
|
106
|
-
!(await acceptedAsData(chain, bytes, toHex(check.unitId), local, data)))
|
|
108
|
+
!(await acceptedAsData(chain, bytes, toHex(check.unitId), p.actor, local, data)))
|
|
107
109
|
continue;
|
|
108
110
|
const actor = toHex(p.actor);
|
|
109
111
|
const last = latest.get(actor);
|
|
@@ -164,13 +166,14 @@ const opaque = (dataProfile) => ({
|
|
|
164
166
|
* not decided here means those steps passed). A unit this client cannot
|
|
165
167
|
* decrypt cannot be verified and stays outside the frontier (W §19).
|
|
166
168
|
*/
|
|
167
|
-
async function acceptedAsData(chain, bytes, unitId, local, data) {
|
|
169
|
+
async function acceptedAsData(chain, bytes, unitId, actor, local, data) {
|
|
168
170
|
if (local.accepted?.has(unitId) === true)
|
|
169
171
|
return true;
|
|
170
172
|
if (local.refused?.has(unitId) === true)
|
|
171
173
|
return false;
|
|
174
|
+
// The receiver's own check of this unit: bound to its Resource and signer (W §26.3 rule 7).
|
|
172
175
|
const profile = data.profile !== undefined && data.profile.dataProfile === chain.state.dataProfile
|
|
173
|
-
? data.profile
|
|
176
|
+
? data.profile.codecFor({ resourceId: chain.state.resourceId, actor })
|
|
174
177
|
: opaque(chain.state.dataProfile);
|
|
175
178
|
const r = await receiveDataUnit(chain, bytes, {
|
|
176
179
|
seen: new InMemorySeenUnits(),
|
|
@@ -191,6 +194,7 @@ async function acceptedAsData(chain, bytes, unitId, local, data) {
|
|
|
191
194
|
* readers and receive nothing.
|
|
192
195
|
*/
|
|
193
196
|
export async function rotateAfter(o) {
|
|
197
|
+
checkFlowProfiles(o);
|
|
194
198
|
const journal = new OperationJournal(o.storage);
|
|
195
199
|
const me = o.installation.signer;
|
|
196
200
|
const R = o.resource;
|
|
@@ -278,11 +282,28 @@ export async function rotateAfter(o) {
|
|
|
278
282
|
* stored accepted units and verified Snapshots, when the storage keeps them),
|
|
279
283
|
* merged into the verified frontier (§8.3).
|
|
280
284
|
*/
|
|
281
|
-
/** The
|
|
285
|
+
/** The profile the application gave for the Resource's Data Profile, if any. */
|
|
282
286
|
const profileFor = (o, chain) => {
|
|
283
|
-
const profile = o.
|
|
287
|
+
const profile = o.profiles?.find((p) => p.dataProfile === chain.state.dataProfile);
|
|
284
288
|
return profile === undefined ? {} : { profile };
|
|
285
289
|
};
|
|
290
|
+
/**
|
|
291
|
+
* A profile given where a per-unit check is needed must be one: a bare codec
|
|
292
|
+
* (0.3.1's FlowOptions.dataProfiles, FrontierData.profile) cannot bind the
|
|
293
|
+
* change's actor to the unit's signer, so it is refused, never used as a
|
|
294
|
+
* weaker check (W §26.3 rule 7).
|
|
295
|
+
*/
|
|
296
|
+
function checkUnitProfile(profile, where) {
|
|
297
|
+
if (typeof profile.codecFor !== "function")
|
|
298
|
+
throw new TypeError(`${where} needs a per-unit profile (dataProfile and codecFor({resourceId, actor})), not a bare codec`);
|
|
299
|
+
}
|
|
300
|
+
/** The flow options of a rotation: 0.3.1's bare `dataProfiles` is refused, never checked less. */
|
|
301
|
+
export function checkFlowProfiles(o) {
|
|
302
|
+
if (o.dataProfiles !== undefined)
|
|
303
|
+
throw new TypeError("FlowOptions.dataProfiles (bare codecs, 0.3.1) is replaced by FlowOptions.profiles (codecFor per unit): a codec without the unit's signer cannot check the change's actor");
|
|
304
|
+
for (const p of o.profiles ?? [])
|
|
305
|
+
checkUnitProfile(p, "FlowOptions.profiles");
|
|
306
|
+
}
|
|
286
307
|
async function localAccepted(o, chain) {
|
|
287
308
|
const st = o.storage;
|
|
288
309
|
if (st.dataUnits === undefined || st.snapshots === undefined)
|
|
@@ -328,6 +349,7 @@ async function localAccepted(o, chain) {
|
|
|
328
349
|
* it (§9.1, §9.2). Run before planning new work on a Resource.
|
|
329
350
|
*/
|
|
330
351
|
export async function resumeRotations(o) {
|
|
352
|
+
checkFlowProfiles(o);
|
|
331
353
|
const journal = new OperationJournal(o.storage);
|
|
332
354
|
const out = [];
|
|
333
355
|
for (const op of await journal.list())
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@openlfcp/client",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "LFCP client: session, Control Plane and Data Plane synchronization.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"openlfcp",
|
|
@@ -42,9 +42,9 @@
|
|
|
42
42
|
"tag": "latest"
|
|
43
43
|
},
|
|
44
44
|
"dependencies": {
|
|
45
|
-
"@openlfcp/core": "^0.
|
|
46
|
-
"@openlfcp/
|
|
47
|
-
"@openlfcp/
|
|
48
|
-
"@openlfcp/
|
|
45
|
+
"@openlfcp/core": "^0.4.0",
|
|
46
|
+
"@openlfcp/storage": "^0.4.0",
|
|
47
|
+
"@openlfcp/wire": "^0.4.0",
|
|
48
|
+
"@openlfcp/crypto": "^0.4.0"
|
|
49
49
|
}
|
|
50
50
|
}
|