@floegence/flowersec-core 2.5.3 → 3.0.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/README.md +46 -18
- package/dist/browser/connectSessionV3.d.ts +13 -0
- package/dist/browser/connectSessionV3.js +75 -0
- package/dist/browser/index.d.ts +3 -2
- package/dist/browser/index.js +2 -1
- package/dist/browser/v2.d.ts +3 -0
- package/dist/browser/v2.js +2 -0
- package/dist/facade.d.ts +15 -8
- package/dist/facade.js +5 -4
- package/dist/framing/jsonframe.d.ts +1 -1
- package/dist/framing/jsonframe.js +3 -1
- package/dist/interop/proxyServerPeer.js +1 -1
- package/dist/interop/serverParityPeer.js +8 -1
- package/dist/node/acceptor.d.ts +10 -0
- package/dist/node/acceptor.js +53 -7
- package/dist/node/acceptorV3.d.ts +57 -0
- package/dist/node/acceptorV3.js +227 -0
- package/dist/node/connectSessionV3.d.ts +21 -0
- package/dist/node/connectSessionV3.js +73 -0
- package/dist/node/index.d.ts +15 -10
- package/dist/node/index.js +6 -5
- package/dist/node/nativeTransportAddon.d.ts +39 -1
- package/dist/node/nativeTransportAddon.js +29 -5
- package/dist/node/rawQuicAdapterV3.d.ts +10 -0
- package/dist/node/rawQuicAdapterV3.js +76 -0
- package/dist/node/rawQuicServerV3.d.ts +24 -0
- package/dist/node/rawQuicServerV3.js +34 -0
- package/dist/node/runtimeAuthorizationV3.d.ts +29 -0
- package/dist/node/runtimeAuthorizationV3.js +126 -0
- package/dist/node/tunnelRuntimeV3.d.ts +57 -0
- package/dist/node/tunnelRuntimeV3.js +857 -0
- package/dist/node/v2.d.ts +11 -0
- package/dist/node/v2.js +6 -0
- package/dist/node/webSocketServerV3.d.ts +27 -0
- package/dist/node/webSocketServerV3.js +219 -0
- package/dist/proxy/runtime.js +39 -6
- package/dist/proxy/serviceWorker.d.ts +2 -0
- package/dist/proxy/serviceWorker.js +63 -7
- package/dist/proxy/windowBridge.d.ts +1 -0
- package/dist/proxy/windowBridge.js +93 -33
- package/dist/proxy/wsPatch.d.ts +1 -0
- package/dist/proxy/wsPatch.js +39 -6
- package/dist/public/streamHandlers.d.ts +3 -0
- package/dist/public/streamHandlers.js +6 -1
- package/dist/rpc/client.js +9 -7
- package/dist/rpc/server.js +44 -20
- package/dist/rpc/validate.d.ts +1 -0
- package/dist/rpc/validate.js +18 -6
- package/dist/v2/index.d.ts +14 -0
- package/dist/v2/index.js +7 -0
- package/dist/v2/protocol.js +23 -32
- package/dist/v2/publicSession.js +4 -0
- package/dist/v2/session.d.ts +5 -0
- package/dist/v2/session.js +94 -35
- package/dist/v3/admissionError.d.ts +4 -0
- package/dist/v3/admissionError.js +8 -0
- package/dist/v3/artifact.d.ts +145 -0
- package/dist/v3/artifact.js +1162 -0
- package/dist/v3/artifactLease.d.ts +17 -0
- package/dist/v3/artifactLease.js +109 -0
- package/dist/v3/browserRuntime.d.ts +37 -0
- package/dist/v3/browserRuntime.js +197 -0
- package/dist/v3/browserSessionRuntime.d.ts +2 -0
- package/dist/v3/browserSessionRuntime.js +21 -0
- package/dist/v3/capability.d.ts +32 -0
- package/dist/v3/capability.js +345 -0
- package/dist/v3/carrier.d.ts +84 -0
- package/dist/v3/carrier.js +411 -0
- package/dist/v3/connectionController.d.ts +87 -0
- package/dist/v3/connectionController.js +805 -0
- package/dist/v3/contract.d.ts +65 -0
- package/dist/v3/contract.js +3 -0
- package/dist/v3/controller.d.ts +43 -0
- package/dist/v3/controller.js +303 -0
- package/dist/v3/handshake.d.ts +81 -0
- package/dist/v3/handshake.js +471 -0
- package/dist/v3/jcs.d.ts +6 -0
- package/dist/v3/jcs.js +69 -0
- package/dist/v3/jsonPreflight.d.ts +1 -0
- package/dist/v3/jsonPreflight.js +251 -0
- package/dist/v3/nodeRuntime.d.ts +40 -0
- package/dist/v3/nodeRuntime.js +315 -0
- package/dist/v3/nodeSessionRuntime.d.ts +2 -0
- package/dist/v3/nodeSessionRuntime.js +13 -0
- package/dist/v3/protocol.d.ts +99 -0
- package/dist/v3/protocol.js +773 -0
- package/dist/v3/publicApi.d.ts +12 -0
- package/dist/v3/publicApi.js +46 -0
- package/dist/v3/publicSession.d.ts +1 -0
- package/dist/v3/publicSession.js +281 -0
- package/dist/v3/retryDisposition.d.ts +16 -0
- package/dist/v3/retryDisposition.js +30 -0
- package/dist/v3/runtimeAdapters.d.ts +9 -0
- package/dist/v3/runtimeAdapters.js +109 -0
- package/dist/v3/security.d.ts +34 -0
- package/dist/v3/security.js +100 -0
- package/dist/v3/serverAdmission.d.ts +38 -0
- package/dist/v3/serverAdmission.js +223 -0
- package/dist/v3/session.d.ts +318 -0
- package/dist/v3/session.js +2289 -0
- package/dist/v3/sessionConnector.d.ts +37 -0
- package/dist/v3/sessionConnector.js +440 -0
- package/dist/v3/streamLifetimeLedger.d.ts +31 -0
- package/dist/v3/streamLifetimeLedger.js +131 -0
- package/dist/v3/streamMetadata.d.ts +1 -0
- package/dist/v3/streamMetadata.js +7 -0
- package/dist/v3/transportConstants.d.ts +56 -0
- package/dist/v3/transportConstants.js +59 -0
- package/dist/v3/unreliableMessage.d.ts +44 -0
- package/dist/v3/unreliableMessage.js +284 -0
- package/dist/v3/webSocketCarrier.d.ts +31 -0
- package/dist/v3/webSocketCarrier.js +370 -0
- package/dist/v3/webTransportCarrier.d.ts +27 -0
- package/dist/v3/webTransportCarrier.js +534 -0
- package/dist/yamux/stream.d.ts +3 -0
- package/dist/yamux/stream.js +17 -4
- package/package.json +4 -4
- package/sbom/cyclonedx.json +6 -6
- package/sbom/spdx.json +11 -11
|
@@ -0,0 +1,1162 @@
|
|
|
1
|
+
import { sha256 } from "@noble/hashes/sha2.js";
|
|
2
|
+
import { base64urlDecode, base64urlEncode } from "../utils/base64url.js";
|
|
3
|
+
import { concatBytes, readU32be, u32be } from "../utils/bin.js";
|
|
4
|
+
import { toASCII } from "../vendor/tr46.js";
|
|
5
|
+
import { canonicalizeJCSV3 } from "./jcs.js";
|
|
6
|
+
import { preflightJSONV3 } from "./jsonPreflight.js";
|
|
7
|
+
import { FLOWERSEC_V3_CRYPTO_LABELS, FLOWERSEC_V3_PROFILE, wireProfileForPathV3, } from "./transportConstants.js";
|
|
8
|
+
export var AdmissionStatusV3;
|
|
9
|
+
(function (AdmissionStatusV3) {
|
|
10
|
+
AdmissionStatusV3[AdmissionStatusV3["Success"] = 0] = "Success";
|
|
11
|
+
AdmissionStatusV3[AdmissionStatusV3["Reject"] = 1] = "Reject";
|
|
12
|
+
AdmissionStatusV3[AdmissionStatusV3["Retryable"] = 2] = "Retryable";
|
|
13
|
+
})(AdmissionStatusV3 || (AdmissionStatusV3 = {}));
|
|
14
|
+
export class ArtifactV3Error extends Error {
|
|
15
|
+
code;
|
|
16
|
+
constructor(code, message) {
|
|
17
|
+
super(message);
|
|
18
|
+
this.name = "ArtifactV3Error";
|
|
19
|
+
this.code = code;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
const PROFILE = FLOWERSEC_V3_PROFILE;
|
|
23
|
+
const MAX_ARTIFACT_JSON_BYTES = 65_536;
|
|
24
|
+
const MAX_CANDIDATES = 4;
|
|
25
|
+
const MAX_CANONICAL_CANDIDATE_BYTES = 2_304;
|
|
26
|
+
const MAX_CANONICAL_CANDIDATE_SET_BYTES = 12 * 1_024;
|
|
27
|
+
const MAX_CANONICAL_FSB3_PAYLOAD = 32_768;
|
|
28
|
+
const MAX_ADMISSION_REASON_BYTES = 64;
|
|
29
|
+
const MAX_ADMISSION_CREDENTIAL_BYTES = 8_192;
|
|
30
|
+
const FSB3_HEADER_BYTES = 12;
|
|
31
|
+
const FSA3_HEADER_BYTES = 8;
|
|
32
|
+
const FORBIDDEN_FSA3_REASONS = new Set([
|
|
33
|
+
"browser_pin_opaque",
|
|
34
|
+
"ca_untrusted",
|
|
35
|
+
"pin_mismatch",
|
|
36
|
+
"pin_tls_unknown",
|
|
37
|
+
"tls_failed",
|
|
38
|
+
"tls_pin_mismatch",
|
|
39
|
+
"tls_policy_expired",
|
|
40
|
+
"tls_untrusted",
|
|
41
|
+
"tls_unsupported",
|
|
42
|
+
"transport_security_failed",
|
|
43
|
+
"transport_security_unsupported",
|
|
44
|
+
]);
|
|
45
|
+
const encoder = new TextEncoder();
|
|
46
|
+
const decoder = new TextDecoder("utf-8", { fatal: true });
|
|
47
|
+
const registryIDPattern = /^[A-Za-z0-9._~-]+$/;
|
|
48
|
+
const candidateIDPattern = /^[a-z0-9][a-z0-9._-]*$/;
|
|
49
|
+
const scopePattern = /^[a-z][a-z0-9._-]{0,63}$/;
|
|
50
|
+
const correlationKeyPattern = /^[a-z][a-z0-9._-]{0,31}$/;
|
|
51
|
+
export function computeSessionContractHashV3(session) {
|
|
52
|
+
validateSession(session);
|
|
53
|
+
const canonicalJSON = canonicalizeJCSV3({
|
|
54
|
+
allowed_suites: session.allowed_suites,
|
|
55
|
+
channel_id: session.channel_id,
|
|
56
|
+
default_suite: session.default_suite,
|
|
57
|
+
establish_timeout_seconds: session.establish_timeout_seconds,
|
|
58
|
+
idle_timeout_seconds: session.idle_timeout_seconds,
|
|
59
|
+
max_inbound_streams: session.max_inbound_streams,
|
|
60
|
+
profile: PROFILE,
|
|
61
|
+
rekey_completion_timeout_seconds: session.rekey_completion_timeout_seconds,
|
|
62
|
+
rekey_prepare_timeout_seconds: session.rekey_prepare_timeout_seconds,
|
|
63
|
+
selected_features: session.selected_features,
|
|
64
|
+
});
|
|
65
|
+
return labeledHash(FLOWERSEC_V3_CRYPTO_LABELS["session-contract"], canonicalJSON);
|
|
66
|
+
}
|
|
67
|
+
export function canonicalizeCandidatesV3(kind, candidates) {
|
|
68
|
+
if (kind !== "direct" && kind !== "tunnel")
|
|
69
|
+
throw invalidCandidate("path kind");
|
|
70
|
+
if (!Array.isArray(candidates) || candidates.length < 1 || candidates.length > MAX_CANDIDATES) {
|
|
71
|
+
throw invalidCandidate("candidate count");
|
|
72
|
+
}
|
|
73
|
+
const ids = new Set();
|
|
74
|
+
const endpoints = new Set();
|
|
75
|
+
const canonical = [];
|
|
76
|
+
for (const candidate of candidates) {
|
|
77
|
+
validateCandidateShape(candidate);
|
|
78
|
+
if (!candidateIDPattern.test(candidate.id) || utf8Length(candidate.id) > 64) {
|
|
79
|
+
throw invalidCandidate("candidate id");
|
|
80
|
+
}
|
|
81
|
+
if (ids.has(candidate.id))
|
|
82
|
+
throw invalidCandidate("duplicate candidate id");
|
|
83
|
+
ids.add(candidate.id);
|
|
84
|
+
if (utf8Length(candidate.url) < 1 || utf8Length(candidate.url) > 2_048) {
|
|
85
|
+
throw invalidCandidate("candidate URL length");
|
|
86
|
+
}
|
|
87
|
+
const normalizedURL = normalizeCandidateURL(kind, candidate.carrier, candidate.url);
|
|
88
|
+
if (utf8Length(normalizedURL) > 2_048)
|
|
89
|
+
throw invalidCandidate("normalized URL length");
|
|
90
|
+
if (candidate.normalized_url !== undefined && candidate.normalized_url !== normalizedURL) {
|
|
91
|
+
throw invalidCandidate("normalized URL mismatch");
|
|
92
|
+
}
|
|
93
|
+
const wireProfile = wireProfileForPathV3(kind);
|
|
94
|
+
if (candidate.wire_profile !== wireProfile)
|
|
95
|
+
throw invalidCandidate("wire profile");
|
|
96
|
+
validateTLSPolicy(candidate.tls, "invalid_candidate");
|
|
97
|
+
const endpoint = `${candidate.carrier}\0${kind}\0${normalizedURL}`;
|
|
98
|
+
if (endpoints.has(endpoint))
|
|
99
|
+
throw invalidCandidate("duplicate endpoint key");
|
|
100
|
+
endpoints.add(endpoint);
|
|
101
|
+
const item = {
|
|
102
|
+
carrier: candidate.carrier,
|
|
103
|
+
id: candidate.id,
|
|
104
|
+
normalized_url: normalizedURL,
|
|
105
|
+
tls: candidate.tls,
|
|
106
|
+
wire_profile: candidate.wire_profile,
|
|
107
|
+
};
|
|
108
|
+
if (utf8Length(canonicalizeJCSV3(item)) > MAX_CANONICAL_CANDIDATE_BYTES) {
|
|
109
|
+
throw invalidCandidate("canonical candidate too large");
|
|
110
|
+
}
|
|
111
|
+
canonical.push(item);
|
|
112
|
+
}
|
|
113
|
+
canonical.sort((left, right) => (left.id < right.id ? -1 : left.id > right.id ? 1 : 0));
|
|
114
|
+
const canonicalJSON = canonicalizeJCSV3(canonical);
|
|
115
|
+
if (utf8Length(canonicalJSON) > MAX_CANONICAL_CANDIDATE_SET_BYTES) {
|
|
116
|
+
throw invalidCandidate("canonical candidate set too large");
|
|
117
|
+
}
|
|
118
|
+
return {
|
|
119
|
+
candidates: canonical,
|
|
120
|
+
...labeledHash(FLOWERSEC_V3_CRYPTO_LABELS.candidates, canonicalJSON),
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
export function decodeArtifactV3JSON(raw) {
|
|
124
|
+
const text = decodeBoundedJSON(raw);
|
|
125
|
+
let value;
|
|
126
|
+
try {
|
|
127
|
+
value = parseStrictJSON(text, true);
|
|
128
|
+
}
|
|
129
|
+
catch (error) {
|
|
130
|
+
throw invalidArtifact(`strict JSON: ${errorMessage(error)}`);
|
|
131
|
+
}
|
|
132
|
+
let artifact;
|
|
133
|
+
let canonicalCandidates;
|
|
134
|
+
try {
|
|
135
|
+
artifact = decodeArtifactValue(value);
|
|
136
|
+
canonicalCandidates = validateArtifactV3(artifact);
|
|
137
|
+
if (text !== decoder.decode(encodeArtifactV3JSON(artifact)))
|
|
138
|
+
throw invalidArtifact("non-canonical JCS");
|
|
139
|
+
}
|
|
140
|
+
catch (error) {
|
|
141
|
+
if (error instanceof ArtifactV3Error && error.code === "invalid_candidate") {
|
|
142
|
+
throw invalidArtifact("candidate");
|
|
143
|
+
}
|
|
144
|
+
throw error;
|
|
145
|
+
}
|
|
146
|
+
const normalizedByID = new Map(canonicalCandidates.candidates.map((candidate) => [candidate.id, candidate.normalized_url]));
|
|
147
|
+
return {
|
|
148
|
+
...artifact,
|
|
149
|
+
path: {
|
|
150
|
+
...artifact.path,
|
|
151
|
+
candidates: artifact.path.candidates.map((candidate) => ({
|
|
152
|
+
...candidate,
|
|
153
|
+
normalized_url: normalizedByID.get(candidate.id),
|
|
154
|
+
})),
|
|
155
|
+
},
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
export function encodeArtifactV3JSON(artifact) {
|
|
159
|
+
validateArtifactV3(artifact);
|
|
160
|
+
const wire = {
|
|
161
|
+
v: artifact.v,
|
|
162
|
+
profile: artifact.profile,
|
|
163
|
+
session: artifactSessionWire(artifact.session),
|
|
164
|
+
path: artifact.path.kind === "direct" ? directPathWire(artifact.path) : tunnelPathWire(artifact.path),
|
|
165
|
+
scoped: artifact.scoped.map((scope) => ({
|
|
166
|
+
scope: scope.scope,
|
|
167
|
+
scope_version: scope.scope_version,
|
|
168
|
+
critical: scope.critical,
|
|
169
|
+
payload: scope.payload,
|
|
170
|
+
})),
|
|
171
|
+
correlation: {
|
|
172
|
+
v: artifact.correlation.v,
|
|
173
|
+
tags: artifact.correlation.tags.map((tag) => ({ key: tag.key, value: tag.value })),
|
|
174
|
+
},
|
|
175
|
+
};
|
|
176
|
+
const raw = encoder.encode(canonicalizeJCSV3(wire));
|
|
177
|
+
if (raw.length > MAX_ARTIFACT_JSON_BYTES) {
|
|
178
|
+
throw new ArtifactV3Error("artifact_too_large", "Flowersec v3 artifact is too large");
|
|
179
|
+
}
|
|
180
|
+
return raw;
|
|
181
|
+
}
|
|
182
|
+
export function validateArtifactV3(artifact) {
|
|
183
|
+
validateArtifactShape(artifact);
|
|
184
|
+
if (artifact.v !== 3 || artifact.profile !== PROFILE)
|
|
185
|
+
throw invalidArtifact("version or profile");
|
|
186
|
+
const sessionHash = computeSessionContractHashV3(artifact.session);
|
|
187
|
+
if (sessionHash.hashBase64URL !== artifact.session.contract_hash_b64u) {
|
|
188
|
+
throw invalidArtifact("session contract hash");
|
|
189
|
+
}
|
|
190
|
+
const candidateSet = canonicalizeCandidatesV3(artifact.path.kind, artifact.path.candidates);
|
|
191
|
+
if (!validRegistryID(artifact.path.rendezvous_group_id, 128) || !validRegistryID(artifact.path.listener_audience, 128)) {
|
|
192
|
+
throw invalidArtifact("rendezvous group or listener audience");
|
|
193
|
+
}
|
|
194
|
+
if (artifact.path.kind === "direct") {
|
|
195
|
+
if (!validASCII(artifact.path.routing_token, MAX_ADMISSION_CREDENTIAL_BYTES)) {
|
|
196
|
+
throw invalidArtifact("direct path variant");
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
else {
|
|
200
|
+
if ((artifact.path.role !== 1 && artifact.path.role !== 2) ||
|
|
201
|
+
!validRegistryID(artifact.path.local_endpoint_instance_id, 128) ||
|
|
202
|
+
!validRegistryID(artifact.path.expected_peer_endpoint_instance_id, 128) ||
|
|
203
|
+
artifact.path.local_endpoint_instance_id === artifact.path.expected_peer_endpoint_instance_id ||
|
|
204
|
+
!validASCII(artifact.path.token, MAX_ADMISSION_CREDENTIAL_BYTES)) {
|
|
205
|
+
throw invalidArtifact("tunnel path variant");
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
validateScopes(artifact.scoped);
|
|
209
|
+
validateCorrelationShape(artifact.correlation);
|
|
210
|
+
for (const candidate of candidateSet.candidates) {
|
|
211
|
+
const request = requestFromValidatedArtifact(artifact, candidateSet, candidate.id);
|
|
212
|
+
if (marshalFSB3Payload(request).length > MAX_CANONICAL_FSB3_PAYLOAD) {
|
|
213
|
+
throw new ArtifactV3Error("fsb3_payload_too_large", "FSB3 canonical payload is too large");
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
return candidateSet;
|
|
217
|
+
}
|
|
218
|
+
export function buildFSB3RequestV3(artifact, chosenCandidateID) {
|
|
219
|
+
const candidateSet = validateArtifactV3(artifact);
|
|
220
|
+
const request = requestFromValidatedArtifact(artifact, candidateSet, chosenCandidateID);
|
|
221
|
+
validateFSB3Request(request);
|
|
222
|
+
return request;
|
|
223
|
+
}
|
|
224
|
+
export function encodeFSB3RequestV3(request) {
|
|
225
|
+
validateFSB3Request(request);
|
|
226
|
+
const payload = marshalFSB3Payload(request);
|
|
227
|
+
if (payload.length > MAX_CANONICAL_FSB3_PAYLOAD) {
|
|
228
|
+
throw new ArtifactV3Error("fsb3_payload_too_large", "FSB3 canonical payload is too large");
|
|
229
|
+
}
|
|
230
|
+
const out = new Uint8Array(FSB3_HEADER_BYTES + payload.length);
|
|
231
|
+
out.set(encoder.encode("FSB3"), 0);
|
|
232
|
+
out[4] = 3;
|
|
233
|
+
out[5] = request.pathKind === "direct" ? 1 : 2;
|
|
234
|
+
out.set(u32be(payload.length), 8);
|
|
235
|
+
out.set(payload, FSB3_HEADER_BYTES);
|
|
236
|
+
return out;
|
|
237
|
+
}
|
|
238
|
+
export function decodeFSB3RequestV3(raw) {
|
|
239
|
+
if (raw.length < FSB3_HEADER_BYTES)
|
|
240
|
+
throw invalidFSB3("truncated header");
|
|
241
|
+
if (raw[0] !== 0x46 ||
|
|
242
|
+
raw[1] !== 0x53 ||
|
|
243
|
+
raw[2] !== 0x42 ||
|
|
244
|
+
raw[3] !== 0x33 ||
|
|
245
|
+
raw[4] !== 3 ||
|
|
246
|
+
raw[6] !== 0 ||
|
|
247
|
+
raw[7] !== 0) {
|
|
248
|
+
throw invalidFSB3("header");
|
|
249
|
+
}
|
|
250
|
+
const pathKind = pathKindFromCode(raw[5]);
|
|
251
|
+
const payloadLength = readU32be(raw, 8);
|
|
252
|
+
if (payloadLength > MAX_CANONICAL_FSB3_PAYLOAD) {
|
|
253
|
+
throw new ArtifactV3Error("fsb3_payload_too_large", "FSB3 canonical payload is too large");
|
|
254
|
+
}
|
|
255
|
+
if (payloadLength === 0 || raw.length !== FSB3_HEADER_BYTES + payloadLength) {
|
|
256
|
+
throw invalidFSB3("payload length");
|
|
257
|
+
}
|
|
258
|
+
const payload = raw.subarray(FSB3_HEADER_BYTES);
|
|
259
|
+
let text;
|
|
260
|
+
let value;
|
|
261
|
+
try {
|
|
262
|
+
text = decoder.decode(payload);
|
|
263
|
+
value = parseStrictJSON(text);
|
|
264
|
+
}
|
|
265
|
+
catch (error) {
|
|
266
|
+
throw invalidFSB3(`strict JSON: ${errorMessage(error)}`);
|
|
267
|
+
}
|
|
268
|
+
const request = decodeFSB3Value(pathKind, value);
|
|
269
|
+
validateFSB3Request(request);
|
|
270
|
+
if (!bytesEqual(payload, marshalFSB3Payload(request))) {
|
|
271
|
+
throw new ArtifactV3Error("noncanonical_fsb3", "FSB3 payload is not canonical");
|
|
272
|
+
}
|
|
273
|
+
const copied = new Uint8Array(raw);
|
|
274
|
+
return {
|
|
275
|
+
request,
|
|
276
|
+
raw: copied,
|
|
277
|
+
localAdmissionBinding: admissionBindingV3(copied),
|
|
278
|
+
};
|
|
279
|
+
}
|
|
280
|
+
export function admissionBindingV3(rawFSB3) {
|
|
281
|
+
return sha256(concatBytes([encoder.encode(FLOWERSEC_V3_CRYPTO_LABELS.admission), rawFSB3]));
|
|
282
|
+
}
|
|
283
|
+
export function acceptorAdmissionsHashV3(framesByCandidateID) {
|
|
284
|
+
if (framesByCandidateID.size < 1 || framesByCandidateID.size > MAX_CANDIDATES) {
|
|
285
|
+
throw invalidFSB3("acceptor admissions count");
|
|
286
|
+
}
|
|
287
|
+
const ordered = [...framesByCandidateID.entries()].sort(([left], [right]) => left < right ? -1 : left > right ? 1 : 0);
|
|
288
|
+
const parts = [encoder.encode(FLOWERSEC_V3_CRYPTO_LABELS["acceptor-admissions"])];
|
|
289
|
+
let previous = "";
|
|
290
|
+
for (const [candidateID, frame] of ordered) {
|
|
291
|
+
if (!candidateIDPattern.test(candidateID) || candidateID <= previous)
|
|
292
|
+
throw invalidFSB3("candidate ordering");
|
|
293
|
+
const decoded = decodeFSB3RequestV3(frame);
|
|
294
|
+
if (decoded.request.chosen_candidate_id !== candidateID)
|
|
295
|
+
throw invalidFSB3("chosen candidate binding");
|
|
296
|
+
parts.push(u32be(frame.length), frame);
|
|
297
|
+
previous = candidateID;
|
|
298
|
+
}
|
|
299
|
+
return sha256(concatBytes(parts));
|
|
300
|
+
}
|
|
301
|
+
// Server-side encoding requires an audited registry for every rejection.
|
|
302
|
+
// Success has no reason and therefore needs no registry entry.
|
|
303
|
+
export function encodeFSA3ResponseV3(response, admissionReasons = new Set()) {
|
|
304
|
+
validateFSA3Response(response, admissionReasons, true);
|
|
305
|
+
const reason = encoder.encode(response.reason);
|
|
306
|
+
const out = new Uint8Array(FSA3_HEADER_BYTES + reason.length);
|
|
307
|
+
out.set(encoder.encode("FSA3"), 0);
|
|
308
|
+
out[4] = 3;
|
|
309
|
+
out[5] = response.status;
|
|
310
|
+
new DataView(out.buffer).setUint16(6, reason.length, false);
|
|
311
|
+
out.set(reason, FSA3_HEADER_BYTES);
|
|
312
|
+
return out;
|
|
313
|
+
}
|
|
314
|
+
// Clients accept any canonical bounded rejection token. Deployment policy is
|
|
315
|
+
// server-owned and is not duplicated as a client-side reason registry.
|
|
316
|
+
export function decodeFSA3ResponseV3(raw) {
|
|
317
|
+
if (raw.length < FSA3_HEADER_BYTES)
|
|
318
|
+
throw invalidFSA3("truncated header");
|
|
319
|
+
if (raw[0] !== 0x46 || raw[1] !== 0x53 || raw[2] !== 0x41 || raw[3] !== 0x33 || raw[4] !== 3) {
|
|
320
|
+
throw invalidFSA3("header");
|
|
321
|
+
}
|
|
322
|
+
const reasonLength = new DataView(raw.buffer, raw.byteOffset, raw.byteLength).getUint16(6, false);
|
|
323
|
+
if (reasonLength > MAX_ADMISSION_REASON_BYTES || raw.length !== FSA3_HEADER_BYTES + reasonLength) {
|
|
324
|
+
throw invalidFSA3("reason length");
|
|
325
|
+
}
|
|
326
|
+
let reason;
|
|
327
|
+
try {
|
|
328
|
+
reason = decoder.decode(raw.subarray(FSA3_HEADER_BYTES));
|
|
329
|
+
}
|
|
330
|
+
catch {
|
|
331
|
+
throw invalidFSA3("reason encoding");
|
|
332
|
+
}
|
|
333
|
+
const response = { status: raw[5], reason };
|
|
334
|
+
validateFSA3Response(response, new Set(), false);
|
|
335
|
+
return response;
|
|
336
|
+
}
|
|
337
|
+
function validateSession(session) {
|
|
338
|
+
assertRecord(session, "session", "invalid_artifact");
|
|
339
|
+
assertExactKeys(session, [
|
|
340
|
+
"channel_id",
|
|
341
|
+
"init_expire_at_unix_s",
|
|
342
|
+
"idle_timeout_seconds",
|
|
343
|
+
"establish_timeout_seconds",
|
|
344
|
+
"rekey_prepare_timeout_seconds",
|
|
345
|
+
"rekey_completion_timeout_seconds",
|
|
346
|
+
"max_inbound_streams",
|
|
347
|
+
"e2ee_psk_b64u",
|
|
348
|
+
"allowed_suites",
|
|
349
|
+
"default_suite",
|
|
350
|
+
"selected_features",
|
|
351
|
+
"contract_hash_b64u",
|
|
352
|
+
], "session", "invalid_artifact");
|
|
353
|
+
if (!validRegistryID(session.channel_id, 128))
|
|
354
|
+
throw invalidArtifact("channel_id");
|
|
355
|
+
assertSafeInteger(session.init_expire_at_unix_s, 1, Number.MAX_SAFE_INTEGER, "init expiry");
|
|
356
|
+
assertSafeInteger(session.idle_timeout_seconds, 0, 0xffff_ffff, "idle timeout");
|
|
357
|
+
if (session.establish_timeout_seconds !== 30 ||
|
|
358
|
+
session.rekey_prepare_timeout_seconds !== 10 ||
|
|
359
|
+
session.rekey_completion_timeout_seconds !== 30) {
|
|
360
|
+
throw invalidArtifact("fixed session timing");
|
|
361
|
+
}
|
|
362
|
+
assertSafeInteger(session.max_inbound_streams, 1, 128, "max inbound streams");
|
|
363
|
+
assertCanonical32(session.e2ee_psk_b64u, "e2ee PSK", "invalid_artifact");
|
|
364
|
+
assertCanonical32(session.contract_hash_b64u, "contract hash", "invalid_artifact");
|
|
365
|
+
if (!Array.isArray(session.allowed_suites) || session.allowed_suites.length < 1) {
|
|
366
|
+
throw invalidArtifact("allowed suites");
|
|
367
|
+
}
|
|
368
|
+
let previous = 0;
|
|
369
|
+
const suites = new Set();
|
|
370
|
+
for (const suite of session.allowed_suites) {
|
|
371
|
+
if ((suite !== 1 && suite !== 2) || suite <= previous || suites.has(suite)) {
|
|
372
|
+
throw invalidArtifact("allowed suites");
|
|
373
|
+
}
|
|
374
|
+
previous = suite;
|
|
375
|
+
suites.add(suite);
|
|
376
|
+
}
|
|
377
|
+
if (!suites.has(session.default_suite))
|
|
378
|
+
throw invalidArtifact("default suite");
|
|
379
|
+
if (session.selected_features !== 0)
|
|
380
|
+
throw invalidArtifact("selected features");
|
|
381
|
+
}
|
|
382
|
+
function validateArtifactShape(artifact) {
|
|
383
|
+
assertRecord(artifact, "artifact", "invalid_artifact");
|
|
384
|
+
assertExactKeys(artifact, ["v", "profile", "session", "path", "scoped", "correlation"], "artifact", "invalid_artifact");
|
|
385
|
+
validateSession(artifact.session);
|
|
386
|
+
validatePathShape(artifact.path, true);
|
|
387
|
+
if (!Array.isArray(artifact.scoped))
|
|
388
|
+
throw invalidArtifact("scoped");
|
|
389
|
+
validateCorrelationShape(artifact.correlation);
|
|
390
|
+
}
|
|
391
|
+
function validatePathShape(path, allowNormalized) {
|
|
392
|
+
assertRecord(path, "path", "invalid_artifact");
|
|
393
|
+
if (path.kind === "direct") {
|
|
394
|
+
assertExactKeys(path, ["kind", "rendezvous_group_id", "listener_audience", "routing_token", "candidates"], "direct path", "invalid_artifact");
|
|
395
|
+
}
|
|
396
|
+
else if (path.kind === "tunnel") {
|
|
397
|
+
assertExactKeys(path, [
|
|
398
|
+
"kind",
|
|
399
|
+
"rendezvous_group_id",
|
|
400
|
+
"listener_audience",
|
|
401
|
+
"role",
|
|
402
|
+
"local_endpoint_instance_id",
|
|
403
|
+
"expected_peer_endpoint_instance_id",
|
|
404
|
+
"token",
|
|
405
|
+
"candidates",
|
|
406
|
+
], "tunnel path", "invalid_artifact");
|
|
407
|
+
}
|
|
408
|
+
else {
|
|
409
|
+
throw invalidArtifact("path kind");
|
|
410
|
+
}
|
|
411
|
+
if (!Array.isArray(path.candidates))
|
|
412
|
+
throw invalidArtifact("candidates");
|
|
413
|
+
for (const candidate of path.candidates)
|
|
414
|
+
validateCandidateShape(candidate, allowNormalized);
|
|
415
|
+
}
|
|
416
|
+
function validateCandidateShape(candidate, allowNormalized = true) {
|
|
417
|
+
assertRecord(candidate, "candidate", "invalid_candidate");
|
|
418
|
+
const keys = allowNormalized
|
|
419
|
+
? ["id", "carrier", "url", "wire_profile", "tls", ...(candidate.normalized_url === undefined ? [] : ["normalized_url"])]
|
|
420
|
+
: ["id", "carrier", "url", "wire_profile", "tls"];
|
|
421
|
+
assertExactKeys(candidate, keys, "candidate", "invalid_candidate");
|
|
422
|
+
if (typeof candidate.id !== "string" ||
|
|
423
|
+
typeof candidate.carrier !== "string" ||
|
|
424
|
+
typeof candidate.url !== "string" ||
|
|
425
|
+
typeof candidate.wire_profile !== "string" ||
|
|
426
|
+
(candidate.normalized_url !== undefined && typeof candidate.normalized_url !== "string")) {
|
|
427
|
+
throw invalidCandidate("candidate field type");
|
|
428
|
+
}
|
|
429
|
+
validateTLSPolicy(candidate.tls, "invalid_candidate");
|
|
430
|
+
}
|
|
431
|
+
export function tlsPolicyDigestV3(policy) {
|
|
432
|
+
validateTLSPolicy(policy, "invalid_artifact");
|
|
433
|
+
return labeledHash("flowersec-v3-tls-policy\0", canonicalizeJCSV3(policy));
|
|
434
|
+
}
|
|
435
|
+
export function activePinsV3(policy, attemptNowUnixSeconds) {
|
|
436
|
+
validateTLSPolicy(policy, "invalid_artifact");
|
|
437
|
+
assertSafeInteger(attemptNowUnixSeconds, 0, Number.MAX_SAFE_INTEGER, "attempt time");
|
|
438
|
+
return policy.mode === "pin"
|
|
439
|
+
? Object.freeze(policy.pins.filter((pin) => attemptNowUnixSeconds < pin.not_after_unix_s))
|
|
440
|
+
: Object.freeze([]);
|
|
441
|
+
}
|
|
442
|
+
function validateTLSPolicy(value, code) {
|
|
443
|
+
const policy = requireRecord(value, "TLS policy", code);
|
|
444
|
+
if (policy.mode === "ca") {
|
|
445
|
+
assertExactKeys(policy, ["mode"], "CA TLS policy", code);
|
|
446
|
+
return;
|
|
447
|
+
}
|
|
448
|
+
if (policy.mode !== "pin")
|
|
449
|
+
throw codecError(code, "TLS policy mode");
|
|
450
|
+
assertExactKeys(policy, ["mode", "pins"], "pin TLS policy", code);
|
|
451
|
+
if (!Array.isArray(policy.pins) || policy.pins.length < 1 || policy.pins.length > 4) {
|
|
452
|
+
throw codecError(code, "pin count");
|
|
453
|
+
}
|
|
454
|
+
let previous = "";
|
|
455
|
+
const seen = new Set();
|
|
456
|
+
for (const input of policy.pins) {
|
|
457
|
+
const pin = requireRecord(input, "pin", code);
|
|
458
|
+
assertExactKeys(pin, ["algorithm", "value_b64u", "not_after_unix_s"], "pin", code);
|
|
459
|
+
if (pin.algorithm !== "sha-256")
|
|
460
|
+
throw codecError(code, "pin algorithm");
|
|
461
|
+
const encoded = requireString(pin.value_b64u, "pin value", code);
|
|
462
|
+
assertCanonical32(encoded, "pin value", code);
|
|
463
|
+
assertSafeInteger(pin.not_after_unix_s, 1, Number.MAX_SAFE_INTEGER, "pin expiry", code);
|
|
464
|
+
if (encoded <= previous || seen.has(encoded))
|
|
465
|
+
throw codecError(code, "pin ordering or duplicate");
|
|
466
|
+
previous = encoded;
|
|
467
|
+
seen.add(encoded);
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
function decodeTLSPolicyValue(value, code) {
|
|
471
|
+
validateTLSPolicy(value, code);
|
|
472
|
+
if (value.mode === "ca")
|
|
473
|
+
return Object.freeze({ mode: "ca" });
|
|
474
|
+
return Object.freeze({
|
|
475
|
+
mode: "pin",
|
|
476
|
+
pins: Object.freeze(value.pins.map((pin) => Object.freeze({ ...pin }))),
|
|
477
|
+
});
|
|
478
|
+
}
|
|
479
|
+
function validateScopes(scopes) {
|
|
480
|
+
if (!Array.isArray(scopes) || scopes.length > 8)
|
|
481
|
+
throw invalidArtifact("scoped");
|
|
482
|
+
const seen = new Set();
|
|
483
|
+
for (const scope of scopes) {
|
|
484
|
+
assertRecord(scope, "scope", "invalid_artifact");
|
|
485
|
+
assertExactKeys(scope, ["scope", "scope_version", "critical", "payload"], "scope", "invalid_artifact");
|
|
486
|
+
const scopeVersion = scope.scope_version;
|
|
487
|
+
if (typeof scope.scope !== "string" ||
|
|
488
|
+
!scopePattern.test(scope.scope) ||
|
|
489
|
+
typeof scopeVersion !== "number" ||
|
|
490
|
+
!Number.isInteger(scopeVersion) ||
|
|
491
|
+
scopeVersion < 1 ||
|
|
492
|
+
scopeVersion > 0xffff ||
|
|
493
|
+
typeof scope.critical !== "boolean" ||
|
|
494
|
+
!isRecord(scope.payload)) {
|
|
495
|
+
throw invalidArtifact("scope metadata");
|
|
496
|
+
}
|
|
497
|
+
validateScopedPayload(scope.payload);
|
|
498
|
+
if (seen.has(scope.scope))
|
|
499
|
+
throw invalidArtifact("duplicate scope");
|
|
500
|
+
seen.add(scope.scope);
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
function validateScopedPayload(payload) {
|
|
504
|
+
let nodes = 0;
|
|
505
|
+
const visit = (value, depth) => {
|
|
506
|
+
nodes += 1;
|
|
507
|
+
if (depth > 16 || nodes > 256)
|
|
508
|
+
throw invalidArtifact("scope payload resources");
|
|
509
|
+
if (value === null || typeof value === "boolean")
|
|
510
|
+
return;
|
|
511
|
+
if (typeof value === "number") {
|
|
512
|
+
if (!Number.isSafeInteger(value) || Object.is(value, -0))
|
|
513
|
+
throw invalidArtifact("scope payload number");
|
|
514
|
+
return;
|
|
515
|
+
}
|
|
516
|
+
if (typeof value === "string") {
|
|
517
|
+
if (utf8Length(value) > 1_024)
|
|
518
|
+
throw invalidArtifact("scope payload string");
|
|
519
|
+
return;
|
|
520
|
+
}
|
|
521
|
+
if (Array.isArray(value)) {
|
|
522
|
+
if (value.length > 64)
|
|
523
|
+
throw invalidArtifact("scope payload array");
|
|
524
|
+
for (const item of value)
|
|
525
|
+
visit(item, depth + 1);
|
|
526
|
+
return;
|
|
527
|
+
}
|
|
528
|
+
if (!isRecord(value))
|
|
529
|
+
throw invalidArtifact("scope payload value");
|
|
530
|
+
const entries = Object.entries(value);
|
|
531
|
+
if (entries.length > 64)
|
|
532
|
+
throw invalidArtifact("scope payload members");
|
|
533
|
+
for (const [key, item] of entries) {
|
|
534
|
+
if (utf8Length(key) > 128)
|
|
535
|
+
throw invalidArtifact("scope payload key");
|
|
536
|
+
visit(item, depth + 1);
|
|
537
|
+
}
|
|
538
|
+
};
|
|
539
|
+
visit(payload, 1);
|
|
540
|
+
let canonical;
|
|
541
|
+
try {
|
|
542
|
+
canonical = canonicalizeJCSV3(payload);
|
|
543
|
+
}
|
|
544
|
+
catch {
|
|
545
|
+
throw invalidArtifact("scope payload JCS value");
|
|
546
|
+
}
|
|
547
|
+
if (utf8Length(canonical) > 4_096)
|
|
548
|
+
throw invalidArtifact("scope payload bytes");
|
|
549
|
+
}
|
|
550
|
+
function validateCorrelationShape(correlation) {
|
|
551
|
+
assertRecord(correlation, "correlation", "invalid_artifact");
|
|
552
|
+
assertExactKeys(correlation, ["v", "tags"], "correlation", "invalid_artifact");
|
|
553
|
+
if (correlation.v !== 3 || !Array.isArray(correlation.tags) || correlation.tags.length > 8) {
|
|
554
|
+
throw invalidArtifact("correlation");
|
|
555
|
+
}
|
|
556
|
+
const seen = new Set();
|
|
557
|
+
for (const tag of correlation.tags) {
|
|
558
|
+
assertRecord(tag, "correlation tag", "invalid_artifact");
|
|
559
|
+
assertExactKeys(tag, ["key", "value"], "correlation tag", "invalid_artifact");
|
|
560
|
+
if (typeof tag.key !== "string" ||
|
|
561
|
+
typeof tag.value !== "string" ||
|
|
562
|
+
!correlationKeyPattern.test(tag.key) ||
|
|
563
|
+
!validASCII(tag.value, 128) ||
|
|
564
|
+
seen.has(tag.key)) {
|
|
565
|
+
throw invalidArtifact("correlation tag");
|
|
566
|
+
}
|
|
567
|
+
seen.add(tag.key);
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
function normalizeCandidateURL(kind, carrier, raw) {
|
|
571
|
+
if (/[\\?#%]/.test(raw))
|
|
572
|
+
throw invalidCandidate("forbidden URL component");
|
|
573
|
+
const separator = raw.indexOf("://");
|
|
574
|
+
if (separator <= 0)
|
|
575
|
+
throw invalidCandidate("absolute URL");
|
|
576
|
+
const scheme = raw.slice(0, separator).toLowerCase();
|
|
577
|
+
const remainder = raw.slice(separator + 3);
|
|
578
|
+
const pathAt = remainder.indexOf("/");
|
|
579
|
+
const authority = pathAt < 0 ? remainder : remainder.slice(0, pathAt);
|
|
580
|
+
let path = pathAt < 0 ? "" : remainder.slice(pathAt);
|
|
581
|
+
if (authority === "" || authority.includes("@"))
|
|
582
|
+
throw invalidCandidate("URL authority");
|
|
583
|
+
const normalizedAuthority = normalizeAuthority(authority);
|
|
584
|
+
let expectedScheme;
|
|
585
|
+
let expectedPath = "";
|
|
586
|
+
switch (carrier) {
|
|
587
|
+
case "websocket":
|
|
588
|
+
expectedScheme = "wss";
|
|
589
|
+
expectedPath = `/flowersec/v3/${kind}`;
|
|
590
|
+
break;
|
|
591
|
+
case "raw_quic":
|
|
592
|
+
expectedScheme = "quic";
|
|
593
|
+
if (path !== "" && path !== "/")
|
|
594
|
+
throw invalidCandidate("raw QUIC path");
|
|
595
|
+
path = "";
|
|
596
|
+
break;
|
|
597
|
+
case "webtransport":
|
|
598
|
+
expectedScheme = "https";
|
|
599
|
+
expectedPath = `/flowersec/webtransport/v3/${kind}`;
|
|
600
|
+
break;
|
|
601
|
+
default:
|
|
602
|
+
throw invalidCandidate("carrier registry");
|
|
603
|
+
}
|
|
604
|
+
if (scheme !== expectedScheme)
|
|
605
|
+
throw invalidCandidate("carrier scheme");
|
|
606
|
+
if (carrier !== "raw_quic" && path !== expectedPath)
|
|
607
|
+
throw invalidCandidate("carrier URL path");
|
|
608
|
+
return `${scheme}://${normalizedAuthority}${path}`;
|
|
609
|
+
}
|
|
610
|
+
function normalizeAuthority(authority) {
|
|
611
|
+
let host;
|
|
612
|
+
let portText = "";
|
|
613
|
+
if (authority.startsWith("[")) {
|
|
614
|
+
const closing = authority.indexOf("]");
|
|
615
|
+
if (closing < 0 || authority.indexOf("]", closing + 1) >= 0)
|
|
616
|
+
throw invalidCandidate("IPv6 authority");
|
|
617
|
+
host = authority.slice(1, closing);
|
|
618
|
+
const tail = authority.slice(closing + 1);
|
|
619
|
+
if (tail !== "") {
|
|
620
|
+
if (!tail.startsWith(":") || tail.length === 1)
|
|
621
|
+
throw invalidCandidate("IPv6 port");
|
|
622
|
+
portText = tail.slice(1);
|
|
623
|
+
}
|
|
624
|
+
if (host.includes("."))
|
|
625
|
+
throw invalidCandidate("IPv6 dotted subset");
|
|
626
|
+
let parsed;
|
|
627
|
+
try {
|
|
628
|
+
parsed = new URL(`http://[${host}]/`);
|
|
629
|
+
}
|
|
630
|
+
catch {
|
|
631
|
+
throw invalidCandidate("IPv6 host");
|
|
632
|
+
}
|
|
633
|
+
const canonical = parsed.hostname;
|
|
634
|
+
if (!canonical.startsWith("[") || !canonical.endsWith("]"))
|
|
635
|
+
throw invalidCandidate("IPv6 host");
|
|
636
|
+
host = canonical.toLowerCase();
|
|
637
|
+
}
|
|
638
|
+
else {
|
|
639
|
+
if ((authority.match(/:/g)?.length ?? 0) > 1)
|
|
640
|
+
throw invalidCandidate("unbracketed IPv6");
|
|
641
|
+
const colon = authority.lastIndexOf(":");
|
|
642
|
+
if (colon >= 0) {
|
|
643
|
+
host = authority.slice(0, colon);
|
|
644
|
+
portText = authority.slice(colon + 1);
|
|
645
|
+
if (portText === "")
|
|
646
|
+
throw invalidCandidate("empty port");
|
|
647
|
+
}
|
|
648
|
+
else {
|
|
649
|
+
host = authority;
|
|
650
|
+
}
|
|
651
|
+
host = normalizeDNSOrIPv4(host);
|
|
652
|
+
}
|
|
653
|
+
if (portText === "")
|
|
654
|
+
return host;
|
|
655
|
+
if (!/^\d+$/.test(portText))
|
|
656
|
+
throw invalidCandidate("port");
|
|
657
|
+
const port = Number(portText);
|
|
658
|
+
if (!Number.isInteger(port) || port < 1 || port > 0xffff)
|
|
659
|
+
throw invalidCandidate("port");
|
|
660
|
+
return port === 443 ? host : `${host}:${port}`;
|
|
661
|
+
}
|
|
662
|
+
function normalizeDNSOrIPv4(host) {
|
|
663
|
+
if (host === "" || host.endsWith("."))
|
|
664
|
+
throw invalidCandidate("DNS host");
|
|
665
|
+
const lower = host.toLowerCase();
|
|
666
|
+
if (/^[0-9.]+$/.test(lower)) {
|
|
667
|
+
const parts = lower.split(".");
|
|
668
|
+
if (parts.length !== 4 ||
|
|
669
|
+
parts.some((part) => !/^(0|[1-9]\d{0,2})$/.test(part) || Number(part) > 255)) {
|
|
670
|
+
throw invalidCandidate("IPv4 host");
|
|
671
|
+
}
|
|
672
|
+
return parts.map((part) => String(Number(part))).join(".");
|
|
673
|
+
}
|
|
674
|
+
const ascii = toASCII(host, {
|
|
675
|
+
checkHyphens: true,
|
|
676
|
+
checkBidi: true,
|
|
677
|
+
checkJoiners: true,
|
|
678
|
+
useSTD3ASCIIRules: true,
|
|
679
|
+
verifyDNSLength: true,
|
|
680
|
+
transitionalProcessing: false,
|
|
681
|
+
});
|
|
682
|
+
if (ascii === null || ascii.endsWith("."))
|
|
683
|
+
throw invalidCandidate("DNS label");
|
|
684
|
+
const normalized = ascii.toLowerCase();
|
|
685
|
+
const finalLabel = normalized.slice(normalized.lastIndexOf(".") + 1);
|
|
686
|
+
if (/^\d+$/.test(finalLabel) || /^0x[0-9a-f]*$/i.test(finalLabel)) {
|
|
687
|
+
throw invalidCandidate("WHATWG numeric host ambiguity");
|
|
688
|
+
}
|
|
689
|
+
return normalized;
|
|
690
|
+
}
|
|
691
|
+
function requestFromValidatedArtifact(artifact, candidateSet, chosenCandidateID) {
|
|
692
|
+
const common = {
|
|
693
|
+
profile: PROFILE,
|
|
694
|
+
channel_id: artifact.session.channel_id,
|
|
695
|
+
session_contract_hash_b64u: artifact.session.contract_hash_b64u,
|
|
696
|
+
rendezvous_group_id: artifact.path.rendezvous_group_id,
|
|
697
|
+
candidates: candidateSet.candidates,
|
|
698
|
+
candidate_set_hash_b64u: candidateSet.hashBase64URL,
|
|
699
|
+
chosen_candidate_id: chosenCandidateID,
|
|
700
|
+
listener_audience: artifact.path.listener_audience,
|
|
701
|
+
};
|
|
702
|
+
if (artifact.path.kind === "direct") {
|
|
703
|
+
return { pathKind: "direct", ...common, routing_token: artifact.path.routing_token };
|
|
704
|
+
}
|
|
705
|
+
return {
|
|
706
|
+
pathKind: "tunnel",
|
|
707
|
+
...common,
|
|
708
|
+
role: artifact.path.role,
|
|
709
|
+
endpoint_instance_id: artifact.path.local_endpoint_instance_id,
|
|
710
|
+
attach_token: artifact.path.token,
|
|
711
|
+
};
|
|
712
|
+
}
|
|
713
|
+
function validateFSB3Request(request) {
|
|
714
|
+
try {
|
|
715
|
+
if (request.pathKind !== "direct" && request.pathKind !== "tunnel")
|
|
716
|
+
throw new Error("path kind");
|
|
717
|
+
if (request.profile !== PROFILE)
|
|
718
|
+
throw new Error("profile");
|
|
719
|
+
if (!validRegistryID(request.channel_id, 128) ||
|
|
720
|
+
!validRegistryID(request.rendezvous_group_id, 128) ||
|
|
721
|
+
!validRegistryID(request.listener_audience, 128)) {
|
|
722
|
+
throw new Error("registry id");
|
|
723
|
+
}
|
|
724
|
+
assertCanonical32(request.session_contract_hash_b64u, "session hash", "invalid_fsb3");
|
|
725
|
+
assertCanonical32(request.candidate_set_hash_b64u, "candidate hash", "invalid_fsb3");
|
|
726
|
+
const source = request.candidates.map((candidate) => ({
|
|
727
|
+
id: candidate.id,
|
|
728
|
+
carrier: candidate.carrier,
|
|
729
|
+
url: candidate.normalized_url,
|
|
730
|
+
normalized_url: candidate.normalized_url,
|
|
731
|
+
tls: candidate.tls,
|
|
732
|
+
wire_profile: candidate.wire_profile,
|
|
733
|
+
}));
|
|
734
|
+
const canonical = canonicalizeCandidatesV3(request.pathKind, source);
|
|
735
|
+
if (canonical.hashBase64URL !== request.candidate_set_hash_b64u ||
|
|
736
|
+
canonicalizeJCSV3(canonical.candidates) !==
|
|
737
|
+
canonicalizeJCSV3(request.candidates)) {
|
|
738
|
+
throw new Error("candidate hash or ordering");
|
|
739
|
+
}
|
|
740
|
+
if (!request.candidates.some((candidate) => candidate.id === request.chosen_candidate_id)) {
|
|
741
|
+
throw new Error("chosen candidate");
|
|
742
|
+
}
|
|
743
|
+
if (request.pathKind === "direct") {
|
|
744
|
+
if (!validASCII(request.routing_token, MAX_ADMISSION_CREDENTIAL_BYTES))
|
|
745
|
+
throw new Error("direct variant");
|
|
746
|
+
}
|
|
747
|
+
else if ((request.role !== 1 && request.role !== 2) ||
|
|
748
|
+
!validRegistryID(request.endpoint_instance_id, 128) ||
|
|
749
|
+
!validASCII(request.attach_token, MAX_ADMISSION_CREDENTIAL_BYTES)) {
|
|
750
|
+
throw new Error("tunnel variant");
|
|
751
|
+
}
|
|
752
|
+
}
|
|
753
|
+
catch (error) {
|
|
754
|
+
if (error instanceof ArtifactV3Error && error.code === "invalid_fsb3")
|
|
755
|
+
throw error;
|
|
756
|
+
throw invalidFSB3(errorMessage(error));
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
function marshalFSB3Payload(request) {
|
|
760
|
+
const wire = request.pathKind === "direct"
|
|
761
|
+
? {
|
|
762
|
+
candidate_set_hash_b64u: request.candidate_set_hash_b64u,
|
|
763
|
+
candidates: request.candidates,
|
|
764
|
+
channel_id: request.channel_id,
|
|
765
|
+
chosen_candidate_id: request.chosen_candidate_id,
|
|
766
|
+
listener_audience: request.listener_audience,
|
|
767
|
+
profile: request.profile,
|
|
768
|
+
rendezvous_group_id: request.rendezvous_group_id,
|
|
769
|
+
routing_token: request.routing_token,
|
|
770
|
+
session_contract_hash_b64u: request.session_contract_hash_b64u,
|
|
771
|
+
}
|
|
772
|
+
: {
|
|
773
|
+
attach_token: request.attach_token,
|
|
774
|
+
candidate_set_hash_b64u: request.candidate_set_hash_b64u,
|
|
775
|
+
candidates: request.candidates,
|
|
776
|
+
channel_id: request.channel_id,
|
|
777
|
+
chosen_candidate_id: request.chosen_candidate_id,
|
|
778
|
+
endpoint_instance_id: request.endpoint_instance_id,
|
|
779
|
+
listener_audience: request.listener_audience,
|
|
780
|
+
profile: request.profile,
|
|
781
|
+
rendezvous_group_id: request.rendezvous_group_id,
|
|
782
|
+
role: request.role,
|
|
783
|
+
session_contract_hash_b64u: request.session_contract_hash_b64u,
|
|
784
|
+
};
|
|
785
|
+
return encoder.encode(canonicalizeJCSV3(wire));
|
|
786
|
+
}
|
|
787
|
+
function decodeArtifactValue(value) {
|
|
788
|
+
const top = requireRecord(value, "artifact");
|
|
789
|
+
assertExactKeys(top, ["v", "profile", "session", "path", "scoped", "correlation"], "artifact", "invalid_artifact");
|
|
790
|
+
if (top.v !== 3 || top.profile !== PROFILE)
|
|
791
|
+
throw invalidArtifact("version or profile");
|
|
792
|
+
const session = decodeSessionValue(top.session);
|
|
793
|
+
const path = decodePathValue(top.path);
|
|
794
|
+
const scoped = decodeScopesValue(top.scoped);
|
|
795
|
+
const correlation = decodeCorrelationValue(top.correlation);
|
|
796
|
+
return { v: 3, profile: PROFILE, session, path, scoped, correlation };
|
|
797
|
+
}
|
|
798
|
+
function decodeSessionValue(value) {
|
|
799
|
+
const session = requireRecord(value, "session");
|
|
800
|
+
assertExactKeys(session, [
|
|
801
|
+
"channel_id",
|
|
802
|
+
"init_expire_at_unix_s",
|
|
803
|
+
"idle_timeout_seconds",
|
|
804
|
+
"establish_timeout_seconds",
|
|
805
|
+
"rekey_prepare_timeout_seconds",
|
|
806
|
+
"rekey_completion_timeout_seconds",
|
|
807
|
+
"max_inbound_streams",
|
|
808
|
+
"e2ee_psk_b64u",
|
|
809
|
+
"allowed_suites",
|
|
810
|
+
"default_suite",
|
|
811
|
+
"selected_features",
|
|
812
|
+
"contract_hash_b64u",
|
|
813
|
+
], "session", "invalid_artifact");
|
|
814
|
+
const decoded = session;
|
|
815
|
+
validateSession(decoded);
|
|
816
|
+
return decoded;
|
|
817
|
+
}
|
|
818
|
+
function decodePathValue(value) {
|
|
819
|
+
const path = requireRecord(value, "path");
|
|
820
|
+
if (path.kind === "direct") {
|
|
821
|
+
assertExactKeys(path, ["kind", "rendezvous_group_id", "listener_audience", "routing_token", "candidates"], "direct path", "invalid_artifact");
|
|
822
|
+
return {
|
|
823
|
+
kind: "direct",
|
|
824
|
+
rendezvous_group_id: requireString(path.rendezvous_group_id, "rendezvous group"),
|
|
825
|
+
listener_audience: requireString(path.listener_audience, "listener audience"),
|
|
826
|
+
routing_token: requireString(path.routing_token, "routing token"),
|
|
827
|
+
candidates: decodeCandidatesValue(path.candidates),
|
|
828
|
+
};
|
|
829
|
+
}
|
|
830
|
+
if (path.kind === "tunnel") {
|
|
831
|
+
assertExactKeys(path, [
|
|
832
|
+
"kind",
|
|
833
|
+
"rendezvous_group_id",
|
|
834
|
+
"listener_audience",
|
|
835
|
+
"role",
|
|
836
|
+
"local_endpoint_instance_id",
|
|
837
|
+
"expected_peer_endpoint_instance_id",
|
|
838
|
+
"token",
|
|
839
|
+
"candidates",
|
|
840
|
+
], "tunnel path", "invalid_artifact");
|
|
841
|
+
if (path.role !== 1 && path.role !== 2)
|
|
842
|
+
throw invalidArtifact("tunnel role");
|
|
843
|
+
return {
|
|
844
|
+
kind: "tunnel",
|
|
845
|
+
rendezvous_group_id: requireString(path.rendezvous_group_id, "rendezvous group"),
|
|
846
|
+
listener_audience: requireString(path.listener_audience, "listener audience"),
|
|
847
|
+
role: path.role,
|
|
848
|
+
local_endpoint_instance_id: requireString(path.local_endpoint_instance_id, "local endpoint"),
|
|
849
|
+
expected_peer_endpoint_instance_id: requireString(path.expected_peer_endpoint_instance_id, "peer endpoint"),
|
|
850
|
+
token: requireString(path.token, "attach token"),
|
|
851
|
+
candidates: decodeCandidatesValue(path.candidates),
|
|
852
|
+
};
|
|
853
|
+
}
|
|
854
|
+
throw invalidArtifact("path kind");
|
|
855
|
+
}
|
|
856
|
+
function decodeCandidatesValue(value) {
|
|
857
|
+
if (!Array.isArray(value))
|
|
858
|
+
throw invalidArtifact("candidates");
|
|
859
|
+
return value.map((item) => {
|
|
860
|
+
const candidate = requireRecord(item, "candidate");
|
|
861
|
+
assertExactKeys(candidate, ["id", "carrier", "url", "wire_profile", "tls"], "candidate", "invalid_artifact");
|
|
862
|
+
const tls = decodeTLSPolicyValue(candidate.tls, "invalid_artifact");
|
|
863
|
+
return {
|
|
864
|
+
id: requireString(candidate.id, "candidate id"),
|
|
865
|
+
carrier: requireString(candidate.carrier, "candidate carrier"),
|
|
866
|
+
url: requireString(candidate.url, "candidate URL"),
|
|
867
|
+
wire_profile: requireString(candidate.wire_profile, "wire profile"),
|
|
868
|
+
tls,
|
|
869
|
+
};
|
|
870
|
+
});
|
|
871
|
+
}
|
|
872
|
+
function decodeScopesValue(value) {
|
|
873
|
+
if (!Array.isArray(value))
|
|
874
|
+
throw invalidArtifact("scoped");
|
|
875
|
+
return value.map((item) => {
|
|
876
|
+
const scope = requireRecord(item, "scope");
|
|
877
|
+
assertExactKeys(scope, ["scope", "scope_version", "critical", "payload"], "scope", "invalid_artifact");
|
|
878
|
+
if (typeof scope.critical !== "boolean" || !isRecord(scope.payload))
|
|
879
|
+
throw invalidArtifact("scope metadata");
|
|
880
|
+
return {
|
|
881
|
+
scope: requireString(scope.scope, "scope name"),
|
|
882
|
+
scope_version: requireNumber(scope.scope_version, "scope version"),
|
|
883
|
+
critical: scope.critical,
|
|
884
|
+
payload: scope.payload,
|
|
885
|
+
};
|
|
886
|
+
});
|
|
887
|
+
}
|
|
888
|
+
function decodeCorrelationValue(value) {
|
|
889
|
+
const correlation = requireRecord(value, "correlation");
|
|
890
|
+
assertExactKeys(correlation, ["v", "tags"], "correlation", "invalid_artifact");
|
|
891
|
+
if (correlation.v !== 3 || !Array.isArray(correlation.tags))
|
|
892
|
+
throw invalidArtifact("correlation");
|
|
893
|
+
return {
|
|
894
|
+
v: 3,
|
|
895
|
+
tags: correlation.tags.map((item) => {
|
|
896
|
+
const tag = requireRecord(item, "correlation tag");
|
|
897
|
+
assertExactKeys(tag, ["key", "value"], "correlation tag", "invalid_artifact");
|
|
898
|
+
return {
|
|
899
|
+
key: requireString(tag.key, "correlation key"),
|
|
900
|
+
value: requireString(tag.value, "correlation value"),
|
|
901
|
+
};
|
|
902
|
+
}),
|
|
903
|
+
};
|
|
904
|
+
}
|
|
905
|
+
function decodeFSB3Value(pathKind, value) {
|
|
906
|
+
const wire = requireRecord(value, "FSB3 payload", "invalid_fsb3");
|
|
907
|
+
const commonKeys = [
|
|
908
|
+
"candidate_set_hash_b64u",
|
|
909
|
+
"candidates",
|
|
910
|
+
"channel_id",
|
|
911
|
+
"chosen_candidate_id",
|
|
912
|
+
"listener_audience",
|
|
913
|
+
"profile",
|
|
914
|
+
"rendezvous_group_id",
|
|
915
|
+
"session_contract_hash_b64u",
|
|
916
|
+
];
|
|
917
|
+
if (pathKind === "direct") {
|
|
918
|
+
assertExactKeys(wire, [...commonKeys, "routing_token"], "direct FSB3", "invalid_fsb3");
|
|
919
|
+
}
|
|
920
|
+
else {
|
|
921
|
+
assertExactKeys(wire, [...commonKeys, "attach_token", "endpoint_instance_id", "role"], "tunnel FSB3", "invalid_fsb3");
|
|
922
|
+
}
|
|
923
|
+
const candidates = decodeCanonicalCandidatesValue(wire.candidates);
|
|
924
|
+
const common = {
|
|
925
|
+
profile: requireString(wire.profile, "profile", "invalid_fsb3"),
|
|
926
|
+
channel_id: requireString(wire.channel_id, "channel ID", "invalid_fsb3"),
|
|
927
|
+
session_contract_hash_b64u: requireString(wire.session_contract_hash_b64u, "session hash", "invalid_fsb3"),
|
|
928
|
+
rendezvous_group_id: requireString(wire.rendezvous_group_id, "rendezvous group", "invalid_fsb3"),
|
|
929
|
+
candidates,
|
|
930
|
+
candidate_set_hash_b64u: requireString(wire.candidate_set_hash_b64u, "candidate hash", "invalid_fsb3"),
|
|
931
|
+
chosen_candidate_id: requireString(wire.chosen_candidate_id, "chosen candidate", "invalid_fsb3"),
|
|
932
|
+
listener_audience: requireString(wire.listener_audience, "listener audience", "invalid_fsb3"),
|
|
933
|
+
};
|
|
934
|
+
if (pathKind === "direct") {
|
|
935
|
+
return {
|
|
936
|
+
pathKind,
|
|
937
|
+
...common,
|
|
938
|
+
routing_token: requireString(wire.routing_token, "routing token", "invalid_fsb3"),
|
|
939
|
+
};
|
|
940
|
+
}
|
|
941
|
+
if (wire.role !== 1 && wire.role !== 2)
|
|
942
|
+
throw invalidFSB3("role");
|
|
943
|
+
return {
|
|
944
|
+
pathKind,
|
|
945
|
+
...common,
|
|
946
|
+
role: wire.role,
|
|
947
|
+
endpoint_instance_id: requireString(wire.endpoint_instance_id, "endpoint instance", "invalid_fsb3"),
|
|
948
|
+
attach_token: requireString(wire.attach_token, "attach token", "invalid_fsb3"),
|
|
949
|
+
};
|
|
950
|
+
}
|
|
951
|
+
function decodeCanonicalCandidatesValue(value) {
|
|
952
|
+
if (!Array.isArray(value))
|
|
953
|
+
throw invalidFSB3("candidates");
|
|
954
|
+
return value.map((item) => {
|
|
955
|
+
const candidate = requireRecord(item, "canonical candidate", "invalid_fsb3");
|
|
956
|
+
assertExactKeys(candidate, ["carrier", "id", "normalized_url", "tls", "wire_profile"], "canonical candidate", "invalid_fsb3");
|
|
957
|
+
return {
|
|
958
|
+
carrier: requireString(candidate.carrier, "carrier", "invalid_fsb3"),
|
|
959
|
+
id: requireString(candidate.id, "candidate ID", "invalid_fsb3"),
|
|
960
|
+
normalized_url: requireString(candidate.normalized_url, "normalized URL", "invalid_fsb3"),
|
|
961
|
+
tls: decodeTLSPolicyValue(candidate.tls, "invalid_fsb3"),
|
|
962
|
+
wire_profile: requireString(candidate.wire_profile, "wire profile", "invalid_fsb3"),
|
|
963
|
+
};
|
|
964
|
+
});
|
|
965
|
+
}
|
|
966
|
+
function artifactSessionWire(session) {
|
|
967
|
+
return {
|
|
968
|
+
channel_id: session.channel_id,
|
|
969
|
+
init_expire_at_unix_s: session.init_expire_at_unix_s,
|
|
970
|
+
idle_timeout_seconds: session.idle_timeout_seconds,
|
|
971
|
+
establish_timeout_seconds: session.establish_timeout_seconds,
|
|
972
|
+
rekey_prepare_timeout_seconds: session.rekey_prepare_timeout_seconds,
|
|
973
|
+
rekey_completion_timeout_seconds: session.rekey_completion_timeout_seconds,
|
|
974
|
+
max_inbound_streams: session.max_inbound_streams,
|
|
975
|
+
e2ee_psk_b64u: session.e2ee_psk_b64u,
|
|
976
|
+
allowed_suites: session.allowed_suites,
|
|
977
|
+
default_suite: session.default_suite,
|
|
978
|
+
selected_features: session.selected_features,
|
|
979
|
+
contract_hash_b64u: session.contract_hash_b64u,
|
|
980
|
+
};
|
|
981
|
+
}
|
|
982
|
+
function directPathWire(path) {
|
|
983
|
+
return {
|
|
984
|
+
kind: path.kind,
|
|
985
|
+
rendezvous_group_id: path.rendezvous_group_id,
|
|
986
|
+
listener_audience: path.listener_audience,
|
|
987
|
+
routing_token: path.routing_token,
|
|
988
|
+
candidates: path.candidates.map(candidateWire),
|
|
989
|
+
};
|
|
990
|
+
}
|
|
991
|
+
function tunnelPathWire(path) {
|
|
992
|
+
return {
|
|
993
|
+
kind: path.kind,
|
|
994
|
+
rendezvous_group_id: path.rendezvous_group_id,
|
|
995
|
+
listener_audience: path.listener_audience,
|
|
996
|
+
role: path.role,
|
|
997
|
+
local_endpoint_instance_id: path.local_endpoint_instance_id,
|
|
998
|
+
expected_peer_endpoint_instance_id: path.expected_peer_endpoint_instance_id,
|
|
999
|
+
token: path.token,
|
|
1000
|
+
candidates: path.candidates.map(candidateWire),
|
|
1001
|
+
};
|
|
1002
|
+
}
|
|
1003
|
+
function candidateWire(candidate) {
|
|
1004
|
+
return {
|
|
1005
|
+
id: candidate.id,
|
|
1006
|
+
carrier: candidate.carrier,
|
|
1007
|
+
url: candidate.url,
|
|
1008
|
+
wire_profile: candidate.wire_profile,
|
|
1009
|
+
tls: candidate.tls,
|
|
1010
|
+
};
|
|
1011
|
+
}
|
|
1012
|
+
function validateFSA3Response(response, admissionReasons, requireRegisteredReason) {
|
|
1013
|
+
switch (response.status) {
|
|
1014
|
+
case AdmissionStatusV3.Success:
|
|
1015
|
+
if (response.reason !== "")
|
|
1016
|
+
throw invalidFSA3("success reason");
|
|
1017
|
+
return;
|
|
1018
|
+
case AdmissionStatusV3.Reject:
|
|
1019
|
+
case AdmissionStatusV3.Retryable:
|
|
1020
|
+
if (!/^[a-z][a-z0-9_]*$/.test(response.reason) || utf8Length(response.reason) > MAX_ADMISSION_REASON_BYTES) {
|
|
1021
|
+
throw invalidFSA3("reason token");
|
|
1022
|
+
}
|
|
1023
|
+
if (response.reason === "expired_artifact" && response.status !== AdmissionStatusV3.Retryable) {
|
|
1024
|
+
throw invalidFSA3("expired artifact status");
|
|
1025
|
+
}
|
|
1026
|
+
if (FORBIDDEN_FSA3_REASONS.has(response.reason))
|
|
1027
|
+
throw invalidFSA3("transport security reason");
|
|
1028
|
+
if (requireRegisteredReason && !admissionReasons.has(response.reason)) {
|
|
1029
|
+
throw invalidFSA3("unregistered reason");
|
|
1030
|
+
}
|
|
1031
|
+
return;
|
|
1032
|
+
default:
|
|
1033
|
+
throw invalidFSA3("status");
|
|
1034
|
+
}
|
|
1035
|
+
}
|
|
1036
|
+
function labeledHash(label, canonicalJSON) {
|
|
1037
|
+
const canonical = encoder.encode(canonicalJSON);
|
|
1038
|
+
const hash = sha256(concatBytes([encoder.encode(label), u32be(canonical.length), canonical]));
|
|
1039
|
+
return { canonicalJSON, hash, hashBase64URL: base64urlEncode(hash) };
|
|
1040
|
+
}
|
|
1041
|
+
function assertCanonical32(value, name, errorCode) {
|
|
1042
|
+
if (typeof value !== "string")
|
|
1043
|
+
throw codecError(errorCode, `${name} type`);
|
|
1044
|
+
try {
|
|
1045
|
+
const decoded = base64urlDecode(value);
|
|
1046
|
+
if (decoded.length !== 32 || base64urlEncode(decoded) !== value)
|
|
1047
|
+
throw new Error("length");
|
|
1048
|
+
return decoded;
|
|
1049
|
+
}
|
|
1050
|
+
catch {
|
|
1051
|
+
throw codecError(errorCode, `${name} encoding`);
|
|
1052
|
+
}
|
|
1053
|
+
}
|
|
1054
|
+
function decodeBoundedJSON(raw) {
|
|
1055
|
+
if (typeof raw === "string") {
|
|
1056
|
+
if (utf8Length(raw) > MAX_ARTIFACT_JSON_BYTES) {
|
|
1057
|
+
throw new ArtifactV3Error("artifact_too_large", "Flowersec v3 artifact is too large");
|
|
1058
|
+
}
|
|
1059
|
+
return raw;
|
|
1060
|
+
}
|
|
1061
|
+
if (raw.length > MAX_ARTIFACT_JSON_BYTES) {
|
|
1062
|
+
throw new ArtifactV3Error("artifact_too_large", "Flowersec v3 artifact is too large");
|
|
1063
|
+
}
|
|
1064
|
+
try {
|
|
1065
|
+
return decoder.decode(raw);
|
|
1066
|
+
}
|
|
1067
|
+
catch {
|
|
1068
|
+
throw invalidArtifact("UTF-8");
|
|
1069
|
+
}
|
|
1070
|
+
}
|
|
1071
|
+
function parseStrictJSON(text, scanScopedPayloads = false) {
|
|
1072
|
+
preflightJSONV3(text, scanScopedPayloads);
|
|
1073
|
+
return JSON.parse(text);
|
|
1074
|
+
}
|
|
1075
|
+
function assertExactKeys(value, expected, name, code) {
|
|
1076
|
+
const actual = Object.keys(value);
|
|
1077
|
+
if (actual.length !== expected.length || expected.some((key) => !Object.prototype.hasOwnProperty.call(value, key))) {
|
|
1078
|
+
throw codecError(code, `${name} fields`);
|
|
1079
|
+
}
|
|
1080
|
+
}
|
|
1081
|
+
function assertRecord(value, name, code) {
|
|
1082
|
+
if (!isRecord(value))
|
|
1083
|
+
throw codecError(code, `${name} object`);
|
|
1084
|
+
}
|
|
1085
|
+
function requireRecord(value, name, code = "invalid_artifact") {
|
|
1086
|
+
if (!isRecord(value))
|
|
1087
|
+
throw codecError(code, `${name} object`);
|
|
1088
|
+
return value;
|
|
1089
|
+
}
|
|
1090
|
+
function requireString(value, name, code = "invalid_artifact") {
|
|
1091
|
+
if (typeof value !== "string")
|
|
1092
|
+
throw codecError(code, `${name} string`);
|
|
1093
|
+
return value;
|
|
1094
|
+
}
|
|
1095
|
+
function requireNumber(value, name) {
|
|
1096
|
+
if (typeof value !== "number")
|
|
1097
|
+
throw invalidArtifact(`${name} number`);
|
|
1098
|
+
return value;
|
|
1099
|
+
}
|
|
1100
|
+
function isRecord(value) {
|
|
1101
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1102
|
+
}
|
|
1103
|
+
function assertSafeInteger(value, minimum, maximum, name, code = "invalid_artifact") {
|
|
1104
|
+
if (typeof value !== "number" || !Number.isSafeInteger(value) || value < minimum || value > maximum) {
|
|
1105
|
+
throw codecError(code, name);
|
|
1106
|
+
}
|
|
1107
|
+
}
|
|
1108
|
+
function validRegistryID(value, maximum) {
|
|
1109
|
+
return typeof value === "string" && utf8Length(value) >= 1 && utf8Length(value) <= maximum && registryIDPattern.test(value);
|
|
1110
|
+
}
|
|
1111
|
+
function validASCII(value, maximum) {
|
|
1112
|
+
if (typeof value !== "string" || value.length < 1 || value.length > maximum)
|
|
1113
|
+
return false;
|
|
1114
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
1115
|
+
if (value.charCodeAt(index) > 0x7f)
|
|
1116
|
+
return false;
|
|
1117
|
+
}
|
|
1118
|
+
return true;
|
|
1119
|
+
}
|
|
1120
|
+
function utf8Length(value) {
|
|
1121
|
+
return encoder.encode(value).length;
|
|
1122
|
+
}
|
|
1123
|
+
function bytesEqual(left, right) {
|
|
1124
|
+
if (left.length !== right.length)
|
|
1125
|
+
return false;
|
|
1126
|
+
let difference = 0;
|
|
1127
|
+
for (let index = 0; index < left.length; index += 1)
|
|
1128
|
+
difference |= left[index] ^ right[index];
|
|
1129
|
+
return difference === 0;
|
|
1130
|
+
}
|
|
1131
|
+
function pathKindFromCode(code) {
|
|
1132
|
+
if (code === 1)
|
|
1133
|
+
return "direct";
|
|
1134
|
+
if (code === 2)
|
|
1135
|
+
return "tunnel";
|
|
1136
|
+
throw invalidFSB3("path code");
|
|
1137
|
+
}
|
|
1138
|
+
function codecError(code, detail) {
|
|
1139
|
+
switch (code) {
|
|
1140
|
+
case "invalid_artifact":
|
|
1141
|
+
return invalidArtifact(detail);
|
|
1142
|
+
case "invalid_candidate":
|
|
1143
|
+
return invalidCandidate(detail);
|
|
1144
|
+
case "invalid_fsb3":
|
|
1145
|
+
return invalidFSB3(detail);
|
|
1146
|
+
}
|
|
1147
|
+
}
|
|
1148
|
+
function invalidArtifact(detail) {
|
|
1149
|
+
return new ArtifactV3Error("invalid_artifact", `invalid Flowersec v3 artifact: ${detail}`);
|
|
1150
|
+
}
|
|
1151
|
+
function invalidCandidate(detail) {
|
|
1152
|
+
return new ArtifactV3Error("invalid_candidate", `invalid Flowersec v3 candidate: ${detail}`);
|
|
1153
|
+
}
|
|
1154
|
+
function invalidFSB3(detail) {
|
|
1155
|
+
return new ArtifactV3Error("invalid_fsb3", `invalid FSB3 admission request: ${detail}`);
|
|
1156
|
+
}
|
|
1157
|
+
function invalidFSA3(detail) {
|
|
1158
|
+
return new ArtifactV3Error("invalid_fsa3", `invalid FSA3 admission response: ${detail}`);
|
|
1159
|
+
}
|
|
1160
|
+
function errorMessage(error) {
|
|
1161
|
+
return error instanceof Error ? error.message : String(error);
|
|
1162
|
+
}
|