@openlfcp/client 0.3.1 → 0.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.
@@ -3,7 +3,7 @@ import { generateResourceDEK, sha256 } from "@openlfcp/crypto";
3
3
  import { ABILITY, actsFor, addSequence, checkDataUnit, hasAbility, hasSequence, InMemorySeenUnits, isExposingRevocation, KEY_EPOCH_REASON, parseDataUnit, receiveDataUnit, rotateEpoch, } from "@openlfcp/wire";
4
4
  import { snapshotFrontier } from "../outbound.js";
5
5
  import { loadControlChain } from "../storage.js";
6
- import { deliverKeys, fetchDeks, readersAt, runControlPhase, storeDek, withoutEquivocations, } from "./context.js";
6
+ import { deliverKeys, fetchDeks, readersAt, revokedByIdentity, runControlPhase, storeDek, withoutEquivocations, } from "./context.js";
7
7
  import { getPendingDek, OperationJournal, PackageLedger, pendingDekRef, phaseOf, putPendingDek, } from "./journal.js";
8
8
  import { IdentityUnavailable } from "./network.js";
9
9
  import { keyEpochAfter, recordSeq } from "./phases.js";
@@ -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(),
@@ -188,9 +191,14 @@ async function acceptedAsData(chain, bytes, unitId, local, data) {
188
191
  * reuse it (§9.4). After the ACK the DEK goes to every reader of the new
189
192
  * head, each package recorded in the ledger; the revoked device, a
190
193
  * consumed or cancelled pairing device and suspended devices are not
191
- * readers and receive nothing.
194
+ * readers and receive nothing. With `identityChain`, neither does a reader that
195
+ * the rotator's Identity Resource has revoked, or whose chain in the
196
+ * Resource passes through such a device: a pair not yet propagated there
197
+ * (IDENTITY-PROFILE-01 §7.1 step 1) is treated as revoked, and the rotation
198
+ * closes the epoch for it as for the device just revoked.
192
199
  */
193
200
  export async function rotateAfter(o) {
201
+ checkFlowProfiles(o);
194
202
  const journal = new OperationJournal(o.storage);
195
203
  const me = o.installation.signer;
196
204
  const R = o.resource;
@@ -262,7 +270,8 @@ export async function rotateAfter(o) {
262
270
  ledger: new PackageLedger(o.storage),
263
271
  chain,
264
272
  sender: me,
265
- recipients: readersAt(chain.state),
273
+ recipients: readersAt(chain.state).filter((r) => o.identityChain === undefined ||
274
+ !revokedByIdentity(o.identityChain, chain.state, r.principalId)),
266
275
  deks: send,
267
276
  });
268
277
  const phase = phaseOf(await journal.load(o.operation), id);
@@ -278,11 +287,28 @@ export async function rotateAfter(o) {
278
287
  * stored accepted units and verified Snapshots, when the storage keeps them),
279
288
  * merged into the verified frontier (§8.3).
280
289
  */
281
- /** The codec the application gave for the Resource's Data Profile, if any. */
290
+ /** The profile the application gave for the Resource's Data Profile, if any. */
282
291
  const profileFor = (o, chain) => {
283
- const profile = o.dataProfiles?.find((p) => p.dataProfile === chain.state.dataProfile);
292
+ const profile = o.profiles?.find((p) => p.dataProfile === chain.state.dataProfile);
284
293
  return profile === undefined ? {} : { profile };
285
294
  };
295
+ /**
296
+ * A profile given where a per-unit check is needed must be one: a bare codec
297
+ * (0.3.1's FlowOptions.dataProfiles, FrontierData.profile) cannot bind the
298
+ * change's actor to the unit's signer, so it is refused, never used as a
299
+ * weaker check (W §26.3 rule 7).
300
+ */
301
+ function checkUnitProfile(profile, where) {
302
+ if (typeof profile.codecFor !== "function")
303
+ throw new TypeError(`${where} needs a per-unit profile (dataProfile and codecFor({resourceId, actor})), not a bare codec`);
304
+ }
305
+ /** The flow options of a rotation: 0.3.1's bare `dataProfiles` is refused, never checked less. */
306
+ export function checkFlowProfiles(o) {
307
+ if (o.dataProfiles !== undefined)
308
+ 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");
309
+ for (const p of o.profiles ?? [])
310
+ checkUnitProfile(p, "FlowOptions.profiles");
311
+ }
286
312
  async function localAccepted(o, chain) {
287
313
  const st = o.storage;
288
314
  if (st.dataUnits === undefined || st.snapshots === undefined)
@@ -328,6 +354,7 @@ async function localAccepted(o, chain) {
328
354
  * it (§9.1, §9.2). Run before planning new work on a Resource.
329
355
  */
330
356
  export async function resumeRotations(o) {
357
+ checkFlowProfiles(o);
331
358
  const journal = new OperationJournal(o.storage);
332
359
  const out = [];
333
360
  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.1",
3
+ "version": "0.5.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.3.1",
46
- "@openlfcp/crypto": "^0.3.1",
47
- "@openlfcp/storage": "^0.3.1",
48
- "@openlfcp/wire": "^0.3.1"
45
+ "@openlfcp/core": "^0.5.0",
46
+ "@openlfcp/crypto": "^0.5.0",
47
+ "@openlfcp/storage": "^0.5.0",
48
+ "@openlfcp/wire": "^0.5.0"
49
49
  }
50
50
  }