@groundnuty/macf-core 0.2.0-rc.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/dist/certs/agent-cert.d.ts +91 -0
- package/dist/certs/agent-cert.d.ts.map +1 -0
- package/dist/certs/agent-cert.js +263 -0
- package/dist/certs/agent-cert.js.map +1 -0
- package/dist/certs/ca.d.ts +103 -0
- package/dist/certs/ca.d.ts.map +1 -0
- package/dist/certs/ca.js +306 -0
- package/dist/certs/ca.js.map +1 -0
- package/dist/certs/challenge-store.d.ts +28 -0
- package/dist/certs/challenge-store.d.ts.map +1 -0
- package/dist/certs/challenge-store.js +94 -0
- package/dist/certs/challenge-store.js.map +1 -0
- package/dist/certs/challenge.d.ts +70 -0
- package/dist/certs/challenge.d.ts.map +1 -0
- package/dist/certs/challenge.js +54 -0
- package/dist/certs/challenge.js.map +1 -0
- package/dist/certs/crypto-provider.d.ts +14 -0
- package/dist/certs/crypto-provider.d.ts.map +1 -0
- package/dist/certs/crypto-provider.js +18 -0
- package/dist/certs/crypto-provider.js.map +1 -0
- package/dist/certs/index.d.ts +7 -0
- package/dist/certs/index.d.ts.map +1 -0
- package/dist/certs/index.js +5 -0
- package/dist/certs/index.js.map +1 -0
- package/dist/config.d.ts +3 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +131 -0
- package/dist/config.js.map +1 -0
- package/dist/errors.d.ts +51 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +78 -0
- package/dist/errors.js.map +1 -0
- package/dist/index.d.ts +24 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +25 -0
- package/dist/index.js.map +1 -0
- package/dist/logger.d.ts +6 -0
- package/dist/logger.d.ts.map +1 -0
- package/dist/logger.js +39 -0
- package/dist/logger.js.map +1 -0
- package/dist/mtls-health-ping.d.ts +26 -0
- package/dist/mtls-health-ping.d.ts.map +1 -0
- package/dist/mtls-health-ping.js +53 -0
- package/dist/mtls-health-ping.js.map +1 -0
- package/dist/registry/factory.d.ts +10 -0
- package/dist/registry/factory.d.ts.map +1 -0
- package/dist/registry/factory.js +26 -0
- package/dist/registry/factory.js.map +1 -0
- package/dist/registry/github-client.d.ts +14 -0
- package/dist/registry/github-client.d.ts.map +1 -0
- package/dist/registry/github-client.js +104 -0
- package/dist/registry/github-client.js.map +1 -0
- package/dist/registry/index.d.ts +7 -0
- package/dist/registry/index.d.ts.map +1 -0
- package/dist/registry/index.js +6 -0
- package/dist/registry/index.js.map +1 -0
- package/dist/registry/registry.d.ts +8 -0
- package/dist/registry/registry.d.ts.map +1 -0
- package/dist/registry/registry.js +65 -0
- package/dist/registry/registry.js.map +1 -0
- package/dist/registry/types.d.ts +56 -0
- package/dist/registry/types.d.ts.map +1 -0
- package/dist/registry/types.js +29 -0
- package/dist/registry/types.js.map +1 -0
- package/dist/registry/variable-name.d.ts +15 -0
- package/dist/registry/variable-name.d.ts.map +1 -0
- package/dist/registry/variable-name.js +17 -0
- package/dist/registry/variable-name.js.map +1 -0
- package/dist/token.d.ts +29 -0
- package/dist/token.d.ts.map +1 -0
- package/dist/token.js +44 -0
- package/dist/token.js.map +1 -0
- package/dist/types.d.ts +151 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +102 -0
- package/dist/types.js.map +1 -0
- package/package.json +37 -0
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { MacfError } from '../errors.js';
|
|
2
|
+
export declare class AgentCertError extends MacfError {
|
|
3
|
+
constructor(message: string);
|
|
4
|
+
}
|
|
5
|
+
export interface AgentCertResult {
|
|
6
|
+
readonly certPem: string;
|
|
7
|
+
readonly keyPem: string;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Import a PEM private key into a WebCrypto CryptoKey for signing.
|
|
11
|
+
*
|
|
12
|
+
* Return type was `Promise<unknown>` historically — DOM CryptoKey types
|
|
13
|
+
* weren't exposed via @types/node < v25. Since @types/node v25 (#17 /
|
|
14
|
+
* PR #130) CryptoKey is resolvable from `globalThis`, so we return
|
|
15
|
+
* the precise type instead of laundering through `unknown` at each
|
|
16
|
+
* call site.
|
|
17
|
+
*
|
|
18
|
+
* Rejects input that contains zero or multiple BEGIN/END marker pairs
|
|
19
|
+
* (e.g. two keys accidentally concatenated) — ultrareview finding H4.
|
|
20
|
+
* Without this shape check, `webcrypto.subtle.importKey` would be
|
|
21
|
+
* handed a concatenated base64 blob and throw a generic DataError,
|
|
22
|
+
* which propagates upstream with no hint that the input file itself
|
|
23
|
+
* was malformed.
|
|
24
|
+
*/
|
|
25
|
+
export declare function importPrivateKey(keyPem: string): Promise<CryptoKey>;
|
|
26
|
+
/**
|
|
27
|
+
* Generate agent certificate signed by the CA.
|
|
28
|
+
* Used when the CA key is available locally.
|
|
29
|
+
*
|
|
30
|
+
* `advertiseHost`, when supplied, is added to the cert's SAN list on
|
|
31
|
+
* top of the default [127.0.0.1, localhost] pair. This is how an agent
|
|
32
|
+
* reachable at a Tailscale IP / DNS name passes server-hostname
|
|
33
|
+
* verification when the routing Action (or a sibling agent) connects
|
|
34
|
+
* over the network. Classification is IPv4-shape vs DNS via
|
|
35
|
+
* `hostToSan()`. See macf#178 Gap 3.
|
|
36
|
+
*/
|
|
37
|
+
export declare function generateAgentCert(config: {
|
|
38
|
+
readonly agentName: string;
|
|
39
|
+
readonly caCertPem: string;
|
|
40
|
+
readonly caKeyPem: string;
|
|
41
|
+
readonly advertiseHost?: string;
|
|
42
|
+
readonly certPath?: string;
|
|
43
|
+
readonly keyPath?: string;
|
|
44
|
+
}): Promise<AgentCertResult>;
|
|
45
|
+
/**
|
|
46
|
+
* Generate a CA-signed client cert with a given CN and validity window.
|
|
47
|
+
* Used for non-peer clients (e.g. the routing Action's mTLS cert, per
|
|
48
|
+
* macf-actions#8 / #119). Unlike generateAgentCert, validity is
|
|
49
|
+
* parameterized in days so operator can pick the policy.
|
|
50
|
+
*
|
|
51
|
+
* Does NOT add SubjectAlternativeName — the routing Action is an
|
|
52
|
+
* mTLS CLIENT, so the server-hostname SAN pattern doesn't apply. Key
|
|
53
|
+
* usage is digital signature only (no key encipherment — we're not
|
|
54
|
+
* doing static-key TLS variants).
|
|
55
|
+
*/
|
|
56
|
+
export declare function generateClientCert(config: {
|
|
57
|
+
readonly commonName: string;
|
|
58
|
+
readonly validityDays: number;
|
|
59
|
+
readonly caCertPem: string;
|
|
60
|
+
readonly caKeyPem: string;
|
|
61
|
+
}): Promise<AgentCertResult>;
|
|
62
|
+
/**
|
|
63
|
+
* Generate a CSR (Certificate Signing Request) for an agent.
|
|
64
|
+
* Used when requesting remote signing via /sign endpoint.
|
|
65
|
+
*/
|
|
66
|
+
export declare function generateCSR(agentName: string): Promise<{
|
|
67
|
+
readonly csrPem: string;
|
|
68
|
+
readonly keyPem: string;
|
|
69
|
+
}>;
|
|
70
|
+
/**
|
|
71
|
+
* Extract the CN from a subject string like "CN=code-agent" or
|
|
72
|
+
* "O=foo,CN=code-agent,OU=bar".
|
|
73
|
+
*
|
|
74
|
+
* Returns undefined if the subject contains ZERO or MULTIPLE CN fields.
|
|
75
|
+
* A multi-CN subject ("CN=attacker,CN=victim") is explicitly rejected
|
|
76
|
+
* — signCSR surfaces a specific error so the confused-deputy attack
|
|
77
|
+
* can't slip through the agent-name equality check. See #89.
|
|
78
|
+
*
|
|
79
|
+
* Exported for unit tests.
|
|
80
|
+
*/
|
|
81
|
+
export declare function extractCN(subject: string): string | undefined;
|
|
82
|
+
/**
|
|
83
|
+
* Sign a CSR using the CA key. Validates CN match and CSR signature (proof-of-possession).
|
|
84
|
+
*/
|
|
85
|
+
export declare function signCSR(config: {
|
|
86
|
+
readonly csrPem: string;
|
|
87
|
+
readonly agentName: string;
|
|
88
|
+
readonly caCertPem: string;
|
|
89
|
+
readonly caKeyPem: string;
|
|
90
|
+
}): Promise<string>;
|
|
91
|
+
//# sourceMappingURL=agent-cert.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"agent-cert.d.ts","sourceRoot":"","sources":["../../src/certs/agent-cert.ts"],"names":[],"mappings":"AAKA,OAAO,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AAEzC,qBAAa,cAAe,SAAQ,SAAS;gBAC/B,OAAO,EAAE,MAAM;CAI5B;AAED,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;CACzB;AAQD;;;;;;;;;;;;;;;GAeG;AACH,wBAAsB,gBAAgB,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,SAAS,CAAC,CA2BzE;AAsGD;;;;;;;;;;GAUG;AACH,wBAAsB,iBAAiB,CAAC,MAAM,EAAE;IAC9C,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,aAAa,CAAC,EAAE,MAAM,CAAC;IAChC,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC;CAC3B,GAAG,OAAO,CAAC,eAAe,CAAC,CAuB3B;AAED;;;;;;;;;;GAUG;AACH,wBAAsB,kBAAkB,CAAC,MAAM,EAAE;IAC/C,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;IAC9B,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;CAC3B,GAAG,OAAO,CAAC,eAAe,CAAC,CAgD3B;AAED;;;GAGG;AACH,wBAAsB,WAAW,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC;IAC5D,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;CACzB,CAAC,CAmBD;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,SAAS,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAK7D;AAED;;GAEG;AACH,wBAAsB,OAAO,CAAC,MAAM,EAAE;IACpC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;CAC3B,GAAG,OAAO,CAAC,MAAM,CAAC,CAiClB"}
|
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
import { randomBytes } from 'node:crypto';
|
|
2
|
+
import { writeFileSync } from 'node:fs';
|
|
3
|
+
import { x509, webcrypto, RSA_ALGORITHM, AGENT_CERT_VALIDITY_YEARS, } from './crypto-provider.js';
|
|
4
|
+
import { MacfError } from '../errors.js';
|
|
5
|
+
export class AgentCertError extends MacfError {
|
|
6
|
+
constructor(message) {
|
|
7
|
+
super('AGENT_CERT_ERROR', message);
|
|
8
|
+
this.name = 'AgentCertError';
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
function exportKeyToPem(exported) {
|
|
12
|
+
const b64 = Buffer.from(exported).toString('base64');
|
|
13
|
+
const lines = b64.match(/.{1,64}/g) ?? [];
|
|
14
|
+
return `-----BEGIN PRIVATE KEY-----\n${lines.join('\n')}\n-----END PRIVATE KEY-----\n`;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Import a PEM private key into a WebCrypto CryptoKey for signing.
|
|
18
|
+
*
|
|
19
|
+
* Return type was `Promise<unknown>` historically — DOM CryptoKey types
|
|
20
|
+
* weren't exposed via @types/node < v25. Since @types/node v25 (#17 /
|
|
21
|
+
* PR #130) CryptoKey is resolvable from `globalThis`, so we return
|
|
22
|
+
* the precise type instead of laundering through `unknown` at each
|
|
23
|
+
* call site.
|
|
24
|
+
*
|
|
25
|
+
* Rejects input that contains zero or multiple BEGIN/END marker pairs
|
|
26
|
+
* (e.g. two keys accidentally concatenated) — ultrareview finding H4.
|
|
27
|
+
* Without this shape check, `webcrypto.subtle.importKey` would be
|
|
28
|
+
* handed a concatenated base64 blob and throw a generic DataError,
|
|
29
|
+
* which propagates upstream with no hint that the input file itself
|
|
30
|
+
* was malformed.
|
|
31
|
+
*/
|
|
32
|
+
export async function importPrivateKey(keyPem) {
|
|
33
|
+
const beginMatches = keyPem.match(/-----BEGIN PRIVATE KEY-----/g);
|
|
34
|
+
const endMatches = keyPem.match(/-----END PRIVATE KEY-----/g);
|
|
35
|
+
if (!beginMatches || beginMatches.length !== 1) {
|
|
36
|
+
throw new AgentCertError(`Malformed private key PEM: expected exactly one BEGIN marker, got ${beginMatches?.length ?? 0}`);
|
|
37
|
+
}
|
|
38
|
+
if (!endMatches || endMatches.length !== 1) {
|
|
39
|
+
throw new AgentCertError(`Malformed private key PEM: expected exactly one END marker, got ${endMatches?.length ?? 0}`);
|
|
40
|
+
}
|
|
41
|
+
const stripped = keyPem
|
|
42
|
+
.replace(/-----BEGIN PRIVATE KEY-----/g, '')
|
|
43
|
+
.replace(/-----END PRIVATE KEY-----/g, '')
|
|
44
|
+
.replace(/\s/g, '');
|
|
45
|
+
const der = Buffer.from(stripped, 'base64');
|
|
46
|
+
return webcrypto.subtle.importKey('pkcs8', der, RSA_ALGORITHM, false, ['sign']);
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Classify a host string as an IP or DNS name for SubjectAlternativeName
|
|
50
|
+
* entries. Shape-only check (matches `999.999.999.999` too — cert
|
|
51
|
+
* generation doesn't validate octet ranges, and we'd rather keep the
|
|
52
|
+
* classifier forgiving than have it silently misclassify a typo'd IP
|
|
53
|
+
* as DNS). IPv6 not handled here; add `:` detection + `[]` URL-wrapping
|
|
54
|
+
* when there's an actual ask.
|
|
55
|
+
*/
|
|
56
|
+
function hostToSan(host) {
|
|
57
|
+
const ipv4Shape = /^(\d{1,3}\.){3}\d{1,3}$/;
|
|
58
|
+
return ipv4Shape.test(host)
|
|
59
|
+
? { type: 'ip', value: host }
|
|
60
|
+
: { type: 'dns', value: host };
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Shared peer-cert builder used by both generateAgentCert (new peer
|
|
64
|
+
* certs via `macf certs init`) and signCSR (CSR-signed peer certs
|
|
65
|
+
* via `/sign`). Produces the DR-004-compliant extension set:
|
|
66
|
+
*
|
|
67
|
+
* - KeyUsage: digitalSignature | keyEncipherment (mTLS client+server use)
|
|
68
|
+
* - SubjectAlternativeName: 127.0.0.1 / localhost (always, for local-debug
|
|
69
|
+
* flows — curl-to-localhost for /health etc.) plus any caller-
|
|
70
|
+
* supplied extraSans (typically the agent's advertised host per
|
|
71
|
+
* macf#178 Gap 3)
|
|
72
|
+
* - ExtendedKeyUsage: serverAuth + clientAuth. Agents are dual-role
|
|
73
|
+
* peers — they act as TLS SERVERS when receiving /notify, /health,
|
|
74
|
+
* /sign POSTs, and as TLS CLIENTS when originating POSTs to other
|
|
75
|
+
* peers. Without serverAuth, OpenSSL/curl server-role validation
|
|
76
|
+
* rejects the presented cert with "unsuitable certificate purpose"
|
|
77
|
+
* (curl error 60). See macf#180. #121 still enforces clientAuth
|
|
78
|
+
* server-side at /health + /notify + /sign; serverAuth is purely
|
|
79
|
+
* additive for the client-side TLS validation of agents-as-servers.
|
|
80
|
+
* `generateClientCert` (routing-action) stays client-only — it's
|
|
81
|
+
* a pure client with no server role.
|
|
82
|
+
*
|
|
83
|
+
* Extracted per ultrareview finding A10 — both callers previously
|
|
84
|
+
* duplicated this ~25-line extension list. When DR-004 extensions
|
|
85
|
+
* evolve, a single edit here affects both paths instead of two in
|
|
86
|
+
* lockstep.
|
|
87
|
+
*/
|
|
88
|
+
async function buildPeerCert(opts) {
|
|
89
|
+
const caCert = new x509.X509Certificate(opts.caCertPem);
|
|
90
|
+
const caKey = await importPrivateKey(opts.caKeyPem);
|
|
91
|
+
const notBefore = new Date();
|
|
92
|
+
const notAfter = new Date();
|
|
93
|
+
notAfter.setFullYear(notAfter.getFullYear() + AGENT_CERT_VALIDITY_YEARS);
|
|
94
|
+
const sans = [
|
|
95
|
+
{ type: 'ip', value: '127.0.0.1' },
|
|
96
|
+
{ type: 'dns', value: 'localhost' },
|
|
97
|
+
...(opts.extraSans ?? []),
|
|
98
|
+
];
|
|
99
|
+
const cert = await x509.X509CertificateGenerator.create({
|
|
100
|
+
serialNumber: randomBytes(8).toString('hex'),
|
|
101
|
+
subject: opts.subject,
|
|
102
|
+
issuer: caCert.subject,
|
|
103
|
+
notBefore,
|
|
104
|
+
notAfter,
|
|
105
|
+
signingAlgorithm: RSA_ALGORITHM,
|
|
106
|
+
publicKey: opts.publicKey,
|
|
107
|
+
signingKey: caKey,
|
|
108
|
+
extensions: [
|
|
109
|
+
new x509.KeyUsagesExtension(x509.KeyUsageFlags.digitalSignature | x509.KeyUsageFlags.keyEncipherment, true),
|
|
110
|
+
new x509.SubjectAlternativeNameExtension(sans),
|
|
111
|
+
new x509.ExtendedKeyUsageExtension([
|
|
112
|
+
// serverAuth OID — agents are TLS servers on /notify, /health,
|
|
113
|
+
// /sign. Without this, OpenSSL/curl server-role validation
|
|
114
|
+
// rejects with "unsuitable certificate purpose" (curl error
|
|
115
|
+
// 60). See macf#180.
|
|
116
|
+
'1.3.6.1.5.5.7.3.1',
|
|
117
|
+
// clientAuth OID (#125) — agents are also TLS clients when
|
|
118
|
+
// originating POSTs to peers. Enforced server-side at /health
|
|
119
|
+
// + /notify + /sign per #121.
|
|
120
|
+
'1.3.6.1.5.5.7.3.2',
|
|
121
|
+
]),
|
|
122
|
+
],
|
|
123
|
+
});
|
|
124
|
+
return cert.toString('pem');
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* Generate agent certificate signed by the CA.
|
|
128
|
+
* Used when the CA key is available locally.
|
|
129
|
+
*
|
|
130
|
+
* `advertiseHost`, when supplied, is added to the cert's SAN list on
|
|
131
|
+
* top of the default [127.0.0.1, localhost] pair. This is how an agent
|
|
132
|
+
* reachable at a Tailscale IP / DNS name passes server-hostname
|
|
133
|
+
* verification when the routing Action (or a sibling agent) connects
|
|
134
|
+
* over the network. Classification is IPv4-shape vs DNS via
|
|
135
|
+
* `hostToSan()`. See macf#178 Gap 3.
|
|
136
|
+
*/
|
|
137
|
+
export async function generateAgentCert(config) {
|
|
138
|
+
const { agentName, caCertPem, caKeyPem, advertiseHost, certPath, keyPath } = config;
|
|
139
|
+
const agentKeys = await webcrypto.subtle.generateKey(RSA_ALGORITHM, true, ['sign', 'verify']);
|
|
140
|
+
const certPem = await buildPeerCert({
|
|
141
|
+
subject: `CN=${agentName}`,
|
|
142
|
+
caCertPem,
|
|
143
|
+
caKeyPem,
|
|
144
|
+
publicKey: agentKeys.publicKey,
|
|
145
|
+
extraSans: advertiseHost ? [hostToSan(advertiseHost)] : undefined,
|
|
146
|
+
});
|
|
147
|
+
const exported = await webcrypto.subtle.exportKey('pkcs8', agentKeys.privateKey);
|
|
148
|
+
const agentKeyPem = exportKeyToPem(exported);
|
|
149
|
+
if (certPath)
|
|
150
|
+
writeFileSync(certPath, certPem, { mode: 0o644 });
|
|
151
|
+
if (keyPath)
|
|
152
|
+
writeFileSync(keyPath, agentKeyPem, { mode: 0o600 });
|
|
153
|
+
return { certPem, keyPem: agentKeyPem };
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* Generate a CA-signed client cert with a given CN and validity window.
|
|
157
|
+
* Used for non-peer clients (e.g. the routing Action's mTLS cert, per
|
|
158
|
+
* macf-actions#8 / #119). Unlike generateAgentCert, validity is
|
|
159
|
+
* parameterized in days so operator can pick the policy.
|
|
160
|
+
*
|
|
161
|
+
* Does NOT add SubjectAlternativeName — the routing Action is an
|
|
162
|
+
* mTLS CLIENT, so the server-hostname SAN pattern doesn't apply. Key
|
|
163
|
+
* usage is digital signature only (no key encipherment — we're not
|
|
164
|
+
* doing static-key TLS variants).
|
|
165
|
+
*/
|
|
166
|
+
export async function generateClientCert(config) {
|
|
167
|
+
const { commonName, validityDays, caCertPem, caKeyPem } = config;
|
|
168
|
+
if (!Number.isInteger(validityDays) || validityDays < 1) {
|
|
169
|
+
throw new AgentCertError(`validityDays must be a positive integer (got ${validityDays})`);
|
|
170
|
+
}
|
|
171
|
+
const caCert = new x509.X509Certificate(caCertPem);
|
|
172
|
+
const caKey = await importPrivateKey(caKeyPem);
|
|
173
|
+
const clientKeys = await webcrypto.subtle.generateKey(RSA_ALGORITHM, true, ['sign', 'verify']);
|
|
174
|
+
const notBefore = new Date();
|
|
175
|
+
const notAfter = new Date();
|
|
176
|
+
notAfter.setDate(notAfter.getDate() + validityDays);
|
|
177
|
+
const cert = await x509.X509CertificateGenerator.create({
|
|
178
|
+
serialNumber: randomBytes(8).toString('hex'),
|
|
179
|
+
subject: `CN=${commonName}`,
|
|
180
|
+
issuer: caCert.subject,
|
|
181
|
+
notBefore,
|
|
182
|
+
notAfter,
|
|
183
|
+
signingAlgorithm: RSA_ALGORITHM,
|
|
184
|
+
publicKey: clientKeys.publicKey,
|
|
185
|
+
signingKey: caKey,
|
|
186
|
+
extensions: [
|
|
187
|
+
new x509.KeyUsagesExtension(x509.KeyUsageFlags.digitalSignature, true),
|
|
188
|
+
new x509.ExtendedKeyUsageExtension([
|
|
189
|
+
// clientAuth OID — explicit "this cert is for TLS client auth"
|
|
190
|
+
'1.3.6.1.5.5.7.3.2',
|
|
191
|
+
]),
|
|
192
|
+
],
|
|
193
|
+
});
|
|
194
|
+
const certPem = cert.toString('pem');
|
|
195
|
+
const exported = await webcrypto.subtle.exportKey('pkcs8', clientKeys.privateKey);
|
|
196
|
+
const keyPem = exportKeyToPem(exported);
|
|
197
|
+
return { certPem, keyPem };
|
|
198
|
+
}
|
|
199
|
+
/**
|
|
200
|
+
* Generate a CSR (Certificate Signing Request) for an agent.
|
|
201
|
+
* Used when requesting remote signing via /sign endpoint.
|
|
202
|
+
*/
|
|
203
|
+
export async function generateCSR(agentName) {
|
|
204
|
+
const keys = await webcrypto.subtle.generateKey(RSA_ALGORITHM, true, ['sign', 'verify']);
|
|
205
|
+
const csr = await x509.Pkcs10CertificateRequestGenerator.create({
|
|
206
|
+
name: `CN=${agentName}`,
|
|
207
|
+
keys,
|
|
208
|
+
signingAlgorithm: RSA_ALGORITHM,
|
|
209
|
+
});
|
|
210
|
+
const exported = await webcrypto.subtle.exportKey('pkcs8', keys.privateKey);
|
|
211
|
+
return {
|
|
212
|
+
csrPem: csr.toString('pem'),
|
|
213
|
+
keyPem: exportKeyToPem(exported),
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
/**
|
|
217
|
+
* Extract the CN from a subject string like "CN=code-agent" or
|
|
218
|
+
* "O=foo,CN=code-agent,OU=bar".
|
|
219
|
+
*
|
|
220
|
+
* Returns undefined if the subject contains ZERO or MULTIPLE CN fields.
|
|
221
|
+
* A multi-CN subject ("CN=attacker,CN=victim") is explicitly rejected
|
|
222
|
+
* — signCSR surfaces a specific error so the confused-deputy attack
|
|
223
|
+
* can't slip through the agent-name equality check. See #89.
|
|
224
|
+
*
|
|
225
|
+
* Exported for unit tests.
|
|
226
|
+
*/
|
|
227
|
+
export function extractCN(subject) {
|
|
228
|
+
const matches = subject.match(/(?:^|,\s*)CN=([^,]+)/gi);
|
|
229
|
+
if (!matches || matches.length !== 1)
|
|
230
|
+
return undefined;
|
|
231
|
+
const inner = /CN=([^,]+)/i.exec(matches[0]);
|
|
232
|
+
return inner?.[1]?.trim();
|
|
233
|
+
}
|
|
234
|
+
/**
|
|
235
|
+
* Sign a CSR using the CA key. Validates CN match and CSR signature (proof-of-possession).
|
|
236
|
+
*/
|
|
237
|
+
export async function signCSR(config) {
|
|
238
|
+
const { csrPem, agentName, caCertPem, caKeyPem } = config;
|
|
239
|
+
const csr = new x509.Pkcs10CertificateRequest(csrPem);
|
|
240
|
+
// Verify CSR signature (proof-of-possession — requester controls the private key)
|
|
241
|
+
const csrValid = await csr.verify();
|
|
242
|
+
if (!csrValid) {
|
|
243
|
+
throw new AgentCertError('CSR signature verification failed');
|
|
244
|
+
}
|
|
245
|
+
// Verify CN matches agent name. extractCN returns undefined when the
|
|
246
|
+
// subject has zero OR multiple CN fields — surface that specifically
|
|
247
|
+
// so operators see "subject malformed" rather than "CN undefined does
|
|
248
|
+
// not match ..." (see #89).
|
|
249
|
+
const cn = extractCN(csr.subject);
|
|
250
|
+
if (cn === undefined) {
|
|
251
|
+
throw new AgentCertError(`CSR subject must contain exactly one CN field (got: "${csr.subject}")`);
|
|
252
|
+
}
|
|
253
|
+
if (cn !== agentName) {
|
|
254
|
+
throw new AgentCertError(`CSR CN "${cn}" does not match agent name "${agentName}"`);
|
|
255
|
+
}
|
|
256
|
+
return buildPeerCert({
|
|
257
|
+
subject: csr.subject,
|
|
258
|
+
caCertPem,
|
|
259
|
+
caKeyPem,
|
|
260
|
+
publicKey: csr.publicKey,
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
//# sourceMappingURL=agent-cert.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"agent-cert.js","sourceRoot":"","sources":["../../src/certs/agent-cert.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import type { GitHubVariablesClient } from '../registry/types.js';
|
|
2
|
+
import { MacfError } from '../errors.js';
|
|
3
|
+
export declare class CaError extends MacfError {
|
|
4
|
+
constructor(message: string);
|
|
5
|
+
}
|
|
6
|
+
export interface CaKeyPair {
|
|
7
|
+
readonly certPem: string;
|
|
8
|
+
readonly keyPem: string;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Create a new CA certificate and key pair.
|
|
12
|
+
* Saves to disk and optionally uploads cert to registry.
|
|
13
|
+
*/
|
|
14
|
+
export declare function createCA(config: {
|
|
15
|
+
readonly project: string;
|
|
16
|
+
readonly certPath: string;
|
|
17
|
+
readonly keyPath: string;
|
|
18
|
+
readonly client?: GitHubVariablesClient;
|
|
19
|
+
}): Promise<CaKeyPair>;
|
|
20
|
+
/**
|
|
21
|
+
* Backup CA key to registry, encrypted with AES-256-CBC + PBKDF2.
|
|
22
|
+
* Format is interoperable with openssl enc -aes-256-cbc -pbkdf2.
|
|
23
|
+
*/
|
|
24
|
+
export declare function backupCAKey(config: {
|
|
25
|
+
readonly project: string;
|
|
26
|
+
readonly keyPem: string;
|
|
27
|
+
readonly passphrase: string;
|
|
28
|
+
readonly client: GitHubVariablesClient;
|
|
29
|
+
}): Promise<void>;
|
|
30
|
+
/**
|
|
31
|
+
* Recover CA key from registry.
|
|
32
|
+
*/
|
|
33
|
+
export declare function recoverCAKey(config: {
|
|
34
|
+
readonly project: string;
|
|
35
|
+
readonly passphrase: string;
|
|
36
|
+
readonly keyPath: string;
|
|
37
|
+
readonly client: GitHubVariablesClient;
|
|
38
|
+
}): Promise<string>;
|
|
39
|
+
export declare const WIRE_FORMAT_VERSION = 2;
|
|
40
|
+
export declare const V2_PBKDF2_ITERS = 600000;
|
|
41
|
+
export declare const V1_PBKDF2_ITERS = 10000;
|
|
42
|
+
/**
|
|
43
|
+
* Encrypt CA key using AES-256-CBC + PBKDF2-SHA256 at 600k iters
|
|
44
|
+
* (DR-011 rev2, OWASP 2023 alignment). Output is a versioned JSON
|
|
45
|
+
* envelope wrapping the OpenSSL-compatible `Salted__` blob:
|
|
46
|
+
*
|
|
47
|
+
* {"v": 2, "iter": 600000, "payload": "<base64 Salted__ ...>"}
|
|
48
|
+
*
|
|
49
|
+
* Manual recovery with openssl CLI (see DR-011-rev2 for full doc):
|
|
50
|
+
* gh api ... --jq '.value' | jq -r .payload | base64 -d | \
|
|
51
|
+
* openssl enc -aes-256-cbc -pbkdf2 -md sha256 -iter 600000 -d -out ca-key.pem
|
|
52
|
+
*/
|
|
53
|
+
export declare function encryptCAKey(keyPem: string, passphrase: string): string;
|
|
54
|
+
/**
|
|
55
|
+
* Decrypt a CA key from the on-wire registry value. Dispatches by
|
|
56
|
+
* wire format:
|
|
57
|
+
*
|
|
58
|
+
* - **v2 (JSON envelope, DR-011 rev2+):** parses `{v, iter, payload}`,
|
|
59
|
+
* decrypts `payload` at the envelope's iter count.
|
|
60
|
+
* - **v1 (raw base64 `Salted__` blob, legacy pre-2026-04-16):** treats
|
|
61
|
+
* the value as a raw base64 blob and decrypts at iter=10000 (the
|
|
62
|
+
* OpenSSL 3.0/3.1 default at the time the blob was written).
|
|
63
|
+
*
|
|
64
|
+
* Both paths share the same PEM-shape check (#94) after AES decryption
|
|
65
|
+
* to catch wrong-passphrase attempts that produce valid PKCS7 padding
|
|
66
|
+
* by chance (~6% of wrong passphrases).
|
|
67
|
+
*
|
|
68
|
+
* Disambiguation is safe by construction — base64 output never starts
|
|
69
|
+
* with `{`, so only v2 JSON envelopes hit the JSON path. See DR-011
|
|
70
|
+
* rev2 \"Wire Format\" section for the full spec.
|
|
71
|
+
*
|
|
72
|
+
* Throws CaError on:
|
|
73
|
+
* - malformed v2 envelope (missing/invalid v/iter/payload fields)
|
|
74
|
+
* - missing `Salted__` header inside the payload
|
|
75
|
+
* - PKCS7 padding failure (wrong passphrase, ~94% of the time)
|
|
76
|
+
* - decrypted content doesn't look like a PEM private key (wrong
|
|
77
|
+
* passphrase that happened to produce valid PKCS7 padding)
|
|
78
|
+
*/
|
|
79
|
+
export declare function decryptCAKey(encryptedValue: string, passphrase: string): string;
|
|
80
|
+
/**
|
|
81
|
+
* Hand-construct a v1-shaped CA key backup (legacy wire format) at
|
|
82
|
+
* 10000 iters. Exported for `test/certs/wire-format-compat.test.ts`
|
|
83
|
+
* regression guard and for any future tooling that needs to produce
|
|
84
|
+
* legacy-shaped backups (none expected). NOT used by `encryptCAKey`
|
|
85
|
+
* itself — `encryptCAKey` always writes v2. (#115)
|
|
86
|
+
*/
|
|
87
|
+
export declare function encryptCAKeyV1Legacy(keyPem: string, passphrase: string): string;
|
|
88
|
+
/**
|
|
89
|
+
* Cheap semantic check: does this look like a PEM-encoded private key?
|
|
90
|
+
* Exported for unit tests. Doesn't validate DER content — only the
|
|
91
|
+
* PEM envelope.
|
|
92
|
+
*
|
|
93
|
+
* Random bytes faking BOTH the 28-char BEGIN and 26-char END markers
|
|
94
|
+
* simultaneously is ~2^-432 per decrypted buffer — effectively
|
|
95
|
+
* impossible. No minimum body length is needed; the markers alone are
|
|
96
|
+
* the distinguisher.
|
|
97
|
+
*/
|
|
98
|
+
export declare function isLikelyPemPrivateKey(text: string): boolean;
|
|
99
|
+
/**
|
|
100
|
+
* Load CA cert and key from disk.
|
|
101
|
+
*/
|
|
102
|
+
export declare function loadCA(certPath: string, keyPath: string): CaKeyPair;
|
|
103
|
+
//# sourceMappingURL=ca.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ca.d.ts","sourceRoot":"","sources":["../../src/certs/ca.ts"],"names":[],"mappings":"AAQA,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,sBAAsB,CAAC;AAElE,OAAO,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AAEzC,qBAAa,OAAQ,SAAQ,SAAS;gBACxB,OAAO,EAAE,MAAM;CAI5B;AAED,MAAM,WAAW,SAAS;IACxB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;CACzB;AAsBD;;;GAGG;AACH,wBAAsB,QAAQ,CAAC,MAAM,EAAE;IACrC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,MAAM,CAAC,EAAE,qBAAqB,CAAC;CACzC,GAAG,OAAO,CAAC,SAAS,CAAC,CA6CrB;AAED;;;GAGG;AACH,wBAAsB,WAAW,CAAC,MAAM,EAAE;IACxC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,MAAM,EAAE,qBAAqB,CAAC;CACxC,GAAG,OAAO,CAAC,IAAI,CAAC,CAIhB;AAED;;GAEG;AACH,wBAAsB,YAAY,CAAC,MAAM,EAAE;IACzC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,MAAM,EAAE,qBAAqB,CAAC;CACxC,GAAG,OAAO,CAAC,MAAM,CAAC,CAalB;AAMD,eAAO,MAAM,mBAAmB,IAAI,CAAC;AACrC,eAAO,MAAM,eAAe,SAAS,CAAC;AACtC,eAAO,MAAM,eAAe,QAAQ,CAAC;AAoFrC;;;;;;;;;;GAUG;AACH,wBAAgB,YAAY,CAAC,MAAM,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,MAAM,CAyBvE;AAED;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,wBAAgB,YAAY,CAAC,cAAc,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,MAAM,CAiB/E;AAED;;;;;;GAMG;AACH,wBAAgB,oBAAoB,CAAC,MAAM,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,MAAM,CAiB/E;AAED;;;;;;;;;GASG;AACH,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAO3D;AAED;;GAEG;AACH,wBAAgB,MAAM,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,SAAS,CAYnE"}
|