@openlfcp/wire 0.1.0-rc.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/LICENSE +201 -0
- package/README.md +27 -0
- package/dist/capability.d.ts +143 -0
- package/dist/capability.js +409 -0
- package/dist/cbor/decode.d.ts +23 -0
- package/dist/cbor/decode.js +163 -0
- package/dist/cbor/encode.d.ts +10 -0
- package/dist/cbor/encode.js +149 -0
- package/dist/cbor/index.d.ts +10 -0
- package/dist/cbor/index.js +10 -0
- package/dist/cbor/text.d.ts +11 -0
- package/dist/cbor/text.js +5 -0
- package/dist/cbor/value.d.ts +30 -0
- package/dist/cbor/value.js +23 -0
- package/dist/chain.d.ts +140 -0
- package/dist/chain.js +339 -0
- package/dist/control-sync.d.ts +69 -0
- package/dist/control-sync.js +44 -0
- package/dist/control.d.ts +173 -0
- package/dist/control.js +369 -0
- package/dist/cose.d.ts +81 -0
- package/dist/cose.js +133 -0
- package/dist/data-unit.d.ts +204 -0
- package/dist/data-unit.js +314 -0
- package/dist/endpoint.d.ts +51 -0
- package/dist/endpoint.js +116 -0
- package/dist/epoch.d.ts +109 -0
- package/dist/epoch.js +128 -0
- package/dist/fields.d.ts +18 -0
- package/dist/fields.js +78 -0
- package/dist/handshake.d.ts +175 -0
- package/dist/handshake.js +297 -0
- package/dist/have.d.ts +101 -0
- package/dist/have.js +268 -0
- package/dist/index.d.ts +21 -0
- package/dist/index.js +21 -0
- package/dist/invite.d.ts +80 -0
- package/dist/invite.js +247 -0
- package/dist/key-package.d.ts +92 -0
- package/dist/key-package.js +132 -0
- package/dist/message.d.ts +367 -0
- package/dist/message.js +690 -0
- package/dist/objects.d.ts +154 -0
- package/dist/objects.js +156 -0
- package/dist/principal.d.ts +43 -0
- package/dist/principal.js +81 -0
- package/dist/session-state.d.ts +47 -0
- package/dist/session-state.js +49 -0
- package/dist/snapshot.d.ts +86 -0
- package/dist/snapshot.js +189 -0
- package/dist/transition.d.ts +97 -0
- package/dist/transition.js +130 -0
- package/package.json +53 -0
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/** Name of this package. */
|
|
2
|
+
export declare const PACKAGE = "@openlfcp/wire";
|
|
3
|
+
export { ABILITY, ABILITY_NAMES, type Authorization, abilitiesOf, authorizeControlRecord, type CapabilityState, canDistributeKey, type Grant, hasAbility, isGrantActive, isStandardAbility, type VerifiedOwnerTransfer, verifyOwnerTransfer, } from "./capability.js";
|
|
4
|
+
export { type ChainOptions, type ChainProblem, type ChainResult, type ControlEpoch, type ControlRoute, type ControlState, type EpochHistory, validateControlChain, } from "./chain.js";
|
|
5
|
+
export { type ControlBody, type ControlBodyType, type ControlRecord, type ControlRecordHeader, controlBodyFromCbor, controlBodyToCbor, controlRecordSigner, controlTypeOf, decodeControlRecord, encodeControlRecordPayload, isMvpSupported, type OwnerTransferAcceptPayload, type OwnerTransferOfferPayload, ownerTransferAcceptPayloadFromCbor, ownerTransferOfferPayloadFromCbor, parseOwnerTransferAccept, parseOwnerTransferOffer, type SignedControlRecord, signControlRecord, verifyGenesis, } from "./control.js";
|
|
6
|
+
export { type ControlSyncPlan, type LocalControl, localControlOf, planControlSync, } from "./control-sync.js";
|
|
7
|
+
export { COSE_ALG_EDDSA, objectId, parseSignedObject, type SignedBytes, type SignedObject, type Signer, signObject, sigStructureBytes, type VerifyResult, verifySignedObject, } from "./cose.js";
|
|
8
|
+
export { checkDataUnit, type DataProfileCodec, type DataUnitAadFields, type DataUnitCheck, type DataUnitCheckOptions, type DataUnitEquivocation, type DataUnitHoldReason, type DataUnitQuarantined, type DataUnitRejected, type DataUnitRejectReason, dataUnitAad, encodeDataUnitPayload, InMemorySeenUnits, type ReceiveDataUnitOptions, type ReceivedDataUnit, receiveDataUnit, type SealedDataUnit, type SeenRecord, type SeenUnits, sealDataUnit, } from "./data-unit.js";
|
|
9
|
+
export { checkReceivedUrl, checkWriterUrl, ENDPOINT_FLAGS, type Endpoint, endpointFromCbor, endpointToCbor, } from "./endpoint.js";
|
|
10
|
+
export { type ControlView, classifyDataUnit, type DataPutDecision, type DataUnitHeader, type EpochClassification, type EpochRotation, isSequenceWithinFrontier, KEY_EPOCH_REASON, rotateEpoch, serverAcceptsDataPut, } from "./epoch.js";
|
|
11
|
+
export { type AuthenticatedSession, type AuthProofCheck, type AuthTranscriptFields, authTranscript, type ClientHandshakeConfig, type ClientSession, type ClientStep, clientReceive, decodeAuthTranscript, isResourceMessage, type RandomSource, type ReadySession, type ServerHandshakeConfig, type ServerSession, type ServerStep, selectWireProfile, serverReceive, signAuthProof, startClientHandshake, startServerSession, verifyAuthProof, WIRE_PROFILE, } from "./handshake.js";
|
|
12
|
+
export { type ActorHave, type ActorRange, actorHaveFromCbor, actorHaveToCbor, addRange, addSequence, batchDataRanges, canonicalFrontierFromCbor, canonicalFrontierToCbor, checkLiveHave, type HaveVector, hasSequence, type LiveHaveEntry, liveHavesOf, MAX_DATA_GET_RANGES, missingAfter, missingFrom, normalizeLiveHaves, type SequenceRange, unionHaves, } from "./have.js";
|
|
13
|
+
export { assembleInviteUri, decodeInviteSecret, encodeInviteSecret, INVITE_SECRET_VERSION, type Invitation, invitationPrincipal, parseInviteUri, verifyInvitationSecret, } from "./invite.js";
|
|
14
|
+
export { type KeyPackageCheck, type KeyPackageRecipient, keyPackageHpkeAad, keyPackageHpkeInfo, openKeyPackage, type ReceivedKeyPackage, receiveKeyPackage, type SealedKeyPackage, sealKeyPackage, verifyKeyPackage, } from "./key-package.js";
|
|
15
|
+
export { type AckBody, type AnyMessage, type AuthBody, type ChallengeBody, type ControlGetBody, type ControlHaveBody, type ControlHeadRef, type CoreMessageType, createMessage, type DataGetBody, type DataHaveBody, type DataRange, DEFAULT_MAX_MESSAGE_BYTES, type DecodedEnvelope, type DecodeOptions, decodeEnvelope, decodeFrame, decodeMessage, ERROR_CODE, type ErrorBody, type ExtensionMessage, encodeMessage, FIRST_EXTENSION_MESSAGE_TYPE, type Frame, type FrameResult, type HelloBody, type KeyPackageGetBody, type LfcpMessage, type LiveActorHave, MESSAGE_TYPE, type MessageBodies, messageErrorWireCode, newMessageId, type ObjectListBody, type PingBody, type PresenceBody, type PresenceLeaveBody, type ReadyBody, type ResourceBody, type ResourceHostBody, type ResourceHostedBody, type ResourceOpenBody, type ResourceOpenedBody, replyTo, type SnapshotBody, type SnapshotGetBody, type SnapshotSummary, type WireErrorName, } from "./message.js";
|
|
16
|
+
export { CONTROL_TYPE, type ControlRecordPayload, controlRecordPayloadFromCbor, type DataUnitPayload, dataUnitPayloadFromCbor, decodeControlRecordPayload, decodeDataUnitPayload, decodeKeyPackagePayload, decodeSnapshotPayload, expectedSignerOf, type KeyPackagePayload, keyPackagePayloadFromCbor, type Parsed, parseControlRecord, parseDataUnit, parseKeyPackage, parseSnapshot, type SnapshotPayload, snapshotPayloadFromCbor, } from "./objects.js";
|
|
17
|
+
export { decodePrincipalDescriptor, derivePrincipalId, encodePrincipalDescriptor, type PrincipalDescriptor, principalDescriptor, principalDescriptorFromCbor, principalDescriptorFromKeys, principalDescriptorToCbor, } from "./principal.js";
|
|
18
|
+
export { type ClientConnectionEvent, type ClientConnectionState, clientConnectionTransition, type ServerSessionEvent, type ServerSessionState, serverSessionTransition, } from "./session-state.js";
|
|
19
|
+
export { beyondCutoff, checkSnapshot, encodeSnapshotPayload, type ReceivedSnapshot, type ReceiveSnapshotOptions, receiveSnapshot, type SealedSnapshot, type SnapshotAadFields, type SnapshotCheck, type SnapshotCheckOptions, type SnapshotRejected, type SnapshotRejectReason, sealSnapshot, snapshotAad, } from "./snapshot.js";
|
|
20
|
+
export { type ControlPutBody, controlPutBodyFromCbor, decodeControlPutBody, proposeControlPut, proposeControlTransition, type TransitionResult, } from "./transition.js";
|
|
21
|
+
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/** Name of this package. */
|
|
2
|
+
export const PACKAGE = "@openlfcp/wire";
|
|
3
|
+
export { ABILITY, ABILITY_NAMES, abilitiesOf, authorizeControlRecord, canDistributeKey, hasAbility, isGrantActive, isStandardAbility, verifyOwnerTransfer, } from "./capability.js";
|
|
4
|
+
export { validateControlChain, } from "./chain.js";
|
|
5
|
+
export { controlBodyFromCbor, controlBodyToCbor, controlRecordSigner, controlTypeOf, decodeControlRecord, encodeControlRecordPayload, isMvpSupported, ownerTransferAcceptPayloadFromCbor, ownerTransferOfferPayloadFromCbor, parseOwnerTransferAccept, parseOwnerTransferOffer, signControlRecord, verifyGenesis, } from "./control.js";
|
|
6
|
+
export { localControlOf, planControlSync, } from "./control-sync.js";
|
|
7
|
+
export { COSE_ALG_EDDSA, objectId, parseSignedObject, signObject, sigStructureBytes, verifySignedObject, } from "./cose.js";
|
|
8
|
+
export { checkDataUnit, dataUnitAad, encodeDataUnitPayload, InMemorySeenUnits, receiveDataUnit, sealDataUnit, } from "./data-unit.js";
|
|
9
|
+
export { checkReceivedUrl, checkWriterUrl, ENDPOINT_FLAGS, endpointFromCbor, endpointToCbor, } from "./endpoint.js";
|
|
10
|
+
export { classifyDataUnit, isSequenceWithinFrontier, KEY_EPOCH_REASON, rotateEpoch, serverAcceptsDataPut, } from "./epoch.js";
|
|
11
|
+
export { authTranscript, clientReceive, decodeAuthTranscript, isResourceMessage, selectWireProfile, serverReceive, signAuthProof, startClientHandshake, startServerSession, verifyAuthProof, WIRE_PROFILE, } from "./handshake.js";
|
|
12
|
+
export { actorHaveFromCbor, actorHaveToCbor, addRange, addSequence, batchDataRanges, canonicalFrontierFromCbor, canonicalFrontierToCbor, checkLiveHave, hasSequence, liveHavesOf, MAX_DATA_GET_RANGES, missingAfter, missingFrom, normalizeLiveHaves, unionHaves, } from "./have.js";
|
|
13
|
+
export { assembleInviteUri, decodeInviteSecret, encodeInviteSecret, INVITE_SECRET_VERSION, invitationPrincipal, parseInviteUri, verifyInvitationSecret, } from "./invite.js";
|
|
14
|
+
export { keyPackageHpkeAad, keyPackageHpkeInfo, openKeyPackage, receiveKeyPackage, sealKeyPackage, verifyKeyPackage, } from "./key-package.js";
|
|
15
|
+
export { createMessage, DEFAULT_MAX_MESSAGE_BYTES, decodeEnvelope, decodeFrame, decodeMessage, ERROR_CODE, encodeMessage, FIRST_EXTENSION_MESSAGE_TYPE, MESSAGE_TYPE, messageErrorWireCode, newMessageId, replyTo, } from "./message.js";
|
|
16
|
+
export { CONTROL_TYPE, controlRecordPayloadFromCbor, dataUnitPayloadFromCbor, decodeControlRecordPayload, decodeDataUnitPayload, decodeKeyPackagePayload, decodeSnapshotPayload, expectedSignerOf, keyPackagePayloadFromCbor, parseControlRecord, parseDataUnit, parseKeyPackage, parseSnapshot, snapshotPayloadFromCbor, } from "./objects.js";
|
|
17
|
+
export { decodePrincipalDescriptor, derivePrincipalId, encodePrincipalDescriptor, principalDescriptor, principalDescriptorFromCbor, principalDescriptorFromKeys, principalDescriptorToCbor, } from "./principal.js";
|
|
18
|
+
export { clientConnectionTransition, serverSessionTransition, } from "./session-state.js";
|
|
19
|
+
export { beyondCutoff, checkSnapshot, encodeSnapshotPayload, receiveSnapshot, sealSnapshot, snapshotAad, } from "./snapshot.js";
|
|
20
|
+
export { controlPutBodyFromCbor, decodeControlPutBody, proposeControlPut, proposeControlTransition, } from "./transition.js";
|
|
21
|
+
//# sourceMappingURL=index.js.map
|
package/dist/invite.d.ts
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { type ControlRecordId, type ResourceId } from "@openlfcp/core";
|
|
2
|
+
import { InvitationSecret } from "@openlfcp/crypto";
|
|
3
|
+
import type { ControlState } from "./chain.js";
|
|
4
|
+
import type { Signer } from "./cose.js";
|
|
5
|
+
import { type PrincipalDescriptor } from "./principal.js";
|
|
6
|
+
/**
|
|
7
|
+
* Invitations (LFCP-WIRE-01 §18, §18.2, LFCP-039b): the invitation secret
|
|
8
|
+
* (deterministic CBOR of the Invitation Principal's private keys), the
|
|
9
|
+
* Invitation Principal it determines, and the canonical lfcp://join URI in
|
|
10
|
+
* its targeted and bearer forms. Pure building blocks; the claim flow
|
|
11
|
+
* (CAPABILITY_CLAIM, the claimant's Key Package) is LFCP-053.
|
|
12
|
+
*
|
|
13
|
+
* §18.2: the secret "MUST NOT be logged, placed in analytics, stored in
|
|
14
|
+
* browser history by an LFCP web landing page, or transmitted to the
|
|
15
|
+
* synchronization server as an opaque URL." An InvitationSecret is
|
|
16
|
+
* redacted when printed or serialized, and no error raised here includes
|
|
17
|
+
* a URI, a secret or key bytes: messages name the part that is wrong only.
|
|
18
|
+
* A bearer URI string itself carries the secret; treat it like a key.
|
|
19
|
+
*/
|
|
20
|
+
/** §18.2 invite-secret field 0: the only secret format version. */
|
|
21
|
+
export declare const INVITE_SECRET_VERSION = 1n;
|
|
22
|
+
/**
|
|
23
|
+
* The §18.2 invite-secret: deterministic CBOR {0: 1, 1: Ed25519 seed,
|
|
24
|
+
* 2: X25519 private key}.
|
|
25
|
+
*
|
|
26
|
+
* WARNING: the result is secret. It belongs only in the `#secret=`
|
|
27
|
+
* fragment of a bearer invitation URI handed to the invitee.
|
|
28
|
+
*/
|
|
29
|
+
export declare function encodeInviteSecret(secret: InvitationSecret): Uint8Array;
|
|
30
|
+
/**
|
|
31
|
+
* Decodes a §18.2 invite-secret: deterministic CBOR, exactly fields 0-2,
|
|
32
|
+
* version 1 and two 32-byte keys. Throws INVALID_INVITATION, never with
|
|
33
|
+
* the bytes in the message.
|
|
34
|
+
*/
|
|
35
|
+
export declare function decodeInviteSecret(bytes: Uint8Array): InvitationSecret;
|
|
36
|
+
/** The Invitation Principal's public descriptor, recomputed from its secret (§7, §18.2). */
|
|
37
|
+
export declare function invitationPrincipal(secret: InvitationSecret): PrincipalDescriptor;
|
|
38
|
+
/**
|
|
39
|
+
* §18.2: "The receiving client MUST recompute the corresponding public
|
|
40
|
+
* Principal Descriptor and MUST verify that it matches the subject of the
|
|
41
|
+
* referenced Invitation Grant before using the secret."
|
|
42
|
+
*
|
|
43
|
+
* `grantId` must name a grant of `state` (MISSING_DEPENDENCY otherwise: the
|
|
44
|
+
* client lacks Control Records) that lists invite/claim (§18: an
|
|
45
|
+
* invitation grant), whose subject is the recomputed Invitation Principal
|
|
46
|
+
* (INVALID_INVITATION otherwise). Returns the Invitation Principal as a
|
|
47
|
+
* signer, for the claim record (§18.1). Whether the grant is still active
|
|
48
|
+
* and claimable is decided where it is used (§18.1, §25.2).
|
|
49
|
+
*/
|
|
50
|
+
export declare function verifyInvitationSecret(state: ControlState, grantId: ControlRecordId, secret: InvitationSecret): Signer;
|
|
51
|
+
/** An invitation: the targeted form, or the bearer form when `secret` is present (§18.2). */
|
|
52
|
+
export interface Invitation {
|
|
53
|
+
readonly resourceId: ResourceId;
|
|
54
|
+
/** One or more endpoint URLs, in URI order. */
|
|
55
|
+
readonly endpoints: readonly string[];
|
|
56
|
+
/** The Control Record ID of the invitation grant. */
|
|
57
|
+
readonly grantId: ControlRecordId;
|
|
58
|
+
readonly secret?: InvitationSecret;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* The canonical §18.2 URI: lfcp://join/<resource>?endpoint=…[&endpoint=…]&grant=<grant>,
|
|
62
|
+
* plus #secret=<secret> for a bearer invitation. IDs and the secret are
|
|
63
|
+
* unpadded base64url; each endpoint must pass the §16 writer rules and is
|
|
64
|
+
* percent-encoded (G-RS4).
|
|
65
|
+
*/
|
|
66
|
+
export declare function assembleInviteUri(invitation: Invitation): string;
|
|
67
|
+
/**
|
|
68
|
+
* Parses a §18.2 invitation URI. The scheme and host compare
|
|
69
|
+
* case-insensitively (RFC 3986 §3.1, §3.2.2); the path is one Resource ID;
|
|
70
|
+
* the query has one or more `endpoint` parameters and exactly one `grant`;
|
|
71
|
+
* other parameters are ignored, for forward compatibility, once their
|
|
72
|
+
* percent-encoding is checked; a fragment, if present, is exactly
|
|
73
|
+
* `secret=<b64url>`.
|
|
74
|
+
* Endpoints are percent-decoded and must use ws or wss (§16). Throws
|
|
75
|
+
* INVALID_INVITATION, never with the URI or the secret in the message.
|
|
76
|
+
* Verify a bearer secret against the grant (verifyInvitationSecret) before
|
|
77
|
+
* using it.
|
|
78
|
+
*/
|
|
79
|
+
export declare function parseInviteUri(uri: string): Invitation;
|
|
80
|
+
//# sourceMappingURL=invite.d.ts.map
|
package/dist/invite.js
ADDED
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
import { bytesEqual, controlRecordId, fromBase64url, LfcpError, resourceId, toBase64url, toHex, } from "@openlfcp/core";
|
|
2
|
+
import { exportSecretKeyBytes, InvitationSecret, importAgreementKey, importSigningKey, } from "@openlfcp/crypto";
|
|
3
|
+
import { ABILITY } from "./capability.js";
|
|
4
|
+
import { cborMap, decodeDeterministic, encode } from "./cbor/index.js";
|
|
5
|
+
import { strictUtf8Decoder, utf8Encoder } from "./cbor/text.js";
|
|
6
|
+
import { checkReceivedUrl, checkWriterUrl } from "./endpoint.js";
|
|
7
|
+
import { Fields } from "./fields.js";
|
|
8
|
+
import { principalDescriptorFromKeys } from "./principal.js";
|
|
9
|
+
/**
|
|
10
|
+
* Invitations (LFCP-WIRE-01 §18, §18.2, LFCP-039b): the invitation secret
|
|
11
|
+
* (deterministic CBOR of the Invitation Principal's private keys), the
|
|
12
|
+
* Invitation Principal it determines, and the canonical lfcp://join URI in
|
|
13
|
+
* its targeted and bearer forms. Pure building blocks; the claim flow
|
|
14
|
+
* (CAPABILITY_CLAIM, the claimant's Key Package) is LFCP-053.
|
|
15
|
+
*
|
|
16
|
+
* §18.2: the secret "MUST NOT be logged, placed in analytics, stored in
|
|
17
|
+
* browser history by an LFCP web landing page, or transmitted to the
|
|
18
|
+
* synchronization server as an opaque URL." An InvitationSecret is
|
|
19
|
+
* redacted when printed or serialized, and no error raised here includes
|
|
20
|
+
* a URI, a secret or key bytes: messages name the part that is wrong only.
|
|
21
|
+
* A bearer URI string itself carries the secret; treat it like a key.
|
|
22
|
+
*/
|
|
23
|
+
/** §18.2 invite-secret field 0: the only secret format version. */
|
|
24
|
+
export const INVITE_SECRET_VERSION = 1n;
|
|
25
|
+
const KEY_LENGTH = 32;
|
|
26
|
+
const ID_LENGTH = 32;
|
|
27
|
+
const PREFIX = "lfcp://join/";
|
|
28
|
+
const invalid = (why) => {
|
|
29
|
+
throw new LfcpError("INVALID_INVITATION", `invalid invitation: ${why}`);
|
|
30
|
+
};
|
|
31
|
+
/**
|
|
32
|
+
* The §18.2 invite-secret: deterministic CBOR {0: 1, 1: Ed25519 seed,
|
|
33
|
+
* 2: X25519 private key}.
|
|
34
|
+
*
|
|
35
|
+
* WARNING: the result is secret. It belongs only in the `#secret=`
|
|
36
|
+
* fragment of a bearer invitation URI handed to the invitee.
|
|
37
|
+
*/
|
|
38
|
+
export function encodeInviteSecret(secret) {
|
|
39
|
+
return encode(cborMap([
|
|
40
|
+
[0, INVITE_SECRET_VERSION],
|
|
41
|
+
[1, exportSecretKeyBytes(secret.signingKey)],
|
|
42
|
+
[2, exportSecretKeyBytes(secret.agreementKey)],
|
|
43
|
+
]));
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Decodes a §18.2 invite-secret: deterministic CBOR, exactly fields 0-2,
|
|
47
|
+
* version 1 and two 32-byte keys. Throws INVALID_INVITATION, never with
|
|
48
|
+
* the bytes in the message.
|
|
49
|
+
*/
|
|
50
|
+
export function decodeInviteSecret(bytes) {
|
|
51
|
+
let fields;
|
|
52
|
+
try {
|
|
53
|
+
fields = new Fields(decodeDeterministic(bytes), "invite-secret", [0, 1, 2]);
|
|
54
|
+
if (fields.uint(0) !== INVITE_SECRET_VERSION)
|
|
55
|
+
fields.fail(0, `must be secret format version ${INVITE_SECRET_VERSION}`);
|
|
56
|
+
return InvitationSecret.fromKeys(importSigningKey(fields.bytes(1, KEY_LENGTH)), importAgreementKey(fields.bytes(2, KEY_LENGTH)));
|
|
57
|
+
}
|
|
58
|
+
catch (e) {
|
|
59
|
+
// Only the error code: no message that could quote a value.
|
|
60
|
+
const code = e instanceof LfcpError ? e.code : "UNKNOWN";
|
|
61
|
+
return invalid(`the secret is not a §18.2 invite-secret (${code})`);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
/** The Invitation Principal's public descriptor, recomputed from its secret (§7, §18.2). */
|
|
65
|
+
export function invitationPrincipal(secret) {
|
|
66
|
+
return principalDescriptorFromKeys(secret.signingKey, secret.agreementKey);
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* §18.2: "The receiving client MUST recompute the corresponding public
|
|
70
|
+
* Principal Descriptor and MUST verify that it matches the subject of the
|
|
71
|
+
* referenced Invitation Grant before using the secret."
|
|
72
|
+
*
|
|
73
|
+
* `grantId` must name a grant of `state` (MISSING_DEPENDENCY otherwise: the
|
|
74
|
+
* client lacks Control Records) that lists invite/claim (§18: an
|
|
75
|
+
* invitation grant), whose subject is the recomputed Invitation Principal
|
|
76
|
+
* (INVALID_INVITATION otherwise). Returns the Invitation Principal as a
|
|
77
|
+
* signer, for the claim record (§18.1). Whether the grant is still active
|
|
78
|
+
* and claimable is decided where it is used (§18.1, §25.2).
|
|
79
|
+
*/
|
|
80
|
+
export function verifyInvitationSecret(state, grantId, secret) {
|
|
81
|
+
const grant = state.grants.get(toHex(grantId));
|
|
82
|
+
if (grant === undefined)
|
|
83
|
+
throw new LfcpError("MISSING_DEPENDENCY", "the invitation's grant is not in the known Control Chain");
|
|
84
|
+
if (!grant.abilities.includes(ABILITY.INVITE_CLAIM))
|
|
85
|
+
invalid("the referenced grant is not an invitation grant (no invite/claim, §18)");
|
|
86
|
+
const descriptor = invitationPrincipal(secret);
|
|
87
|
+
if (!bytesEqual(descriptor.principalId, grant.subject))
|
|
88
|
+
invalid("the secret's Invitation Principal is not the subject of the grant (§18.2)");
|
|
89
|
+
return Object.freeze({ key: secret.signingKey, descriptor });
|
|
90
|
+
}
|
|
91
|
+
const UNRESERVED = /^[A-Za-z0-9\-._~]$/;
|
|
92
|
+
/** §18.2 (G-RS4): every UTF-8 byte outside the RFC 3986 unreserved set as %XX, upper-case hex. */
|
|
93
|
+
function percentEncode(text) {
|
|
94
|
+
let out = "";
|
|
95
|
+
for (const ch of text) {
|
|
96
|
+
if (UNRESERVED.test(ch))
|
|
97
|
+
out += ch;
|
|
98
|
+
else
|
|
99
|
+
for (const b of utf8Encoder.encode(ch))
|
|
100
|
+
out += `%${b.toString(16).toUpperCase().padStart(2, "0")}`;
|
|
101
|
+
}
|
|
102
|
+
return out;
|
|
103
|
+
}
|
|
104
|
+
/** RFC 3986 percent-decoding of a query value into UTF-8 text. */
|
|
105
|
+
function percentDecode(text, what) {
|
|
106
|
+
const bytes = [];
|
|
107
|
+
for (let i = 0; i < text.length; i++) {
|
|
108
|
+
const ch = text[i];
|
|
109
|
+
if (ch === "%") {
|
|
110
|
+
const hex = text.slice(i + 1, i + 3);
|
|
111
|
+
if (!/^[0-9A-Fa-f]{2}$/.test(hex))
|
|
112
|
+
invalid(`${what} has a broken percent-encoding`);
|
|
113
|
+
bytes.push(Number.parseInt(hex, 16));
|
|
114
|
+
i += 2;
|
|
115
|
+
}
|
|
116
|
+
else {
|
|
117
|
+
const code = ch.charCodeAt(0);
|
|
118
|
+
if (code < 0x21 || code > 0x7e)
|
|
119
|
+
invalid(`${what} has a character that must be percent-encoded`);
|
|
120
|
+
bytes.push(code);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
try {
|
|
124
|
+
return strictUtf8Decoder.decode(Uint8Array.from(bytes));
|
|
125
|
+
}
|
|
126
|
+
catch {
|
|
127
|
+
return invalid(`${what} is not UTF-8`);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
/** A canonical unpadded base64url 32-byte ID (§18.2). */
|
|
131
|
+
function id32(text, what) {
|
|
132
|
+
let bytes;
|
|
133
|
+
try {
|
|
134
|
+
bytes = fromBase64url(text);
|
|
135
|
+
}
|
|
136
|
+
catch {
|
|
137
|
+
return invalid(`${what} is not canonical unpadded base64url`);
|
|
138
|
+
}
|
|
139
|
+
if (bytes.length !== ID_LENGTH)
|
|
140
|
+
invalid(`${what} is not ${ID_LENGTH} bytes`);
|
|
141
|
+
return bytes;
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* The canonical §18.2 URI: lfcp://join/<resource>?endpoint=…[&endpoint=…]&grant=<grant>,
|
|
145
|
+
* plus #secret=<secret> for a bearer invitation. IDs and the secret are
|
|
146
|
+
* unpadded base64url; each endpoint must pass the §16 writer rules and is
|
|
147
|
+
* percent-encoded (G-RS4).
|
|
148
|
+
*/
|
|
149
|
+
export function assembleInviteUri(invitation) {
|
|
150
|
+
if (invitation.endpoints.length === 0)
|
|
151
|
+
invalid("at least one endpoint is required (§18.2)");
|
|
152
|
+
for (const url of invitation.endpoints) {
|
|
153
|
+
try {
|
|
154
|
+
checkWriterUrl(url);
|
|
155
|
+
}
|
|
156
|
+
catch {
|
|
157
|
+
invalid("an endpoint is not a URL a writer may use (§16)");
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
const query = [
|
|
161
|
+
...invitation.endpoints.map((url) => `endpoint=${percentEncode(url)}`),
|
|
162
|
+
`grant=${toBase64url(invitation.grantId)}`,
|
|
163
|
+
].join("&");
|
|
164
|
+
const fragment = invitation.secret === undefined
|
|
165
|
+
? ""
|
|
166
|
+
: `#secret=${toBase64url(encodeInviteSecret(invitation.secret))}`;
|
|
167
|
+
return `${PREFIX}${toBase64url(invitation.resourceId)}?${query}${fragment}`;
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* Parses a §18.2 invitation URI. The scheme and host compare
|
|
171
|
+
* case-insensitively (RFC 3986 §3.1, §3.2.2); the path is one Resource ID;
|
|
172
|
+
* the query has one or more `endpoint` parameters and exactly one `grant`;
|
|
173
|
+
* other parameters are ignored, for forward compatibility, once their
|
|
174
|
+
* percent-encoding is checked; a fragment, if present, is exactly
|
|
175
|
+
* `secret=<b64url>`.
|
|
176
|
+
* Endpoints are percent-decoded and must use ws or wss (§16). Throws
|
|
177
|
+
* INVALID_INVITATION, never with the URI or the secret in the message.
|
|
178
|
+
* Verify a bearer secret against the grant (verifyInvitationSecret) before
|
|
179
|
+
* using it.
|
|
180
|
+
*/
|
|
181
|
+
export function parseInviteUri(uri) {
|
|
182
|
+
if (typeof uri !== "string")
|
|
183
|
+
invalid("the URI is not a string");
|
|
184
|
+
if (uri.slice(0, PREFIX.length).toLowerCase() !== PREFIX)
|
|
185
|
+
invalid("the URI does not start with lfcp://join/");
|
|
186
|
+
const hash = uri.indexOf("#");
|
|
187
|
+
const beforeFragment = hash === -1 ? uri : uri.slice(0, hash);
|
|
188
|
+
const question = beforeFragment.indexOf("?");
|
|
189
|
+
if (question === -1)
|
|
190
|
+
invalid("the URI has no query");
|
|
191
|
+
const path = beforeFragment.slice(PREFIX.length, question);
|
|
192
|
+
const resource = resourceId(id32(path, "the Resource ID"));
|
|
193
|
+
const endpoints = [];
|
|
194
|
+
let grant;
|
|
195
|
+
for (const parameter of beforeFragment.slice(question + 1).split("&")) {
|
|
196
|
+
const eq = parameter.indexOf("=");
|
|
197
|
+
const name = eq === -1 ? parameter : parameter.slice(0, eq);
|
|
198
|
+
const value = eq === -1 ? "" : parameter.slice(eq + 1);
|
|
199
|
+
if ((name === "endpoint" || name === "grant") && value === "")
|
|
200
|
+
invalid(`the ${name} parameter has no value`);
|
|
201
|
+
if (name === "endpoint") {
|
|
202
|
+
const url = percentDecode(value, "an endpoint");
|
|
203
|
+
try {
|
|
204
|
+
checkReceivedUrl(url);
|
|
205
|
+
}
|
|
206
|
+
catch {
|
|
207
|
+
invalid("an endpoint is not a ws or wss URL (§16)");
|
|
208
|
+
}
|
|
209
|
+
endpoints.push(url);
|
|
210
|
+
}
|
|
211
|
+
else if (name === "grant") {
|
|
212
|
+
if (grant !== undefined)
|
|
213
|
+
invalid("the grant parameter appears twice");
|
|
214
|
+
grant = controlRecordId(id32(value, "the grant ID"));
|
|
215
|
+
}
|
|
216
|
+
else {
|
|
217
|
+
// Not defined by §18.2: ignored, but still a well-formed query parameter.
|
|
218
|
+
percentDecode(name, "a query parameter name");
|
|
219
|
+
percentDecode(value, "a query parameter value");
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
if (endpoints.length === 0)
|
|
223
|
+
invalid("the URI has no endpoint parameter");
|
|
224
|
+
if (grant === undefined)
|
|
225
|
+
invalid("the URI has no grant parameter");
|
|
226
|
+
let secret;
|
|
227
|
+
if (hash !== -1) {
|
|
228
|
+
const fragment = uri.slice(hash + 1);
|
|
229
|
+
if (!fragment.startsWith("secret="))
|
|
230
|
+
invalid("the fragment is not secret=<b64url>");
|
|
231
|
+
let bytes;
|
|
232
|
+
try {
|
|
233
|
+
bytes = fromBase64url(fragment.slice("secret=".length));
|
|
234
|
+
}
|
|
235
|
+
catch {
|
|
236
|
+
return invalid("the secret is not canonical unpadded base64url");
|
|
237
|
+
}
|
|
238
|
+
secret = decodeInviteSecret(bytes);
|
|
239
|
+
}
|
|
240
|
+
return Object.freeze({
|
|
241
|
+
resourceId: resource,
|
|
242
|
+
endpoints: Object.freeze(endpoints),
|
|
243
|
+
grantId: grant,
|
|
244
|
+
...(secret !== undefined ? { secret } : {}),
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
//# sourceMappingURL=invite.js.map
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { type DataEpoch, type Hash32, type PrincipalId, type ResourceId } from "@openlfcp/core";
|
|
2
|
+
import { type AgreementKeyPair, type ResourceDEK } from "@openlfcp/crypto";
|
|
3
|
+
import { type Signer } from "./cose.js";
|
|
4
|
+
import type { ControlView } from "./epoch.js";
|
|
5
|
+
import { type KeyPackagePayload, type Parsed } from "./objects.js";
|
|
6
|
+
import type { PrincipalDescriptor } from "./principal.js";
|
|
7
|
+
/** §25.1: HPKE info = deterministic CBOR of ["LFCP-KEY-v1", resource_id, data_epoch, recipient]. */
|
|
8
|
+
export declare function keyPackageHpkeInfo(resource: ResourceId, epoch: DataEpoch, recipient: PrincipalId): Uint8Array;
|
|
9
|
+
/** §25.1: HPKE AAD = deterministic CBOR of [resource_id, data_epoch, control_head]. */
|
|
10
|
+
export declare function keyPackageHpkeAad(resource: ResourceId, epoch: DataEpoch, controlHead: Uint8Array): Uint8Array;
|
|
11
|
+
export type KeyPackageCheck = {
|
|
12
|
+
readonly kind: "authorized";
|
|
13
|
+
readonly sender: PrincipalDescriptor;
|
|
14
|
+
} | {
|
|
15
|
+
readonly kind: "rejected";
|
|
16
|
+
readonly reason: "OTHER_RESOURCE" | "UNKNOWN_CONTROL_HEAD" | "UNKNOWN_EPOCH" | "UNKNOWN_SENDER" | "SIGNATURE" | "UNAUTHORIZED";
|
|
17
|
+
readonly wireCode: string;
|
|
18
|
+
readonly message: string;
|
|
19
|
+
};
|
|
20
|
+
/**
|
|
21
|
+
* The §25.2 checks of a received Key Package against the validated chain,
|
|
22
|
+
* all at the package's referenced Control Head:
|
|
23
|
+
*
|
|
24
|
+
* - the head is on the chain and the package's epoch is known there.
|
|
25
|
+
* Packages of closed epochs stay valid (§25.2, G-EP6): they are needed
|
|
26
|
+
* to read history. Otherwise MISSING_DEPENDENCY (§25.2, §26.3);
|
|
27
|
+
* - the sender resolves to a descriptor on the chain: MISSING_DEPENDENCY
|
|
28
|
+
* (§10.5);
|
|
29
|
+
* - the package is signed by its sender (kid = sender): INVALID_SIGNATURE;
|
|
30
|
+
* - the sender held key/distribute and the recipient data/read, or an
|
|
31
|
+
* active invite grant (canDistributeKey, LFCP-021): AUTHORIZATION_FAILED.
|
|
32
|
+
*
|
|
33
|
+
* Opening the package and checking the DEK commitment come after this.
|
|
34
|
+
*/
|
|
35
|
+
export declare function verifyKeyPackage(view: ControlView, parsed: Parsed<KeyPackagePayload>): KeyPackageCheck;
|
|
36
|
+
/** The keys of the Principal a package is opened for: its descriptor and its X25519 key pair. */
|
|
37
|
+
export interface KeyPackageRecipient {
|
|
38
|
+
readonly descriptor: PrincipalDescriptor;
|
|
39
|
+
readonly agreement: AgreementKeyPair;
|
|
40
|
+
}
|
|
41
|
+
export interface SealedKeyPackage {
|
|
42
|
+
/** The exact signed Key Package bytes. */
|
|
43
|
+
readonly bytes: Uint8Array;
|
|
44
|
+
/** §25: the §10.6 object ID of those bytes. */
|
|
45
|
+
readonly packageId: Hash32;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Creates a Key Package (§25): the DEK sealed with HPKE to the recipient's
|
|
49
|
+
* X25519 key, with the exact §25.1 info and AAD, in a payload signed by the
|
|
50
|
+
* sender. Sealing always uses a fresh ephemeral key. Whether the sender may
|
|
51
|
+
* distribute and the recipient may receive is checked by receivers
|
|
52
|
+
* (verifyKeyPackage); a sender can check it first with canDistributeKey.
|
|
53
|
+
*/
|
|
54
|
+
export declare function sealKeyPackage(options: {
|
|
55
|
+
readonly resourceId: ResourceId;
|
|
56
|
+
readonly epoch: DataEpoch;
|
|
57
|
+
/** The Control Head the package is authorized at. */
|
|
58
|
+
readonly controlHead: Uint8Array;
|
|
59
|
+
readonly recipient: PrincipalDescriptor;
|
|
60
|
+
readonly dek: ResourceDEK;
|
|
61
|
+
readonly signer: Signer;
|
|
62
|
+
}): Promise<SealedKeyPackage>;
|
|
63
|
+
/**
|
|
64
|
+
* Opens a Key Package for its named recipient and checks the DEK against
|
|
65
|
+
* the epoch's commitment (§25, §25.2). The caller passes the keys of the
|
|
66
|
+
* Principal the package names; there is no trying of other keys.
|
|
67
|
+
*
|
|
68
|
+
* Throws KEY_PACKAGE_RECIPIENT_MISMATCH (keys of another Principal),
|
|
69
|
+
* KEY_PACKAGE_OPEN_FAILED or DEK_COMMITMENT_MISMATCH. All are client-local:
|
|
70
|
+
* ignore the package and surface it (ADR 0001 N5).
|
|
71
|
+
*/
|
|
72
|
+
export declare function openKeyPackage(parsed: Parsed<KeyPackagePayload>, recipient: KeyPackageRecipient, expectedCommitment: Uint8Array): Promise<ResourceDEK>;
|
|
73
|
+
export type ReceivedKeyPackage = {
|
|
74
|
+
readonly kind: "opened";
|
|
75
|
+
readonly dek: ResourceDEK;
|
|
76
|
+
readonly epoch: DataEpoch;
|
|
77
|
+
} | Extract<KeyPackageCheck, {
|
|
78
|
+
kind: "rejected";
|
|
79
|
+
}>
|
|
80
|
+
/** Client-local (N5): ignore the package and surface it to the application. */
|
|
81
|
+
| {
|
|
82
|
+
readonly kind: "ignored";
|
|
83
|
+
readonly code: "KEY_PACKAGE_RECIPIENT_MISMATCH" | "KEY_PACKAGE_OPEN_FAILED" | "DEK_COMMITMENT_MISMATCH";
|
|
84
|
+
readonly message: string;
|
|
85
|
+
};
|
|
86
|
+
/**
|
|
87
|
+
* A received Key Package end to end: parse, the §25.2 checks at its head
|
|
88
|
+
* (verifyKeyPackage), then open it for `recipient` and check the DEK
|
|
89
|
+
* against the epoch's commitment from the chain.
|
|
90
|
+
*/
|
|
91
|
+
export declare function receiveKeyPackage(view: ControlView, bytes: Uint8Array, recipient: KeyPackageRecipient): Promise<ReceivedKeyPackage>;
|
|
92
|
+
//# sourceMappingURL=key-package.d.ts.map
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import { bytesEqual, dataEpoch, LfcpError, toHex, } from "@openlfcp/core";
|
|
2
|
+
import { dekCommitment, openDek, sealDek, } from "@openlfcp/crypto";
|
|
3
|
+
import { canDistributeKey } from "./capability.js";
|
|
4
|
+
import { cborMap, encode } from "./cbor/index.js";
|
|
5
|
+
import { signObject, verifySignedObject } from "./cose.js";
|
|
6
|
+
import { expectedSignerOf, parseKeyPackage, } from "./objects.js";
|
|
7
|
+
/**
|
|
8
|
+
* Key Packages (LFCP-WIRE-01 §25): HPKE delivery of a Resource DEK to one
|
|
9
|
+
* recipient Principal, signed by its sender.
|
|
10
|
+
*
|
|
11
|
+
* This module builds and receives packages: the exact §25.1 info and AAD,
|
|
12
|
+
* the §25.2 checks against the Control Chain, and sealing and opening
|
|
13
|
+
* through @openlfcp/crypto HPKE. Opening failures (the package does not open
|
|
14
|
+
* for its recipient, or its DEK does not match the commitment) are
|
|
15
|
+
* client-local: the package is ignored and surfaced; there is no wire code
|
|
16
|
+
* (ADR 0001 N5).
|
|
17
|
+
*/
|
|
18
|
+
const KEY_LABEL = "LFCP-KEY-v1";
|
|
19
|
+
/** §25.1: HPKE info = deterministic CBOR of ["LFCP-KEY-v1", resource_id, data_epoch, recipient]. */
|
|
20
|
+
export function keyPackageHpkeInfo(resource, epoch, recipient) {
|
|
21
|
+
return encode([KEY_LABEL, resource, dataEpoch(epoch), recipient]);
|
|
22
|
+
}
|
|
23
|
+
/** §25.1: HPKE AAD = deterministic CBOR of [resource_id, data_epoch, control_head]. */
|
|
24
|
+
export function keyPackageHpkeAad(resource, epoch, controlHead) {
|
|
25
|
+
return encode([resource, dataEpoch(epoch), controlHead]);
|
|
26
|
+
}
|
|
27
|
+
const reject = (reason, wireCode, message) => Object.freeze({ kind: "rejected", reason, wireCode, message });
|
|
28
|
+
/**
|
|
29
|
+
* The §25.2 checks of a received Key Package against the validated chain,
|
|
30
|
+
* all at the package's referenced Control Head:
|
|
31
|
+
*
|
|
32
|
+
* - the head is on the chain and the package's epoch is known there.
|
|
33
|
+
* Packages of closed epochs stay valid (§25.2, G-EP6): they are needed
|
|
34
|
+
* to read history. Otherwise MISSING_DEPENDENCY (§25.2, §26.3);
|
|
35
|
+
* - the sender resolves to a descriptor on the chain: MISSING_DEPENDENCY
|
|
36
|
+
* (§10.5);
|
|
37
|
+
* - the package is signed by its sender (kid = sender): INVALID_SIGNATURE;
|
|
38
|
+
* - the sender held key/distribute and the recipient data/read, or an
|
|
39
|
+
* active invite grant (canDistributeKey, LFCP-021): AUTHORIZATION_FAILED.
|
|
40
|
+
*
|
|
41
|
+
* Opening the package and checking the DEK commitment come after this.
|
|
42
|
+
*/
|
|
43
|
+
export function verifyKeyPackage(view, parsed) {
|
|
44
|
+
const p = parsed.payload;
|
|
45
|
+
if (!bytesEqual(p.resourceId, view.state.resourceId))
|
|
46
|
+
return reject("OTHER_RESOURCE", "MALFORMED_MESSAGE", "the package is for another Resource");
|
|
47
|
+
const atHead = view.stateAt(p.controlHead);
|
|
48
|
+
if (atHead === undefined)
|
|
49
|
+
return reject("UNKNOWN_CONTROL_HEAD", "MISSING_DEPENDENCY", `Control Head ${toHex(p.controlHead)} is not on the chain`);
|
|
50
|
+
if (!atHead.epochs.has(String(p.dataEpoch)))
|
|
51
|
+
return reject("UNKNOWN_EPOCH", "MISSING_DEPENDENCY", `epoch ${p.dataEpoch} is not known at the package's head`);
|
|
52
|
+
// §10.5: the sender resolves from the whole chain; whether it held
|
|
53
|
+
// key/distribute at the head is the authority check below.
|
|
54
|
+
const sender = view.state.principals.get(toHex(expectedSignerOf(p)));
|
|
55
|
+
if (sender === undefined)
|
|
56
|
+
return reject("UNKNOWN_SENDER", "MISSING_DEPENDENCY", "no Control Record describes the sender (§10.5)");
|
|
57
|
+
const signature = verifySignedObject(parsed.signed, sender);
|
|
58
|
+
if (!signature.valid)
|
|
59
|
+
return reject("SIGNATURE", "INVALID_SIGNATURE", `the package is not signed by its sender (${signature.reason})`);
|
|
60
|
+
const authority = canDistributeKey(atHead, p.sender, p.recipient, p.dataEpoch);
|
|
61
|
+
if (!authority.allowed)
|
|
62
|
+
return reject("UNAUTHORIZED", "AUTHORIZATION_FAILED", authority.reason);
|
|
63
|
+
return Object.freeze({ kind: "authorized", sender });
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Creates a Key Package (§25): the DEK sealed with HPKE to the recipient's
|
|
67
|
+
* X25519 key, with the exact §25.1 info and AAD, in a payload signed by the
|
|
68
|
+
* sender. Sealing always uses a fresh ephemeral key. Whether the sender may
|
|
69
|
+
* distribute and the recipient may receive is checked by receivers
|
|
70
|
+
* (verifyKeyPackage); a sender can check it first with canDistributeKey.
|
|
71
|
+
*/
|
|
72
|
+
export async function sealKeyPackage(options) {
|
|
73
|
+
const epoch = dataEpoch(options.epoch);
|
|
74
|
+
const { enc, ciphertext } = await sealDek(options.recipient.x25519PublicKey, options.dek, keyPackageHpkeInfo(options.resourceId, epoch, options.recipient.principalId), keyPackageHpkeAad(options.resourceId, epoch, options.controlHead));
|
|
75
|
+
const payload = encode(cborMap([
|
|
76
|
+
[0, options.resourceId],
|
|
77
|
+
[1, epoch],
|
|
78
|
+
[2, options.recipient.principalId],
|
|
79
|
+
[3, options.controlHead],
|
|
80
|
+
[4, options.signer.descriptor.principalId],
|
|
81
|
+
[5, enc],
|
|
82
|
+
[6, ciphertext],
|
|
83
|
+
]));
|
|
84
|
+
const signed = signObject(payload, options.signer);
|
|
85
|
+
return Object.freeze({ bytes: signed.bytes, packageId: signed.id });
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Opens a Key Package for its named recipient and checks the DEK against
|
|
89
|
+
* the epoch's commitment (§25, §25.2). The caller passes the keys of the
|
|
90
|
+
* Principal the package names; there is no trying of other keys.
|
|
91
|
+
*
|
|
92
|
+
* Throws KEY_PACKAGE_RECIPIENT_MISMATCH (keys of another Principal),
|
|
93
|
+
* KEY_PACKAGE_OPEN_FAILED or DEK_COMMITMENT_MISMATCH. All are client-local:
|
|
94
|
+
* ignore the package and surface it (ADR 0001 N5).
|
|
95
|
+
*/
|
|
96
|
+
export async function openKeyPackage(parsed, recipient, expectedCommitment) {
|
|
97
|
+
const p = parsed.payload;
|
|
98
|
+
if (!bytesEqual(recipient.descriptor.principalId, p.recipient) ||
|
|
99
|
+
!bytesEqual(recipient.agreement.publicKey, recipient.descriptor.x25519PublicKey))
|
|
100
|
+
throw new LfcpError("KEY_PACKAGE_RECIPIENT_MISMATCH", "the keys given are not those of the package's recipient");
|
|
101
|
+
const dek = await openDek(recipient.agreement, p.hpkeEnc, p.hpkeCiphertext, keyPackageHpkeInfo(p.resourceId, p.dataEpoch, p.recipient), keyPackageHpkeAad(p.resourceId, p.dataEpoch, p.controlHead));
|
|
102
|
+
if (!bytesEqual(dekCommitment(p.resourceId, p.dataEpoch, dek), expectedCommitment))
|
|
103
|
+
throw new LfcpError("DEK_COMMITMENT_MISMATCH", "the opened DEK does not match the epoch's commitment");
|
|
104
|
+
return dek;
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* A received Key Package end to end: parse, the §25.2 checks at its head
|
|
108
|
+
* (verifyKeyPackage), then open it for `recipient` and check the DEK
|
|
109
|
+
* against the epoch's commitment from the chain.
|
|
110
|
+
*/
|
|
111
|
+
export async function receiveKeyPackage(view, bytes, recipient) {
|
|
112
|
+
const parsed = parseKeyPackage(bytes);
|
|
113
|
+
const check = verifyKeyPackage(view, parsed);
|
|
114
|
+
if (check.kind === "rejected")
|
|
115
|
+
return check;
|
|
116
|
+
const commitment = view.state.epochs.get(String(parsed.payload.dataEpoch))?.dekCommitment;
|
|
117
|
+
if (commitment === undefined)
|
|
118
|
+
return reject("UNKNOWN_EPOCH", "MISSING_DEPENDENCY", "the epoch has no known commitment");
|
|
119
|
+
try {
|
|
120
|
+
const dek = await openKeyPackage(parsed, recipient, commitment);
|
|
121
|
+
return Object.freeze({ kind: "opened", dek, epoch: parsed.payload.dataEpoch });
|
|
122
|
+
}
|
|
123
|
+
catch (e) {
|
|
124
|
+
if (e instanceof LfcpError &&
|
|
125
|
+
(e.code === "KEY_PACKAGE_RECIPIENT_MISMATCH" ||
|
|
126
|
+
e.code === "KEY_PACKAGE_OPEN_FAILED" ||
|
|
127
|
+
e.code === "DEK_COMMITMENT_MISMATCH"))
|
|
128
|
+
return Object.freeze({ kind: "ignored", code: e.code, message: e.message });
|
|
129
|
+
throw e;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
//# sourceMappingURL=key-package.js.map
|