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' ) ,
2022-11-11 18:09:10 +01:00
paths = require ( './paths.js' ) ,
2021-05-07 22:44:13 -07:00
promiseRetry = require ( './promise-retry.js' ) ,
2018-09-10 15:19:10 -07:00
safe = require ( 'safetydance' ) ,
2024-02-20 23:09:49 +01:00
shell = require ( './shell.js' ) ,
superagent = require ( 'superagent' ) ,
2023-05-25 11:27:23 +02:00
users = require ( './users.js' ) ;
2018-09-10 15:19:10 -07:00
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
2022-11-17 08:58:20 +01:00
function Acme2 ( fqdn , domainObject , email ) {
assert . strictEqual ( typeof fqdn , 'string' ) ;
assert . strictEqual ( typeof domainObject , 'object' ) ;
assert . strictEqual ( typeof email , 'string' ) ;
2018-09-10 15:19:10 -07:00
2022-11-17 08:58:20 +01:00
this . fqdn = fqdn ;
2022-11-29 13:57:58 +01:00
this . accountKey = null ;
2022-11-17 08:58:20 +01:00
this . email = email ;
2018-09-10 15:19:10 -07:00
this . keyId = null ;
2022-11-17 08:58:20 +01:00
const prod = domainObject . tlsConfig . provider . match ( /.*-prod/ ) !== null ; // matches 'le-prod' or 'letsencrypt-prod'
this . caDirectory = prod ? CA _PROD _DIRECTORY _URL : CA _STAGING _DIRECTORY _URL ;
2018-09-10 15:19:10 -07:00
this . directory = { } ;
2023-05-02 23:01:14 +02:00
this . forceHttpAuthorization = domainObject . provider . match ( /noop|manual|wildcard/ ) !== null ;
2022-11-17 08:58:20 +01:00
this . wildcard = ! ! domainObject . tlsConfig . wildcard ;
this . domain = domainObject . domain ;
2023-02-25 02:47:40 +01:00
if ( fqdn !== this . domain && this . wildcard ) { // bare domain is not part of wildcard SAN
this . cn = dns . makeWildcard ( fqdn ) ;
this . altNames = [ this . cn ] ;
if ( fqdn . startsWith ( '*.' ) ) this . altNames . push ( fqdn . replace ( /^\*\./ , '' ) ) ; // add bare domain to cert for wildcard certs
} else {
this . cn = fqdn ;
this . altNames = [ this . cn ] ;
}
2022-11-17 08:58:20 +01:00
this . certName = this . cn . replace ( '*.' , '_.' ) ;
2023-05-02 23:01:14 +02:00
debug ( ` Acme2: will get cert for fqdn: ${ this . fqdn } cn: ${ this . cn } certName: ${ this . certName } wildcard: ${ this . wildcard } http: ${ this . forceHttpAuthorization } ` ) ;
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 ) {
2022-04-14 17:41:41 -05:00
const buf = Buffer . isBuffer ( str ) ? str : Buffer . from ( str ) ;
2018-09-10 15:19:10 -07:00
return urlBase64Encode ( buf . toString ( 'base64' ) ) ;
}
2024-02-20 23:09:49 +01:00
async function getModulus ( pem ) {
2022-11-29 13:57:58 +01:00
assert . strictEqual ( typeof pem , 'string' ) ;
2018-09-10 15:19:10 -07:00
2024-02-21 19:40:27 +01:00
const stdout = await shell . exec ( 'getModulus' , 'openssl rsa -modulus -noout' , { input : pem } ) ;
2022-04-14 17:41:41 -05:00
const match = stdout . match ( /Modulus=([0-9a-fA-F]+)$/m ) ;
2024-02-20 23:09:49 +01:00
if ( ! match ) throw new BoxError ( BoxError . OPENSSL _ERROR , 'Could not get modulus' ) ;
2018-09-10 15:19:10 -07:00
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' ) ;
2022-11-29 13:57:58 +01:00
assert . strictEqual ( typeof this . accountKey , 'string' ) ;
2018-09-10 15:19:10 -07:00
const that = this ;
2023-02-01 15:43:59 +01:00
const header = {
2018-09-10 15:19:10 -07:00
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' ,
2024-02-20 23:09:49 +01:00
n : b64 ( await getModulus ( this . accountKey ) )
2018-09-10 15:19:10 -07:00
} ;
}
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
2023-02-01 15:43:59 +01:00
debug ( ` sendSignedRequest: using nonce ${ nonce } for url ${ url } ` ) ;
2018-09-10 15:19:10 -07:00
2023-05-25 11:27:23 +02:00
const protected64 = b64 ( JSON . stringify ( Object . assign ( { } , 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' ) ;
2022-11-29 13:57:58 +01:00
const signature64 = urlBase64Encode ( signer . sign ( that . accountKey , '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 ( ) {
2024-02-21 19:40:27 +01:00
const acmeAccountKey = await shell . exec ( 'generateAccountKey' , 'openssl genrsa 4096' , { } ) ;
2021-11-16 22:56:35 -08:00
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' ) ;
2022-11-29 13:57:58 +01:00
this . accountKey = await blobs . getString ( blobs . ACME _ACCOUNT _KEY ) ;
if ( ! this . accountKey ) {
2021-11-16 22:56:35 -08:00
debug ( 'ensureAccount: generating new account keys' ) ;
2022-11-29 13:57:58 +01:00
this . accountKey = await generateAccountKey ( ) ;
await blobs . setString ( blobs . ACME _ACCOUNT _KEY , this . accountKey ) ;
2021-11-16 22:56:35 -08:00
}
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 ` ) ;
2022-11-29 13:57:58 +01:00
this . accountKey = await generateAccountKey ( ) ;
await blobs . setString ( blobs . ACME _ACCOUNT _KEY , this . accountKey ) ;
2021-11-16 22:56:35 -08:00
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
} ;
2022-11-17 08:58:20 +01:00
Acme2 . prototype . newOrder = async function ( ) {
2023-02-25 02:47:40 +01:00
const payload = { identifiers : [ ] } ;
this . altNames . forEach ( an => {
payload . identifiers . push ( {
2018-09-10 15:19:10 -07:00
type : 'dns' ,
2023-02-25 02:47:40 +01:00
value : an
} ) ;
} ) ;
2018-09-10 15:19:10 -07:00
2023-02-25 02:47:40 +01:00
debug ( ` newOrder: ${ JSON . stringify ( this . altNames ) } ` ) ;
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
const order = result . body , orderUrl = result . headers . location ;
2023-02-01 15:43:59 +01:00
debug ( ` newOrder: created order ${ this . cn } order: ${ JSON . stringify ( result . body ) } orderUrl: ${ orderUrl } ` ) ;
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-12-07 11:18:26 -08:00
return await promiseRetry ( { times : 15 , interval : 20000 , debug } , 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
} ;
2024-02-20 23:09:49 +01:00
Acme2 . prototype . getKeyAuthorization = async function ( token ) {
2022-11-29 13:57:58 +01:00
assert ( typeof this . accountKey , 'string' ) ;
2018-09-10 15:19:10 -07:00
2023-02-01 15:43:59 +01:00
const jwk = {
2018-09-10 15:19:10 -07:00
e : b64 ( Buffer . from ( [ 0x01 , 0x00 , 0x01 ] ) ) , // Exponent - 65537
kty : 'RSA' ,
2024-02-20 23:09:49 +01:00
n : b64 ( await getModulus ( this . accountKey ) )
2018-09-10 15:19:10 -07:00
} ;
2023-02-01 15:43:59 +01:00
const shasum = crypto . createHash ( 'sha256' ) ;
2018-09-10 15:19:10 -07:00
shasum . update ( JSON . stringify ( jwk ) ) ;
2023-02-01 15:43:59 +01:00
const thumbprint = urlBase64Encode ( shasum . digest ( 'base64' ) ) ;
2018-09-10 20:50:36 -07:00
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 }
2023-02-01 15:43:59 +01:00
debug ( ` notifyChallengeReady: ${ challenge . url } was met ` ) ;
2018-09-10 15:19:10 -07:00
2024-02-20 23:09:49 +01:00
const keyAuthorization = await 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' ) ;
2023-02-01 15:43:59 +01:00
debug ( ` waitingForChallenge: ${ JSON . stringify ( challenge ) } ` ) ;
2018-09-10 15:19:10 -07:00
2021-12-07 11:18:26 -08:00
await promiseRetry ( { times : 15 , interval : 20000 , debug } , 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
2022-11-29 13:57:58 +01:00
Acme2 . prototype . signCertificate = async function ( finalizationUrl , csrPem ) {
2018-09-10 15:19:10 -07:00
assert . strictEqual ( typeof finalizationUrl , 'string' ) ;
2022-11-29 13:57:58 +01:00
assert . strictEqual ( typeof csrPem , 'string' ) ;
2024-02-28 18:13:44 +01:00
const csrDer = await shell . exec ( 'signCertificate' , 'openssl req -inform pem -outform der' , { input : csrPem , encoding : 'buffer' } ) ;
2018-09-10 15:19:10 -07:00
const payload = {
csr : b64 ( csrDer )
} ;
2023-02-01 15:43:59 +01:00
debug ( ` signCertificate: sending sign request to ${ finalizationUrl } ` ) ;
2018-09-10 15:19:10 -07:00
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
} ;
2022-11-17 08:58:20 +01:00
Acme2 . prototype . ensureKey = async function ( ) {
2022-11-29 13:57:58 +01:00
const key = await blobs . getString ( ` ${ blobs . CERT _PREFIX } - ${ this . certName } .key ` ) ;
2022-11-17 08:58:20 +01:00
if ( key ) {
debug ( ` ensureKey: reuse existing key for ${ this . cn } ` ) ;
return key ;
}
2018-09-10 15:19:10 -07:00
2022-11-17 08:58:20 +01:00
debug ( ` ensureKey: generating new key for ${ this . cn } ` ) ;
2024-02-09 21:51:13 +01:00
// same as prime256v1. openssl ecparam -list_curves. we used to use secp384r1 but it doesn't seem to be accepted by few mail servers
2024-02-21 19:40:27 +01:00
const newKey = await shell . exec ( 'ensureKey' , 'openssl ecparam -genkey -name secp256r1' , { } ) ;
2022-11-17 08:58:20 +01:00
return newKey ;
} ;
2018-09-10 15:19:10 -07:00
2022-11-17 08:58:20 +01:00
Acme2 . prototype . createCsr = async function ( key ) {
2022-11-29 13:57:58 +01:00
assert . strictEqual ( typeof key , 'string' ) ;
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 } ` ) ;
2022-11-17 08:58:20 +01:00
const keyFilePath = path . join ( tmpdir , 'key' ) ;
if ( ! safe . fs . writeFileSync ( keyFilePath , key ) ) throw new BoxError ( BoxError . FS _ERROR , ` Failed to write key file: ${ safe . 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
2023-02-25 02:47:40 +01:00
let conf = '[req]\nreq_extensions = v3_req\ndistinguished_name = req_distinguished_name\n'
2021-06-04 16:48:56 -07:00
+ '[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'
2023-02-25 02:47:40 +01:00
+ '[alt_names]\n' ;
this . altNames . forEach ( ( an , i ) => conf += ` DNS. ${ i + 1 } = ${ an } \n ` ) ;
2021-06-04 14:53:23 -07:00
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
2024-02-21 19:40:27 +01:00
const csrPem = await shell . exec ( 'createCsr' , ` openssl req -new -key ${ keyFilePath } -outform PEM -subj /CN= ${ this . cn } -config ${ opensslConfigFile } ` , { } ) ;
2022-02-25 16:43:16 -08:00
await safe ( fs . promises . rm ( tmpdir , { recursive : true , force : true } ) ) ;
2022-11-17 08:58:20 +01:00
debug ( ` createCsr: csr file created for ${ this . cn } ` ) ;
2022-11-29 13:57:58 +01:00
return csrPem ; // inspect with openssl req -text -noout -in hostname.csr -inform pem
2018-09-10 15:19:10 -07:00
} ;
2022-11-17 08:58:20 +01:00
Acme2 . prototype . downloadCertificate = async function ( certUrl ) {
2018-09-10 15:19:10 -07:00
assert . strictEqual ( typeof certUrl , 'string' ) ;
2022-11-17 08:58:20 +01:00
return await promiseRetry ( { times : 5 , interval : 20000 , debug } , async ( ) => {
debug ( ` downloadCertificate: downloading certificate of ${ this . cn } ` ) ;
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
2022-11-29 13:57:58 +01:00
const fullChainPem = result . body . toString ( 'utf8' ) ; // buffer
2022-11-17 08:58:20 +01:00
return fullChainPem ;
2021-05-06 22:29:34 -07:00
} ) ;
2018-09-10 15:19:10 -07:00
} ;
2023-05-02 23:01:14 +02:00
Acme2 . prototype . prepareHttpChallenge = async function ( challenge ) {
assert . strictEqual ( typeof challenge , 'object' ) ;
2018-09-10 20:50:36 -07:00
2023-02-01 15:43:59 +01:00
debug ( ` prepareHttpChallenge: preparing for challenge ${ JSON . stringify ( challenge ) } ` ) ;
2018-09-10 20:50:36 -07:00
2024-02-20 23:09:49 +01:00
const keyAuthorization = await this . getKeyAuthorization ( challenge . token ) ;
2018-09-10 20:50:36 -07:00
2023-02-01 15:43:59 +01:00
const challengeFilePath = path . join ( paths . ACME _CHALLENGES _DIR , challenge . token ) ;
debug ( ` prepareHttpChallenge: writing ${ keyAuthorization } to ${ challengeFilePath } ` ) ;
2018-09-10 20:50:36 -07:00
2023-02-01 15:43:59 +01:00
if ( ! safe . fs . writeFileSync ( challengeFilePath , keyAuthorization ) ) throw new BoxError ( BoxError . FS _ERROR , ` Error writing challenge: ${ safe . error . message } ` ) ;
2018-09-10 20:50:36 -07:00
} ;
2022-11-17 08:58:20 +01:00
Acme2 . prototype . cleanupHttpChallenge = async function ( challenge ) {
2018-09-10 20:50:36 -07:00
assert . strictEqual ( typeof challenge , 'object' ) ;
2023-02-01 15:43:59 +01:00
const challengeFilePath = path . join ( paths . ACME _CHALLENGES _DIR , challenge . token ) ;
debug ( ` cleanupHttpChallenge: unlinking ${ challengeFilePath } ` ) ;
2018-09-28 17:05:53 -07:00
2023-02-01 15:43:59 +01:00
if ( ! safe . fs . unlinkSync ( challengeFilePath ) ) throw new BoxError ( BoxError . FS _ERROR , ` Error unlinking challenge: ${ safe . error . message } ` ) ;
2018-09-10 20:50:36 -07:00
} ;
2022-11-17 08:58:20 +01:00
function getChallengeSubdomain ( cn , domain ) {
2018-09-28 12:13:12 -07:00
let challengeSubdomain ;
2022-11-17 08:58:20 +01:00
if ( cn === domain ) {
2018-09-28 12:13:12 -07:00
challengeSubdomain = '_acme-challenge' ;
2022-11-17 08:58:20 +01:00
} else if ( cn . includes ( '*' ) ) { // wildcard
let subdomain = cn . slice ( 0 , - domain . length - 1 ) ;
2018-10-31 15:41:02 -07:00
challengeSubdomain = subdomain ? subdomain . replace ( '*' , '_acme-challenge' ) : '_acme-challenge' ;
2018-09-28 12:13:12 -07:00
} else {
2022-11-17 08:58:20 +01:00
challengeSubdomain = '_acme-challenge.' + cn . slice ( 0 , - domain . length - 1 ) ;
2018-09-28 12:13:12 -07:00
}
2022-11-17 08:58:20 +01:00
debug ( ` getChallengeSubdomain: challenge subdomain for cn ${ cn } at domain ${ domain } is ${ challengeSubdomain } ` ) ;
2018-10-31 15:41:02 -07:00
2018-09-28 12:13:12 -07:00
return challengeSubdomain ;
}
2023-05-02 23:01:14 +02:00
Acme2 . prototype . prepareDnsChallenge = async function ( cn , challenge ) {
assert . strictEqual ( typeof challenge , 'object' ) ;
2018-09-10 20:50:36 -07:00
2023-05-02 23:01:14 +02:00
debug ( ` prepareDnsChallenge: preparing for challenge: ${ JSON . stringify ( challenge ) } ` ) ;
2018-09-10 20:50:36 -07:00
2024-02-20 23:09:49 +01:00
const keyAuthorization = await 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' ) ) ;
2023-02-25 02:47:40 +01:00
const challengeSubdomain = getChallengeSubdomain ( cn , this . 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
2022-11-17 08:58:20 +01:00
await dns . upsertDnsRecords ( challengeSubdomain , this . domain , 'TXT' , [ ` " ${ txtValue } " ` ] ) ;
2018-09-10 20:50:36 -07:00
2022-11-17 08:58:20 +01:00
await dns . waitForDnsRecord ( challengeSubdomain , this . domain , 'TXT' , txtValue , { times : 200 } ) ;
2018-09-10 20:50:36 -07:00
} ;
2023-02-25 02:47:40 +01:00
Acme2 . prototype . cleanupDnsChallenge = async function ( cn , challenge ) {
assert . strictEqual ( typeof cn , 'string' ) ;
2018-09-10 20:50:36 -07:00
assert . strictEqual ( typeof challenge , 'object' ) ;
2024-02-20 23:09:49 +01:00
const keyAuthorization = await this . getKeyAuthorization ( challenge . token ) ;
2022-11-17 08:58:20 +01:00
const shasum = crypto . createHash ( 'sha256' ) ;
2018-09-10 20:50:36 -07:00
shasum . update ( keyAuthorization ) ;
const txtValue = urlBase64Encode ( shasum . digest ( 'base64' ) ) ;
2023-02-25 02:47:40 +01:00
const challengeSubdomain = getChallengeSubdomain ( cn , this . 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
2022-11-17 08:58:20 +01:00
await dns . removeDnsRecords ( challengeSubdomain , this . domain , 'TXT' , [ ` " ${ txtValue } " ` ] ) ;
2018-09-10 20:50:36 -07:00
} ;
2023-02-25 02:47:40 +01:00
Acme2 . prototype . prepareChallenge = async function ( cn , authorization ) {
assert . strictEqual ( typeof cn , 'string' ) ;
assert . strictEqual ( typeof authorization , 'object' ) ;
2018-09-10 15:19:10 -07:00
2023-05-02 23:01:14 +02:00
debug ( ` prepareChallenge: http: ${ this . forceHttpAuthorization } cn: ${ cn } authorization: ${ JSON . stringify ( authorization ) } ` ) ;
2018-09-10 20:50:36 -07:00
2023-05-02 23:01:14 +02:00
// validation is cached by LE for 60 days or so. if a user switches from non-wildcard DNS (http challenge) to programmatic DNS (dns challenge), then
// LE remembers the challenge type and won't give us a dns challenge for 60 days!
// https://letsencrypt.org/docs/faq/#i-successfully-renewed-a-certificate-but-validation-didn-t-happen-this-time-how-is-that-possible
const dnsChallenges = authorization . challenges . filter ( function ( x ) { return x . type === 'dns-01' ; } ) ;
const httpChallenges = authorization . challenges . filter ( function ( x ) { return x . type === 'http-01' ; } ) ;
if ( this . forceHttpAuthorization || dnsChallenges . length === 0 ) {
if ( httpChallenges . length === 0 ) throw new BoxError ( BoxError . ACME _ERROR , 'no http challenges' ) ;
await this . prepareHttpChallenge ( httpChallenges [ 0 ] ) ;
return httpChallenges [ 0 ] ;
2021-05-06 22:29:34 -07:00
}
2023-05-02 23:01:14 +02:00
await this . prepareDnsChallenge ( cn , dnsChallenges [ 0 ] ) ;
return dnsChallenges [ 0 ] ;
2018-09-10 15:19:10 -07:00
} ;
2023-02-25 02:47:40 +01:00
Acme2 . prototype . cleanupChallenge = async function ( cn , challenge ) {
assert . strictEqual ( typeof cn , 'string' ) ;
2018-09-10 20:50:36 -07:00
assert . strictEqual ( typeof challenge , 'object' ) ;
2023-05-02 23:01:14 +02:00
debug ( ` cleanupChallenge: http: ${ this . forceHttpAuthorization } ` ) ;
2019-10-03 10:36:57 -07:00
2023-05-02 23:01:14 +02:00
if ( this . forceHttpAuthorization ) {
2022-11-17 08:58:20 +01:00
await this . cleanupHttpChallenge ( challenge ) ;
2018-09-10 20:50:36 -07:00
} else {
2023-02-25 02:47:40 +01:00
await this . cleanupDnsChallenge ( cn , challenge ) ;
2018-09-10 20:50:36 -07:00
}
} ;
2022-11-17 08:58:20 +01:00
Acme2 . prototype . acmeFlow = async function ( ) {
2021-11-16 22:56:35 -08:00
await this . ensureAccount ( ) ;
2022-11-17 08:58:20 +01:00
const { order , orderUrl } = await this . newOrder ( ) ;
2023-02-25 02:47:40 +01:00
for ( const authorizationUrl of order . authorizations ) {
2021-05-06 22:29:34 -07:00
debug ( ` acmeFlow: authorizing ${ authorizationUrl } ` ) ;
2023-02-25 02:47:40 +01:00
const response = await this . postAsGet ( authorizationUrl ) ;
if ( response . status !== 200 ) throw new BoxError ( BoxError . ACME _ERROR , ` Invalid response code getting authorization : ${ response . status } ` ) ;
const authorization = response . body ; // { identifier, status, expires, challenges, wildcard }
const cn = authorization . wildcard ? ` *. ${ authorization . identifier . value } ` : authorization . identifier . value ;
const challenge = await this . prepareChallenge ( cn , authorization ) ;
2021-05-06 22:29:34 -07:00
await this . notifyChallengeReady ( challenge ) ;
await this . waitForChallenge ( challenge ) ;
2023-02-25 02:47:40 +01:00
await safe ( this . cleanupChallenge ( cn , challenge ) , { debug } ) ;
2021-05-06 22:29:34 -07:00
}
2022-11-17 08:58:20 +01:00
2023-02-25 02:47:40 +01:00
const key = await this . ensureKey ( ) ;
const csr = await this . createCsr ( key ) ;
await this . signCertificate ( order . finalize , csr ) ;
const certUrl = await this . waitForOrder ( orderUrl ) ;
const cert = await this . downloadCertificate ( certUrl ) ;
return { cert , key , csr } ;
2018-09-10 15:19:10 -07:00
} ;
2021-05-06 22:29:34 -07:00
Acme2 . prototype . loadDirectory = async function ( ) {
2021-12-07 11:18:26 -08:00
await promiseRetry ( { times : 3 , interval : 20000 , debug } , 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
} ;
2022-11-17 08:58:20 +01:00
Acme2 . prototype . getCertificate = async function ( ) {
debug ( ` getCertificate: start acme flow for ${ this . cn } from ${ this . caDirectory } ` ) ;
2018-09-10 15:19:10 -07:00
2022-11-17 08:58:20 +01:00
await this . loadDirectory ( ) ;
const result = await this . acmeFlow ( ) ;
2018-09-10 15:19:10 -07:00
2023-02-25 02:47:40 +01:00
debug ( ` getCertificate: acme flow completed for ${ this . cn } ` ) ;
2018-09-11 22:46:17 -07:00
2023-02-25 02:47:40 +01:00
await blobs . setString ( ` ${ blobs . CERT _PREFIX } - ${ this . certName } .key ` , result . key ) ;
await blobs . setString ( ` ${ blobs . CERT _PREFIX } - ${ this . certName } .cert ` , result . cert ) ;
await blobs . setString ( ` ${ blobs . CERT _PREFIX } - ${ this . certName } .csr ` , result . csr ) ;
2022-11-17 08:58:20 +01:00
2023-02-25 02:47:40 +01:00
return result ;
2018-09-10 15:19:10 -07:00
} ;
2022-11-17 08:58:20 +01:00
async function getCertificate ( fqdn , domainObject ) {
2022-07-13 09:26:27 +05:30
assert . strictEqual ( typeof fqdn , 'string' ) ; // this can also be a wildcard domain (for alias domains)
2022-11-17 08:58:20 +01:00
assert . strictEqual ( typeof domainObject , 'object' ) ;
// registering user with an email requires A or MX record (https://github.com/letsencrypt/boulder/issues/1197)
// we cannot use admin@fqdn because the user might not have set it up.
// we simply update the account with the latest email we have each time when getting letsencrypt certs
// https://github.com/ietf-wg-acme/acme/issues/30
const owner = await users . getOwner ( ) ;
const email = owner ? . email || 'webmaster@cloudron.io' ; // can error if not activated yet
2018-09-10 15:19:10 -07:00
2022-11-17 08:58:20 +01:00
return await promiseRetry ( { times : 3 , interval : 0 , debug } , async function ( ) {
debug ( ` getCertificate: for fqdn ${ fqdn } and domain ${ domainObject . domain } ` ) ;
2019-10-03 14:47:18 -07:00
2022-11-17 08:58:20 +01:00
const acme = new Acme2 ( fqdn , domainObject , email ) ;
return await acme . getCertificate ( ) ;
2021-09-07 09:34:23 -07:00
} ) ;
2018-09-10 15:19:10 -07:00
}