2015-12-08 15:51:03 -08:00
/* jslint node:true */
'use strict' ;
var assert = require ( 'assert' ) ,
async = require ( 'async' ) ,
2015-12-08 19:15:17 -08:00
config = require ( './config.js' ) ,
2015-12-08 15:51:03 -08:00
crypto = require ( 'crypto' ) ,
debug = require ( 'debug' ) ( 'acme' ) ,
fs = require ( 'fs' ) ,
path = require ( 'path' ) ,
2015-12-08 19:04:48 -08:00
paths = require ( './paths.js' ) ,
2015-12-09 19:22:53 -08:00
safe = require ( 'safetydance' ) ,
2015-12-08 15:51:03 -08:00
superagent = require ( 'superagent' ) ,
ursa = require ( 'ursa' ) ,
util = require ( 'util' ) ,
_ = require ( 'underscore' ) ;
2015-12-09 18:34:27 -08:00
var CA _PROD = 'https://acme-v01.api.letsencrypt.org' ,
2015-12-08 18:23:14 -08:00
CA _STAGING = 'https://acme-staging.api.letsencrypt.org/' ,
2015-12-08 15:51:03 -08:00
LE _AGREEMENT = 'https://letsencrypt.org/documents/LE-SA-v1.0.1-July-27-2015.pdf' ;
exports = module . exports = {
getCertificate : getCertificate
} ;
function AcmeError ( reason , errorOrMessage ) {
assert . strictEqual ( typeof reason , 'string' ) ;
assert ( errorOrMessage instanceof Error || typeof errorOrMessage === 'string' || typeof errorOrMessage === 'undefined' ) ;
Error . call ( this ) ;
Error . captureStackTrace ( this , this . constructor ) ;
this . name = this . constructor . name ;
this . reason = reason ;
if ( typeof errorOrMessage === 'undefined' ) {
this . message = reason ;
} else if ( typeof errorOrMessage === 'string' ) {
this . message = errorOrMessage ;
} else {
this . message = 'Internal error' ;
this . nestedError = errorOrMessage ;
}
}
util . inherits ( AcmeError , Error ) ;
AcmeError . INTERNAL _ERROR = 'Internal Error' ;
AcmeError . EXTERNAL _ERROR = 'External Error' ;
AcmeError . ALREADY _EXISTS = 'Already Exists' ;
AcmeError . NOT _COMPLETED = 'Not Completed' ;
AcmeError . FORBIDDEN = 'Forbidden' ;
// 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 getNonce ( callback ) {
2015-12-08 18:23:14 -08:00
superagent . get ( CA _STAGING + '/directory' , function ( error , response ) {
2015-12-08 15:51:03 -08:00
if ( error ) return callback ( error ) ;
if ( response . statusCode !== 200 ) return callback ( new Error ( 'Invalid response code when fetching nonce : ' + response . statusCode ) ) ;
return callback ( null , response . headers [ 'Replay-Nonce' . toLowerCase ( ) ] ) ;
} ) ;
}
// urlsafe base64 encoding (jose)
2015-12-09 18:34:27 -08:00
function urlBase64Encode ( string ) {
return string . replace ( /\+/g , '-' ) . replace ( /\//g , '_' ) . replace ( /=/g , '' ) ;
}
2015-12-08 15:51:03 -08:00
function b64 ( str ) {
var buf = util . isBuffer ( str ) ? str : new Buffer ( str ) ;
return urlBase64Encode ( buf . toString ( 'base64' ) ) ;
}
2015-12-09 19:23:19 -08:00
function sendSignedRequest ( url , accountKeyPem , payload , callback ) {
2015-12-08 15:51:03 -08:00
assert . strictEqual ( typeof url , 'string' ) ;
2015-12-09 19:23:19 -08:00
assert ( util . isBuffer ( accountKeyPem ) ) ;
2015-12-08 15:51:03 -08:00
assert . strictEqual ( typeof payload , 'string' ) ;
assert . strictEqual ( typeof callback , 'function' ) ;
2015-12-09 19:23:19 -08:00
var privateKey = ursa . createPrivateKey ( accountKeyPem ) ;
2015-12-08 15:51:03 -08:00
var header = {
alg : 'RS256' ,
jwk : {
e : b64 ( privateKey . getExponent ( ) ) ,
kty : 'RSA' ,
n : b64 ( privateKey . getModulus ( ) )
}
} ;
var payload64 = b64 ( payload ) ;
getNonce ( function ( error , nonce ) {
if ( error ) return callback ( error ) ;
debug ( 'Using nonce %s' , nonce ) ;
var protected64 = b64 ( JSON . stringify ( _ . extend ( { } , header , { nonce : nonce } ) ) ) ;
var signer = ursa . createSigner ( 'sha256' ) ;
signer . update ( protected64 + '.' + payload64 , 'utf8' ) ;
var signature64 = urlBase64Encode ( signer . sign ( privateKey , 'base64' ) ) ;
var data = {
header : header ,
protected : protected64 ,
payload : payload64 ,
signature : signature64
} ;
2015-12-10 09:56:46 -08:00
superagent . post ( url ) . set ( 'Content-Type' , 'application/x-www-form-urlencoded' ) . send ( JSON . stringify ( data ) ) . end ( function ( error , res ) {
2015-12-08 15:51:03 -08:00
if ( error && ! error . response ) return callback ( error ) ; // network errors
callback ( null , res ) ;
} ) ;
} ) ;
}
2015-12-09 19:23:19 -08:00
function registerUser ( accountKeyPem , email , callback ) {
assert ( util . isBuffer ( accountKeyPem ) ) ;
2015-12-08 15:51:03 -08:00
assert . strictEqual ( typeof email , 'string' ) ;
assert . strictEqual ( typeof callback , 'function' ) ;
var payload = {
resource : 'new-reg' ,
contact : [ 'mailto:' + email ] ,
agreement : LE _AGREEMENT
} ;
debug ( 'registerUser: %s' , email ) ;
2015-12-09 19:23:19 -08:00
sendSignedRequest ( CA _STAGING + '/acme/new-reg' , accountKeyPem , JSON . stringify ( payload ) , function ( error , result ) {
2015-12-08 15:51:03 -08:00
if ( error ) return callback ( new AcmeError ( AcmeError . EXTERNAL _ERROR , 'Network error when registering user: ' + error . message ) ) ;
if ( result . statusCode === 409 ) return callback ( new AcmeError ( AcmeError . ALREADY _EXISTS , result . body . detail ) ) ;
if ( result . statusCode !== 201 ) return callback ( new AcmeError ( AcmeError . EXTERNAL _ERROR , util . format ( 'Failed to register user. Expecting 201, got %s %s' , result . statusCode , result . text ) ) ) ;
debug ( 'registerUser: registered user %s' , email ) ;
callback ( ) ;
} ) ;
}
2015-12-09 19:23:19 -08:00
function registerDomain ( accountKeyPem , domain , callback ) {
assert ( util . isBuffer ( accountKeyPem ) ) ;
2015-12-08 15:51:03 -08:00
assert . strictEqual ( typeof domain , 'string' ) ;
assert . strictEqual ( typeof callback , 'function' ) ;
var payload = {
resource : 'new-authz' ,
identifier : {
type : 'dns' ,
value : domain
}
} ;
debug ( 'registerDomain: %s' , domain ) ;
2015-12-09 19:23:19 -08:00
sendSignedRequest ( CA _STAGING + '/acme/new-authz' , accountKeyPem , JSON . stringify ( payload ) , function ( error , result ) {
2015-12-08 15:51:03 -08:00
if ( error ) return callback ( new AcmeError ( AcmeError . EXTERNAL _ERROR , 'Network error when registering domain: ' + error . message ) ) ;
if ( result . statusCode === 403 ) return callback ( new AcmeError ( AcmeError . FORBIDDEN , result . body . detail ) ) ;
if ( result . statusCode !== 201 ) return callback ( new AcmeError ( AcmeError . EXTERNAL _ERROR , util . format ( 'Failed to register user. Expecting 201, got %s %s' , result . statusCode , result . text ) ) ) ;
debug ( 'registerDomain: registered %s' , domain ) ;
callback ( null , result . body ) ;
} ) ;
}
2015-12-09 19:23:19 -08:00
function prepareHttpChallenge ( accountKeyPem , challenge , callback ) {
assert ( util . isBuffer ( accountKeyPem ) ) ;
2015-12-08 15:51:03 -08:00
assert . strictEqual ( typeof challenge , 'object' ) ;
assert . strictEqual ( typeof callback , 'function' ) ;
debug ( 'prepareHttpChallenge: preparing for challenge %j' , challenge ) ;
var token = challenge . token ;
2015-12-09 19:23:19 -08:00
var privateKey = ursa . createPrivateKey ( accountKeyPem ) ;
2015-12-08 15:51:03 -08:00
var jwk = {
e : b64 ( privateKey . getExponent ( ) ) ,
kty : 'RSA' ,
n : b64 ( privateKey . getModulus ( ) )
} ;
var shasum = crypto . createHash ( 'sha256' ) ;
shasum . update ( JSON . stringify ( jwk ) ) ;
var thumbprint = urlBase64Encode ( shasum . digest ( 'base64' ) ) ;
var keyAuthorization = token + '.' + thumbprint ;
2015-12-08 19:04:48 -08:00
debug ( 'prepareHttpChallenge: writing %s to %s' , keyAuthorization , path . join ( paths . ACME _CHALLENGES _DIR , token ) ) ;
2015-12-08 15:51:03 -08:00
2015-12-08 19:04:48 -08:00
fs . writeFile ( path . join ( paths . ACME _CHALLENGES _DIR , token ) , token + '.' + thumbprint , function ( error ) {
2015-12-08 15:51:03 -08:00
if ( error ) return callback ( new AcmeError ( AcmeError . INTERNAL _ERROR , error ) ) ;
callback ( ) ;
} ) ;
}
2015-12-09 19:23:19 -08:00
function notifyChallengeReady ( accountKeyPem , challenge , callback ) {
assert ( util . isBuffer ( accountKeyPem ) ) ;
2015-12-08 15:51:03 -08:00
assert . strictEqual ( typeof challenge , 'object' ) ;
assert . strictEqual ( typeof callback , 'function' ) ;
2015-12-08 19:54:37 -08:00
debug ( 'notifyChallengeReady: %s was met' , challenge . uri ) ;
2015-12-08 15:51:03 -08:00
2015-12-08 19:04:48 -08:00
var keyAuthorization = fs . readFileSync ( path . join ( paths . ACME _CHALLENGES _DIR , challenge . token ) , 'utf8' ) ;
2015-12-08 15:51:03 -08:00
var payload = {
resource : 'challenge' ,
keyAuthorization : keyAuthorization
} ;
2015-12-09 19:23:19 -08:00
sendSignedRequest ( challenge . uri , accountKeyPem , JSON . stringify ( payload ) , function ( error , result ) {
2015-12-08 15:51:03 -08:00
if ( error ) return callback ( new AcmeError ( AcmeError . EXTERNAL _ERROR , 'Network error when notifying challenge: ' + error . message ) ) ;
if ( result . statusCode !== 202 ) return callback ( new AcmeError ( AcmeError . EXTERNAL _ERROR , util . format ( 'Failed to notify challenge. Expecting 202, got %s %s' , result . statusCode , result . text ) ) ) ;
callback ( ) ;
} ) ;
}
function waitForChallenge ( challenge , callback ) {
assert . strictEqual ( typeof challenge , 'object' ) ;
assert . strictEqual ( typeof callback , 'function' ) ;
debug ( 'waitingForChallenge: %j' , challenge ) ;
async . retry ( { times : 10 , interval : 5000 } , function ( retryCallback ) {
debug ( 'waitingForChallenge: getting status' ) ;
superagent . get ( challenge . uri ) . end ( function ( error , result ) {
if ( error && ! error . response ) {
debug ( 'waitForChallenge: network error getting uri %s' , challenge . uri ) ;
return retryCallback ( new AcmeError ( AcmeError . EXTERNAL _ERROR , error . message ) ) ; // network error
}
if ( result . statusCode !== 202 ) {
debug ( 'waitForChallenge: invalid response code getting uri %s' , result . statusCode ) ;
return retryCallback ( new AcmeError ( AcmeError . EXTERNAL _ERROR , 'Bad response code:' + result . statusCode ) ) ;
}
debug ( 'waitForChallenge: status is "%s"' , result . body . status ) ;
if ( result . body . status === 'pending' ) return retryCallback ( new AcmeError ( AcmeError . NOT _COMPLETED ) ) ;
else if ( result . body . status === 'valid' ) return retryCallback ( ) ;
else return retryCallback ( new AcmeError ( AcmeError . EXTERNAL _ERROR , 'Unexpected status: ' + result . body . status ) ) ;
} ) ;
} , callback ) ;
}
// https://community.letsencrypt.org/t/public-beta-rate-limits/4772 for rate limits
2015-12-09 21:16:18 -08:00
function signCertificate ( accountKeyPem , csrDer , callback ) {
2015-12-09 19:23:19 -08:00
assert ( util . isBuffer ( accountKeyPem ) ) ;
2015-12-09 21:16:18 -08:00
assert ( util . isBuffer ( csrDer ) ) ;
2015-12-08 15:51:03 -08:00
assert . strictEqual ( typeof callback , 'function' ) ;
var payload = {
resource : 'new-cert' ,
2015-12-09 21:16:18 -08:00
csr : b64 ( csrDer )
2015-12-08 15:51:03 -08:00
} ;
debug ( 'signCertificate: signing %s' , payload . csr ) ;
2015-12-09 19:23:19 -08:00
sendSignedRequest ( CA _STAGING + '/acme/new-cert' , accountKeyPem , JSON . stringify ( payload ) , function ( error , result ) {
2015-12-08 15:51:03 -08:00
if ( error ) return callback ( new AcmeError ( AcmeError . EXTERNAL _ERROR , 'Network error when signing certificate: ' + error . message ) ) ;
if ( result . statusCode !== 201 ) return callback ( new AcmeError ( AcmeError . EXTERNAL _ERROR , util . format ( 'Failed to sign certificate. Expecting 201, got %s %s' , result . statusCode , result . text ) ) ) ;
2015-12-10 09:54:21 -08:00
if ( ! ( 'location' in result . headers ) ) return callback ( new AcmeError ( AcmeError . EXTERNAL _ERROR , 'Missing location in downloadCertificate' ) ) ;
2015-12-10 09:56:46 -08:00
var certificateLocation = result . headers . location ;
debug ( 'signCertificate: certificate is available at %s' , certificateLocation ) ;
2015-12-08 15:51:03 -08:00
2015-12-10 09:56:46 -08:00
superagent . get ( certificateLocation ) . buffer ( ) . parse ( function ( res , done ) {
var data = [ ] ;
res . on ( 'data' , function ( chunk ) { data . push ( chunk ) ; } ) ;
res . on ( 'end' , function ( ) { res . text = Buffer . concat ( data ) ; done ( ) ; } ) ;
} ) . end ( function ( error , result ) {
if ( error && ! error . response ) return callback ( new AcmeError ( AcmeError . EXTERNAL _ERROR , 'Network error when downloading certificate' ) ) ;
if ( result . statusCode === 202 ) return callback ( new AcmeError ( AcmeError . INTERNAL _ERROR , 'Retry not implemented yet' ) ) ;
if ( result . statusCode !== 200 ) return callback ( new AcmeError ( AcmeError . EXTERNAL _ERROR , util . format ( 'Failed to get cert. Expecting 200, got %s %s' , result . statusCode , result . text ) ) ) ;
callback ( null , result . text ) ;
} ) ;
2015-12-08 15:51:03 -08:00
} ) ;
}
2015-12-09 21:16:18 -08:00
function downloadCertificate ( accountKeyPem , domain , outdir , callback ) {
assert ( util . isBuffer ( accountKeyPem ) ) ;
assert . strictEqual ( typeof domain , 'string' ) ;
assert . strictEqual ( typeof outdir , 'string' ) ;
assert . strictEqual ( typeof callback , 'function' ) ;
var execSync = safe . child _process . execSync ;
var privateKeyFile = path . join ( outdir , domain + '.key' ) ;
var key = execSync ( 'openssl genrsa 4096' ) ;
if ( ! key ) return callback ( new AcmeError ( AcmeError . INTERNAL _ERROR , safe . error ) ) ;
if ( ! safe . fs . writeFileSync ( privateKeyFile , key ) ) return callback ( new AcmeError ( AcmeError . INTERNAL _ERROR , safe . error ) ) ;
debug ( 'downloadCertificate: key file saved at %s' , privateKeyFile ) ;
var csrDer = execSync ( util . format ( 'openssl req -new -key %s -outform DER -subj /CN=%s' , privateKeyFile , domain ) ) ;
if ( ! csrDer ) return callback ( new AcmeError ( AcmeError . INTERNAL _ERROR , safe . error ) ) ;
signCertificate ( accountKeyPem , csrDer , function ( error , certificateDer ) {
if ( error ) return callback ( error ) ;
safe . fs . writeFileSync ( path . join ( outdir , domain + '.der' ) , certificateDer ) ;
2015-12-10 09:56:46 -08:00
debug ( 'downloadCertificate: cert der file saved' ) ;
2015-12-09 21:16:18 -08:00
var certificatePem = execSync ( 'openssl x509 -inform DER -outform PEM' , { input : certificateDer } ) ; // this is really just base64 encoding with header
if ( ! certificatePem ) return callback ( new AcmeError ( AcmeError . INTERNAL _ERROR , safe . error ) ) ;
2015-12-10 09:07:03 -08:00
var chainPem = safe . fs . readFileSync ( _ _dirname + '/cert/lets-encrypt-x1-cross-signed.pem.txt' ) ;
if ( ! chainPem ) return callback ( new AcmeError ( AcmeError . INTERNAL _ERROR , safe . error ) ) ;
2015-12-09 21:16:18 -08:00
var certificateFile = path . join ( outdir , domain + '.cert' ) ;
2015-12-10 09:56:46 -08:00
var fullChainPem = Buffer . concat ( [ certificatePem , chainPem ] ) ;
2015-12-10 09:07:03 -08:00
if ( ! safe . fs . writeFileSync ( certificateFile , fullChainPem ) ) return callback ( new AcmeError ( AcmeError . INTERNAL _ERROR , safe . error ) ) ;
2015-12-09 21:16:18 -08:00
callback ( ) ;
} ) ;
}
function acmeFlow ( domain , email , accountKeyPem , outdir , callback ) {
2015-12-08 15:51:03 -08:00
assert . strictEqual ( typeof domain , 'string' ) ;
2015-12-08 19:42:33 -08:00
assert . strictEqual ( typeof email , 'string' ) ;
2015-12-09 19:23:19 -08:00
assert ( util . isBuffer ( accountKeyPem ) ) ;
2015-12-09 21:16:18 -08:00
assert . strictEqual ( typeof outdir , 'string' ) ;
2015-12-08 15:51:03 -08:00
assert . strictEqual ( typeof callback , 'function' ) ;
2015-12-09 19:23:19 -08:00
registerUser ( accountKeyPem , email , function ( error ) {
2015-12-08 15:51:03 -08:00
if ( error && error . reason !== AcmeError . ALREADY _EXISTS ) return callback ( error ) ;
2015-12-09 19:23:19 -08:00
registerDomain ( accountKeyPem , domain , function ( error , result ) {
2015-12-08 15:51:03 -08:00
if ( error ) return callback ( error ) ;
2015-12-09 21:16:18 -08:00
debug ( 'acmeFlow: challenges: %j' , result ) ;
2015-12-08 15:51:03 -08:00
var httpChallenges = result . challenges . filter ( function ( x ) { return x . type === 'http-01' ; } ) ;
if ( httpChallenges . length === 0 ) return callback ( new AcmeError ( AcmeError . EXTERNAL _ERROR , 'no http challenges' ) ) ;
var challenge = httpChallenges [ 0 ] ;
2015-12-09 21:16:18 -08:00
async . series ( [
prepareHttpChallenge . bind ( null , accountKeyPem , challenge ) ,
notifyChallengeReady . bind ( null , accountKeyPem , challenge ) ,
waitForChallenge . bind ( null , challenge ) ,
downloadCertificate . bind ( null , accountKeyPem , domain , outdir )
] , callback ) ;
2015-12-08 15:51:03 -08:00
} ) ;
} ) ;
}
2015-12-09 21:16:18 -08:00
function getCertificate ( domain , outdir , callback ) {
2015-12-08 19:42:33 -08:00
var email = 'admin@' + config . fqdn ( ) ;
2015-12-09 19:23:19 -08:00
var accountKeyPem ;
2015-12-08 19:42:33 -08:00
if ( ! fs . existsSync ( paths . ACME _ACCOUNT _KEY _FILE ) ) {
debug ( 'getCertificate: generating acme account key on first run' ) ;
2015-12-09 19:23:19 -08:00
accountKeyPem = safe . execSync ( 'openssl genrsa 4096' ) ;
if ( ! accountKeyPem ) return callback ( new AcmeError ( AcmeError . INTERNAL _ERROR , safe . error ) ) ;
2015-12-09 19:22:53 -08:00
2015-12-09 19:23:19 -08:00
safe . fs . writeFileSync ( paths . ACME _ACCOUNT _KEY _FILE , accountKeyPem ) ;
2015-12-08 19:42:33 -08:00
} else {
2015-12-09 19:23:19 -08:00
accountKeyPem = fs . readFileSync ( paths . ACME _ACCOUNT _KEY _FILE ) ;
2015-12-08 19:42:33 -08:00
}
2015-12-09 21:16:18 -08:00
acmeFlow ( domain , email , accountKeyPem , outdir , callback ) ;
2015-12-08 19:42:33 -08:00
}