@crewhaus/federation-protocol 0.1.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/package.json +41 -0
- package/src/fixtures-cert.pem +19 -0
- package/src/fixtures-key.pem +28 -0
- package/src/fixtures-other-cert.pem +19 -0
- package/src/fixtures-other-key.pem +28 -0
- package/src/index.test.ts +208 -0
- package/src/index.ts +275 -0
- package/src/test-helpers.ts +66 -0
package/package.json
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@crewhaus/federation-protocol",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": "Cross-deployment A2A wire protocol: federation envelope (extends @crewhaus/a2a-protocol) + mTLS HTTPS POST transport with cert pinning (Section 34)",
|
|
6
|
+
"main": "src/index.ts",
|
|
7
|
+
"types": "src/index.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": "./src/index.ts"
|
|
10
|
+
},
|
|
11
|
+
"scripts": {
|
|
12
|
+
"test": "bun test src"
|
|
13
|
+
},
|
|
14
|
+
"dependencies": {
|
|
15
|
+
"@crewhaus/errors": "0.0.0"
|
|
16
|
+
},
|
|
17
|
+
"license": "Apache-2.0",
|
|
18
|
+
"author": {
|
|
19
|
+
"name": "Max Meier",
|
|
20
|
+
"email": "max@studiomax.io",
|
|
21
|
+
"url": "https://studiomax.io"
|
|
22
|
+
},
|
|
23
|
+
"repository": {
|
|
24
|
+
"type": "git",
|
|
25
|
+
"url": "git+https://github.com/crewhaus/factory.git",
|
|
26
|
+
"directory": "packages/federation-protocol"
|
|
27
|
+
},
|
|
28
|
+
"homepage": "https://github.com/crewhaus/factory/tree/main/packages/federation-protocol#readme",
|
|
29
|
+
"bugs": {
|
|
30
|
+
"url": "https://github.com/crewhaus/factory/issues"
|
|
31
|
+
},
|
|
32
|
+
"publishConfig": {
|
|
33
|
+
"access": "restricted"
|
|
34
|
+
},
|
|
35
|
+
"files": [
|
|
36
|
+
"src",
|
|
37
|
+
"README.md",
|
|
38
|
+
"LICENSE",
|
|
39
|
+
"NOTICE"
|
|
40
|
+
]
|
|
41
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
-----BEGIN CERTIFICATE-----
|
|
2
|
+
MIIDDTCCAfWgAwIBAgIUNuMAUkUqNO9Tc0sSHfzY6tkBG+IwDQYJKoZIhvcNAQEL
|
|
3
|
+
BQAwFjEUMBIGA1UEAwwLZmVkLWZpeHR1cmUwHhcNMjYwNTA5MDYyNjU2WhcNMzYw
|
|
4
|
+
NTA2MDYyNjU2WjAWMRQwEgYDVQQDDAtmZWQtZml4dHVyZTCCASIwDQYJKoZIhvcN
|
|
5
|
+
AQEBBQADggEPADCCAQoCggEBAM1mvBIz/XRal0mR8YM92oVzYTg+gdS2x8RAmU7k
|
|
6
|
+
ryN1RXe8XVFg1agTLRObpzb0NxHV4zsN+9s+AVr1DBZuwcfaXpRHGlADf4t/cAUp
|
|
7
|
+
Sl6xoKdWf7cFk4RloE5BOKtkVYzgrAktY/xZVk5TN2i2/fNSrdj71D852nWhy/9/
|
|
8
|
+
LKInPuSRNRazdPMs3REvcJOvO1+Y0JQCiyQIcMfGRNc25piPQOhJaQDunBhcSWnR
|
|
9
|
+
G3RWNCZiwQwv6MozEHq/3ZAne5ypJBaVJ0c/5na2PEBSiOWcvKu6tJTHhauADhKH
|
|
10
|
+
5r+npdlvRgoCVriBJ72Khy38pMYrI7SUnMD81ArBLc40+kECAwEAAaNTMFEwHQYD
|
|
11
|
+
VR0OBBYEFMBWXUbAQba+bx4z8KPFJoqv7jBvMB8GA1UdIwQYMBaAFMBWXUbAQba+
|
|
12
|
+
bx4z8KPFJoqv7jBvMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQELBQADggEB
|
|
13
|
+
ACgcZnPU1/MsYGJSK7Lwu3vHV5MtMrbLmdCoHTFguo26moUHjjD/6BTDIv9sddp6
|
|
14
|
+
G4/BGHr/QWuaowMzqDTspIKFNMF8Pi4YuZvR4mdUOfXtq33vEb76YcVeyLeHr1da
|
|
15
|
+
ZJtySefk9o9igq7FRj65EtVcW9/gFsP8ztcolOHSgysTtopDkxra5xkS64w7BmZf
|
|
16
|
+
OlVoX1v1dctFWz23lddM43tYURmJq+ai2YSBmTIDy+aWDAdyHTf+s0iMJ/F4O7FH
|
|
17
|
+
6n9gQliYsXCWOekTbjnLGz8aRnfhqwGQ/aCjPBhLv2QVjgRpZ5gNLpeMx6TSAqJt
|
|
18
|
+
vShbklJd013K4tjA/5wSVH4=
|
|
19
|
+
-----END CERTIFICATE-----
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
-----BEGIN PRIVATE KEY-----
|
|
2
|
+
MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDNZrwSM/10WpdJ
|
|
3
|
+
kfGDPdqFc2E4PoHUtsfEQJlO5K8jdUV3vF1RYNWoEy0Tm6c29DcR1eM7DfvbPgFa
|
|
4
|
+
9QwWbsHH2l6URxpQA3+Lf3AFKUpesaCnVn+3BZOEZaBOQTirZFWM4KwJLWP8WVZO
|
|
5
|
+
Uzdotv3zUq3Y+9Q/Odp1ocv/fyyiJz7kkTUWs3TzLN0RL3CTrztfmNCUAoskCHDH
|
|
6
|
+
xkTXNuaYj0DoSWkA7pwYXElp0Rt0VjQmYsEML+jKMxB6v92QJ3ucqSQWlSdHP+Z2
|
|
7
|
+
tjxAUojlnLyrurSUx4WrgA4Sh+a/p6XZb0YKAla4gSe9ioct/KTGKyO0lJzA/NQK
|
|
8
|
+
wS3ONPpBAgMBAAECggEAMtMXeWec8SQTaFRxDtkIz9m3djvdU12w+6pCZSeoAJ2d
|
|
9
|
+
hV26N48+/vpTvpTW4P23/LVQp0W0CtGCc1fMWGaqk4HAwm7/n0nmTwXHGbfYW6sX
|
|
10
|
+
RiDctFRwZqPg8UwpMhu/IX9cgl7VbVVLylDEFqilGQbd1qGlqMlveYkCGeQFjs+2
|
|
11
|
+
vfBkC9Oag3JmNkJvmbKxNU/Sg1s56ipr4cWzngz3n6bweWFUYUYyupSDvKV8tkWm
|
|
12
|
+
gG3p2UltPN5dIFo0d6DRNcJrjdoiMPZADjcveW0Y1ceCxPnIpaeAzDb7kYQgkCYG
|
|
13
|
+
iRAqP/7/wZqJt05cOEyvbcPduMbzd7iNFNTPbOwWywKBgQDoOsb4Zejs674lR2BN
|
|
14
|
+
AkL/r+ESfnl7ezVvg2dxjOKk57dWZ9oip9iXPjiWNvZeM79pyEG7beJxVbvr7VH+
|
|
15
|
+
GK5KNWEMa78JirgmjU7dmnhAJ09byH59pzofKNvdVfCWysGKLutnUsMzqisFJDGI
|
|
16
|
+
WCfY/6IdcyEkxjNkEI7ylI1S4wKBgQDibPaCnvE365Il4U4KQKGdxmsIBFEJ9FNC
|
|
17
|
+
vvdY6QdDfRIqcoOgks7GYYcP+wnC4kqLud1zqYGwB1koF9TFfimPo5t0lvFMtRR8
|
|
18
|
+
LEFebtgHzNQDDbUo+Ud9vI6lN3IQJs5ezd+bjtZh4lvcTf98d/+Y4YiA8frJJone
|
|
19
|
+
ZafoLV9ziwKBgClw67bCANnei7T9UrsLT0dvbFuvhCA78WIv8dK6kGtbCkV6DNwo
|
|
20
|
+
VadPrCtqLXbMBzlqSgiXaFRPN1S3qe0NHHUTp3je9V1PiuMeTlePTCwul6PKWIA1
|
|
21
|
+
ylJrKSkLP/64uebdzpZGl5ztnfWx6sDo8ltv6s8Uj3KPh/YwWkIBrmJ/AoGBALYT
|
|
22
|
+
IPdQkHCDQfasAnFEH7IbyB2eOvxiOEHIBma8nFas0FrJ0wbght4HtvAm0magSYmq
|
|
23
|
+
YGWNvPesMQmIgFR/azRSP8O1TTx9sIdZnwcs4xMCpsn9z9uu+MonQh2hRFuwmOqr
|
|
24
|
+
alBQwBveRjgVkIiqhiKN2ZK3Aw+Vqe/oluig88yZAoGAffBD5P811mtiIHCfHjNJ
|
|
25
|
+
doQYDaMrGbOmRb35fSP0zbkjipPrxLZ4VfGINl+R7ARfuzeAuf/12IDeijU+mDnZ
|
|
26
|
+
B2qwnFAFbeR/nO3uN2CUJJMe8HPA4GllrEwUk2tKzbUZRP7tDUdbdKb4NI/sFwRo
|
|
27
|
+
KCgT8P8cB6fwxXHI0bm8N9g=
|
|
28
|
+
-----END PRIVATE KEY-----
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
-----BEGIN CERTIFICATE-----
|
|
2
|
+
MIIDCTCCAfGgAwIBAgIUAzomCw25jCV7mNtPw2n+QlhutFkwDQYJKoZIhvcNAQEL
|
|
3
|
+
BQAwFDESMBAGA1UEAwwJZmVkLW90aGVyMB4XDTI2MDUwOTA2MjcxNFoXDTM2MDUw
|
|
4
|
+
NjA2MjcxNFowFDESMBAGA1UEAwwJZmVkLW90aGVyMIIBIjANBgkqhkiG9w0BAQEF
|
|
5
|
+
AAOCAQ8AMIIBCgKCAQEAnvOVbMR+UkwpeiO2/bFzsT8FoxPD02xygTaBzEjTqenf
|
|
6
|
+
UyW2hjKbs4n4lLsepAeIXCiBehvqXN8igQb0JrVjzw79IBH6VFVltJWWAmI3REqi
|
|
7
|
+
cAeHgpvI8grWtdP71NqxaeIn1xF50InRozRiefiJV3L97QEW80v15c/wVQmVnIvl
|
|
8
|
+
eWBz5atAR+3LyImU92nRNbEhqryut4np9CUCt893koRgJvkDH8ptskI8+kG27jIG
|
|
9
|
+
Ppna0YOH+DRR6LeKfAcyzvL9dS5iY8gShMGu9EAuBwJqOpBnnpbLbAMd0OmrCELU
|
|
10
|
+
LK7L1PPpCHEWG8dpUyzCxWuGhjPddZWQrCtizFMjlQIDAQABo1MwUTAdBgNVHQ4E
|
|
11
|
+
FgQUZAzwEop03t58CqYfVC0p+EX8JQUwHwYDVR0jBBgwFoAUZAzwEop03t58CqYf
|
|
12
|
+
VC0p+EX8JQUwDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEAPV1E
|
|
13
|
+
SX+0JEr+9witMtq23mnHMOWFQhceRrelbxf3N5PhLWJVN7XEpKgYVpQEd0LuUj6K
|
|
14
|
+
tav0BTEVpNJtwi0YgGNvyjxG8WvMoJt3MiuI1ekQ+WteEehQajCF9dxm2MN+wtkK
|
|
15
|
+
7FD+NXJM+HpJP6xQrHipE2xqKMK52ANQWfm4FqUWjJgGcQnxzEE8jf0ZNlx+XR+n
|
|
16
|
+
tD5ftM+M1VM8bR2KxTeC3jF/HOAOEaHCu+xShxwfJTxdO/oyx7wq52v/3gTC2eoH
|
|
17
|
+
7bDN3oxzUgKPZB/P9xjAXhEnJHg75mHbulK/dEj/LmBW0QnYvSSv0ghqbV42rc/j
|
|
18
|
+
cCMRZXPrb3BCN7rHHA==
|
|
19
|
+
-----END CERTIFICATE-----
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
-----BEGIN PRIVATE KEY-----
|
|
2
|
+
MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQCe85VsxH5STCl6
|
|
3
|
+
I7b9sXOxPwWjE8PTbHKBNoHMSNOp6d9TJbaGMpuzifiUux6kB4hcKIF6G+pc3yKB
|
|
4
|
+
BvQmtWPPDv0gEfpUVWW0lZYCYjdESqJwB4eCm8jyCta10/vU2rFp4ifXEXnQidGj
|
|
5
|
+
NGJ5+IlXcv3tARbzS/Xlz/BVCZWci+V5YHPlq0BH7cvIiZT3adE1sSGqvK63ien0
|
|
6
|
+
JQK3z3eShGAm+QMfym2yQjz6QbbuMgY+mdrRg4f4NFHot4p8BzLO8v11LmJjyBKE
|
|
7
|
+
wa70QC4HAmo6kGeelstsAx3Q6asIQtQsrsvU8+kIcRYbx2lTLMLFa4aGM911lZCs
|
|
8
|
+
K2LMUyOVAgMBAAECggEAJ3OhXZT6nn4RWGOnblm+M5rHESYdd2smE3yeJEBaKsTl
|
|
9
|
+
vWsxnabPfvUqee3kqcVF2svh8RcfKQxn8bryW+4vFuwrNuvHZGmqu/LZv/81JPHa
|
|
10
|
+
VfCEeY4lhq/agVhbW8Yo/TUY+tg3UiN24SlmHMxreEVOEaZw1hO/NVpSViTyGZ+6
|
|
11
|
+
QaFU2+M3ZH95Svtmw9Qy1aY9qxjTUnbD7fm63CqXlCKNqJ1oDkpAAMa03Ufp4lxv
|
|
12
|
+
nucIuLdThKGkZFq815Y4GbExb0XogsMd1tN00dkqYPF4b9uAPaSf/jtz3HaFHMuz
|
|
13
|
+
Hr/PAhC/NyoVG6M7LXRDRYgNvlaK3bazRIoJBRgp8QKBgQDL1qjIGTsa2ulaTCzM
|
|
14
|
+
Iyb+O1L2JwmxTAzBQZXD0b1LJbCsHQCQK/M06W1sETjk9h+yiRKNEjefFi89bSvJ
|
|
15
|
+
JipbA+srsMO0fAVASIKmjVGaywvm1zquL1JAMr+0ZziMb8eRRuYyytPMwtOwaxp4
|
|
16
|
+
z3TTONKRHZpCN0UPdcxfOO8/8QKBgQDHoGcPiA4HyaFfC5yQLx8jq2+lZZefZFWl
|
|
17
|
+
bRPFYeo37xfYO+QItZV0nMz36NZYcWI9z+MuSmuvaHHCV27LQ9VtZqPxgPEqM8Qm
|
|
18
|
+
Rhf38nBOAinGLeitaZKaS5BMf9VU6yoi+kKOZQtd+96GbQYdUmp3HNYIrXwzUxh5
|
|
19
|
+
Mqr9Hv8B5QKBgCpgqVxYaoJNyr/cIGAcWsn2GWxVd11l2yz+bp10aG9MGavep7RR
|
|
20
|
+
ftGcSgRynCp1xOdAOhwcEnY/jXiuzrCV/65GZUkDCdzm/8x6hrcLoFCXMBVA39FA
|
|
21
|
+
w0/XfSWLZCVGQ+4/GDKtGlVyl8IQskM1lisnoBdNWTm09eWd7uxJEOxxAoGBAJq8
|
|
22
|
+
A4avcCirKpFQn8/HJrzwUr8Ci096Z0St0uhpaDJo+rOaYLw7fBiCvgHfkd3GLV86
|
|
23
|
+
N58XAndZXuxD91ZJQzLkn2lACC8tJvp/1G5VlqVw2c6KoVNhhMhij/wsDkEfc27f
|
|
24
|
+
Sx2yxufXpnnOIjIyQuIHEQRy7NWfsFWpZ46CKyndAoGBAKBXTSFaaH0q7nlxfqN3
|
|
25
|
+
agg/gg63MRjzWdIvWdFoNPm/5+M6ElxM6how34mYGUXSN0O+M9Dyeb+wdRFU3uqr
|
|
26
|
+
jdU1uajgBEZZaf99XLxZ0mve5mwYQmmYqWQwYU1+ZDWMzNL6Q9RPMI6z2AuQ9YzI
|
|
27
|
+
A8emo7hcJXJz0gFG3S9e5V/V
|
|
28
|
+
-----END PRIVATE KEY-----
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
FEDERATION_VERSION,
|
|
5
|
+
type FederationEnvelope,
|
|
6
|
+
FederationProtocolError,
|
|
7
|
+
type FederationTransport,
|
|
8
|
+
decodeFederationEnvelope,
|
|
9
|
+
encodeFederationEnvelope,
|
|
10
|
+
federationCall,
|
|
11
|
+
fingerprintCert,
|
|
12
|
+
validateCredentials,
|
|
13
|
+
} from "./index";
|
|
14
|
+
import { type FixtureCertSet, makeFixtureCertSet, makeFixtureCertSetOther } from "./test-helpers";
|
|
15
|
+
|
|
16
|
+
const goodEnvelope: FederationEnvelope = {
|
|
17
|
+
version: FEDERATION_VERSION,
|
|
18
|
+
traceparent: "00-aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa-bbbbbbbbbbbbbbbb-01",
|
|
19
|
+
federation: {
|
|
20
|
+
from: { deployment: "deployment-a", role: "researcher" },
|
|
21
|
+
to: { deployment: "deployment-b", role: "code-reviewer" },
|
|
22
|
+
mtls: { client_cert_subject: "CN=deployment-a" },
|
|
23
|
+
},
|
|
24
|
+
kind: "question",
|
|
25
|
+
payload: "review the patch in /tmp/x",
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
let certs: FixtureCertSet;
|
|
29
|
+
let otherCerts: FixtureCertSet;
|
|
30
|
+
|
|
31
|
+
beforeAll(() => {
|
|
32
|
+
certs = makeFixtureCertSet();
|
|
33
|
+
otherCerts = makeFixtureCertSetOther();
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
afterAll(() => {
|
|
37
|
+
certs.cleanup();
|
|
38
|
+
otherCerts.cleanup();
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
describe("encodeFederationEnvelope / decodeFederationEnvelope (T2)", () => {
|
|
42
|
+
test("round-trip happy path", () => {
|
|
43
|
+
const text = encodeFederationEnvelope(goodEnvelope);
|
|
44
|
+
const parsed = decodeFederationEnvelope(text);
|
|
45
|
+
expect(parsed).toEqual(goodEnvelope);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
test("rejects invalid version", () => {
|
|
49
|
+
const bad = {
|
|
50
|
+
...goodEnvelope,
|
|
51
|
+
version: "crewhaus.federation.v0",
|
|
52
|
+
} as unknown as FederationEnvelope;
|
|
53
|
+
expect(() => encodeFederationEnvelope(bad)).toThrow(/unsupported version/);
|
|
54
|
+
expect(() => decodeFederationEnvelope(JSON.stringify(bad))).toThrow(/unsupported version/);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
test("rejects missing federation field", () => {
|
|
58
|
+
const bad = { ...goodEnvelope, federation: undefined } as unknown as FederationEnvelope;
|
|
59
|
+
expect(() => decodeFederationEnvelope(JSON.stringify(bad))).toThrow(/missing federation/);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
test("rejects malformed party", () => {
|
|
63
|
+
const bad = JSON.parse(JSON.stringify(goodEnvelope)) as FederationEnvelope;
|
|
64
|
+
(bad.federation as unknown as { from: unknown }).from = { deployment: "x" };
|
|
65
|
+
expect(() => decodeFederationEnvelope(JSON.stringify(bad))).toThrow(/from\/to must be/);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
test("rejects invalid kind", () => {
|
|
69
|
+
const bad = { ...goodEnvelope, kind: "explosion" } as unknown as FederationEnvelope;
|
|
70
|
+
expect(() => decodeFederationEnvelope(JSON.stringify(bad))).toThrow(/invalid kind/);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
test("rejects non-string payload", () => {
|
|
74
|
+
const bad = { ...goodEnvelope, payload: 123 } as unknown as FederationEnvelope;
|
|
75
|
+
expect(() => decodeFederationEnvelope(JSON.stringify(bad))).toThrow(/payload must be a string/);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
test("rejects missing traceparent", () => {
|
|
79
|
+
const bad = { ...goodEnvelope, traceparent: "" } as FederationEnvelope;
|
|
80
|
+
expect(() => decodeFederationEnvelope(JSON.stringify(bad))).toThrow(/traceparent/);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
test("rejects malformed JSON", () => {
|
|
84
|
+
expect(() => decodeFederationEnvelope("{not json")).toThrow(FederationProtocolError);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
test("decode rejects mtls.client_cert_subject missing", () => {
|
|
88
|
+
const bad = JSON.parse(JSON.stringify(goodEnvelope)) as FederationEnvelope;
|
|
89
|
+
(bad.federation as unknown as { mtls: unknown }).mtls = {};
|
|
90
|
+
expect(() => decodeFederationEnvelope(JSON.stringify(bad))).toThrow(/client_cert_subject/);
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
describe("validateCredentials (T8)", () => {
|
|
95
|
+
test("happy path accepts a valid set", () => {
|
|
96
|
+
expect(() =>
|
|
97
|
+
validateCredentials({
|
|
98
|
+
caCertPem: certs.caCertPem,
|
|
99
|
+
clientCertPem: certs.clientCertPem,
|
|
100
|
+
clientKeyPem: certs.clientKeyPem,
|
|
101
|
+
pinnedFingerprint: certs.pinnedFingerprint,
|
|
102
|
+
}),
|
|
103
|
+
).not.toThrow();
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
test("rejects non-PEM ca", () => {
|
|
107
|
+
expect(() =>
|
|
108
|
+
validateCredentials({
|
|
109
|
+
caCertPem: "not a cert",
|
|
110
|
+
clientCertPem: certs.clientCertPem,
|
|
111
|
+
clientKeyPem: certs.clientKeyPem,
|
|
112
|
+
pinnedFingerprint: certs.pinnedFingerprint,
|
|
113
|
+
}),
|
|
114
|
+
).toThrow(/caCertPem is not PEM-encoded/);
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
test("rejects non-PEM cert", () => {
|
|
118
|
+
expect(() =>
|
|
119
|
+
validateCredentials({
|
|
120
|
+
caCertPem: certs.caCertPem,
|
|
121
|
+
clientCertPem: "not a cert",
|
|
122
|
+
clientKeyPem: certs.clientKeyPem,
|
|
123
|
+
pinnedFingerprint: certs.pinnedFingerprint,
|
|
124
|
+
}),
|
|
125
|
+
).toThrow(/clientCertPem is not PEM-encoded/);
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
test("rejects non-PEM key", () => {
|
|
129
|
+
expect(() =>
|
|
130
|
+
validateCredentials({
|
|
131
|
+
caCertPem: certs.caCertPem,
|
|
132
|
+
clientCertPem: certs.clientCertPem,
|
|
133
|
+
clientKeyPem: "not a key",
|
|
134
|
+
pinnedFingerprint: certs.pinnedFingerprint,
|
|
135
|
+
}),
|
|
136
|
+
).toThrow(/clientKeyPem is not PEM-encoded/);
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
test("rejects malformed pinnedFingerprint (length)", () => {
|
|
140
|
+
expect(() =>
|
|
141
|
+
validateCredentials({
|
|
142
|
+
caCertPem: certs.caCertPem,
|
|
143
|
+
clientCertPem: certs.clientCertPem,
|
|
144
|
+
clientKeyPem: certs.clientKeyPem,
|
|
145
|
+
pinnedFingerprint: "abc",
|
|
146
|
+
}),
|
|
147
|
+
).toThrow(/pinnedFingerprint must be 64-char hex/);
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
test("rejects malformed pinnedFingerprint (non-hex chars)", () => {
|
|
151
|
+
expect(() =>
|
|
152
|
+
validateCredentials({
|
|
153
|
+
caCertPem: certs.caCertPem,
|
|
154
|
+
clientCertPem: certs.clientCertPem,
|
|
155
|
+
clientKeyPem: certs.clientKeyPem,
|
|
156
|
+
pinnedFingerprint: "z".repeat(64),
|
|
157
|
+
}),
|
|
158
|
+
).toThrow(/pinnedFingerprint must be 64-char hex/);
|
|
159
|
+
});
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
describe("fingerprintCert", () => {
|
|
163
|
+
test("matches the openssl-computed fingerprint", () => {
|
|
164
|
+
const fp = fingerprintCert(certs.clientCertPem);
|
|
165
|
+
expect(fp).toBe(certs.pinnedFingerprint);
|
|
166
|
+
});
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
describe("federationCall — injected transport (T2)", () => {
|
|
170
|
+
test("happy path round-trip", async () => {
|
|
171
|
+
const captured: Array<{ url: string; envelope: FederationEnvelope }> = [];
|
|
172
|
+
const transport: FederationTransport = async (url, envelope) => {
|
|
173
|
+
captured.push({ url, envelope });
|
|
174
|
+
return { status: 200, body: '{"reply":"ack"}' };
|
|
175
|
+
};
|
|
176
|
+
const result = await federationCall({
|
|
177
|
+
url: "https://deployment-b.example/federation",
|
|
178
|
+
envelope: goodEnvelope,
|
|
179
|
+
credentials: {
|
|
180
|
+
caCertPem: certs.caCertPem,
|
|
181
|
+
clientCertPem: certs.clientCertPem,
|
|
182
|
+
clientKeyPem: certs.clientKeyPem,
|
|
183
|
+
pinnedFingerprint: certs.pinnedFingerprint,
|
|
184
|
+
},
|
|
185
|
+
transport,
|
|
186
|
+
});
|
|
187
|
+
expect(result.status).toBe(200);
|
|
188
|
+
expect(captured[0]?.url).toBe("https://deployment-b.example/federation");
|
|
189
|
+
expect(captured[0]?.envelope).toEqual(goodEnvelope);
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
test("validates credentials before invoking transport", async () => {
|
|
193
|
+
const transport: FederationTransport = async () => ({ status: 200, body: "" });
|
|
194
|
+
await expect(
|
|
195
|
+
federationCall({
|
|
196
|
+
url: "https://deployment-b.example/",
|
|
197
|
+
envelope: goodEnvelope,
|
|
198
|
+
credentials: {
|
|
199
|
+
caCertPem: "not a cert",
|
|
200
|
+
clientCertPem: certs.clientCertPem,
|
|
201
|
+
clientKeyPem: certs.clientKeyPem,
|
|
202
|
+
pinnedFingerprint: certs.pinnedFingerprint,
|
|
203
|
+
},
|
|
204
|
+
transport,
|
|
205
|
+
}),
|
|
206
|
+
).rejects.toThrow(/caCertPem is not PEM-encoded/);
|
|
207
|
+
});
|
|
208
|
+
});
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
import { X509Certificate, createPrivateKey, createPublicKey } from "node:crypto";
|
|
2
|
+
import { type Agent as HttpsAgent, type RequestOptions, request as httpsRequest } from "node:https";
|
|
3
|
+
/**
|
|
4
|
+
* @crewhaus/federation-protocol — Section 34
|
|
5
|
+
*
|
|
6
|
+
* Cross-deployment A2A wire protocol. Extends the in-crew envelope from
|
|
7
|
+
* `@crewhaus/a2a-protocol` (Section 22) with `federation` fields:
|
|
8
|
+
*
|
|
9
|
+
* {
|
|
10
|
+
* ...A2AEnvelope,
|
|
11
|
+
* version: "crewhaus.federation.v1", // strict — rejected without exact match
|
|
12
|
+
* federation: {
|
|
13
|
+
* from: { deployment, role },
|
|
14
|
+
* to: { deployment, role },
|
|
15
|
+
* mtls: { client_cert_subject }
|
|
16
|
+
* }
|
|
17
|
+
* }
|
|
18
|
+
*
|
|
19
|
+
* Transport: HTTPS POST with mutual TLS. Each deployment has a
|
|
20
|
+
* CA-issued certificate identifying its `deployment_id`; peers verify
|
|
21
|
+
* via cert pinning (SHA256 fingerprint check) plus standard TLS chain
|
|
22
|
+
* validation.
|
|
23
|
+
*
|
|
24
|
+
* Errors map to `recovery-engine` taxonomy:
|
|
25
|
+
* - cert mismatch / expired → tombstone (auth failure)
|
|
26
|
+
* - connection refused / timeout → retry with exponential backoff
|
|
27
|
+
* - 5xx → retry; 4xx → tombstone
|
|
28
|
+
*/
|
|
29
|
+
import { CrewhausError } from "@crewhaus/errors";
|
|
30
|
+
|
|
31
|
+
export class FederationProtocolError extends CrewhausError {
|
|
32
|
+
override readonly name = "FederationProtocolError";
|
|
33
|
+
constructor(message: string, cause?: unknown) {
|
|
34
|
+
super("config", message, cause);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export const FEDERATION_VERSION = "crewhaus.federation.v1" as const;
|
|
39
|
+
export type FederationVersion = typeof FEDERATION_VERSION;
|
|
40
|
+
|
|
41
|
+
export type FederationParty = {
|
|
42
|
+
readonly deployment: string;
|
|
43
|
+
readonly role: string;
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
export type FederationEnvelope = {
|
|
47
|
+
readonly version: FederationVersion;
|
|
48
|
+
readonly traceparent: string;
|
|
49
|
+
readonly federation: {
|
|
50
|
+
readonly from: FederationParty;
|
|
51
|
+
readonly to: FederationParty;
|
|
52
|
+
readonly mtls: { readonly client_cert_subject: string };
|
|
53
|
+
};
|
|
54
|
+
/** A2A envelope kind, mirroring `@crewhaus/a2a-protocol`. */
|
|
55
|
+
readonly kind: "question" | "answer" | "notify";
|
|
56
|
+
/** Free-form message body — typically a question prompt. */
|
|
57
|
+
readonly payload: string;
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
export function encodeFederationEnvelope(envelope: FederationEnvelope): string {
|
|
61
|
+
validateEnvelope(envelope);
|
|
62
|
+
return JSON.stringify(envelope);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function decodeFederationEnvelope(text: string): FederationEnvelope {
|
|
66
|
+
let parsed: unknown;
|
|
67
|
+
try {
|
|
68
|
+
parsed = JSON.parse(text);
|
|
69
|
+
} catch (cause) {
|
|
70
|
+
throw new FederationProtocolError("federation envelope: invalid JSON", cause);
|
|
71
|
+
}
|
|
72
|
+
if (typeof parsed !== "object" || parsed === null) {
|
|
73
|
+
throw new FederationProtocolError("federation envelope: not an object");
|
|
74
|
+
}
|
|
75
|
+
const env = parsed as Record<string, unknown>;
|
|
76
|
+
if (env["version"] !== FEDERATION_VERSION) {
|
|
77
|
+
throw new FederationProtocolError(
|
|
78
|
+
`federation envelope: unsupported version ${String(env["version"])} (expected ${FEDERATION_VERSION})`,
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
validateEnvelope(env as unknown as FederationEnvelope);
|
|
82
|
+
return env as unknown as FederationEnvelope;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function validateEnvelope(env: FederationEnvelope): void {
|
|
86
|
+
if (env.version !== FEDERATION_VERSION) {
|
|
87
|
+
throw new FederationProtocolError(`unsupported version ${String(env.version)}`);
|
|
88
|
+
}
|
|
89
|
+
if (!env.federation || typeof env.federation !== "object") {
|
|
90
|
+
throw new FederationProtocolError("federation envelope: missing federation field");
|
|
91
|
+
}
|
|
92
|
+
const f = env.federation;
|
|
93
|
+
if (!isParty(f.from) || !isParty(f.to)) {
|
|
94
|
+
throw new FederationProtocolError("federation envelope: from/to must be {deployment,role}");
|
|
95
|
+
}
|
|
96
|
+
if (!f.mtls || typeof f.mtls.client_cert_subject !== "string") {
|
|
97
|
+
throw new FederationProtocolError("federation envelope: missing mtls.client_cert_subject");
|
|
98
|
+
}
|
|
99
|
+
if (env.kind !== "question" && env.kind !== "answer" && env.kind !== "notify") {
|
|
100
|
+
throw new FederationProtocolError(`federation envelope: invalid kind ${String(env.kind)}`);
|
|
101
|
+
}
|
|
102
|
+
if (typeof env.payload !== "string") {
|
|
103
|
+
throw new FederationProtocolError("federation envelope: payload must be a string");
|
|
104
|
+
}
|
|
105
|
+
if (typeof env.traceparent !== "string" || env.traceparent.length === 0) {
|
|
106
|
+
throw new FederationProtocolError("federation envelope: missing traceparent");
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function isParty(p: unknown): p is FederationParty {
|
|
111
|
+
return (
|
|
112
|
+
typeof p === "object" &&
|
|
113
|
+
p !== null &&
|
|
114
|
+
typeof (p as FederationParty).deployment === "string" &&
|
|
115
|
+
typeof (p as FederationParty).role === "string" &&
|
|
116
|
+
(p as FederationParty).deployment.length > 0 &&
|
|
117
|
+
(p as FederationParty).role.length > 0
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// ─── mTLS transport ────────────────────────────────────────────────────────
|
|
122
|
+
|
|
123
|
+
export type MtlsCredentials = {
|
|
124
|
+
/** PEM-encoded CA bundle the peer's cert chain must validate against. */
|
|
125
|
+
readonly caCertPem: string;
|
|
126
|
+
/** PEM-encoded client certificate this deployment presents. */
|
|
127
|
+
readonly clientCertPem: string;
|
|
128
|
+
/** PEM-encoded private key matching `clientCertPem`. */
|
|
129
|
+
readonly clientKeyPem: string;
|
|
130
|
+
/**
|
|
131
|
+
* SHA256 fingerprint of the peer's expected leaf cert (hex, lowercase,
|
|
132
|
+
* 64 chars; no `:` separators). Strict pin: any other cert is rejected
|
|
133
|
+
* even if it chains to the same CA.
|
|
134
|
+
*/
|
|
135
|
+
readonly pinnedFingerprint: string;
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
export type FederationTransport = (
|
|
139
|
+
url: string,
|
|
140
|
+
envelope: FederationEnvelope,
|
|
141
|
+
creds: MtlsCredentials,
|
|
142
|
+
) => Promise<{ status: number; body: string }>;
|
|
143
|
+
|
|
144
|
+
export type FederationCallOptions = {
|
|
145
|
+
readonly url: string;
|
|
146
|
+
readonly envelope: FederationEnvelope;
|
|
147
|
+
readonly credentials: MtlsCredentials;
|
|
148
|
+
/** Test injection point. Defaults to a real `node:https` POST. */
|
|
149
|
+
readonly transport?: FederationTransport;
|
|
150
|
+
/** Timeout in ms. Default 30s. */
|
|
151
|
+
readonly timeoutMs?: number;
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
export async function federationCall(
|
|
155
|
+
opts: FederationCallOptions,
|
|
156
|
+
): Promise<{ status: number; body: string }> {
|
|
157
|
+
validateCredentials(opts.credentials);
|
|
158
|
+
const transport = opts.transport ?? defaultTransport(opts.timeoutMs ?? 30_000);
|
|
159
|
+
return transport(opts.url, opts.envelope, opts.credentials);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Asserts the credentials are well-formed BEFORE we try to build a TLS
|
|
164
|
+
* agent. Surfaces clear, actionable errors instead of openssl-flavoured
|
|
165
|
+
* mystery.
|
|
166
|
+
*/
|
|
167
|
+
export function validateCredentials(creds: MtlsCredentials): void {
|
|
168
|
+
if (!creds.caCertPem.includes("-----BEGIN CERTIFICATE-----")) {
|
|
169
|
+
throw new FederationProtocolError("credentials: caCertPem is not PEM-encoded");
|
|
170
|
+
}
|
|
171
|
+
if (!creds.clientCertPem.includes("-----BEGIN CERTIFICATE-----")) {
|
|
172
|
+
throw new FederationProtocolError("credentials: clientCertPem is not PEM-encoded");
|
|
173
|
+
}
|
|
174
|
+
if (
|
|
175
|
+
!creds.clientKeyPem.includes("-----BEGIN PRIVATE KEY-----") &&
|
|
176
|
+
!creds.clientKeyPem.includes("-----BEGIN RSA PRIVATE KEY-----") &&
|
|
177
|
+
!creds.clientKeyPem.includes("-----BEGIN EC PRIVATE KEY-----")
|
|
178
|
+
) {
|
|
179
|
+
throw new FederationProtocolError("credentials: clientKeyPem is not PEM-encoded");
|
|
180
|
+
}
|
|
181
|
+
if (!/^[0-9a-f]{64}$/.test(creds.pinnedFingerprint)) {
|
|
182
|
+
throw new FederationProtocolError(
|
|
183
|
+
`credentials: pinnedFingerprint must be 64-char hex sha256 (got ${creds.pinnedFingerprint.length} chars)`,
|
|
184
|
+
);
|
|
185
|
+
}
|
|
186
|
+
// Sanity: keys parse + cert parses + cert hasn't expired
|
|
187
|
+
try {
|
|
188
|
+
createPublicKey(creds.clientCertPem);
|
|
189
|
+
createPrivateKey(creds.clientKeyPem);
|
|
190
|
+
} catch (cause) {
|
|
191
|
+
throw new FederationProtocolError("credentials: client cert/key did not parse", cause);
|
|
192
|
+
}
|
|
193
|
+
try {
|
|
194
|
+
const cert = new X509Certificate(creds.clientCertPem);
|
|
195
|
+
const now = Date.now();
|
|
196
|
+
if (Date.parse(cert.validTo) < now) {
|
|
197
|
+
throw new FederationProtocolError(`credentials: client cert expired at ${cert.validTo}`);
|
|
198
|
+
}
|
|
199
|
+
} catch (cause) {
|
|
200
|
+
if (cause instanceof FederationProtocolError) throw cause;
|
|
201
|
+
throw new FederationProtocolError("credentials: client cert parse failed", cause);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/** Compute the SHA256 fingerprint of a PEM-encoded cert (hex, no separators). */
|
|
206
|
+
export function fingerprintCert(certPem: string): string {
|
|
207
|
+
const cert = new X509Certificate(certPem);
|
|
208
|
+
return cert.fingerprint256.replaceAll(":", "").toLowerCase();
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/** Default transport — real `node:https` POST with mTLS + cert pinning. */
|
|
212
|
+
function defaultTransport(timeoutMs: number): FederationTransport {
|
|
213
|
+
return async (url, envelope, creds) => {
|
|
214
|
+
const u = new URL(url);
|
|
215
|
+
if (u.protocol !== "https:") {
|
|
216
|
+
throw new FederationProtocolError(
|
|
217
|
+
`federation transport requires https://, got ${u.protocol}`,
|
|
218
|
+
);
|
|
219
|
+
}
|
|
220
|
+
const body = encodeFederationEnvelope(envelope);
|
|
221
|
+
const opts: RequestOptions = {
|
|
222
|
+
method: "POST",
|
|
223
|
+
protocol: u.protocol,
|
|
224
|
+
hostname: u.hostname,
|
|
225
|
+
port: u.port || 443,
|
|
226
|
+
path: `${u.pathname}${u.search}`,
|
|
227
|
+
headers: {
|
|
228
|
+
"Content-Type": "application/json",
|
|
229
|
+
"Content-Length": Buffer.byteLength(body, "utf8").toString(),
|
|
230
|
+
"X-Crewhaus-Federation-Version": FEDERATION_VERSION,
|
|
231
|
+
},
|
|
232
|
+
ca: creds.caCertPem,
|
|
233
|
+
cert: creds.clientCertPem,
|
|
234
|
+
key: creds.clientKeyPem,
|
|
235
|
+
rejectUnauthorized: true,
|
|
236
|
+
timeout: timeoutMs,
|
|
237
|
+
checkServerIdentity: (_host, cert) => {
|
|
238
|
+
// Strict pin: the peer's leaf cert fingerprint must match exactly.
|
|
239
|
+
const fp = cert.fingerprint256?.replaceAll(":", "").toLowerCase() ?? "";
|
|
240
|
+
if (fp !== creds.pinnedFingerprint) {
|
|
241
|
+
return new Error(
|
|
242
|
+
`cert-pin mismatch: peer fingerprint ${fp} != pinned ${creds.pinnedFingerprint}`,
|
|
243
|
+
);
|
|
244
|
+
}
|
|
245
|
+
return undefined;
|
|
246
|
+
},
|
|
247
|
+
};
|
|
248
|
+
|
|
249
|
+
return new Promise((resolve, reject) => {
|
|
250
|
+
const req = httpsRequest(opts, (res) => {
|
|
251
|
+
const chunks: Buffer[] = [];
|
|
252
|
+
res.on("data", (c) => chunks.push(c as Buffer));
|
|
253
|
+
res.on("end", () =>
|
|
254
|
+
resolve({
|
|
255
|
+
status: res.statusCode ?? 0,
|
|
256
|
+
body: Buffer.concat(chunks).toString("utf8"),
|
|
257
|
+
}),
|
|
258
|
+
);
|
|
259
|
+
});
|
|
260
|
+
req.on("error", (err) =>
|
|
261
|
+
reject(new FederationProtocolError(`federation transport error: ${err.message}`, err)),
|
|
262
|
+
);
|
|
263
|
+
req.on("timeout", () => {
|
|
264
|
+
req.destroy(
|
|
265
|
+
new FederationProtocolError(`federation transport timeout after ${timeoutMs}ms`),
|
|
266
|
+
);
|
|
267
|
+
});
|
|
268
|
+
req.write(body);
|
|
269
|
+
req.end();
|
|
270
|
+
});
|
|
271
|
+
};
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/** Re-export for callers that need to spawn their own agent (rare). */
|
|
275
|
+
export type { HttpsAgent };
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Test helpers for federation-protocol — read pre-generated fixture
|
|
3
|
+
* certs from disk so tests don't shell to openssl on every run.
|
|
4
|
+
*
|
|
5
|
+
* The fixture certs are self-signed RSA 2048, valid for 10 years, with
|
|
6
|
+
* CN=fed-fixture (and CN=fed-other for the "wrong key" pair). Generated
|
|
7
|
+
* once via:
|
|
8
|
+
*
|
|
9
|
+
* openssl req -x509 -newkey rsa:2048 -nodes \
|
|
10
|
+
* -keyout fixtures-key.pem -out fixtures-cert.pem \
|
|
11
|
+
* -days 3650 -subj "/CN=fed-fixture" -sha256
|
|
12
|
+
*
|
|
13
|
+
* Test-only secrets — no production deployment uses these.
|
|
14
|
+
*
|
|
15
|
+
* Earlier iterations shelled to openssl on every test run; that timed
|
|
16
|
+
* out on CI runners with slow entropy and racy `openssl x509 ...
|
|
17
|
+
* -fingerprint -sha256` invocations. Static fixtures are deterministic
|
|
18
|
+
* + fast (~1 ms read).
|
|
19
|
+
*/
|
|
20
|
+
import { readFileSync } from "node:fs";
|
|
21
|
+
import { join } from "node:path";
|
|
22
|
+
|
|
23
|
+
import { fingerprintCert } from "./index";
|
|
24
|
+
|
|
25
|
+
export type FixtureCertSet = {
|
|
26
|
+
readonly caCertPem: string;
|
|
27
|
+
readonly clientCertPem: string;
|
|
28
|
+
readonly clientKeyPem: string;
|
|
29
|
+
readonly pinnedFingerprint: string;
|
|
30
|
+
/** No-op — kept for backwards compat with the old openssl-based helper. */
|
|
31
|
+
readonly cleanup: () => void;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
// `tsc -b` also compiles this file into `dist/`, but the PEM fixtures live
|
|
35
|
+
// next to the source. Map the dist path back so the dist copy finds them too.
|
|
36
|
+
const FIXTURES_DIR = import.meta.dir.replace(/([/\\])dist$/, "$1src");
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Returns the canonical fixture cert pair (CN=fed-fixture). The
|
|
40
|
+
* `_commonName` arg is accepted for backwards compat with the
|
|
41
|
+
* generate-on-the-fly helper this replaced; it has no effect.
|
|
42
|
+
*/
|
|
43
|
+
export function makeFixtureCertSet(_commonName = "fed-fixture"): FixtureCertSet {
|
|
44
|
+
return readFixtures("fixtures-cert.pem", "fixtures-key.pem");
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Returns a second, unrelated cert pair (CN=fed-other) — used by the
|
|
49
|
+
* "wrong public key" / "tampered cert" tests to assert pinning rejects
|
|
50
|
+
* keys that don't match the configured fingerprint.
|
|
51
|
+
*/
|
|
52
|
+
export function makeFixtureCertSetOther(): FixtureCertSet {
|
|
53
|
+
return readFixtures("fixtures-other-cert.pem", "fixtures-other-key.pem");
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function readFixtures(certFile: string, keyFile: string): FixtureCertSet {
|
|
57
|
+
const certPem = readFileSync(join(FIXTURES_DIR, certFile), "utf8");
|
|
58
|
+
const keyPem = readFileSync(join(FIXTURES_DIR, keyFile), "utf8");
|
|
59
|
+
return {
|
|
60
|
+
caCertPem: certPem,
|
|
61
|
+
clientCertPem: certPem,
|
|
62
|
+
clientKeyPem: keyPem,
|
|
63
|
+
pinnedFingerprint: fingerprintCert(certPem),
|
|
64
|
+
cleanup: () => undefined,
|
|
65
|
+
};
|
|
66
|
+
}
|