2018-09-10 15:19:10 -07:00
'use strict' ;
2021-05-06 22:29:34 -07:00
const assert = require ( 'assert' ) ,
2018-09-10 15:19:10 -07:00
async = require ( 'async' ) ,
2021-05-07 20:19:18 -07:00
blobs = require ( '../blobs.js' ) ,
2019-09-25 14:13:10 -07:00
BoxError = require ( '../boxerror.js' ) ,
2018-09-10 15:19:10 -07:00
crypto = require ( 'crypto' ) ,
debug = require ( 'debug' ) ( 'box:cert/acme2' ) ,
2018-09-10 20:50:36 -07:00
domains = require ( '../domains.js' ) ,
2018-09-10 15:19:10 -07:00
fs = require ( 'fs' ) ,
path = require ( 'path' ) ,
paths = require ( '../paths.js' ) ,
2021-05-06 22:29:34 -07:00
promiseRetry = require ( '../promise-retry.js' ) ,
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' ;
exports = module . exports = {
2021-04-29 15:25:14 -07:00
getCertificate ,
2018-09-10 15:19:10 -07:00
// testing
2018-10-31 15:41:02 -07:00
_name : 'acme' ,
_getChallengeSubdomain : getChallengeSubdomain
2018-09-10 15:19:10 -07:00
} ;
// 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-05-02 23:28:41 -07:00
this . accountKeyPem = options . accountKeyPem ; // 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-05-06 22:29:34 -07:00
if ( response . status !== 204 ) throw new BoxError ( BoxError . EXTERNAL _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 ( ) ] ;
if ( ! nonce ) throw new BoxError ( BoxError . EXTERNAL _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 ) ) ;
if ( result . status !== 200 ) throw new BoxError ( BoxError . EXTERNAL _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-05-06 22:29:34 -07:00
Acme2 . prototype . registerUser = async function ( ) {
const payload = {
2018-09-10 15:19:10 -07:00
termsOfServiceAgreed : true
} ;
debug ( 'registerUser: registering user' ) ;
2021-05-06 22:29:34 -07:00
const result = await this . sendSignedRequest ( this . directory . newAccount , JSON . stringify ( payload ) ) ;
// 200 if already exists. 201 for new accounts
if ( result . status !== 200 && result . status !== 201 ) return new BoxError ( BoxError . EXTERNAL _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-05-06 22:29:34 -07:00
debug ( ` registerUser: 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 } ` ) ;
if ( result . status !== 201 ) throw new BoxError ( BoxError . EXTERNAL _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-05-06 22:29:34 -07:00
if ( ! Array . isArray ( order . authorizations ) ) throw new BoxError ( BoxError . EXTERNAL _ERROR , 'invalid authorizations in order' ) ;
if ( typeof order . finalize !== 'string' ) throw new BoxError ( BoxError . EXTERNAL _ERROR , 'invalid finalize in order' ) ;
if ( typeof orderUrl !== 'string' ) throw new BoxError ( BoxError . EXTERNAL _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 } ` ) ;
throw new BoxError ( BoxError . EXTERNAL _ERROR , ` Bad response code: ${ result . status } ` ) ;
}
debug ( 'waitForOrder: status is "%s %j' , result . body . status , result . body ) ;
if ( result . body . status === 'pending' || result . body . status === 'processing' ) throw new BoxError ( BoxError . TRY _AGAIN , ` Request is in ${ result . body . status } state ` ) ;
else if ( result . body . status === 'valid' && result . body . certificate ) return result . body . certificate ;
else throw new BoxError ( BoxError . EXTERNAL _ERROR , ` Unexpected status or invalid response: ${ JSON . stringify ( result . body ) } ` ) ;
} ) ;
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 ) ) ;
if ( result . status !== 200 ) throw new BoxError ( BoxError . EXTERNAL _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 } ` ) ;
throw new BoxError ( BoxError . EXTERNAL _ERROR , 'Bad response code:' + result . statusCode ) ;
}
debug ( ` waitForChallenge: status is " ${ result . body . status } " " ${ JSON . stringify ( result . body ) } " ` ) ;
if ( result . body . status === 'pending' ) throw new BoxError ( BoxError . TRY _AGAIN ) ;
else if ( result . body . status === 'valid' ) return ;
else throw new BoxError ( BoxError . EXTERNAL _ERROR , ` Unexpected status: ${ 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
if ( result . status !== 200 ) throw new BoxError ( BoxError . EXTERNAL _ERROR , ` Failed to sign certificate. 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
Acme2 . prototype . createKeyAndCsr = async function ( hostname ) {
2018-09-11 22:46:17 -07:00
assert . strictEqual ( typeof hostname , 'string' ) ;
2018-09-10 15:19:10 -07:00
2021-05-07 20:19:18 -07:00
const outdir = paths . NGINX _CERT _DIR ;
2018-09-11 22:46:17 -07:00
const certName = hostname . replace ( '*.' , '_.' ) ;
2021-04-16 11:17:13 -07:00
const csrFile = path . join ( outdir , ` ${ certName } .csr ` ) ;
const privateKeyFile = path . join ( outdir , ` ${ certName } .key ` ) ;
2018-09-10 15:19:10 -07:00
2021-05-07 20:19:18 -07:00
let privateKey = await blobs . get ( ` ${ blobs . CERT _PREFIX } - ${ certName } .key ` ) ;
if ( ! privateKeyFile ) {
debug ( ` createKeyAndCsr: reuse the key for renewal at ${ privateKeyFile } ` ) ;
} else {
debug ( 'createKeyAndCsr: create new key' ) ;
privateKey = safe . child _process . execSync ( 'openssl ecparam -genkey -name secp384r1' ) ; // openssl ecparam -list_curves
if ( ! privateKey ) throw new BoxError ( BoxError . OPENSSL _ERROR , safe . error ) ;
await blobs . set ( ` ${ blobs . CERT _PREFIX } - ${ certName } .key ` , privateKey ) ;
}
if ( ! safe . fs . writeFileSync ( privateKeyFile , privateKey ) ) throw new BoxError ( BoxError . FS _ERROR , ` Could not write private key: ${ safe . error . message } ` ) ;
debug ( ` createKeyAndCsr: key file saved at ${ privateKeyFile } ` ) ;
2018-09-10 15:19:10 -07:00
if ( safe . fs . existsSync ( privateKeyFile ) ) {
// in some old releases, csr file was corrupt. so always regenerate it
debug ( 'createKeyAndCsr: reuse the key for renewal at %s' , privateKeyFile ) ;
} 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 ) ;
if ( ! safe . fs . writeFileSync ( privateKeyFile , key ) ) throw new BoxError ( BoxError . FS _ERROR , safe . error ) ;
2018-09-10 15:19:10 -07:00
debug ( 'createKeyAndCsr: key file saved at %s' , privateKeyFile ) ;
}
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-04-16 11:17:13 -07:00
const extensionArgs = ` -addext "subjectAltName = DNS: ${ hostname } " `
+ ' -addext "basicConstraints = CA:FALSE"' // this is not for a CA cert. cannot sign other certs with this
2021-04-16 13:33:32 -07:00
+ ' -addext "keyUsage = nonRepudiation, digitalSignature, keyEncipherment"' ;
2021-04-16 11:17:13 -07:00
const csrDer = safe . child _process . execSync ( ` openssl req -new -key ${ privateKeyFile } -outform DER -subj /CN= ${ hostname } ${ extensionArgs } ` ) ;
2021-05-06 22:29:34 -07:00
if ( ! csrDer ) throw new BoxError ( BoxError . OPENSSL _ERROR , safe . error ) ;
2021-05-07 20:19:18 -07:00
await blobs . set ( ` ${ blobs . CERT _PREFIX } - ${ certName } .csr ` , csrDer ) ;
2021-05-06 22:29:34 -07:00
if ( ! safe . fs . writeFileSync ( csrFile , 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
debug ( 'createKeyAndCsr: csr file (DER) saved at %s' , csrFile ) ;
2021-05-06 22:29:34 -07:00
return csrDer ;
2018-09-10 15:19:10 -07:00
} ;
2021-05-06 22:29:34 -07:00
Acme2 . prototype . downloadCertificate = async function ( hostname , certUrl ) {
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-05-07 20:19:18 -07:00
const outdir = paths . NGINX _CERT _DIR ;
2018-09-10 15:19:10 -07:00
2021-05-06 22:29:34 -07:00
await promiseRetry ( { times : 5 , interval : 20000 } , async ( ) => {
2019-10-03 14:37:12 -07:00
debug ( 'downloadCertificate: downloading certificate' ) ;
2018-09-10 15:19:10 -07:00
2021-05-07 15:56:43 -07:00
const result = await this . postAsGet ( certUrl ) ;
2021-05-06 22:29:34 -07:00
if ( result . statusCode === 202 ) throw new BoxError ( BoxError . TRY _AGAIN , 'Retry downloading certificate' ) ;
if ( result . statusCode !== 200 ) throw new BoxError ( BoxError . EXTERNAL _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-06 22:29:34 -07:00
const certName = hostname . replace ( '*.' , '_.' ) ;
const certificateFile = path . join ( outdir , ` ${ certName } .cert ` ) ;
2021-05-07 20:19:18 -07:00
await blobs . set ( ` ${ blobs . CERT _PREFIX } - ${ certName } .cert ` , fullChainPem ) ;
2021-05-06 22:29:34 -07:00
if ( ! safe . fs . writeFileSync ( certificateFile , fullChainPem ) ) throw new BoxError ( BoxError . FS _ERROR , safe . error ) ;
2018-09-10 15:19:10 -07:00
2021-05-06 22:29:34 -07:00
debug ( ` downloadCertificate: cert file for ${ hostname } saved at ${ certificateFile } ` ) ;
} ) ;
2018-09-10 15:19:10 -07:00
} ;
2021-05-06 22:29:34 -07:00
Acme2 . prototype . prepareHttpChallenge = 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 ( '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-05-06 22:29:34 -07:00
if ( httpChallenges . length === 0 ) throw new BoxError ( BoxError . EXTERNAL _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 ) ;
debug ( 'prepareHttpChallenge: writing %s to %s' , keyAuthorization , path . join ( paths . ACME _CHALLENGES _DIR , challenge . token ) ) ;
2021-05-07 15:56:43 -07:00
if ( ! fs . writeFileSync ( path . join ( paths . ACME _CHALLENGES _DIR , challenge . token ) , keyAuthorization ) ) throw new BoxError ( BoxError . FS _ERROR , safe . error ) ;
2021-05-06 22:29:34 -07:00
return challenge ;
2018-09-10 20:50:36 -07:00
} ;
2021-05-06 22:29:34 -07:00
Acme2 . prototype . cleanupHttpChallenge = 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' ) ;
2018-09-28 17:05:53 -07:00
debug ( 'cleanupHttpChallenge: unlinking %s' , path . join ( paths . ACME _CHALLENGES _DIR , challenge . token ) ) ;
2021-05-06 22:29:34 -07:00
await fs . promises . unlink ( path . join ( paths . ACME _CHALLENGES _DIR , challenge . token ) ) ;
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' ; } ) ;
if ( dnsChallenges . length === 0 ) throw new BoxError ( BoxError . EXTERNAL _ERROR , 'no dns challenges' ) ;
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-05-06 22:29:34 -07:00
return new Promise ( ( resolve , reject ) => {
domains . upsertDnsRecords ( challengeSubdomain , domain , 'TXT' , [ ` " ${ txtValue } " ` ] , function ( error ) {
if ( error ) return reject ( error ) ;
2018-09-10 20:50:36 -07:00
2021-05-06 22:29:34 -07:00
domains . waitForDnsRecord ( challengeSubdomain , domain , 'TXT' , txtValue , { times : 200 } , function ( error ) {
if ( error ) return reject ( error ) ;
2018-09-11 19:23:10 -07:00
2021-05-06 22:29:34 -07:00
resolve ( challenge ) ;
} ) ;
2018-09-11 19:23:10 -07:00
} ) ;
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-05-06 22:29:34 -07:00
return new Promise ( ( resolve , reject ) => {
domains . removeDnsRecords ( challengeSubdomain , domain , 'TXT' , [ ` " ${ txtValue } " ` ] , function ( error ) {
if ( error ) return reject ( error ) ;
2018-09-10 20:50:36 -07:00
2021-05-06 22:29:34 -07:00
resolve ( null ) ;
} ) ;
2018-09-10 20:50:36 -07:00
} ) ;
} ;
2021-05-06 22:29:34 -07:00
Acme2 . prototype . prepareChallenge = async function ( hostname , domain , authorizationUrl ) {
2018-09-10 20:50:36 -07:00
assert . strictEqual ( typeof hostname , 'string' ) ;
assert . strictEqual ( typeof domain , 'string' ) ;
assert . strictEqual ( typeof authorizationUrl , 'string' ) ;
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 ) ;
if ( response . status !== 200 ) throw new BoxError ( BoxError . EXTERNAL _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 ) {
return await this . prepareHttpChallenge ( hostname , domain , authorization ) ;
} else {
return await this . prepareDnsChallenge ( hostname , domain , authorization ) ;
}
2018-09-10 15:19:10 -07:00
} ;
2021-05-06 22:29:34 -07:00
Acme2 . prototype . cleanupChallenge = 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' ) ;
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-06 22:29:34 -07:00
await this . cleanupHttpChallenge ( hostname , domain , challenge ) ;
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-06 22:29:34 -07:00
Acme2 . prototype . acmeFlow = async function ( hostname , domain ) {
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-06 22:29:34 -07:00
await this . registerUser ( ) ;
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 } ` ) ;
const challenge = await this . prepareChallenge ( hostname , domain , authorizationUrl ) ;
await this . notifyChallengeReady ( challenge ) ;
await this . waitForChallenge ( challenge ) ;
const csrDer = await this . createKeyAndCsr ( hostname ) ;
await this . signCertificate ( hostname , order . finalize , csrDer ) ;
const certUrl = await this . waitForOrder ( orderUrl ) ;
await this . downloadCertificate ( hostname , certUrl ) ;
try {
await this . cleanupChallenge ( hostname , domain , challenge ) ;
} 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-05-06 22:29:34 -07:00
if ( response . status !== 200 ) throw new BoxError ( BoxError . EXTERNAL _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' ||
typeof response . body . newAccount !== 'string' ) throw new BoxError ( BoxError . EXTERNAL _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-06 22:29:34 -07:00
Acme2 . prototype . getCertificate = async function ( vhost , domain ) {
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-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
vhost = domains . makeWildcard ( vhost ) ;
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 ( ) ;
await this . acmeFlow ( vhost , domain ) ;
2018-09-10 15:19:10 -07:00
2021-05-07 20:19:18 -07:00
const outdir = paths . NGINX _CERT _DIR ;
2021-05-06 22:29:34 -07:00
const certName = vhost . replace ( '*.' , '_.' ) ;
2018-09-10 15:19:10 -07:00
2021-05-06 22:29:34 -07:00
return { certFilePath : path . join ( outdir , ` ${ certName } .cert ` ) , keyFilePath : path . join ( outdir , ` ${ certName } .key ` ) } ;
2018-09-10 15:19:10 -07:00
} ;
2021-01-19 13:47:11 -08:00
function getCertificate ( vhost , domain , options , callback ) {
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' ) ;
assert . strictEqual ( typeof options , 'object' ) ;
assert . strictEqual ( typeof callback , 'function' ) ;
2019-10-03 14:47:18 -07:00
let attempt = 1 ;
async . retry ( { times : 3 , interval : 0 } , function ( retryCallback ) {
debug ( ` getCertificate: attempt ${ attempt ++ } ` ) ;
let acme = new Acme2 ( options || { } ) ;
2021-05-06 22:29:34 -07:00
acme . getCertificate ( vhost , domain ) . then ( ( result ) => {
callback ( null , result . certFilePath , result . keyFilePath ) ;
} ) . catch ( retryCallback ) ;
2019-10-03 14:47:18 -07:00
} , callback ) ;
2018-09-10 15:19:10 -07:00
}