sliftutils 1.7.141 → 1.7.142
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/package.json +1 -1
- package/security/machines/identity2.ts +46 -51
package/package.json
CHANGED
|
@@ -5,36 +5,32 @@ import { getTrustedMachines } from "./machines";
|
|
|
5
5
|
|
|
6
6
|
const MAX_SIGNED_AGE = 5 * 60 * 1000;
|
|
7
7
|
|
|
8
|
-
// A request says who it is from AND who it believes it is talking to. The "to" is whatever both
|
|
9
|
-
// sides use to identify the server - an IP, a machineId, a threadId - and being inside the signed
|
|
10
|
-
// payload is what stops a machine in the middle: a request addressed to someone else cannot be
|
|
11
|
-
// replayed at the real server, and the middleman cannot write requests of its own because it is
|
|
12
|
-
// not a trusted machine.
|
|
13
8
|
export type SignedRequest<T> = {
|
|
14
9
|
signature: string;
|
|
15
10
|
payload: {
|
|
11
|
+
// When it was signed - anything older than MAX_SIGNED_AGE is refused
|
|
16
12
|
time: number;
|
|
17
|
-
to:
|
|
13
|
+
// Who the sender believes it is talking to: an IP, a machineId, a threadId - whatever
|
|
14
|
+
// both sides use to name the server. Being inside the signed payload is what stops a
|
|
15
|
+
// machine in the middle: a request addressed to someone else cannot be replayed at the
|
|
16
|
+
// real server, and the middleman cannot write requests of its own because it is not a
|
|
17
|
+
// trusted machine.
|
|
18
|
+
targetId: string;
|
|
19
|
+
// The thread certificate that signed this
|
|
18
20
|
cert: string;
|
|
21
|
+
// The machine CA that issued cert - the machineId comes from its common name
|
|
19
22
|
certIssuer: string;
|
|
20
23
|
data: T;
|
|
21
24
|
};
|
|
22
25
|
};
|
|
23
26
|
|
|
24
|
-
// The reply echoes the request it answers, with the data collapsed to its hash - resending the
|
|
25
|
-
// data would say nothing the hash does not. Signed by the replying thread's certificate, which
|
|
26
|
-
// chains to its machine CA, which is where the machineId comes from.
|
|
27
27
|
export type SignedReply<T> = {
|
|
28
28
|
signature: string;
|
|
29
29
|
payload: {
|
|
30
30
|
time: number;
|
|
31
|
-
request
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
cert: string;
|
|
35
|
-
certIssuer: string;
|
|
36
|
-
dataHash: string;
|
|
37
|
-
};
|
|
31
|
+
// The request this reply answers, with the data collapsed to its hash - resending the
|
|
32
|
+
// data would say nothing the hash does not
|
|
33
|
+
request: Omit<SignedRequest<unknown>["payload"], "data"> & { dataHash: string };
|
|
38
34
|
cert: string;
|
|
39
35
|
certIssuer: string;
|
|
40
36
|
data: T;
|
|
@@ -45,12 +41,12 @@ function dataHash(data: unknown) {
|
|
|
45
41
|
return sha256.sha256(JSON.stringify(data));
|
|
46
42
|
}
|
|
47
43
|
|
|
48
|
-
export function signRequest<T>(domain: string, config: {
|
|
44
|
+
export function signRequest<T>(domain: string, config: { targetId: string; data: T }): SignedRequest<T> {
|
|
49
45
|
let threadKeyCert = getThreadKeyCert(domain);
|
|
50
46
|
let issuer = getIdentityCA(domain);
|
|
51
|
-
let payload = {
|
|
47
|
+
let payload: SignedRequest<T>["payload"] = {
|
|
52
48
|
time: Date.now(),
|
|
53
|
-
|
|
49
|
+
targetId: config.targetId,
|
|
54
50
|
cert: threadKeyCert.cert.toString(),
|
|
55
51
|
certIssuer: issuer.cert.toString(),
|
|
56
52
|
data: config.data,
|
|
@@ -58,11 +54,17 @@ export function signRequest<T>(domain: string, config: { to: string; data: T }):
|
|
|
58
54
|
return { signature: sign(threadKeyCert, payload), payload };
|
|
59
55
|
}
|
|
60
56
|
|
|
61
|
-
/** Returns the machineId that signed,
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
57
|
+
/** Returns the machineId that signed, the data, and reply - which signs a reply to exactly this
|
|
58
|
+
request, so a reply cannot be produced for a request that was never verified. Throws if the
|
|
59
|
+
signature, certificate chain, or time do not check out, or if the request was addressed to
|
|
60
|
+
someone we are not: ownIdentities is every name this server answers to - its IPs (there are
|
|
61
|
+
always several, internal and external), its machineId, its threadId - and the signed targetId
|
|
62
|
+
must be one of them. */
|
|
63
|
+
export function verifyRequest<T>(domain: string, signed: SignedRequest<T>, ownIdentities: string[]): {
|
|
64
|
+
machineId: string;
|
|
65
|
+
data: T;
|
|
66
|
+
reply<R>(data: R): SignedReply<R>;
|
|
67
|
+
} {
|
|
66
68
|
let { signature, payload } = signed;
|
|
67
69
|
let signedThreshold = Date.now() - MAX_SIGNED_AGE;
|
|
68
70
|
if (payload.time < signedThreshold) {
|
|
@@ -70,30 +72,27 @@ export function verifyRequest<T>(domain: string, signed: SignedRequest<T>, ownId
|
|
|
70
72
|
}
|
|
71
73
|
verify(payload.cert, signature, payload);
|
|
72
74
|
validateCertificate(domain, payload.cert, payload.certIssuer);
|
|
73
|
-
if (!ownIdentities.includes(payload.
|
|
74
|
-
throw new Error(`Request is for someone else. It is addressed to ${payload.
|
|
75
|
+
if (!ownIdentities.includes(payload.targetId)) {
|
|
76
|
+
throw new Error(`Request is for someone else. It is addressed to ${payload.targetId}, we are ${ownIdentities.join(", ")}`);
|
|
75
77
|
}
|
|
76
78
|
let machineId = getMachineId(getCommonName(payload.certIssuer), domain);
|
|
77
|
-
return {
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
79
|
+
return {
|
|
80
|
+
machineId,
|
|
81
|
+
data: payload.data,
|
|
82
|
+
reply<R>(data: R): SignedReply<R> {
|
|
83
|
+
let threadKeyCert = getThreadKeyCert(domain);
|
|
84
|
+
let issuer = getIdentityCA(domain);
|
|
85
|
+
let { data: requestData, ...requestRest } = payload;
|
|
86
|
+
let replyPayload: SignedReply<R>["payload"] = {
|
|
87
|
+
time: Date.now(),
|
|
88
|
+
request: { ...requestRest, dataHash: dataHash(requestData) },
|
|
89
|
+
cert: threadKeyCert.cert.toString(),
|
|
90
|
+
certIssuer: issuer.cert.toString(),
|
|
91
|
+
data,
|
|
92
|
+
};
|
|
93
|
+
return { signature: sign(threadKeyCert, replyPayload), payload: replyPayload };
|
|
91
94
|
},
|
|
92
|
-
cert: threadKeyCert.cert.toString(),
|
|
93
|
-
certIssuer: issuer.cert.toString(),
|
|
94
|
-
data,
|
|
95
95
|
};
|
|
96
|
-
return { signature: sign(threadKeyCert, payload), payload };
|
|
97
96
|
}
|
|
98
97
|
|
|
99
98
|
/** Returns the machineId that replied, and the data. Throws if the signature, certificate chain,
|
|
@@ -108,13 +107,9 @@ export async function verifyReply<T>(domain: string, request: SignedRequest<unkn
|
|
|
108
107
|
}
|
|
109
108
|
verify(payload.cert, signature, payload);
|
|
110
109
|
validateCertificate(domain, payload.cert, payload.certIssuer);
|
|
111
|
-
let
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|| echoed.cert !== request.payload.cert
|
|
115
|
-
|| echoed.certIssuer !== request.payload.certIssuer
|
|
116
|
-
|| echoed.dataHash !== dataHash(request.payload.data)
|
|
117
|
-
) {
|
|
110
|
+
let { data: requestData, ...requestRest } = request.payload;
|
|
111
|
+
let { dataHash: echoedHash, ...echoedRest } = payload.request;
|
|
112
|
+
if (echoedHash !== dataHash(requestData) || JSON.stringify(echoedRest) !== JSON.stringify(requestRest)) {
|
|
118
113
|
throw new Error(`Reply answers a different request than the one we sent`);
|
|
119
114
|
}
|
|
120
115
|
let machineId = getMachineId(getCommonName(payload.certIssuer), domain);
|