sliftutils 1.7.144 → 1.7.145
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/index.d.ts +0 -1
- package/misc/random.d.ts +0 -1
- package/misc/random.ts +0 -10
- package/package.json +1 -1
- package/security/machines/identity2.ts +10 -63
package/index.d.ts
CHANGED
|
@@ -417,7 +417,6 @@ declare module "sliftutils/misc/random" {
|
|
|
417
417
|
export declare function getSeededRandom(seed: number): () => number;
|
|
418
418
|
export declare function shuffle<T>(array: T[], seed: number): T[];
|
|
419
419
|
export declare function secureRandom(): number;
|
|
420
|
-
export declare function secureRandomHex(byteLength: number): string;
|
|
421
420
|
|
|
422
421
|
}
|
|
423
422
|
|
package/misc/random.d.ts
CHANGED
package/misc/random.ts
CHANGED
|
@@ -43,13 +43,3 @@ export function secureRandom(): number {
|
|
|
43
43
|
}
|
|
44
44
|
return randomDataF64[0];
|
|
45
45
|
}
|
|
46
|
-
|
|
47
|
-
export function secureRandomHex(byteLength: number): string {
|
|
48
|
-
let data = new Uint8Array(byteLength);
|
|
49
|
-
if (!isNode()) {
|
|
50
|
-
window.crypto.getRandomValues(data);
|
|
51
|
-
} else {
|
|
52
|
-
crypto.getRandomValues(data);
|
|
53
|
-
}
|
|
54
|
-
return Array.from(data).map(x => x.toString(16).padStart(2, "0")).join("");
|
|
55
|
-
}
|
package/package.json
CHANGED
|
@@ -18,36 +18,14 @@
|
|
|
18
18
|
|
|
19
19
|
import sha256 from "js-sha256";
|
|
20
20
|
import { isNode } from "socket-function/src/misc";
|
|
21
|
-
import { secureRandomHex } from "../../misc/random";
|
|
22
21
|
import { getCommonName, getIdentityCA, getMachineId, getThreadKeyCert, sign, validateCertificate, verify } from "../../misc/https/certs";
|
|
23
22
|
import { getTrustedMachines } from "./machines";
|
|
24
23
|
|
|
25
|
-
const MAX_SIGNED_AGE = 5 * 60 * 1000;
|
|
26
|
-
// Clocks disagree, so a payload from slightly ahead is normal - but one from far ahead would
|
|
27
|
-
// never age out, and so would never stop being accepted.
|
|
28
|
-
const MAX_SIGNED_FUTURE = 60 * 60 * 1000;
|
|
29
|
-
const NONCE_BYTES = 16;
|
|
30
|
-
// Nonces of the requests we have accepted, so none of them can be replayed. Cleared wholesale at
|
|
31
|
-
// the limit rather than expired one by one: reaching it takes a million signed requests inside
|
|
32
|
-
// the five minute window, which is a server being hammered - something we would notice for other
|
|
33
|
-
// reasons - and all it buys is one replay of one request.
|
|
34
|
-
const MAX_SEEN_NONCES = 1000 * 1000;
|
|
35
|
-
let seenNonces = new Set<string>();
|
|
36
|
-
|
|
37
24
|
export type SignedRequest<T> = {
|
|
38
25
|
signature: string;
|
|
39
26
|
payload: {
|
|
40
|
-
//
|
|
41
|
-
time: number;
|
|
42
|
-
// Who the sender believes it is talking to: an IP, a machineId, a threadId - whatever
|
|
43
|
-
// both sides use to name the server. Being inside the signed payload is what stops a
|
|
44
|
-
// machine in the middle: a request addressed to someone else cannot be replayed at the
|
|
45
|
-
// real server, and the middleman cannot write requests of its own because it is not a
|
|
46
|
-
// trusted machine.
|
|
27
|
+
// The ip we are talking to.
|
|
47
28
|
targetId: string;
|
|
48
|
-
targetIsMachineId?: boolean;
|
|
49
|
-
// Makes the request one use only - the server refuses a nonce it has already accepted
|
|
50
|
-
nonce: string;
|
|
51
29
|
// The thread certificate that signed this
|
|
52
30
|
cert: string;
|
|
53
31
|
// The machine CA that issued cert - the machineId comes from its common name
|
|
@@ -59,7 +37,6 @@ export type SignedRequest<T> = {
|
|
|
59
37
|
export type SignedReply = {
|
|
60
38
|
signature: string;
|
|
61
39
|
payload: {
|
|
62
|
-
time: number;
|
|
63
40
|
// The request this reply answers, with the data collapsed to its hash - resending the
|
|
64
41
|
// data would say nothing the hash does not
|
|
65
42
|
request: Omit<SignedRequest<unknown>["payload"], "data"> & { dataHash: string };
|
|
@@ -72,25 +49,11 @@ function dataHash(data: unknown) {
|
|
|
72
49
|
return sha256.sha256(JSON.stringify(data));
|
|
73
50
|
}
|
|
74
51
|
|
|
75
|
-
function
|
|
76
|
-
let oldest = Date.now() - MAX_SIGNED_AGE;
|
|
77
|
-
if (time < oldest) {
|
|
78
|
-
throw new Error(`Signed ${kind} is too old, ${time} < ${oldest}`);
|
|
79
|
-
}
|
|
80
|
-
let newest = Date.now() + MAX_SIGNED_FUTURE;
|
|
81
|
-
if (time > newest) {
|
|
82
|
-
throw new Error(`Signed ${kind} is too far in the future, ${time} > ${newest}`);
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
export function signRequest<T>(domain: string, config: { targetId: string; targetIsMachineId?: boolean; data: T }): SignedRequest<T> {
|
|
52
|
+
export function signRequest<T>(domain: string, config: { targetId: string; data: T }): SignedRequest<T> {
|
|
87
53
|
let threadKeyCert = getThreadKeyCert(domain);
|
|
88
54
|
let issuer = getIdentityCA(domain);
|
|
89
55
|
let payload: SignedRequest<T>["payload"] = {
|
|
90
|
-
time: Date.now(),
|
|
91
56
|
targetId: config.targetId,
|
|
92
|
-
targetIsMachineId: config.targetIsMachineId,
|
|
93
|
-
nonce: secureRandomHex(NONCE_BYTES),
|
|
94
57
|
cert: threadKeyCert.cert.toString(),
|
|
95
58
|
certIssuer: issuer.cert.toString(),
|
|
96
59
|
data: config.data,
|
|
@@ -99,11 +62,10 @@ export function signRequest<T>(domain: string, config: { targetId: string; targe
|
|
|
99
62
|
}
|
|
100
63
|
|
|
101
64
|
/** Returns the machineId that signed, the data, and the signed reply to send back - always
|
|
102
|
-
produced, so the caller's only job is to return it. Throws if the signature
|
|
103
|
-
chain
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
them.
|
|
65
|
+
produced, so the caller's only job is to return it. Throws if the signature or certificate
|
|
66
|
+
chain do not check out, or if the request was addressed to someone we are not: ownIdentities
|
|
67
|
+
is every address this server answers to - there are always several, internal and external -
|
|
68
|
+
and the signed targetId must be one of them.
|
|
107
69
|
|
|
108
70
|
IMPORTANT! You still need to check the machine with isMachineAccepted if you want to know if
|
|
109
71
|
the machine is trusted. We just verify that they are who they say they are, not that who they
|
|
@@ -114,26 +76,16 @@ export function verifyRequest<T>(domain: string, signed: SignedRequest<T>, ownId
|
|
|
114
76
|
reply: SignedReply;
|
|
115
77
|
} {
|
|
116
78
|
let { signature, payload } = signed;
|
|
117
|
-
assertSignedTime("request", payload.time);
|
|
118
79
|
verify(payload.cert, signature, payload);
|
|
119
80
|
validateCertificate(domain, payload.cert, payload.certIssuer);
|
|
120
81
|
if (!ownIdentities.includes(payload.targetId)) {
|
|
121
82
|
throw new Error(`Request is for someone else. It is addressed to ${payload.targetId}, we are ${ownIdentities.join(", ")}`);
|
|
122
83
|
}
|
|
123
|
-
// After the signature, so only a real signer can take up a slot
|
|
124
|
-
if (seenNonces.has(payload.nonce)) {
|
|
125
|
-
throw new Error(`Request ${payload.nonce} was already used, so it is being refused as a replay`);
|
|
126
|
-
}
|
|
127
|
-
if (seenNonces.size >= MAX_SEEN_NONCES) {
|
|
128
|
-
seenNonces.clear();
|
|
129
|
-
}
|
|
130
|
-
seenNonces.add(payload.nonce);
|
|
131
84
|
let machineId = getMachineId(getCommonName(payload.certIssuer), domain);
|
|
132
85
|
let threadKeyCert = getThreadKeyCert(domain);
|
|
133
86
|
let issuer = getIdentityCA(domain);
|
|
134
87
|
let { data: requestData, ...requestRest } = payload;
|
|
135
88
|
let replyPayload: SignedReply["payload"] = {
|
|
136
|
-
time: Date.now(),
|
|
137
89
|
request: { ...requestRest, dataHash: dataHash(requestData) },
|
|
138
90
|
cert: threadKeyCert.cert.toString(),
|
|
139
91
|
certIssuer: issuer.cert.toString(),
|
|
@@ -145,14 +97,12 @@ export function verifyRequest<T>(domain: string, signed: SignedRequest<T>, ownId
|
|
|
145
97
|
};
|
|
146
98
|
}
|
|
147
99
|
|
|
148
|
-
/** Returns the machineId that replied. Throws if the signature
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
browser has no machine list, so there that check is skipped. */
|
|
100
|
+
/** Returns the machineId that replied. Throws if the signature or certificate chain do not check
|
|
101
|
+
out, or if the reply does not answer the request we actually sent. On node the replying
|
|
102
|
+
machine must also be one of the trusted machines - the browser has no machine list, so there
|
|
103
|
+
that check is skipped. */
|
|
153
104
|
export async function verifyReply(domain: string, request: SignedRequest<unknown>, reply: SignedReply): Promise<{ machineId: string }> {
|
|
154
105
|
let { signature, payload } = reply;
|
|
155
|
-
assertSignedTime("reply", payload.time);
|
|
156
106
|
verify(payload.cert, signature, payload);
|
|
157
107
|
validateCertificate(domain, payload.cert, payload.certIssuer);
|
|
158
108
|
let { data: requestData, ...requestRest } = request.payload;
|
|
@@ -161,9 +111,6 @@ export async function verifyReply(domain: string, request: SignedRequest<unknown
|
|
|
161
111
|
throw new Error(`Reply answers a different request than the one we sent`);
|
|
162
112
|
}
|
|
163
113
|
let machineId = getMachineId(getCommonName(payload.certIssuer), domain);
|
|
164
|
-
if (request.payload.targetIsMachineId && machineId !== request.payload.targetId) {
|
|
165
|
-
throw new Error(`Reply is from machine ${machineId}, but the request was addressed to machine ${request.payload.targetId}`);
|
|
166
|
-
}
|
|
167
114
|
if (isNode()) {
|
|
168
115
|
if (!(await getTrustedMachines()).some(machine => machine.machineId === machineId)) {
|
|
169
116
|
throw new Error(`Reply is signed by ${machineId}, which is not a trusted machine`);
|