Skip to content

Commit 4d79cdc

Browse files
committed
add option contrainToDomains to generateCACertificate
1 parent 5778dd0 commit 4d79cdc

File tree

1 file changed

+81
-6
lines changed

1 file changed

+81
-6
lines changed

src/util/tls.ts

Lines changed: 81 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import * as fs from 'fs/promises';
33
import { v4 as uuid } from "uuid";
44
import * as forge from 'node-forge';
55

6-
const { pki, md, util: { encode64 } } = forge;
6+
const { asn1, pki, md, util } = forge;
77

88
export type CAOptions = (CertDataOptions | CertPathOptions);
99

@@ -63,7 +63,8 @@ export async function generateCACertificate(options: {
6363
commonName?: string,
6464
organizationName?: string,
6565
countryName?: string,
66-
bits?: number
66+
bits?: number,
67+
contrainToDomains?: string[]
6768
} = {}) {
6869
options = _.defaults({}, options, {
6970
commonName: 'Mockttp Testing CA - DO NOT TRUST - TESTING ONLY',
@@ -98,11 +99,21 @@ export async function generateCACertificate(options: {
9899
{ name: 'organizationName', value: options.organizationName }
99100
]);
100101

101-
cert.setExtensions([
102+
const extensions: any[] = [
102103
{ name: 'basicConstraints', cA: true, critical: true },
103104
{ name: 'keyUsage', keyCertSign: true, digitalSignature: true, nonRepudiation: true, cRLSign: true, critical: true },
104-
{ name: 'subjectKeyIdentifier' }
105-
]);
105+
{ name: 'subjectKeyIdentifier' },
106+
];
107+
if(options.contrainToDomains && options.contrainToDomains.length > 0) {
108+
extensions.push({
109+
critical: true,
110+
name: 'nameConstraints',
111+
value: generateNameConstraints({
112+
permitted: options.contrainToDomains,
113+
}),
114+
})
115+
}
116+
cert.setExtensions(extensions);
106117

107118
// Self-issued too
108119
cert.setIssuer(cert.subject.attributes);
@@ -116,9 +127,73 @@ export async function generateCACertificate(options: {
116127
};
117128
}
118129

130+
131+
type GenerateNameConstraintsInput = {
132+
/**
133+
* Array of excluded domains
134+
*/
135+
excluded?: string[];
136+
137+
/**
138+
* Array of permitted domains
139+
*/
140+
permitted?: string[];
141+
};
142+
143+
/**
144+
* Generate name constraints in conformance with
145+
* [RFC 5280 § 4.2.1.10](https://datatracker.ietf.org/doc/html/rfc5280#section-4.2.1.10)
146+
*/
147+
function generateNameConstraints(
148+
input: GenerateNameConstraintsInput
149+
): forge.asn1.Asn1 {
150+
const ipsToSequence = (ips: string[]) =>
151+
ips.map((domain) => {
152+
return asn1.create(asn1.Class.UNIVERSAL, asn1.Type.SEQUENCE, true, [
153+
asn1.create(
154+
asn1.Class.CONTEXT_SPECIFIC,
155+
2,
156+
false,
157+
util.encodeUtf8(domain)
158+
),
159+
]);
160+
});
161+
162+
const permittedAndExcluded: forge.asn1.Asn1[] = [];
163+
164+
if (input.permitted !== undefined) {
165+
permittedAndExcluded.push(
166+
asn1.create(
167+
asn1.Class.CONTEXT_SPECIFIC,
168+
0,
169+
true,
170+
ipsToSequence(input.permitted)
171+
)
172+
);
173+
}
174+
175+
if (input.excluded !== undefined) {
176+
permittedAndExcluded.push(
177+
asn1.create(
178+
asn1.Class.CONTEXT_SPECIFIC,
179+
1,
180+
true,
181+
ipsToSequence(input.excluded)
182+
)
183+
);
184+
}
185+
186+
return asn1.create(
187+
asn1.Class.UNIVERSAL,
188+
asn1.Type.SEQUENCE,
189+
true,
190+
permittedAndExcluded
191+
);
192+
}
193+
119194
export function generateSPKIFingerprint(certPem: PEM) {
120195
let cert = pki.certificateFromPem(certPem.toString('utf8'));
121-
return encode64(
196+
return util.encode64(
122197
pki.getPublicKeyFingerprint(cert.publicKey, {
123198
type: 'SubjectPublicKeyInfo',
124199
md: md.sha256.create(),

0 commit comments

Comments
 (0)