Skip to content

Commit 435efe5

Browse files
zsteinkampivanitskiy
authored andcommitted
feat: Ensures the configured server names match the server names in the certificate.
Also refactors some repeated expressions into utility methods and renames some internal variables and methods to be more accorate and adds a second hostname to the example configuration.
1 parent 7c6b138 commit 435efe5

File tree

5 files changed

+169
-91
lines changed

5 files changed

+169
-91
lines changed

docker-compose.yml

+4
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,10 @@ services:
5050
timeout: 90s
5151
retries: 3
5252
start_period: 10s
53+
networks:
54+
default:
55+
aliases:
56+
- proxy2.nginx.com
5357
volumes:
5458
certs:
5559
node_dist:

examples/nginx.conf

+2-2
Original file line numberDiff line numberDiff line change
@@ -35,8 +35,8 @@ http {
3535
# The full set of configuration variables. These can also be defined in
3636
# environment variables. Use the same name as below, just UPPER_CASE.
3737
## Mandatory Variables
38-
js_var $njs_acme_server_names proxy.nginx.com;
39-
js_var $njs_acme_account_email [email protected];
38+
js_var $njs_acme_server_names "proxy.nginx.com proxy2.nginx.com";
39+
js_var $njs_acme_account_email "[email protected]";
4040

4141
## Optional Variables
4242
# js_var $njs_acme_dir /etc/nginx/njs-acme;

src/client.ts

+6-25
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,10 @@ import { LogLevel, Logger } from './logger'
33
import {
44
formatResponseError,
55
getPemBodyAsB64u,
6-
readCsrDomainNames,
6+
readX509ServerNames,
77
retry,
88
toPEM,
9+
uniqueDomains,
910
} from './utils'
1011
import OGCrypto from 'crypto'
1112

@@ -941,31 +942,11 @@ async function auto(
941942
throw new Error('csr is required')
942943
}
943944

944-
const csrDomains = readCsrDomainNames(toPEM(opts.csr, 'CERTIFICATE REQUEST'))
945-
946-
// Work around issue in x509.get_oid_value (called from readCsrDomainNames)
947-
// where altNames comes back as an Array in an Array, e.g.:
948-
// [[ 'hostname1', 'hostname2' ]]
949-
// We just want an Array:
950-
// [ 'hostname1', 'hostname2' ]
951-
const uniqueDomains = [csrDomains.commonName]
952-
// Hacky stuff to make Typescript happy
953-
const origAltNames = csrDomains.altNames as unknown as string[][]
954-
let altNames = csrDomains.altNames
955-
if (altNames && altNames[0] && altNames[0][0]) {
956-
altNames = origAltNames[0]
957-
}
958-
959-
if (altNames) {
960-
for (const altName of altNames) {
961-
if (uniqueDomains.indexOf(altName) === -1) {
962-
uniqueDomains.push(altName)
963-
}
964-
}
965-
}
945+
const csrDomains = readX509ServerNames(toPEM(opts.csr, 'CERTIFICATE REQUEST'))
946+
const domains = uniqueDomains(csrDomains)
966947

967948
log.info(
968-
`Resolved ${uniqueDomains.length} unique domains (${uniqueDomains.join(
949+
`Resolved ${domains.length} unique domains (${domains.join(
969950
', '
970951
)}) from parsing the Certificate Signing Request`
971952
)
@@ -974,7 +955,7 @@ async function auto(
974955
* Place order
975956
*/
976957
const orderPayload = {
977-
identifiers: uniqueDomains.map((d) => ({ type: 'dns', value: d })),
958+
identifiers: domains.map((d) => ({ type: 'dns', value: d })),
978959
}
979960
const order = await client.createOrder(orderPayload)
980961
const authorizations = await client.getAuthorizations(order)

src/index.ts

+47-29
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,21 @@
11
import {
2-
toPEM,
3-
readOrCreateAccountKey,
4-
generateKey,
5-
createCsr,
6-
readCertificateInfo,
7-
acmeServerNames,
8-
getVariable,
9-
joinPaths,
10-
acmeDir,
11-
acmeChallengeDir,
122
acmeAccountPrivateJWKPath,
3+
acmeAltNames,
4+
acmeChallengeDir,
5+
acmeCommonName,
6+
acmeDir,
137
acmeDirectoryURI,
8+
acmeServerNames,
149
acmeVerifyProviderHTTPS,
1510
acmeZoneName,
11+
areEqualSets,
12+
createCsr,
13+
generateKey,
14+
getVariable,
15+
joinPaths,
16+
readCertificateInfo,
17+
readOrCreateAccountKey,
18+
toPEM,
1619
} from './utils'
1720
import { HttpClient } from './api'
1821
import { AcmeClient } from './client'
@@ -21,6 +24,7 @@ import { LogLevel, Logger } from './logger'
2124

2225
const KEY_SUFFIX = '.key'
2326
const CERTIFICATE_SUFFIX = '.crt'
27+
const CERTIFICATE_REQ_SUFFIX = '.csr'
2428
const log = new Logger()
2529

2630
/**
@@ -62,11 +66,11 @@ async function clientNewAccount(r: NginxHTTPRequest): Promise<void> {
6266
async function clientAutoMode(r: NginxHTTPRequest): Promise<void> {
6367
const log = new Logger('auto')
6468
const prefix = acmeDir(r)
65-
const serverNames = acmeServerNames(r)
69+
const commonName = acmeCommonName(r)
70+
const altNames = acmeAltNames(r)
6671

67-
const commonName = serverNames[0]
6872
const pkeyPath = joinPaths(prefix, commonName + KEY_SUFFIX)
69-
const csrPath = joinPaths(prefix, commonName + '.csr')
73+
const csrPath = joinPaths(prefix, commonName + CERTIFICATE_REQ_SUFFIX)
7074
const certPath = joinPaths(prefix, commonName + CERTIFICATE_SUFFIX)
7175

7276
let email
@@ -88,16 +92,31 @@ async function clientAutoMode(r: NginxHTTPRequest): Promise<void> {
8892
const privateKeyData = fs.readFileSync(pkeyPath, 'utf8')
8993

9094
certInfo = await readCertificateInfo(certData)
91-
// Calculate the date 30 days before the certificate expiration
92-
const renewalThreshold = new Date(certInfo.notAfter as string)
93-
renewalThreshold.setDate(renewalThreshold.getDate() - 30)
9495

95-
const currentDate = new Date()
96-
if (currentDate > renewalThreshold) {
96+
const configDomains = acmeServerNames(r)
97+
const certDomains = certInfo.domains.altNames // altNames includes the common name
98+
99+
if (!areEqualSets(certDomains, configDomains)) {
100+
log.info(
101+
`Renewing certificate because the hostnames in the certificate (${certDomains.join(
102+
', '
103+
)}) do not match the configured njs_acme_server_names (${configDomains.join(
104+
','
105+
)})`
106+
)
97107
renewCertificate = true
98108
} else {
99-
certificatePem = certData
100-
pkeyPem = privateKeyData
109+
// Calculate the date 30 days before the certificate expiration
110+
const renewalThreshold = new Date(certInfo.notAfter)
111+
renewalThreshold.setDate(renewalThreshold.getDate() - 30)
112+
113+
const currentDate = new Date()
114+
if (currentDate > renewalThreshold) {
115+
renewCertificate = true
116+
} else {
117+
certificatePem = certData
118+
pkeyPem = privateKeyData
119+
}
101120
}
102121
} catch {
103122
renewCertificate = true
@@ -117,17 +136,17 @@ async function clientAutoMode(r: NginxHTTPRequest): Promise<void> {
117136

118137
// Create a new CSR
119138
const params = {
120-
altNames: serverNames.length > 1 ? serverNames.slice(1) : [],
121-
commonName: commonName,
139+
commonName,
140+
altNames,
122141
emailAddress: email,
123142
}
124143

125-
const result = await createCsr(params)
126-
fs.writeFileSync(csrPath, toPEM(result.pkcs10Ber, 'CERTIFICATE REQUEST'))
144+
const csr = await createCsr(params)
145+
fs.writeFileSync(csrPath, toPEM(csr.pkcs10Ber, 'CERTIFICATE REQUEST'))
127146

128147
const privKey = (await crypto.subtle.exportKey(
129148
'pkcs8',
130-
result.keys.privateKey
149+
csr.keys.privateKey
131150
)) as ArrayBuffer
132151
pkeyPem = toPEM(privKey, 'PRIVATE KEY')
133152
fs.writeFileSync(pkeyPath, pkeyPem)
@@ -146,8 +165,8 @@ async function clientAutoMode(r: NginxHTTPRequest): Promise<void> {
146165
}
147166

148167
certificatePem = await client.auto({
149-
csr: Buffer.from(result.pkcs10Ber),
150-
email: email,
168+
csr: Buffer.from(csr.pkcs10Ber),
169+
email,
151170
termsOfServiceAgreed: true,
152171
challengeCreateFn: async (authz, challenge, keyAuthorization) => {
153172
log.info('Challenge Create', { authz, challenge, keyAuthorization })
@@ -272,8 +291,7 @@ function read_cert_or_key(r: NginxHTTPRequest, suffix: string) {
272291
let data = ''
273292
let path = ''
274293
const prefix = acmeDir(r)
275-
const serverNames = acmeServerNames(r)
276-
const commonName = serverNames[0].toLowerCase()
294+
const commonName = acmeCommonName(r)
277295
const zone = acmeZoneName(r)
278296
path = joinPaths(prefix, commonName + suffix)
279297
const key = ['acme', path].join(':')

0 commit comments

Comments
 (0)