2018-09-10 15:19:10 -07:00
'use strict' ;
2021-06-04 23:53:30 -07:00
exports = module . exports = {
getCertificate ,
// testing
_name : 'acme' ,
_getChallengeSubdomain : getChallengeSubdomain
} ;
2021-05-06 22:29:34 -07:00
const assert = require ( 'assert' ) ,
2021-11-16 22:56:35 -08:00
blobs = require ( './blobs.js' ) ,
2021-05-07 22:44:13 -07:00
BoxError = require ( './boxerror.js' ) ,
2018-09-10 15:19:10 -07:00
crypto = require ( 'crypto' ) ,
debug = require ( 'debug' ) ( 'box:cert/acme2' ) ,
2021-08-13 17:22:28 -07:00
dns = require ( './dns.js' ) ,
2018-09-10 15:19:10 -07:00
fs = require ( 'fs' ) ,
2021-06-04 14:53:23 -07:00
os = require ( 'os' ) ,
2018-09-10 15:19:10 -07:00
path = require ( 'path' ) ,
2021-05-07 22:44:13 -07:00
promiseRetry = require ( './promise-retry.js' ) ,
2021-05-06 22:29:34 -07:00
superagent = require ( 'superagent' ) ,
2018-09-10 15:19:10 -07:00
safe = require ( 'safetydance' ) ,
_ = require ( 'underscore' ) ;
const CA _PROD _DIRECTORY _URL = 'https://acme-v02.api.letsencrypt.org/directory' ,
CA _STAGING _DIRECTORY _URL = 'https://acme-staging-v02.api.letsencrypt.org/directory' ;
// http://jose.readthedocs.org/en/latest/
// https://www.ietf.org/proceedings/92/slides/slides-92-acme-1.pdf
// https://community.letsencrypt.org/t/list-of-client-implementations/2103
function Acme2 ( options ) {
assert . strictEqual ( typeof options , 'object' ) ;
2021-11-16 22:56:35 -08:00
this . accountKeyPem = null ; // Buffer .
2018-09-10 15:19:10 -07:00
this . email = options . email ;
this . keyId = null ;
this . caDirectory = options . prod ? CA _PROD _DIRECTORY _URL : CA _STAGING _DIRECTORY _URL ;
this . directory = { } ;
2018-09-10 20:50:36 -07:00
this . performHttpAuthorization = ! ! options . performHttpAuthorization ;
2018-09-11 22:46:17 -07:00
this . wildcard = ! ! options . wildcard ;
2018-09-10 15:19:10 -07:00
}
// urlsafe base64 encoding (jose)
function urlBase64Encode ( string ) {
return string . replace ( /\+/g , '-' ) . replace ( /\//g , '_' ) . replace ( /=/g , '' ) ;
}
function b64 ( str ) {
2021-04-29 15:37:32 -07:00
var buf = Buffer . isBuffer ( str ) ? str : Buffer . from ( str ) ;
2018-09-10 15:19:10 -07:00
return urlBase64Encode ( buf . toString ( 'base64' ) ) ;
}
function getModulus ( pem ) {
2021-04-29 15:37:32 -07:00
assert ( Buffer . isBuffer ( pem ) ) ;
2018-09-10 15:19:10 -07:00
2018-11-23 11:39:00 -08:00
var stdout = safe . child _process . execSync ( 'openssl rsa -modulus -noout' , { input : pem , encoding : 'utf8' } ) ;
2018-09-10 15:19:10 -07:00
if ( ! stdout ) return null ;
var match = stdout . match ( /Modulus=([0-9a-fA-F]+)$/m ) ;
if ( ! match ) return null ;
return Buffer . from ( match [ 1 ] , 'hex' ) ;
}
2021-05-06 22:29:34 -07:00
Acme2 . prototype . sendSignedRequest = async function ( url , payload ) {
2018-09-10 15:19:10 -07:00
assert . strictEqual ( typeof url , 'string' ) ;
assert . strictEqual ( typeof payload , 'string' ) ;
2021-04-29 15:25:14 -07:00
assert ( Buffer . isBuffer ( this . accountKeyPem ) ) ;
2018-09-10 15:19:10 -07:00
const that = this ;
let header = {
url : url ,
alg : 'RS256'
} ;
// keyId is null when registering account
if ( this . keyId ) {
header . kid = this . keyId ;
} else {
header . jwk = {
e : b64 ( Buffer . from ( [ 0x01 , 0x00 , 0x01 ] ) ) , // exponent - 65537
kty : 'RSA' ,
n : b64 ( getModulus ( this . accountKeyPem ) )
} ;
}
2021-05-06 22:29:34 -07:00
const payload64 = b64 ( payload ) ;
2019-12-08 18:37:25 -08:00
2021-05-07 15:56:43 -07:00
let [ error , response ] = await safe ( superagent . get ( this . directory . newNonce ) . timeout ( 30000 ) . ok ( ( ) => true ) ) ;
if ( error ) throw new BoxError ( BoxError . NETWORK _ERROR , ` Network error sending signed request: ${ error . message } ` ) ;
2021-11-17 10:54:26 -08:00
if ( response . status !== 204 ) throw new BoxError ( BoxError . ACME _ERROR , ` Invalid response code when fetching nonce : ${ response . status } ` ) ;
2018-09-10 15:19:10 -07:00
2021-05-06 22:29:34 -07:00
const nonce = response . headers [ 'Replay-Nonce' . toLowerCase ( ) ] ;
2021-11-17 10:54:26 -08:00
if ( ! nonce ) throw new BoxError ( BoxError . ACME _ERROR , 'No nonce in response' ) ;
2018-09-10 15:19:10 -07:00
2021-05-06 22:29:34 -07:00
debug ( 'sendSignedRequest: using nonce %s for url %s' , nonce , url ) ;
2018-09-10 15:19:10 -07:00
2021-05-06 22:29:34 -07:00
const protected64 = b64 ( JSON . stringify ( _ . extend ( { } , header , { nonce : nonce } ) ) ) ;
2018-09-10 15:19:10 -07:00
2021-05-06 22:29:34 -07:00
const signer = crypto . createSign ( 'RSA-SHA256' ) ;
signer . update ( protected64 + '.' + payload64 , 'utf8' ) ;
const signature64 = urlBase64Encode ( signer . sign ( that . accountKeyPem , 'base64' ) ) ;
2018-09-10 15:19:10 -07:00
2021-05-06 22:29:34 -07:00
const data = {
protected : protected64 ,
payload : payload64 ,
signature : signature64
} ;
2018-09-10 15:19:10 -07:00
2021-05-07 15:56:43 -07:00
[ error , response ] = await safe ( superagent . post ( url ) . send ( data ) . set ( 'Content-Type' , 'application/jose+json' ) . set ( 'User-Agent' , 'acme-cloudron' ) . timeout ( 30000 ) . ok ( ( ) => true ) ) ;
if ( error ) throw new BoxError ( BoxError . NETWORK _ERROR , ` Network error sending signed request: ${ error . message } ` ) ;
2019-12-08 18:37:25 -08:00
2021-05-07 15:56:43 -07:00
return response ;
2018-09-10 15:19:10 -07:00
} ;
2019-12-08 18:37:25 -08:00
// https://tools.ietf.org/html/rfc8555#section-6.3
2021-05-06 22:29:34 -07:00
Acme2 . prototype . postAsGet = async function ( url ) {
return await this . sendSignedRequest ( url , '' ) ;
2019-12-08 18:37:25 -08:00
} ;
2021-05-06 22:29:34 -07:00
Acme2 . prototype . updateContact = async function ( registrationUri ) {
2018-09-10 15:19:10 -07:00
assert . strictEqual ( typeof registrationUri , 'string' ) ;
debug ( ` updateContact: registrationUri: ${ registrationUri } email: ${ this . email } ` ) ;
// https://github.com/ietf-wg-acme/acme/issues/30
const payload = {
contact : [ 'mailto:' + this . email ]
} ;
2021-05-06 22:29:34 -07:00
const result = await this . sendSignedRequest ( registrationUri , JSON . stringify ( payload ) ) ;
2021-11-17 10:54:26 -08:00
if ( result . status !== 200 ) throw new BoxError ( BoxError . ACME _ERROR , ` Failed to update contact. Expecting 200, got ${ result . status } ${ JSON . stringify ( result . body ) } ` ) ;
2018-09-10 15:19:10 -07:00
2021-05-06 22:29:34 -07:00
debug ( ` updateContact: contact of user updated to ${ this . email } ` ) ;
2018-09-10 15:19:10 -07:00
} ;
2021-11-16 22:56:35 -08:00
async function generateAccountKey ( ) {
const acmeAccountKey = safe . child _process . execSync ( 'openssl genrsa 4096' ) ;
if ( ! acmeAccountKey ) throw new BoxError ( BoxError . OPENSSL _ERROR , ` Could not generate acme account key: ${ safe . error . message } ` ) ;
return acmeAccountKey ;
}
Acme2 . prototype . ensureAccount = async function ( ) {
2021-05-06 22:29:34 -07:00
const payload = {
2018-09-10 15:19:10 -07:00
termsOfServiceAgreed : true
} ;
2021-11-16 22:56:35 -08:00
debug ( 'ensureAccount: registering user' ) ;
this . accountKeyPem = await blobs . get ( blobs . ACME _ACCOUNT _KEY ) ;
if ( ! this . accountKeyPem ) {
debug ( 'ensureAccount: generating new account keys' ) ;
this . accountKeyPem = await generateAccountKey ( ) ;
await blobs . set ( blobs . ACME _ACCOUNT _KEY , this . accountKeyPem ) ;
}
let result = await this . sendSignedRequest ( this . directory . newAccount , JSON . stringify ( payload ) ) ;
if ( result . status === 403 && result . body . type === 'urn:ietf:params:acme:error:unauthorized' ) {
debug ( ` ensureAccount: key was revoked. ${ result . status } ${ JSON . stringify ( result . body ) } . generating new account key ` ) ;
this . accountKeyPem = await generateAccountKey ( ) ;
await blobs . set ( blobs . ACME _ACCOUNT _KEY , this . accountKeyPem ) ;
result = await this . sendSignedRequest ( this . directory . newAccount , JSON . stringify ( payload ) ) ;
}
2018-09-10 15:19:10 -07:00
2021-05-06 22:29:34 -07:00
// 200 if already exists. 201 for new accounts
2021-11-17 10:54:26 -08:00
if ( result . status !== 200 && result . status !== 201 ) throw new BoxError ( BoxError . ACME _ERROR , ` Failed to register new account. Expecting 200 or 201, got ${ result . status } ${ JSON . stringify ( result . body ) } ` ) ;
2018-09-10 15:19:10 -07:00
2021-11-16 22:56:35 -08:00
debug ( ` ensureAccount: user registered keyid: ${ result . headers . location } ` ) ;
2018-09-10 15:19:10 -07:00
2021-05-06 22:29:34 -07:00
this . keyId = result . headers . location ;
2018-09-10 15:19:10 -07:00
2021-05-06 22:29:34 -07:00
await this . updateContact ( result . headers . location ) ;
2018-09-10 15:19:10 -07:00
} ;
2021-05-06 22:29:34 -07:00
Acme2 . prototype . newOrder = async function ( domain ) {
2018-09-10 15:19:10 -07:00
assert . strictEqual ( typeof domain , 'string' ) ;
2021-05-06 22:29:34 -07:00
const payload = {
2018-09-10 15:19:10 -07:00
identifiers : [ {
type : 'dns' ,
value : domain
} ]
} ;
2021-05-06 22:29:34 -07:00
debug ( ` newOrder: ${ domain } ` ) ;
2018-09-10 15:19:10 -07:00
2021-05-06 22:29:34 -07:00
const result = await this . sendSignedRequest ( this . directory . newOrder , JSON . stringify ( payload ) ) ;
if ( result . status === 403 ) throw new BoxError ( BoxError . ACCESS _DENIED , ` Forbidden sending new order: ${ result . body . detail } ` ) ;
2021-11-17 10:54:26 -08:00
if ( result . status !== 201 ) throw new BoxError ( BoxError . ACME _ERROR , ` Failed to send new order. Expecting 201, got ${ result . statusCode } ${ JSON . stringify ( result . body ) } ` ) ;
2018-09-10 15:19:10 -07:00
2021-05-06 22:29:34 -07:00
debug ( 'newOrder: created order %s %j' , domain , result . body ) ;
2018-09-10 15:19:10 -07:00
2021-05-06 22:29:34 -07:00
const order = result . body , orderUrl = result . headers . location ;
2018-09-10 15:19:10 -07:00
2021-11-17 10:54:26 -08:00
if ( ! Array . isArray ( order . authorizations ) ) throw new BoxError ( BoxError . ACME _ERROR , 'invalid authorizations in order' ) ;
if ( typeof order . finalize !== 'string' ) throw new BoxError ( BoxError . ACME _ERROR , 'invalid finalize in order' ) ;
if ( typeof orderUrl !== 'string' ) throw new BoxError ( BoxError . ACME _ERROR , 'invalid order location in order header' ) ;
2018-09-10 15:19:10 -07:00
2021-05-06 22:29:34 -07:00
return { order , orderUrl } ;
2018-09-10 15:19:10 -07:00
} ;
2021-05-06 22:29:34 -07:00
Acme2 . prototype . waitForOrder = async function ( orderUrl ) {
2018-09-10 15:19:10 -07:00
assert . strictEqual ( typeof orderUrl , 'string' ) ;
debug ( ` waitForOrder: ${ orderUrl } ` ) ;
2021-05-06 22:29:34 -07:00
return await promiseRetry ( { times : 15 , interval : 20000 } , async ( ) => {
2018-09-10 15:19:10 -07:00
debug ( 'waitForOrder: getting status' ) ;
2021-05-07 15:56:43 -07:00
const result = await this . postAsGet ( orderUrl ) ;
2021-05-06 22:29:34 -07:00
if ( result . status !== 200 ) {
debug ( ` waitForOrder: invalid response code getting uri ${ result . status } ` ) ;
2021-11-17 10:54:26 -08:00
throw new BoxError ( BoxError . ACME _ERROR , ` Bad response when waiting for order. code: ${ result . status } ` ) ;
2021-05-06 22:29:34 -07:00
}
debug ( 'waitForOrder: status is "%s %j' , result . body . status , result . body ) ;
2021-11-17 10:54:26 -08:00
if ( result . body . status === 'pending' || result . body . status === 'processing' ) throw new BoxError ( BoxError . ACME _ERROR , ` Request is in ${ result . body . status } state ` ) ;
2021-05-06 22:29:34 -07:00
else if ( result . body . status === 'valid' && result . body . certificate ) return result . body . certificate ;
2021-11-17 10:54:26 -08:00
else throw new BoxError ( BoxError . ACME _ERROR , ` Unexpected status or invalid response when waiting for order: ${ JSON . stringify ( result . body ) } ` ) ;
2021-05-06 22:29:34 -07:00
} ) ;
2018-09-10 15:19:10 -07:00
} ;
2018-09-10 20:50:36 -07:00
Acme2 . prototype . getKeyAuthorization = function ( token ) {
2021-04-29 15:25:14 -07:00
assert ( Buffer . isBuffer ( this . accountKeyPem ) ) ;
2018-09-10 15:19:10 -07:00
2018-09-10 20:50:36 -07:00
let jwk = {
2018-09-10 15:19:10 -07:00
e : b64 ( Buffer . from ( [ 0x01 , 0x00 , 0x01 ] ) ) , // Exponent - 65537
kty : 'RSA' ,
n : b64 ( getModulus ( this . accountKeyPem ) )
} ;
2018-09-10 20:50:36 -07:00
let shasum = crypto . createHash ( 'sha256' ) ;
2018-09-10 15:19:10 -07:00
shasum . update ( JSON . stringify ( jwk ) ) ;
2018-09-10 20:50:36 -07:00
let thumbprint = urlBase64Encode ( shasum . digest ( 'base64' ) ) ;
return token + '.' + thumbprint ;
2018-09-10 15:19:10 -07:00
} ;
2021-05-06 22:29:34 -07:00
Acme2 . prototype . notifyChallengeReady = async function ( challenge ) {
2018-09-10 15:19:10 -07:00
assert . strictEqual ( typeof challenge , 'object' ) ; // { type, status, url, token }
debug ( 'notifyChallengeReady: %s was met' , challenge . url ) ;
2018-09-10 20:50:36 -07:00
const keyAuthorization = this . getKeyAuthorization ( challenge . token ) ;
2018-09-10 15:19:10 -07:00
2021-05-06 22:29:34 -07:00
const payload = {
2018-09-10 15:19:10 -07:00
resource : 'challenge' ,
keyAuthorization : keyAuthorization
} ;
2021-05-06 22:29:34 -07:00
const result = await this . sendSignedRequest ( challenge . url , JSON . stringify ( payload ) ) ;
2021-11-17 10:54:26 -08:00
if ( result . status !== 200 ) throw new BoxError ( BoxError . ACME _ERROR , ` Failed to notify challenge. Expecting 200, got ${ result . statusCode } ${ JSON . stringify ( result . body ) } ` ) ;
2018-09-10 15:19:10 -07:00
} ;
2021-05-06 22:29:34 -07:00
Acme2 . prototype . waitForChallenge = async function ( challenge ) {
2018-09-10 15:19:10 -07:00
assert . strictEqual ( typeof challenge , 'object' ) ;
debug ( 'waitingForChallenge: %j' , challenge ) ;
2021-05-06 22:29:34 -07:00
await promiseRetry ( { times : 15 , interval : 20000 } , async ( ) => {
2018-09-10 15:19:10 -07:00
debug ( 'waitingForChallenge: getting status' ) ;
2021-05-06 22:29:34 -07:00
const result = await this . postAsGet ( challenge . url ) ;
if ( result . status !== 200 ) {
debug ( ` waitForChallenge: invalid response code getting uri ${ result . status } ` ) ;
2021-11-17 10:54:26 -08:00
throw new BoxError ( BoxError . ACME _ERROR , ` Bad response code when waiting for challenge : ${ result . status } ` ) ;
2021-05-06 22:29:34 -07:00
}
debug ( ` waitForChallenge: status is " ${ result . body . status } " " ${ JSON . stringify ( result . body ) } " ` ) ;
2021-11-17 10:54:26 -08:00
if ( result . body . status === 'pending' ) throw new BoxError ( BoxError . ACME _ERROR , 'Challenge is in pending state' ) ;
2021-05-06 22:29:34 -07:00
else if ( result . body . status === 'valid' ) return ;
2021-11-17 10:54:26 -08:00
else throw new BoxError ( BoxError . ACME _ERROR , ` Unexpected status when waiting for challenge: ${ result . body . status } ` ) ;
2018-09-10 15:19:10 -07:00
} ) ;
} ;
// https://community.letsencrypt.org/t/public-beta-rate-limits/4772 for rate limits
2021-05-06 22:29:34 -07:00
Acme2 . prototype . signCertificate = async function ( domain , finalizationUrl , csrDer ) {
2018-09-10 15:19:10 -07:00
assert . strictEqual ( typeof domain , 'string' ) ;
assert . strictEqual ( typeof finalizationUrl , 'string' ) ;
2021-04-29 15:37:32 -07:00
assert ( Buffer . isBuffer ( csrDer ) ) ;
2018-09-10 15:19:10 -07:00
const payload = {
csr : b64 ( csrDer )
} ;
debug ( 'signCertificate: sending sign request' ) ;
2021-05-06 22:29:34 -07:00
const result = await this . sendSignedRequest ( finalizationUrl , JSON . stringify ( payload ) ) ;
// 429 means we reached the cert limit for this domain
2021-11-17 10:54:26 -08:00
if ( result . status !== 200 ) throw new BoxError ( BoxError . ACME _ERROR , ` Failed to sign certificate. Expecting 200, got ${ result . status } ${ JSON . stringify ( result . body ) } ` ) ;
2018-09-10 15:19:10 -07:00
} ;
2021-05-07 22:43:30 -07:00
Acme2 . prototype . createKeyAndCsr = async function ( hostname , keyFilePath , csrFilePath ) {
2018-09-11 22:46:17 -07:00
assert . strictEqual ( typeof hostname , 'string' ) ;
2018-09-10 15:19:10 -07:00
2021-05-07 22:43:30 -07:00
if ( safe . fs . existsSync ( keyFilePath ) ) {
debug ( 'createKeyAndCsr: reuse the key for renewal at %s' , keyFilePath ) ;
2018-09-10 15:19:10 -07:00
} else {
2021-04-16 11:17:13 -07:00
let key = safe . child _process . execSync ( 'openssl ecparam -genkey -name secp384r1' ) ; // openssl ecparam -list_curves
2021-05-06 22:29:34 -07:00
if ( ! key ) throw new BoxError ( BoxError . OPENSSL _ERROR , safe . error ) ;
2021-05-07 22:43:30 -07:00
if ( ! safe . fs . writeFileSync ( keyFilePath , key ) ) throw new BoxError ( BoxError . FS _ERROR , safe . error ) ;
2018-09-10 15:19:10 -07:00
2021-05-07 22:43:30 -07:00
debug ( 'createKeyAndCsr: key file saved at %s' , keyFilePath ) ;
2018-09-10 15:19:10 -07:00
}
2021-06-04 14:53:23 -07:00
const [ error , tmpdir ] = await safe ( fs . promises . mkdtemp ( path . join ( os . tmpdir ( ) , 'acme-' ) ) ) ;
if ( error ) throw new BoxError ( BoxError . FS _ERROR , ` Error creating temporary directory for openssl config: ${ error . message } ` ) ;
2021-04-16 13:33:32 -07:00
// OCSP must-staple is currently disabled because nginx does not provide staple on the first request (https://forum.cloudron.io/topic/4917/ocsp-stapling-for-tls-ssl/)
// ' -addext "tlsfeature = status_request"'; // this adds OCSP must-staple
2021-06-04 14:53:23 -07:00
// we used to use -addext to the CLI to add these but that arg doesn't work on Ubuntu 16.04
2021-06-04 16:48:56 -07:00
// empty distinguished_name section is required for Ubuntu 16 openssl
const conf = '[req]\nreq_extensions = v3_req\ndistinguished_name = req_distinguished_name\n'
+ '[req_distinguished_name]\n\n'
2021-06-04 14:53:23 -07:00
+ '[v3_req]\nbasicConstraints = CA:FALSE\nkeyUsage = nonRepudiation, digitalSignature, keyEncipherment\nsubjectAltName = @alt_names\n'
+ ` [alt_names] \n DNS.1 = ${ hostname } \n ` ;
const opensslConfigFile = path . join ( tmpdir , 'openssl.conf' ) ;
if ( ! safe . fs . writeFileSync ( opensslConfigFile , conf ) ) throw new BoxError ( BoxError . FS _ERROR , ` Failed to write openssl config: ${ safe . error . message } ` ) ;
2021-04-16 11:17:13 -07:00
2021-06-04 14:53:23 -07:00
// while we pass the CN anyways, subjectAltName takes precedence
const csrDer = safe . child _process . execSync ( ` openssl req -new -key ${ keyFilePath } -outform DER -subj /CN= ${ hostname } -config ${ opensslConfigFile } ` ) ;
2021-05-06 22:29:34 -07:00
if ( ! csrDer ) throw new BoxError ( BoxError . OPENSSL _ERROR , safe . error ) ;
2021-05-07 22:43:30 -07:00
if ( ! safe . fs . writeFileSync ( csrFilePath , csrDer ) ) throw new BoxError ( BoxError . FS _ERROR , safe . error ) ; // bookkeeping. inspect with openssl req -text -noout -in hostname.csr -inform der
2018-09-10 15:19:10 -07:00
2021-06-04 14:53:23 -07:00
await safe ( fs . promises . rmdir ( tmpdir , { recursive : true } ) ) ;
2021-05-07 22:43:30 -07:00
debug ( 'createKeyAndCsr: csr file (DER) saved at %s' , csrFilePath ) ;
2018-09-10 15:19:10 -07:00
2021-05-06 22:29:34 -07:00
return csrDer ;
2018-09-10 15:19:10 -07:00
} ;
2021-05-07 22:43:30 -07:00
Acme2 . prototype . downloadCertificate = async function ( hostname , certUrl , certFilePath ) {
2018-09-11 22:46:17 -07:00
assert . strictEqual ( typeof hostname , 'string' ) ;
2018-09-10 15:19:10 -07:00
assert . strictEqual ( typeof certUrl , 'string' ) ;
2021-12-07 11:14:24 -08:00
await promiseRetry ( { times : 5 , interval : 20000 , debug } , async ( ) => {
debug ( ` downloadCertificate: downloading certificate of ${ hostname } ` ) ;
2018-09-10 15:19:10 -07:00
2021-05-07 15:56:43 -07:00
const result = await this . postAsGet ( certUrl ) ;
2021-11-17 10:54:26 -08:00
if ( result . statusCode === 202 ) throw new BoxError ( BoxError . ACME _ERROR , 'Retry downloading certificate' ) ;
if ( result . statusCode !== 200 ) throw new BoxError ( BoxError . ACME _ERROR , ` Failed to get cert. Expecting 200, got ${ result . statusCode } ${ JSON . stringify ( result . body ) } ` ) ;
2018-09-10 15:19:10 -07:00
2021-05-06 22:29:34 -07:00
const fullChainPem = result . body ; // buffer
2018-09-10 15:19:10 -07:00
2021-05-07 22:43:30 -07:00
if ( ! safe . fs . writeFileSync ( certFilePath , fullChainPem ) ) throw new BoxError ( BoxError . FS _ERROR , safe . error ) ;
2018-09-10 15:19:10 -07:00
2021-05-07 22:43:30 -07:00
debug ( ` downloadCertificate: cert file for ${ hostname } saved at ${ certFilePath } ` ) ;
2021-05-06 22:29:34 -07:00
} ) ;
2018-09-10 15:19:10 -07:00
} ;
2021-05-07 22:43:30 -07:00
Acme2 . prototype . prepareHttpChallenge = async function ( hostname , domain , authorization , acmeChallengesDir ) {
2018-09-10 20:50:36 -07:00
assert . strictEqual ( typeof hostname , 'string' ) ;
assert . strictEqual ( typeof domain , 'string' ) ;
assert . strictEqual ( typeof authorization , 'object' ) ;
2021-05-07 22:43:30 -07:00
assert . strictEqual ( typeof acmeChallengesDir , 'string' ) ;
2018-09-10 20:50:36 -07:00
2021-04-29 15:25:14 -07:00
debug ( 'prepareHttpChallenge: challenges: %j' , authorization ) ;
2018-09-10 20:50:36 -07:00
let httpChallenges = authorization . challenges . filter ( function ( x ) { return x . type === 'http-01' ; } ) ;
2021-11-17 10:54:26 -08:00
if ( httpChallenges . length === 0 ) throw new BoxError ( BoxError . ACME _ERROR , 'no http challenges' ) ;
2018-09-10 20:50:36 -07:00
let challenge = httpChallenges [ 0 ] ;
debug ( 'prepareHttpChallenge: preparing for challenge %j' , challenge ) ;
let keyAuthorization = this . getKeyAuthorization ( challenge . token ) ;
2021-05-07 22:43:30 -07:00
debug ( 'prepareHttpChallenge: writing %s to %s' , keyAuthorization , path . join ( acmeChallengesDir , challenge . token ) ) ;
2018-09-10 20:50:36 -07:00
2021-06-04 17:51:26 -07:00
if ( ! safe . fs . writeFileSync ( path . join ( acmeChallengesDir , challenge . token ) , keyAuthorization ) ) throw new BoxError ( BoxError . FS _ERROR , ` Error writing challenge: ${ safe . error . message } ` ) ;
2021-05-07 15:56:43 -07:00
2021-05-06 22:29:34 -07:00
return challenge ;
2018-09-10 20:50:36 -07:00
} ;
2021-05-07 22:43:30 -07:00
Acme2 . prototype . cleanupHttpChallenge = async function ( hostname , domain , challenge , acmeChallengesDir ) {
2018-09-10 20:50:36 -07:00
assert . strictEqual ( typeof hostname , 'string' ) ;
assert . strictEqual ( typeof domain , 'string' ) ;
assert . strictEqual ( typeof challenge , 'object' ) ;
2021-05-07 22:43:30 -07:00
assert . strictEqual ( typeof acmeChallengesDir , 'string' ) ;
2018-09-10 20:50:36 -07:00
2021-05-07 22:43:30 -07:00
debug ( 'cleanupHttpChallenge: unlinking %s' , path . join ( acmeChallengesDir , challenge . token ) ) ;
2018-09-28 17:05:53 -07:00
2021-06-04 17:51:26 -07:00
if ( ! safe . fs . unlinkSync ( path . join ( acmeChallengesDir , challenge . token ) ) ) throw new BoxError ( BoxError . FS _ERROR , ` Error unlinking challenge: ${ safe . error . message } ` ) ;
2018-09-10 20:50:36 -07:00
} ;
2018-09-28 12:13:12 -07:00
function getChallengeSubdomain ( hostname , domain ) {
let challengeSubdomain ;
if ( hostname === domain ) {
challengeSubdomain = '_acme-challenge' ;
} else if ( hostname . includes ( '*' ) ) { // wildcard
2018-10-31 15:41:02 -07:00
let subdomain = hostname . slice ( 0 , - domain . length - 1 ) ;
challengeSubdomain = subdomain ? subdomain . replace ( '*' , '_acme-challenge' ) : '_acme-challenge' ;
2018-09-28 12:13:12 -07:00
} else {
challengeSubdomain = '_acme-challenge.' + hostname . slice ( 0 , - domain . length - 1 ) ;
}
2018-10-31 15:41:02 -07:00
debug ( ` getChallengeSubdomain: challenge subdomain for hostname ${ hostname } at domain ${ domain } is ${ challengeSubdomain } ` ) ;
2018-09-28 12:13:12 -07:00
return challengeSubdomain ;
}
2021-05-06 22:29:34 -07:00
Acme2 . prototype . prepareDnsChallenge = async function ( hostname , domain , authorization ) {
2018-09-10 20:50:36 -07:00
assert . strictEqual ( typeof hostname , 'string' ) ;
assert . strictEqual ( typeof domain , 'string' ) ;
assert . strictEqual ( typeof authorization , 'object' ) ;
2021-04-29 15:25:14 -07:00
debug ( 'prepareDnsChallenge: challenges: %j' , authorization ) ;
2021-05-06 22:29:34 -07:00
const dnsChallenges = authorization . challenges . filter ( function ( x ) { return x . type === 'dns-01' ; } ) ;
2021-11-17 10:54:26 -08:00
if ( dnsChallenges . length === 0 ) throw new BoxError ( BoxError . ACME _ERROR , 'no dns challenges' ) ;
2021-05-06 22:29:34 -07:00
const challenge = dnsChallenges [ 0 ] ;
2018-09-10 20:50:36 -07:00
const keyAuthorization = this . getKeyAuthorization ( challenge . token ) ;
2021-05-06 22:29:34 -07:00
const shasum = crypto . createHash ( 'sha256' ) ;
2018-09-10 20:50:36 -07:00
shasum . update ( keyAuthorization ) ;
const txtValue = urlBase64Encode ( shasum . digest ( 'base64' ) ) ;
2021-05-06 22:29:34 -07:00
const challengeSubdomain = getChallengeSubdomain ( hostname , domain ) ;
2018-09-10 20:50:36 -07:00
2018-09-11 22:46:17 -07:00
debug ( ` prepareDnsChallenge: update ${ challengeSubdomain } with ${ txtValue } ` ) ;
2018-09-10 20:50:36 -07:00
2021-08-27 09:52:24 -07:00
await dns . upsertDnsRecords ( challengeSubdomain , domain , 'TXT' , [ ` " ${ txtValue } " ` ] ) ;
2018-09-10 20:50:36 -07:00
2021-08-27 09:52:24 -07:00
await dns . waitForDnsRecord ( challengeSubdomain , domain , 'TXT' , txtValue , { times : 200 } ) ;
2018-09-11 19:23:10 -07:00
2021-08-27 09:52:24 -07:00
return challenge ;
2018-09-10 20:50:36 -07:00
} ;
2021-05-06 22:29:34 -07:00
Acme2 . prototype . cleanupDnsChallenge = async function ( hostname , domain , challenge ) {
2018-09-10 20:50:36 -07:00
assert . strictEqual ( typeof hostname , 'string' ) ;
assert . strictEqual ( typeof domain , 'string' ) ;
assert . strictEqual ( typeof challenge , 'object' ) ;
const keyAuthorization = this . getKeyAuthorization ( challenge . token ) ;
let shasum = crypto . createHash ( 'sha256' ) ;
shasum . update ( keyAuthorization ) ;
const txtValue = urlBase64Encode ( shasum . digest ( 'base64' ) ) ;
2018-09-28 12:13:12 -07:00
let challengeSubdomain = getChallengeSubdomain ( hostname , domain ) ;
2018-09-10 20:50:36 -07:00
2018-09-28 12:13:12 -07:00
debug ( ` cleanupDnsChallenge: remove ${ challengeSubdomain } with ${ txtValue } ` ) ;
2018-09-10 20:50:36 -07:00
2021-08-27 09:52:24 -07:00
await dns . removeDnsRecords ( challengeSubdomain , domain , 'TXT' , [ ` " ${ txtValue } " ` ] ) ;
2018-09-10 20:50:36 -07:00
} ;
2021-05-07 22:43:30 -07:00
Acme2 . prototype . prepareChallenge = async function ( hostname , domain , authorizationUrl , acmeChallengesDir ) {
2018-09-10 20:50:36 -07:00
assert . strictEqual ( typeof hostname , 'string' ) ;
assert . strictEqual ( typeof domain , 'string' ) ;
assert . strictEqual ( typeof authorizationUrl , 'string' ) ;
2021-05-07 22:43:30 -07:00
assert . strictEqual ( typeof acmeChallengesDir , 'string' ) ;
2018-09-10 20:50:36 -07:00
2019-10-03 10:36:57 -07:00
debug ( ` prepareChallenge: http: ${ this . performHttpAuthorization } ` ) ;
2021-05-06 22:29:34 -07:00
const response = await this . postAsGet ( authorizationUrl ) ;
2021-11-17 10:54:26 -08:00
if ( response . status !== 200 ) throw new BoxError ( BoxError . ACME _ERROR , ` Invalid response code getting authorization : ${ response . status } ` ) ;
2018-09-10 15:19:10 -07:00
2021-05-06 22:29:34 -07:00
const authorization = response . body ;
2018-09-10 20:50:36 -07:00
2021-05-06 22:29:34 -07:00
if ( this . performHttpAuthorization ) {
2021-05-07 22:43:30 -07:00
return await this . prepareHttpChallenge ( hostname , domain , authorization , acmeChallengesDir ) ;
2021-05-06 22:29:34 -07:00
} else {
return await this . prepareDnsChallenge ( hostname , domain , authorization ) ;
}
2018-09-10 15:19:10 -07:00
} ;
2021-05-07 22:43:30 -07:00
Acme2 . prototype . cleanupChallenge = async function ( hostname , domain , challenge , acmeChallengesDir ) {
2018-09-10 20:50:36 -07:00
assert . strictEqual ( typeof hostname , 'string' ) ;
assert . strictEqual ( typeof domain , 'string' ) ;
assert . strictEqual ( typeof challenge , 'object' ) ;
2021-05-07 22:43:30 -07:00
assert . strictEqual ( typeof acmeChallengesDir , 'string' ) ;
2018-09-10 20:50:36 -07:00
2019-10-03 10:36:57 -07:00
debug ( ` cleanupChallenge: http: ${ this . performHttpAuthorization } ` ) ;
2018-09-10 20:50:36 -07:00
if ( this . performHttpAuthorization ) {
2021-05-07 22:43:30 -07:00
await this . cleanupHttpChallenge ( hostname , domain , challenge , acmeChallengesDir ) ;
2018-09-10 20:50:36 -07:00
} else {
2021-05-06 22:29:34 -07:00
await this . cleanupDnsChallenge ( hostname , domain , challenge ) ;
2018-09-10 20:50:36 -07:00
}
} ;
2021-05-07 22:43:30 -07:00
Acme2 . prototype . acmeFlow = async function ( hostname , domain , paths ) {
2018-09-10 20:50:36 -07:00
assert . strictEqual ( typeof hostname , 'string' ) ;
2018-09-10 15:19:10 -07:00
assert . strictEqual ( typeof domain , 'string' ) ;
2021-05-07 22:43:30 -07:00
assert . strictEqual ( typeof paths , 'object' ) ;
const { certFilePath , keyFilePath , csrFilePath , acmeChallengesDir } = paths ;
2018-09-10 15:19:10 -07:00
2021-11-16 22:56:35 -08:00
await this . ensureAccount ( ) ;
2021-05-06 22:29:34 -07:00
const { order , orderUrl } = await this . newOrder ( hostname ) ;
for ( let i = 0 ; i < order . authorizations . length ; i ++ ) {
const authorizationUrl = order . authorizations [ i ] ;
debug ( ` acmeFlow: authorizing ${ authorizationUrl } ` ) ;
2021-05-07 22:43:30 -07:00
const challenge = await this . prepareChallenge ( hostname , domain , authorizationUrl , acmeChallengesDir ) ;
2021-05-06 22:29:34 -07:00
await this . notifyChallengeReady ( challenge ) ;
await this . waitForChallenge ( challenge ) ;
2021-05-07 22:43:30 -07:00
const csrDer = await this . createKeyAndCsr ( hostname , keyFilePath , csrFilePath ) ;
2021-05-06 22:29:34 -07:00
await this . signCertificate ( hostname , order . finalize , csrDer ) ;
const certUrl = await this . waitForOrder ( orderUrl ) ;
2021-05-07 22:43:30 -07:00
await this . downloadCertificate ( hostname , certUrl , certFilePath ) ;
2021-05-06 22:29:34 -07:00
try {
2021-05-07 22:43:30 -07:00
await this . cleanupChallenge ( hostname , domain , challenge , acmeChallengesDir ) ;
2021-05-06 22:29:34 -07:00
} catch ( cleanupError ) {
debug ( 'acmeFlow: ignoring error when cleaning up challenge:' , cleanupError ) ;
}
}
2018-09-10 15:19:10 -07:00
} ;
2021-05-06 22:29:34 -07:00
Acme2 . prototype . loadDirectory = async function ( ) {
await promiseRetry ( { times : 3 , interval : 20000 } , async ( ) => {
2021-05-07 15:56:43 -07:00
const response = await superagent . get ( this . caDirectory ) . timeout ( 30000 ) . ok ( ( ) => true ) ;
2018-09-10 15:19:10 -07:00
2021-11-17 10:54:26 -08:00
if ( response . status !== 200 ) throw new BoxError ( BoxError . ACME _ERROR , ` Invalid response code when fetching directory : ${ response . status } ` ) ;
2018-09-10 15:19:10 -07:00
2021-05-06 22:29:34 -07:00
if ( typeof response . body . newNonce !== 'string' ||
typeof response . body . newOrder !== 'string' ||
2021-11-17 10:54:26 -08:00
typeof response . body . newAccount !== 'string' ) throw new BoxError ( BoxError . ACME _ERROR , ` Invalid response body : ${ response . body } ` ) ;
2018-09-10 15:19:10 -07:00
2021-05-06 22:29:34 -07:00
this . directory = response . body ;
} ) ;
2018-09-10 15:19:10 -07:00
} ;
2021-05-07 22:43:30 -07:00
Acme2 . prototype . getCertificate = async function ( vhost , domain , paths ) {
2021-01-19 13:47:11 -08:00
assert . strictEqual ( typeof vhost , 'string' ) ;
2018-09-10 15:19:10 -07:00
assert . strictEqual ( typeof domain , 'string' ) ;
2021-05-07 22:43:30 -07:00
assert . strictEqual ( typeof paths , 'object' ) ;
2018-09-10 15:19:10 -07:00
2021-01-19 13:47:11 -08:00
debug ( ` getCertificate: start acme flow for ${ vhost } from ${ this . caDirectory } ` ) ;
2018-09-10 15:19:10 -07:00
2021-01-19 13:47:11 -08:00
if ( vhost !== domain && this . wildcard ) { // bare domain is not part of wildcard SAN
2021-08-13 17:22:28 -07:00
vhost = dns . makeWildcard ( vhost ) ;
2021-01-19 13:47:11 -08:00
debug ( ` getCertificate: will get wildcard cert for ${ vhost } ` ) ;
2018-09-11 22:46:17 -07:00
}
2021-05-06 22:29:34 -07:00
await this . loadDirectory ( ) ;
2021-05-07 22:43:30 -07:00
await this . acmeFlow ( vhost , domain , paths ) ;
2018-09-10 15:19:10 -07:00
} ;
2021-09-07 09:34:23 -07:00
async function getCertificate ( vhost , domain , paths , options ) {
2021-01-19 13:47:11 -08:00
assert . strictEqual ( typeof vhost , 'string' ) ; // this can also be a wildcard domain (for alias domains)
2018-09-10 15:19:10 -07:00
assert . strictEqual ( typeof domain , 'string' ) ;
2021-05-07 22:43:30 -07:00
assert . strictEqual ( typeof paths , 'object' ) ;
2018-09-10 15:19:10 -07:00
assert . strictEqual ( typeof options , 'object' ) ;
2019-10-03 14:47:18 -07:00
let attempt = 1 ;
2021-09-23 17:47:49 -07:00
await promiseRetry ( { times : 3 , interval : 0 } , async function ( ) {
2019-10-03 14:47:18 -07:00
debug ( ` getCertificate: attempt ${ attempt ++ } ` ) ;
2021-09-07 09:34:23 -07:00
const acme = new Acme2 ( options || { } ) ;
return await acme . getCertificate ( vhost , domain , paths ) ;
} ) ;
2018-09-10 15:19:10 -07:00
}