2019-05-07 07:52:33 +00:00
// Copyright 2018-present AJ ONeal. All rights reserved
/ * T h i s S o u r c e C o d e F o r m i s s u b j e c t t o t h e t e r m s o f t h e M o z i l l a P u b l i c
* License , v . 2.0 . If a copy of the MPL was not distributed with this
* file , You can obtain one at http : //mozilla.org/MPL/2.0/. */
2019-10-04 23:35:59 +00:00
'use strict' ;
/* globals Promise */
2019-10-08 10:33:14 +00:00
require ( '@root/encoding/bytes' ) ;
var Enc = require ( '@root/encoding/base64' ) ;
2019-10-04 23:35:59 +00:00
var ACME = module . exports ;
2019-10-21 19:45:47 +00:00
var Keypairs = require ( '@root/keypairs' ) ;
var CSR = require ( '@root/csr' ) ;
2019-10-15 11:01:52 +00:00
var sha2 = require ( '@root/keypairs/lib/node/sha2.js' ) ;
2019-10-08 19:40:01 +00:00
var http = require ( './lib/node/http.js' ) ;
2019-10-23 07:44:55 +00:00
var A = require ( './account.js' ) ;
var U = require ( './utils.js' ) ;
2019-10-25 10:54:54 +00:00
var E = require ( './errors.js' ) ;
var M = require ( './maintainers.js' ) ;
2019-10-04 23:35:59 +00:00
2019-10-23 01:50:08 +00:00
var native = require ( './lib/native.js' ) ;
2019-10-21 19:45:47 +00:00
2019-10-25 00:49:42 +00:00
ACME . create = function create ( me ) {
if ( ! me ) {
me = { } ;
}
// me.debug = true;
me . _nonces = [ ] ;
me . _canCheck = { } ;
2019-10-25 10:54:54 +00:00
if ( ! /.+@.+\..+/ . test ( me . maintainerEmail ) ) {
throw new Error (
'you should supply `maintainerEmail` as a contact for security and critical bug notices'
) ;
}
if ( ! /\w\/v?\d/ . test ( me . packageAgent ) && false !== me . packageAgent ) {
console . error (
"\nyou should supply `packageAgent` as an rfc7231-style User-Agent such as Foo/v1.1\n\n\t// your package agent should be this:\n\tvar pkg = require('./package.json');\n\tvar agent = pkg.name + '/' + pkg.version\n"
) ;
process . exit ( 1 ) ;
return ;
}
2019-10-25 00:49:42 +00:00
if ( ! me . dns01 ) {
me . dns01 = function ( ch ) {
return native . _dns01 ( me , ch ) ;
} ;
}
if ( ! me . http01 ) {
// for browser version only
if ( ! me . _baseUrl ) {
me . _baseUrl = '' ;
}
me . http01 = function ( ch ) {
return native . _http01 ( me , ch ) ;
} ;
}
2019-10-25 10:54:54 +00:00
if ( ! me . _ _request ) {
me . _ _request = http . request ;
2019-10-25 00:49:42 +00:00
}
// passed to dependencies
2019-10-25 10:54:54 +00:00
me . request = function ( opts ) {
2019-10-25 00:49:42 +00:00
return U . _request ( me , opts ) ;
} ;
me . init = function ( opts ) {
2019-10-25 10:54:54 +00:00
M . init ( me ) ;
2019-10-25 00:49:42 +00:00
function fin ( dir ) {
me . _directoryUrls = dir ;
me . _tos = dir . meta . termsOfService ;
return dir ;
}
if ( opts && opts . meta && opts . termsOfService ) {
return Promise . resolve ( fin ( opts ) ) ;
}
if ( ! me . directoryUrl ) {
me . directoryUrl = opts ;
}
if ( 'string' !== typeof me . directoryUrl ) {
throw new Error (
'you must supply either the ACME directory url as a string or an object of the ACME urls'
) ;
}
var p = Promise . resolve ( ) ;
if ( ! me . skipChallengeTest ) {
p = native . _canCheck ( me ) ;
}
return p . then ( function ( ) {
return ACME . _directory ( me ) . then ( function ( resp ) {
return fin ( resp . body ) ;
} ) ;
2019-10-04 23:35:59 +00:00
} ) ;
2019-10-25 00:49:42 +00:00
} ;
me . accounts = {
create : function ( options ) {
try {
return A . _registerAccount ( me , options ) ;
} catch ( e ) {
return Promise . reject ( e ) ;
}
}
} ;
/ *
me . authorizations = {
// create + get challlenges
get : function ( options ) {
return A . _getAccountKid ( me , options ) . then ( function ( kid ) {
2019-10-26 06:03:43 +00:00
ACME . _normalizePresenters ( me , options , options . challenges ) ;
2019-10-25 00:49:42 +00:00
return ACME . _orderCert ( me , options , kid ) . then ( function ( order ) {
return order . claims ;
} ) ;
} ) ;
} ,
// set challenges, check challenges, finalize order, return order
present : function ( options ) {
return A . _getAccountKid ( me , options ) . then ( function ( kid ) {
2019-10-26 06:03:43 +00:00
ACME . _normalizePresenters ( me , options , options . challenges ) ;
2019-10-25 00:49:42 +00:00
return ACME . _finalizeOrder ( me , options , kid , options . order ) ;
} ) ;
}
} ;
* /
me . certificates = {
create : function ( options ) {
return A . _getAccountKid ( me , options ) . then ( function ( kid ) {
2019-10-26 06:03:43 +00:00
ACME . _normalizePresenters ( me , options , options . challenges ) ;
2019-10-25 00:49:42 +00:00
return ACME . _getCertificate ( me , options , kid ) ;
} ) ;
}
} ;
return me ;
2019-10-04 23:35:59 +00:00
} ;
// http-01: GET https://example.org/.well-known/acme-challenge/{{token}} => {{keyAuth}}
// dns-01: TXT _acme-challenge.example.org. => "{{urlSafeBase64(sha256(keyAuth))}}"
ACME . challengePrefixes = {
'http-01' : '/.well-known/acme-challenge' ,
'dns-01' : '_acme-challenge'
} ;
ACME . challengeTests = {
'http-01' : function ( me , auth ) {
2019-10-08 10:33:14 +00:00
var ch = auth . challenge ;
return me . http01 ( ch ) . then ( function ( keyAuth ) {
2019-10-04 23:35:59 +00:00
var err ;
2019-10-02 21:04:54 +00:00
2019-10-04 23:35:59 +00:00
// TODO limit the number of bytes that are allowed to be downloaded
2019-10-08 10:33:14 +00:00
if ( ch . keyAuthorization === ( keyAuth || '' ) . trim ( ) ) {
2019-10-04 23:35:59 +00:00
return true ;
}
2019-10-02 21:04:54 +00:00
2019-10-04 23:35:59 +00:00
err = new Error (
'Error: Failed HTTP-01 Pre-Flight / Dry Run.\n' +
"curl '" +
2019-10-08 10:33:14 +00:00
ch . challengeUrl +
2019-10-04 23:35:59 +00:00
"'\n" +
"Expected: '" +
2019-10-08 10:33:14 +00:00
ch . keyAuthorization +
2019-10-04 23:35:59 +00:00
"'\n" +
"Got: '" +
keyAuth +
"'\n" +
2019-10-23 01:50:08 +00:00
'See https://git.rootprojects.org/root/acme.js/issues/4'
2019-10-04 23:35:59 +00:00
) ;
err . code = 'E_FAIL_DRY_CHALLENGE' ;
2019-10-21 23:03:26 +00:00
throw err ;
2019-10-04 23:35:59 +00:00
} ) ;
} ,
'dns-01' : function ( me , auth ) {
// remove leading *. on wildcard domains
2019-10-08 10:33:14 +00:00
var ch = auth . challenge ;
return me . dns01 ( ch ) . then ( function ( ans ) {
2019-10-04 23:35:59 +00:00
var err ;
2019-10-02 21:04:54 +00:00
2019-10-04 23:35:59 +00:00
if (
ans . answer . some ( function ( txt ) {
2019-10-08 10:33:14 +00:00
return ch . dnsAuthorization === txt . data [ 0 ] ;
2019-10-04 23:35:59 +00:00
} )
) {
return true ;
2019-10-02 21:04:54 +00:00
}
2019-10-04 23:35:59 +00:00
err = new Error (
'Error: Failed DNS-01 Pre-Flight Dry Run.\n' +
"dig TXT '" +
2019-10-08 10:33:14 +00:00
ch . dnsHost +
2019-10-04 23:35:59 +00:00
"' does not return '" +
2019-10-08 10:33:14 +00:00
ch . dnsAuthorization +
2019-10-04 23:35:59 +00:00
"'\n" +
2019-10-23 01:50:08 +00:00
'See https://git.rootprojects.org/root/acme.js/issues/4'
2019-10-04 23:35:59 +00:00
) ;
err . code = 'E_FAIL_DRY_CHALLENGE' ;
2019-10-21 23:03:26 +00:00
throw err ;
2019-10-04 23:35:59 +00:00
} ) ;
}
} ;
ACME . _directory = function ( me ) {
2019-10-21 19:45:47 +00:00
// TODO cache the directory URL
2019-10-23 01:50:08 +00:00
// GET-as-GET ok
2019-10-25 00:49:42 +00:00
return U . _request ( me , { method : 'GET' , url : me . directoryUrl , json : true } ) ;
} ;
// registerAccount
// postChallenge
// finalizeOrder
// getCertificate
ACME . _getCertificate = function ( me , options , kid ) {
//#console.debug('[ACME.js] certificates.create');
return ACME . _orderCert ( me , options , kid ) . then ( function ( order ) {
return ACME . _finalizeOrder ( me , options , kid , order ) ;
} ) ;
} ;
2019-10-26 06:03:43 +00:00
ACME . _normalizePresenters = function ( me , options , presenters ) {
2019-10-25 00:49:42 +00:00
// Prefer this order for efficiency:
// * http-01 is the fasest
// * tls-alpn-01 is for networks that don't allow plain traffic
// * dns-01 is the slowest (due to DNS propagation),
// but is required for private networks and wildcards
var presenterTypes = Object . keys ( options . challenges || { } ) ;
options . _presenterTypes = [ 'http-01' , 'tls-alpn-01' , 'dns-01' ] . filter (
function ( typ ) {
return - 1 !== presenterTypes . indexOf ( typ ) ;
}
) ;
2019-10-26 06:03:43 +00:00
if (
presenters [ 'dns-01' ] &&
'number' !== typeof presenters [ 'dns-01' ] . propagationDelay
) {
if ( ! ACME . _propagationDelayWarning ) {
var err = new Error (
"dns-01 challenge's `propagationDelay` not set, defaulting to 5000ms"
) ;
err . code = 'E_NO_DNS_DELAY' ;
err . description =
"Each dns-01 challenge should specify challenges['dns-01'].propagationDelay as an estimate of how long DNS propagation will take." ;
ACME . _notify ( me , options , 'warning' , err ) ;
presenters [ 'dns-01' ] . propagationDelay = 5000 ;
ACME . _propagationDelayWarning = true ;
}
}
2019-10-25 00:49:42 +00:00
Object . keys ( presenters || { } ) . forEach ( function ( k ) {
var ch = presenters [ k ] ;
var warned = false ;
if ( ! ch . set || ! ch . remove ) {
throw new Error ( 'challenge plugin must have set() and remove()' ) ;
}
if ( ! ch . get ) {
if ( 'dns-01' === k ) {
console . warn ( 'dns-01 challenge plugin should have get()' ) ;
} else {
throw new Error (
'http-01 and tls-alpn-01 challenge plugins must have get()'
) ;
}
}
if ( 'dns-01' === k ) {
if ( ! ch . zones ) {
console . warn ( 'dns-01 challenge plugin should have zones()' ) ;
}
}
function warn ( ) {
if ( warned ) {
return ;
}
warned = true ;
console . warn (
"'" +
k +
"' may have incorrect function signatures, or contains deprecated use of callbacks"
) ;
}
function promisify ( fn ) {
return function ( opts ) {
new Promise ( function ( resolve , reject ) {
fn ( opts , function ( err , result ) {
if ( err ) {
reject ( err ) ;
return ;
}
resolve ( result ) ;
} ) ;
} ) ;
} ;
}
// init, zones, set, get, remove
if ( ch . init && 2 === ch . init . length ) {
warn ( ) ;
ch . _thunk _init = ch . init ;
ch . init = promisify ( ch . _thunk _init ) ;
}
if ( ch . zones && 2 === ch . zones . length ) {
warn ( ) ;
ch . _thunk _zones = ch . zones ;
ch . zones = promisify ( ch . _thunk _zones ) ;
}
if ( 2 === ch . set . length ) {
warn ( ) ;
ch . _thunk _set = ch . set ;
ch . set = promisify ( ch . _thunk _set ) ;
}
if ( 2 === ch . remove . length ) {
warn ( ) ;
ch . _thunk _remove = ch . remove ;
ch . remove = promisify ( ch . _thunk _remove ) ;
}
if ( ch . get && 2 === ch . get . length ) {
warn ( ) ;
ch . _thunk _get = ch . get ;
ch . get = promisify ( ch . _thunk _get ) ;
}
return ch ;
} ) ;
2019-10-04 23:35:59 +00:00
} ;
2019-10-23 01:50:08 +00:00
2019-10-04 23:35:59 +00:00
/ *
2019-05-07 07:52:33 +00:00
POST / acme / new - order HTTP / 1.1
Host : example . com
Content - Type : application / jose + json
{
"protected" : base64url ( {
"alg" : "ES256" ,
"kid" : "https://example.com/acme/acct/1" ,
"nonce" : "5XJ1L3lEkMG7tR6pA00clA" ,
"url" : "https://example.com/acme/new-order"
} ) ,
"payload" : base64url ( {
"identifiers" : [ { "type:" dns "," value ":" example . com " } ] ,
"notBefore" : "2016-01-01T00:00:00Z" ,
"notAfter" : "2016-01-08T00:00:00Z"
} ) ,
"signature" : "H6ZXtGjTZyUnPeKn...wEA4TklBdh3e454g"
}
* /
2019-10-25 00:49:42 +00:00
ACME . _getAuthorization = function ( me , options , kid , zonenames , authUrl ) {
//#console.debug('\n[DEBUG] getAuthorization\n');
2019-10-04 23:35:59 +00:00
2019-10-23 07:44:55 +00:00
return U . _jwsRequest ( me , {
2019-10-25 00:49:42 +00:00
accountKey : options . accountKey ,
url : authUrl ,
protected : { kid : kid } ,
payload : ''
2019-10-04 23:35:59 +00:00
} ) . then ( function ( resp ) {
2019-10-23 07:44:55 +00:00
// Pre-emptive rather than lazy for interfaces that need to show the
// challenges to the user first
2019-10-25 00:49:42 +00:00
return ACME . _computeAuths (
me ,
options ,
'' ,
resp . body ,
zonenames ,
false
) . then ( function ( auths ) {
resp . body . _rawChallenges = resp . body . challenges ;
resp . body . challenges = auths ;
return resp . body ;
} ) ;
2019-10-04 23:35:59 +00:00
} ) ;
} ;
ACME . _testChallengeOptions = function ( ) {
2019-10-23 07:44:55 +00:00
// we want this to be the same for the whole group
2019-10-04 23:35:59 +00:00
var chToken = ACME . _prnd ( 16 ) ;
return [
{
type : 'http-01' ,
status : 'pending' ,
url : 'https://acme-staging-v02.example.com/0' ,
token : 'test-' + chToken + '-0'
} ,
{
type : 'dns-01' ,
status : 'pending' ,
url : 'https://acme-staging-v02.example.com/1' ,
token : 'test-' + chToken + '-1' ,
_wildcard : true
} ,
{
type : 'tls-alpn-01' ,
status : 'pending' ,
url : 'https://acme-staging-v02.example.com/3' ,
token : 'test-' + chToken + '-3'
2019-10-02 21:04:54 +00:00
}
2019-10-04 23:35:59 +00:00
] ;
} ;
2019-10-05 11:21:07 +00:00
2019-10-25 00:49:42 +00:00
ACME . _thumber = function ( options , thumb ) {
2019-10-23 07:44:55 +00:00
var thumbPromise ;
2019-10-24 17:39:25 +00:00
return function ( key ) {
2019-10-23 07:44:55 +00:00
if ( thumb ) {
return Promise . resolve ( thumb ) ;
}
if ( thumbPromise ) {
return thumbPromise ;
}
2019-10-24 17:39:25 +00:00
if ( ! key ) {
key = options . accountKey || options . accountKeypair ;
}
2019-10-25 00:49:42 +00:00
thumbPromise = U . _importKeypair ( key ) . then ( function ( pair ) {
2019-10-23 07:44:55 +00:00
return Keypairs . thumbprint ( {
2019-10-23 01:50:08 +00:00
jwk : pair . public
} ) ;
} ) ;
2019-10-05 11:21:07 +00:00
return thumbPromise ;
} ;
2019-10-23 07:44:55 +00:00
} ;
2019-10-25 00:49:42 +00:00
ACME . _dryRun = function ( me , realOptions , zonenames ) {
2019-10-23 07:44:55 +00:00
var noopts = { } ;
Object . keys ( realOptions ) . forEach ( function ( key ) {
noopts [ key ] = realOptions [ key ] ;
} ) ;
noopts . order = { } ;
// memoized so that it doesn't run until it's first called
2019-10-25 00:49:42 +00:00
var getThumbprint = ACME . _thumber ( noopts , '' ) ;
2019-10-05 11:21:07 +00:00
2019-10-04 23:35:59 +00:00
return Promise . all (
2019-10-23 07:44:55 +00:00
noopts . domains . map ( function ( identifierValue ) {
2019-10-04 23:35:59 +00:00
// TODO we really only need one to pass, not all to pass
var challenges = ACME . _testChallengeOptions ( ) ;
2019-10-23 07:44:55 +00:00
var wild = '*.' === identifierValue . slice ( 0 , 2 ) ;
if ( wild ) {
2019-10-04 23:35:59 +00:00
challenges = challenges . filter ( function ( ch ) {
return ch . _wildcard ;
2019-10-02 21:04:54 +00:00
} ) ;
2019-10-04 23:35:59 +00:00
}
2019-10-23 07:44:55 +00:00
challenges = challenges . filter ( function ( auth ) {
return me . _canCheck [ auth . type ] ;
2019-10-04 23:35:59 +00:00
} ) ;
2019-10-02 21:04:54 +00:00
2019-10-23 07:44:55 +00:00
return getThumbprint ( ) . then ( function ( accountKeyThumb ) {
var resp = {
body : {
identifier : {
type : 'dns' ,
value : identifierValue . replace ( /^\*\./ , '' )
} ,
challenges : challenges ,
expires : new Date ( Date . now ( ) + 60 * 1000 ) . toISOString ( ) ,
wildcard : identifierValue . includes ( '*.' ) || undefined
}
2019-10-04 23:35:59 +00:00
} ;
// The dry-run comes first in the spirit of "fail fast"
// (and protecting against challenge failure rate limits)
var dryrun = true ;
2019-10-23 07:44:55 +00:00
return ACME . _computeAuths (
2019-10-04 23:35:59 +00:00
me ,
2019-10-23 07:44:55 +00:00
noopts ,
2019-10-05 11:21:07 +00:00
accountKeyThumb ,
2019-10-23 07:44:55 +00:00
resp . body ,
2019-10-25 00:49:42 +00:00
zonenames ,
2019-10-04 23:35:59 +00:00
dryrun
2019-10-23 07:44:55 +00:00
) . then ( function ( auths ) {
resp . body . challenges = auths ;
return resp . body ;
2019-10-02 21:04:54 +00:00
} ) ;
} ) ;
2019-10-04 23:35:59 +00:00
} )
2019-10-23 07:44:55 +00:00
) . then ( function ( claims ) {
var selected = [ ] ;
noopts . order . _claims = claims . slice ( 0 ) ;
noopts . notify = function ( ev , params ) {
if ( 'challenge_select' === ev ) {
selected . push ( params . challenge ) ;
}
} ;
function clear ( ) {
selected . forEach ( function ( ch ) {
ACME . _notify ( me , noopts , 'challenge_remove' , {
altname : ch . altname ,
type : ch . type
//challenge: ch
} ) ;
2019-10-24 17:39:25 +00:00
noopts . challenges [ ch . type ]
. remove ( { challenge : ch } )
. catch ( function ( err ) {
err . action = 'challenge_remove' ;
err . altname = ch . altname ;
err . type = ch . type ;
ACME . _notify ( me , noopts , 'error' , err ) ;
} ) ;
2019-10-23 07:44:55 +00:00
} ) ;
2019-10-02 21:04:54 +00:00
}
2019-10-23 07:44:55 +00:00
return ACME . _setChallenges ( me , noopts , noopts . order )
. catch ( function ( err ) {
clear ( ) ;
throw err ;
} )
. then ( clear ) ;
2019-10-04 23:35:59 +00:00
} ) ;
} ;
2019-10-23 07:44:55 +00:00
// Get the list of challenge types we can validate,
// which is already ordered by preference.
// Select the first matching offered challenge type
2019-10-04 23:35:59 +00:00
ACME . _chooseChallenge = function ( options , results ) {
// For each of the challenge types that we support
var challenge ;
2019-10-23 07:44:55 +00:00
options . _presenterTypes . some ( function ( chType ) {
2019-10-04 23:35:59 +00:00
// And for each of the challenge types that are allowed
return results . challenges . some ( function ( ch ) {
// Check to see if there are any matches
if ( ch . type === chType ) {
challenge = ch ;
return true ;
}
2019-10-02 21:04:54 +00:00
} ) ;
2019-10-04 23:35:59 +00:00
} ) ;
return challenge ;
} ;
2019-10-23 07:44:55 +00:00
2019-10-25 00:49:42 +00:00
ACME . _getZones = function ( me , challenges , domains ) {
var presenter = challenges [ 'dns-01' ] ;
if ( ! presenter ) {
return Promise . resolve ( [ ] ) ;
}
if ( 'function' !== typeof presenter . zones ) {
return Promise . resolve ( [ ] ) ;
}
// a little bit of random to ensure that getZones()
// actually returns the zones and not the hosts as zones
var dnsHosts = domains . map ( function ( d ) {
var rnd = ACME . _prnd ( 2 ) ;
return rnd + '.' + d ;
} ) ;
var authChallenge = {
type : 'dns-01' ,
dnsHosts : dnsHosts
} ;
return presenter . zones ( { challenge : authChallenge } ) ;
} ;
2019-10-23 07:44:55 +00:00
ACME . _challengesMap = { 'http-01' : 0 , 'dns-01' : 0 , 'tls-alpn-01' : 0 } ;
2019-10-25 00:49:42 +00:00
ACME . _computeAuths = function ( me , options , thumb , authz , zonenames , dryrun ) {
2019-10-04 23:35:59 +00:00
// we don't poison the dns cache with our dummy request
var dnsPrefix = ACME . challengePrefixes [ 'dns-01' ] ;
if ( dryrun ) {
dnsPrefix = dnsPrefix . replace (
'acme-challenge' ,
'greenlock-dryrun-' + ACME . _prnd ( 4 )
) ;
}
2019-10-25 00:49:42 +00:00
var getThumbprint = ACME . _thumber ( options , thumb ) ;
2019-10-04 23:35:59 +00:00
2019-10-24 17:39:25 +00:00
return Promise . all (
2019-10-25 00:49:42 +00:00
authz . challenges . map ( function ( challenge ) {
2019-10-24 17:39:25 +00:00
// Don't do extra work for challenges that we can't satisfy
2019-10-25 00:49:42 +00:00
var _types = options . _presenterTypes ;
if ( _types && ! _types . includes ( challenge . type ) ) {
2019-10-24 17:39:25 +00:00
return null ;
}
2019-10-04 23:35:59 +00:00
2019-10-24 17:39:25 +00:00
var auth = { } ;
2019-10-04 23:35:59 +00:00
2019-10-24 17:39:25 +00:00
// straight copy from the new order response
// { identifier, status, expires, challenges, wildcard }
2019-10-25 00:49:42 +00:00
Object . keys ( authz ) . forEach ( function ( key ) {
auth [ key ] = authz [ key ] ;
2019-10-24 17:39:25 +00:00
} ) ;
2019-10-05 11:21:07 +00:00
2019-10-24 17:39:25 +00:00
// copy from the challenge we've chosen
// { type, status, url, token }
// (note the duplicate status overwrites the one above, but they should be the same)
Object . keys ( challenge ) . forEach ( function ( key ) {
// don't confused devs with the id url
auth [ key ] = challenge [ key ] ;
} ) ;
2019-10-23 07:44:55 +00:00
2019-10-24 17:39:25 +00:00
// batteries-included helpers
auth . hostname = auth . identifier . value ;
// because I'm not 100% clear if the wildcard identifier does or doesn't
// have the leading *. in all cases
auth . altname = ACME . _untame ( auth . identifier . value , auth . wildcard ) ;
2019-10-23 07:44:55 +00:00
2019-10-25 00:49:42 +00:00
var zone = pluckZone ( zonenames || [ ] , auth . identifier . value ) ;
2019-10-23 07:44:55 +00:00
2019-10-24 17:39:25 +00:00
return ACME . computeChallenge ( {
accountKey : options . accountKey ,
_getThumbprint : getThumbprint ,
challenge : auth ,
zone : zone ,
dnsPrefix : dnsPrefix
} ) . then ( function ( resp ) {
Object . keys ( resp ) . forEach ( function ( k ) {
auth [ k ] = resp [ k ] ;
} ) ;
return auth ;
} ) ;
} )
) . then ( function ( auths ) {
return auths . filter ( Boolean ) ;
} ) ;
} ;
2019-10-23 07:44:55 +00:00
2019-10-24 17:39:25 +00:00
ACME . computeChallenge = function ( opts ) {
var auth = opts . challenge ;
var hostname = auth . hostname || opts . hostname ;
var zone = opts . zone ;
var thumb = opts . thumbprint || '' ;
var accountKey = opts . accountKey ;
2019-10-25 00:49:42 +00:00
var getThumbprint = opts . _getThumbprint || ACME . _thumber ( opts , thumb ) ;
2019-10-24 17:39:25 +00:00
var dnsPrefix = opts . dnsPrefix || ACME . challengePrefixes [ 'dns-01' ] ;
return getThumbprint ( accountKey ) . then ( function ( thumb ) {
var resp = { } ;
resp . thumbprint = thumb ;
// keyAuthorization = token + '.' + base64url(JWK_Thumbprint(accountKey))
resp . keyAuthorization = auth . token + '.' + thumb ;
if ( 'http-01' === auth . type ) {
// conflicts with ACME challenge id url is already in use,
// so we call this challengeUrl instead
// TODO auth.http01Url ?
resp . challengeUrl =
'http://' +
// `hostname` is an alias of `auth.indentifier.value`
hostname +
ACME . challengePrefixes [ 'http-01' ] +
'/' +
auth . token ;
}
2019-10-23 07:44:55 +00:00
2019-10-24 17:39:25 +00:00
if ( 'dns-01' !== auth . type ) {
return resp ;
}
2019-10-23 07:44:55 +00:00
2019-10-24 17:39:25 +00:00
// Always calculate dnsAuthorization because we
// may need to present to the user for confirmation / instruction
// _as part of_ the decision making process
return sha2
. sum ( 256 , resp . keyAuthorization )
. then ( function ( hash ) {
return Enc . bufToUrlBase64 ( Uint8Array . from ( hash ) ) ;
2019-10-23 07:44:55 +00:00
} )
2019-10-24 17:39:25 +00:00
. then ( function ( hash64 ) {
resp . dnsHost = dnsPrefix + '.' + hostname ; // .replace('*.', '');
// deprecated
resp . dnsAuthorization = hash64 ;
// should use this instead
resp . keyAuthorizationDigest = hash64 ;
if ( zone ) {
resp . dnsZone = zone ;
resp . dnsPrefix = resp . dnsHost
. replace ( newZoneRegExp ( zone ) , '' )
. replace ( /\.$/ , '' ) ;
}
return resp ;
} ) ;
2019-10-23 07:44:55 +00:00
} ) ;
2019-10-04 23:35:59 +00:00
} ;
ACME . _untame = function ( name , wild ) {
if ( wild ) {
name = '*.' + name . replace ( '*.' , '' ) ;
}
return name ;
} ;
// https://tools.ietf.org/html/draft-ietf-acme-acme-10#section-7.5.1
2019-10-25 00:49:42 +00:00
ACME . _postChallenge = function ( me , options , kid , auth ) {
2019-10-04 23:35:59 +00:00
var RETRY _INTERVAL = me . retryInterval || 1000 ;
var DEAUTH _INTERVAL = me . deauthWait || 10 * 1000 ;
var MAX _POLL = me . retryPoll || 8 ;
var MAX _PEND = me . retryPending || 4 ;
var count = 0 ;
2019-10-23 07:44:55 +00:00
var altname = ACME . _untame ( auth . identifier . value , auth . wildcard ) ;
2019-10-02 21:04:54 +00:00
2019-10-04 23:35:59 +00:00
/ *
2019-05-07 07:52:33 +00:00
POST / acme / authz / 1234 HTTP / 1.1
Host : example . com
Content - Type : application / jose + json
{
"protected" : base64url ( {
"alg" : "ES256" ,
"kid" : "https://example.com/acme/acct/1" ,
"nonce" : "xWCM9lGbIyCgue8di6ueWQ" ,
"url" : "https://example.com/acme/authz/1234"
} ) ,
"payload" : base64url ( {
"status" : "deactivated"
} ) ,
"signature" : "srX9Ji7Le9bjszhu...WTFdtujObzMtZcx4"
}
* /
2019-10-04 23:35:59 +00:00
function deactivate ( ) {
2019-10-23 01:50:08 +00:00
//#console.debug('[ACME.js] deactivate:');
2019-10-23 07:44:55 +00:00
return U . _jwsRequest ( me , {
2019-10-25 00:49:42 +00:00
accountKey : options . accountKey ,
2019-10-23 07:44:55 +00:00
url : auth . url ,
2019-10-25 00:49:42 +00:00
protected : { kid : kid } ,
2019-10-04 23:35:59 +00:00
payload : Enc . strToBuf ( JSON . stringify ( { status : 'deactivated' } ) )
2019-10-23 07:44:55 +00:00
} ) . then ( function ( /*#resp*/ ) {
//#console.debug('deactivate challenge: resp.body:');
//#console.debug(resp.body);
2019-10-04 23:35:59 +00:00
return ACME . _wait ( DEAUTH _INTERVAL ) ;
} ) ;
}
function pollStatus ( ) {
if ( count >= MAX _POLL ) {
2019-10-23 07:44:55 +00:00
var err = new Error (
"[ACME.js] stuck in bad pending/processing state for '" +
altname +
"'"
2019-10-04 23:35:59 +00:00
) ;
2019-10-23 07:44:55 +00:00
err . context = 'present_challenge' ;
return Promise . reject ( err ) ;
2019-10-04 23:35:59 +00:00
}
2019-10-02 21:04:54 +00:00
2019-10-04 23:35:59 +00:00
count += 1 ;
2019-10-02 21:04:54 +00:00
2019-10-23 01:50:08 +00:00
//#console.debug('\n[DEBUG] statusChallenge\n');
// POST-as-GET
2019-10-23 07:44:55 +00:00
return U . _jwsRequest ( me , {
2019-10-25 00:49:42 +00:00
accountKey : options . accountKey ,
2019-10-23 07:44:55 +00:00
url : auth . url ,
2019-10-25 00:49:42 +00:00
protected : { kid : kid } ,
2019-10-23 01:50:08 +00:00
payload : Enc . binToBuf ( '' )
2019-10-23 07:44:55 +00:00
} )
. then ( checkResult )
. catch ( transformError ) ;
}
function checkResult ( resp ) {
ACME . _notify ( me , options , 'challenge_status' , {
// API-locked
status : resp . body . status ,
type : auth . type ,
altname : altname
} ) ;
if ( 'processing' === resp . body . status ) {
//#console.debug('poll: again', auth.url);
return ACME . _wait ( RETRY _INTERVAL ) . then ( pollStatus ) ;
}
// This state should never occur
if ( 'pending' === resp . body . status ) {
if ( count >= MAX _PEND ) {
return ACME . _wait ( RETRY _INTERVAL )
. then ( deactivate )
. then ( respondToChallenge ) ;
2019-10-23 01:50:08 +00:00
}
2019-10-23 07:44:55 +00:00
//#console.debug('poll: again', auth.url);
return ACME . _wait ( RETRY _INTERVAL ) . then ( respondToChallenge ) ;
}
2019-10-02 21:04:54 +00:00
2019-10-23 07:44:55 +00:00
// REMOVE DNS records as soon as the state is non-processing
// (valid or invalid or other)
try {
options . challenges [ auth . type ] . remove ( { challenge : auth } ) ;
} catch ( e ) { }
if ( 'valid' === resp . body . status ) {
if ( me . debug ) {
console . debug ( 'poll: valid' ) ;
2019-10-23 01:50:08 +00:00
}
2019-10-02 21:04:54 +00:00
2019-10-23 07:44:55 +00:00
return resp . body ;
}
2019-10-02 21:04:54 +00:00
2019-10-23 07:44:55 +00:00
var errmsg ;
if ( ! resp . body . status ) {
errmsg =
"[ACME.js] (E_STATE_EMPTY) empty challenge state for '" +
altname +
"':" +
JSON . stringify ( resp . body ) ;
} else if ( 'invalid' === resp . body . status ) {
errmsg =
"[ACME.js] (E_STATE_INVALID) challenge state for '" +
altname +
"': '" +
//resp.body.status +
JSON . stringify ( resp . body ) +
"'" ;
} else {
errmsg =
"[ACME.js] (E_STATE_UKN) challenge state for '" +
altname +
"': '" +
resp . body . status +
"'" ;
}
2019-10-02 21:04:54 +00:00
2019-10-23 07:44:55 +00:00
return Promise . reject ( new Error ( errmsg ) ) ;
}
2019-10-04 23:35:59 +00:00
2019-10-23 07:44:55 +00:00
function transformError ( e ) {
var err = e ;
if ( err . urn ) {
err = new Error (
'[acme-v2] ' +
auth . altname +
' status:' +
e . status +
' ' +
e . detail
) ;
err . auth = auth ;
err . altname = auth . altname ;
err . type = auth . type ;
err . code =
'invalid' === e . status ? 'E_ACME_CHALLENGE' : 'E_ACME_UNKNOWN' ;
}
2019-10-23 01:50:08 +00:00
2019-10-23 07:44:55 +00:00
throw err ;
2019-10-04 23:35:59 +00:00
}
function respondToChallenge ( ) {
2019-10-23 01:50:08 +00:00
//#console.debug('[ACME.js] responding to accept challenge:');
2019-10-23 07:44:55 +00:00
// POST-as-POST (empty JSON object)
return U . _jwsRequest ( me , {
2019-10-25 00:49:42 +00:00
accountKey : options . accountKey ,
2019-10-23 07:44:55 +00:00
url : auth . url ,
2019-10-25 00:49:42 +00:00
protected : { kid : kid } ,
2019-10-04 23:35:59 +00:00
payload : Enc . strToBuf ( JSON . stringify ( { } ) )
2019-10-23 07:44:55 +00:00
} )
. then ( checkResult )
. catch ( transformError ) ;
2019-10-04 23:35:59 +00:00
}
2019-10-02 21:04:54 +00:00
2019-10-04 23:35:59 +00:00
return respondToChallenge ( ) ;
} ;
2019-10-23 07:44:55 +00:00
// options = { domains, claims, challenges }
ACME . _setChallenges = function ( me , options , order ) {
var claims = order . _claims . slice ( 0 ) ;
var valids = [ ] ;
var auths = [ ] ;
2019-10-24 17:39:25 +00:00
var placed = [ ] ;
2019-10-23 07:44:55 +00:00
var USE _DNS = false ;
var DNS _DELAY = 0 ;
// Set any challenges, excpting ones that have already been validated
function setNext ( ) {
var claim = claims . shift ( ) ;
// check false for testing
if ( ! claim || false === options . challenges ) {
return Promise . resolve ( ) ;
2019-10-05 11:21:07 +00:00
}
2019-10-23 07:44:55 +00:00
return Promise . resolve ( )
. then ( function ( ) {
// For any challenges that are already valid,
// add to the list and skip any checks.
if (
claim . challenges . some ( function ( ch ) {
if ( 'valid' === ch . status ) {
valids . push ( ch ) ;
return true ;
}
} )
) {
return ;
}
var selected = ACME . _chooseChallenge ( options , claim ) ;
if ( ! selected ) {
throw E . NO _SUITABLE _CHALLENGE (
claim . altname ,
claim . challenges ,
options . _presenterTypes
) ;
}
auths . push ( selected ) ;
2019-10-24 17:39:25 +00:00
placed . push ( selected ) ;
2019-10-23 07:44:55 +00:00
ACME . _notify ( me , options , 'challenge_select' , {
// API-locked
altname : ACME . _untame (
claim . identifier . value ,
claim . wildcard
) ,
type : selected . type ,
challenge : selected
2019-10-04 23:35:59 +00:00
} ) ;
2019-10-23 07:44:55 +00:00
// Set a delay for nameservers a moment to propagate
if ( 'dns-01' === selected . type ) {
if ( options . challenges [ 'dns-01' ] && ! USE _DNS ) {
USE _DNS = true ;
DNS _DELAY = parseInt (
options . challenges [ 'dns-01' ] . propagationDelay ,
10
) ;
}
}
var ch = options . challenges [ selected . type ] || { } ;
if ( ! ch . set ) {
throw new Error ( 'no handler for setting challenge' ) ;
}
return ch . set ( { challenge : selected } ) ;
} )
. then ( setNext ) ;
}
function waitAll ( ) {
//#console.debug('\n[DEBUG] waitChallengeDelay %s\n', DELAY);
if ( ! DNS _DELAY || DNS _DELAY <= 0 ) {
DNS _DELAY = 5000 ;
}
return ACME . _wait ( DNS _DELAY ) ;
}
function checkNext ( ) {
var auth = auths . shift ( ) ;
if ( ! auth ) {
return Promise . resolve ( valids ) ;
}
// These are not as much "valids" as they are "not invalids"
if ( ! me . _canCheck [ auth . type ] || me . skipChallengeTest ) {
valids . push ( auth ) ;
return checkNext ( ) ;
}
return ACME . challengeTests [ auth . type ] ( me , { challenge : auth } )
. then ( function ( ) {
valids . push ( auth ) ;
} )
. then ( checkNext ) ;
}
2019-10-25 00:49:42 +00:00
function removeAll ( ch ) {
options . challenges [ ch . type ]
. remove ( { challenge : ch } )
. catch ( function ( err ) {
err . action = 'challenge_remove' ;
err . altname = ch . altname ;
err . type = ch . type ;
ACME . _notify ( me , options , 'error' , err ) ;
} ) ;
}
2019-10-23 07:44:55 +00:00
// The reason we set every challenge in a batch first before checking any
// is so that we don't poison our own DNS cache with misses.
return setNext ( )
. then ( waitAll )
2019-10-24 17:39:25 +00:00
. then ( checkNext )
. catch ( function ( err ) {
if ( ! options . debug ) {
2019-10-25 00:49:42 +00:00
placed . forEach ( removeAll ) ;
2019-10-24 17:39:25 +00:00
}
throw err ;
} ) ;
2019-10-23 07:44:55 +00:00
} ;
2019-10-25 00:49:42 +00:00
ACME . _presentChallenges = function ( me , options , kid , readyToPresent ) {
// Actually sets the challenge via ACME
function challengeNext ( ) {
// First set, First presented
var auth = readyToPresent . shift ( ) ;
if ( ! auth ) {
return Promise . resolve ( ) ;
2019-10-23 07:44:55 +00:00
}
2019-10-25 00:49:42 +00:00
return ACME . _postChallenge ( me , options , kid , auth ) . then ( challengeNext ) ;
}
2019-10-23 07:44:55 +00:00
2019-10-25 00:49:42 +00:00
// BTW, these are done serially rather than parallel on purpose
// (rate limits, propagation delays, etc)
return challengeNext ( ) . then ( function ( ) {
return readyToPresent ;
} ) ;
} ;
2019-10-23 07:44:55 +00:00
2019-10-25 00:49:42 +00:00
ACME . _pollOrderStatus = function ( me , options , kid , order , verifieds ) {
var csr64 = ACME . _csrToUrlBase64 ( options . csr ) ;
2019-10-23 07:44:55 +00:00
var body = { csr : csr64 } ;
var payload = JSON . stringify ( body ) ;
2019-10-02 21:04:54 +00:00
2019-10-23 07:44:55 +00:00
function pollCert ( ) {
//#console.debug('[ACME.js] pollCert:', order._finalizeUrl);
return U . _jwsRequest ( me , {
2019-10-25 00:49:42 +00:00
accountKey : options . accountKey ,
2019-10-23 07:44:55 +00:00
url : order . _finalizeUrl ,
2019-10-25 00:49:42 +00:00
protected : { kid : kid } ,
2019-10-23 07:44:55 +00:00
payload : Enc . strToBuf ( payload )
} ) . then ( function ( resp ) {
ACME . _notify ( me , options , 'certificate_status' , {
subject : options . domains [ 0 ] ,
status : resp . body . status
} ) ;
2019-10-02 21:04:54 +00:00
2019-10-23 07:44:55 +00:00
// https://tools.ietf.org/html/draft-ietf-acme-acme-12#section-7.1.3
// Possible values are: "pending" => ("invalid" || "ready") => "processing" => "valid"
if ( 'valid' === resp . body . status ) {
var voucher = resp . body ;
voucher . _certificateUrl = resp . body . certificate ;
2019-10-02 21:04:54 +00:00
2019-10-23 07:44:55 +00:00
return voucher ;
}
2019-10-02 21:04:54 +00:00
2019-10-23 07:44:55 +00:00
if ( 'processing' === resp . body . status ) {
return ACME . _wait ( ) . then ( pollCert ) ;
}
2019-10-04 23:35:59 +00:00
2019-10-23 07:44:55 +00:00
if ( me . debug ) {
console . debug (
'Error: bad status:\n' + JSON . stringify ( resp . body , null , 2 )
) ;
}
2019-10-04 23:35:59 +00:00
2019-10-23 07:44:55 +00:00
if ( 'pending' === resp . body . status ) {
2019-10-02 21:04:54 +00:00
return Promise . reject (
new Error (
2019-10-23 07:44:55 +00:00
"Did not finalize order: status 'pending'." +
' Best guess: You have not accepted at least one challenge for each domain:\n' +
2019-10-04 23:35:59 +00:00
"Requested: '" +
options . domains . join ( ', ' ) +
"'\n" +
"Validated: '" +
2019-10-23 07:44:55 +00:00
verifieds . join ( ', ' ) +
2019-10-04 23:35:59 +00:00
"'\n" +
2019-10-23 07:44:55 +00:00
JSON . stringify ( resp . body , null , 2 )
2019-10-02 21:04:54 +00:00
)
) ;
2019-10-23 07:44:55 +00:00
}
if ( 'invalid' === resp . body . status ) {
return Promise . reject (
E . ORDER _INVALID ( options , verifieds , resp )
) ;
}
2019-10-04 23:35:59 +00:00
2019-10-23 07:44:55 +00:00
if ( 'ready' === resp . body . status ) {
return Promise . reject (
E . DOUBLE _READY _ORDER ( options , verifieds , resp )
) ;
}
return Promise . reject (
E . UNHANDLED _ORDER _STATUS ( options , verifieds , resp )
) ;
} ) ;
}
return pollCert ( ) ;
} ;
2019-10-25 00:49:42 +00:00
ACME . _redeemCert = function ( me , options , kid , voucher ) {
2019-10-23 07:44:55 +00:00
//#console.debug('ACME.js: order was finalized');
// POST-as-GET
return U . _jwsRequest ( me , {
2019-10-25 00:49:42 +00:00
accountKey : options . accountKey ,
2019-10-23 07:44:55 +00:00
url : voucher . _certificateUrl ,
2019-10-25 00:49:42 +00:00
protected : { kid : kid } ,
2019-10-23 07:44:55 +00:00
payload : Enc . binToBuf ( '' ) ,
json : true
} ) . then ( function ( resp ) {
//#console.debug('ACME.js: csr submitted and cert received:');
// https://github.com/certbot/certbot/issues/5721
var certsarr = ACME . splitPemChain ( ACME . formatPemChain ( resp . body || '' ) ) ;
// cert, chain, fullchain, privkey, /*TODO, subject, altnames, issuedAt, expiresAt */
var certs = {
expires : voucher . expires ,
identifiers : voucher . identifiers ,
//, authorizations: order.authorizations
cert : certsarr . shift ( ) ,
//, privkey: privkeyPem
chain : certsarr . join ( '\n' )
} ;
//#console.debug(certs);
return certs ;
2019-10-04 23:35:59 +00:00
} ) ;
} ;
2019-10-25 00:49:42 +00:00
ACME . _finalizeOrder = function ( me , options , kid , order ) {
2019-10-23 07:44:55 +00:00
//#console.debug('[ACME.js] finalizeOrder:');
var readyToPresent ;
2019-10-25 00:49:42 +00:00
return A . _getAccountKid ( me , options ) . then ( function ( kid ) {
return ACME . _setChallenges ( me , options , order )
. then ( function ( _readyToPresent ) {
readyToPresent = _readyToPresent ;
return ACME . _presentChallenges (
me ,
options ,
kid ,
readyToPresent
) ;
} )
. then ( function ( ) {
return ACME . _pollOrderStatus (
me ,
options ,
kid ,
order ,
readyToPresent . map ( function ( ch ) {
return ACME . _untame ( ch . identifier . value , ch . wildcard ) ;
} )
) ;
} )
. then ( function ( voucher ) {
return ACME . _redeemCert ( me , options , kid , voucher ) ;
} ) ;
} ) ;
2019-10-23 07:44:55 +00:00
} ;
// Order a certificate request with all domains
2019-10-25 00:49:42 +00:00
ACME . _orderCert = function ( me , options , kid ) {
2019-10-23 07:44:55 +00:00
var certificateRequest = {
// raw wildcard syntax MUST be used here
identifiers : options . domains . map ( function ( hostname ) {
return { type : 'dns' , value : hostname } ;
} )
//, "notBefore": "2016-01-01T00:00:00Z"
//, "notAfter": "2016-01-08T00:00:00Z"
} ;
2019-10-21 23:03:26 +00:00
2019-10-23 07:44:55 +00:00
return ACME . _prepRequest ( me , options )
. then ( function ( ) {
2019-10-25 00:49:42 +00:00
return ACME . _getZones ( me , options . challenges , options . domains ) ;
2019-10-23 07:44:55 +00:00
} )
2019-10-25 00:49:42 +00:00
. then ( function ( zonenames ) {
var p ;
// Do a little dry-run / self-test
if ( ! me . skipDryRun && ! options . skipDryRun ) {
p = ACME . _dryRun ( me , options , zonenames ) ;
} else {
p = Promise . resolve ( null ) ;
2019-10-23 07:44:55 +00:00
}
2019-10-25 00:49:42 +00:00
return p . then ( function ( ) {
return A . _getAccountKid ( me , options )
. then ( function ( kid ) {
ACME . _notify ( me , options , 'certificate_order' , {
// API-locked
account : { key : { kid : kid } } ,
subject : options . domains [ 0 ] ,
altnames : options . domains ,
challengeTypes : options . _presenterTypes
} ) ;
var payload = JSON . stringify ( certificateRequest ) ;
//#console.debug('\n[DEBUG] newOrder\n');
return U . _jwsRequest ( me , {
accountKey : options . accountKey ,
url : me . _directoryUrls . newOrder ,
protected : { kid : kid } ,
payload : Enc . binToBuf ( payload )
} ) ;
} )
. then ( function ( resp ) {
var order = resp . body ;
order . _orderUrl = resp . headers . location ;
order . _finalizeUrl = resp . body . finalize ;
order . _identifiers = certificateRequest . identifiers ;
//#console.debug('[ordered]', location); // the account id url
//#console.debug(resp);
if ( ! order . authorizations ) {
return Promise . reject (
E . NO _AUTHORIZATIONS ( options , resp )
) ;
}
return order ;
} )
. then ( function ( order ) {
return ACME . _getAllChallenges (
me ,
options ,
kid ,
zonenames ,
order
) . then ( function ( claims ) {
order . _claims = claims ;
return order ;
} ) ;
} ) ;
2019-10-23 07:44:55 +00:00
} ) ;
} ) ;
} ;
ACME . _prepRequest = function ( me , options ) {
2019-10-25 00:49:42 +00:00
return Promise . resolve ( ) . then ( function ( ) {
// TODO check that all presenterTypes are represented in challenges
if ( ! options . _presenterTypes . length ) {
return Promise . reject (
new Error ( 'options.challenges must be specified' )
) ;
}
2019-10-08 10:33:14 +00:00
2019-10-25 00:49:42 +00:00
if ( ! options . csr ) {
throw new Error (
'no `csr` option given (should be in DER or PEM format)'
) ;
}
// TODO validate csr signature?
var _csr = CSR . _info ( options . csr ) ;
options . domains = options . domains || _csr . altnames ;
_csr . altnames = _csr . altnames || [ ] ;
if (
options . domains
. slice ( 0 )
. sort ( )
. join ( ' ' ) !==
_csr . altnames
. slice ( 0 )
. sort ( )
. join ( ' ' )
) {
return Promise . reject (
new Error ( 'certificate altnames do not match requested domains' )
) ;
}
if ( _csr . subject !== options . domains [ 0 ] ) {
return Promise . reject (
new Error (
'certificate subject (commonName) does not match first altname (SAN)'
)
) ;
}
if ( ! ( options . domains && options . domains . length ) ) {
return Promise . reject (
new Error (
'options.domains must be a list of string domain names,' +
' with the first being the subject of the certificate'
)
) ;
2019-10-02 21:04:54 +00:00
}
2019-10-25 00:49:42 +00:00
// a cheap check to see if there are non-ascii characters in any of the domains
var nonAsciiDomains = options . domains . some ( function ( d ) {
// IDN / unicode / utf-8 / punycode
return Enc . strToBin ( d ) !== d ;
2019-10-05 11:21:07 +00:00
} ) ;
2019-10-25 00:49:42 +00:00
if ( nonAsciiDomains ) {
throw new Error (
"please use the 'punycode' module to convert unicode domain names to punycode"
) ;
2019-10-23 07:44:55 +00:00
}
2019-10-25 00:49:42 +00:00
// TODO Promise.all()?
( options . _presenterTypes || [ ] ) . forEach ( function ( key ) {
var presenter = options . challenges [ key ] ;
if (
'function' === typeof presenter . init &&
! presenter . _acme _initialized
) {
presenter . _acme _initialized = true ;
2019-10-25 10:54:54 +00:00
return presenter . init ( { type : '*' , request : me . request } ) ;
2019-10-25 00:49:42 +00:00
}
} ) ;
2019-10-23 07:44:55 +00:00
} ) ;
} ;
2019-10-05 11:21:07 +00:00
2019-10-23 07:44:55 +00:00
// Request a challenge for each authorization in the order
2019-10-25 00:49:42 +00:00
ACME . _getAllChallenges = function ( me , options , kid , zonenames , order ) {
2019-10-23 07:44:55 +00:00
var claims = [ ] ;
//#console.debug("[acme-v2] POST newOrder has authorizations");
var challengeAuths = order . authorizations . slice ( 0 ) ;
2019-10-02 21:04:54 +00:00
2019-10-23 07:44:55 +00:00
function getNext ( ) {
var authUrl = challengeAuths . shift ( ) ;
if ( ! authUrl ) {
return claims ;
}
2019-10-04 23:35:59 +00:00
2019-10-25 00:49:42 +00:00
return ACME . _getAuthorization (
me ,
options ,
kid ,
zonenames ,
authUrl
) . then ( function ( claim ) {
2019-10-23 07:44:55 +00:00
// var domain = options.domains[i]; // claim.identifier.value
claims . push ( claim ) ;
return getNext ( ) ;
} ) ;
}
2019-10-05 11:21:07 +00:00
2019-10-23 07:44:55 +00:00
return getNext ( ) . then ( function ( ) {
return claims ;
} ) ;
} ;
2019-10-05 11:21:07 +00:00
2019-10-25 00:49:42 +00:00
ACME . formatPemChain = function formatPemChain ( str ) {
return (
str
. trim ( )
. replace ( /[\r\n]+/g , '\n' )
. replace ( /\-\n\-/g , '-\n\n-' ) + '\n'
) ;
2019-10-04 23:35:59 +00:00
} ;
2019-10-05 11:21:07 +00:00
2019-10-25 00:49:42 +00:00
ACME . splitPemChain = function splitPemChain ( str ) {
return str
. trim ( )
. split ( /[\r\n]{2,}/g )
. map ( function ( str ) {
return str + '\n' ;
} ) ;
} ;
ACME . _csrToUrlBase64 = function ( csr ) {
2019-10-23 07:44:55 +00:00
// if der, convert to base64
if ( 'string' !== typeof csr ) {
csr = Enc . bufToUrlBase64 ( csr ) ;
}
2019-10-25 00:49:42 +00:00
// TODO use PEM.parseBlock()
2019-10-23 07:44:55 +00:00
// nix PEM headers, if any
if ( '-' === csr [ 0 ] ) {
csr = csr
. split ( /\n+/ )
. slice ( 1 , - 1 )
. join ( '' ) ;
2019-10-04 23:35:59 +00:00
}
2019-10-23 07:44:55 +00:00
return Enc . base64ToUrlBase64 ( csr . trim ( ) . replace ( /\s+/g , '' ) ) ;
2019-10-04 23:35:59 +00:00
} ;
// In v8 this is crypto random, but we're just using it for pseudorandom
ACME . _prnd = function ( n ) {
var rnd = '' ;
while ( rnd . length / 2 < n ) {
2019-10-25 00:49:42 +00:00
var i = Math . random ( )
2019-10-04 23:35:59 +00:00
. toString ( )
. substr ( 2 ) ;
2019-10-25 00:49:42 +00:00
var h = parseInt ( i , 10 ) . toString ( 16 ) ;
if ( h . length % 2 ) {
h = '0' + h ;
2019-10-02 21:04:54 +00:00
}
2019-10-25 00:49:42 +00:00
rnd += h ;
2019-10-04 23:35:59 +00:00
}
return rnd . substr ( 0 , n * 2 ) ;
} ;
2019-10-05 11:21:07 +00:00
2019-10-23 07:44:55 +00:00
ACME . _notify = function ( me , options , ev , params ) {
if ( ! options . notify && ! me . notify ) {
2019-10-25 10:57:09 +00:00
//console.info(ev, params);
2019-10-23 07:44:55 +00:00
return ;
}
try {
( options . notify || me . notify ) ( ev , params ) ;
} catch ( e ) {
console . error ( '`acme.notify(ev, params)` Error:' ) ;
console . error ( e ) ;
}
} ;
2019-10-25 00:49:42 +00:00
ACME . _wait = function wait ( ms ) {
return new Promise ( function ( resolve ) {
setTimeout ( resolve , ms || 1100 ) ;
2019-10-05 11:21:07 +00:00
} ) ;
2019-10-04 23:35:59 +00:00
} ;
2019-10-05 11:21:07 +00:00
function newZoneRegExp ( zonename ) {
// (^|\.)example\.com$
// which matches:
// foo.example.com
// example.com
// but not:
// fooexample.com
return new RegExp ( '(^|\\.)' + zonename . replace ( /\./g , '\\.' ) + '$' ) ;
}
function pluckZone ( zonenames , dnsHost ) {
return zonenames
. filter ( function ( zonename ) {
// the only character that needs to be escaped for regex
// and is allowed in a domain name is '.'
return newZoneRegExp ( zonename ) . test ( dnsHost ) ;
} )
. sort ( function ( a , b ) {
// longest match first
return b . length - a . length ;
} ) [ 0 ] ;
}