mirror of
				https://github.com/therootcompany/acme.js.git
				synced 2024-11-16 17:29:00 +00:00 
			
		
		
		
	yay for backwards compat tested and working
This commit is contained in:
		
							parent
							
								
									27fb85ed9c
								
							
						
					
					
						commit
						fe5a48764b
					
				
							
								
								
									
										42
									
								
								README.md
									
									
									
									
									
								
							
							
						
						
									
										42
									
								
								README.md
									
									
									
									
									
								
							| @ -31,27 +31,52 @@ Todo | |||||||
| * export http and dns challenge tests | * export http and dns challenge tests | ||||||
| * support ECDSA keys | * support ECDSA keys | ||||||
| 
 | 
 | ||||||
|  | ## Let's Encrypt Directory URLs | ||||||
|  | 
 | ||||||
|  | ``` | ||||||
|  | # Production URL | ||||||
|  | https://acme-v02.api.letsencrypt.org/directory | ||||||
|  | ``` | ||||||
|  | 
 | ||||||
|  | ``` | ||||||
|  | # Staging URL | ||||||
|  | https://acme-staging-v02.api.letsencrypt.org/directory | ||||||
|  | ``` | ||||||
|  | 
 | ||||||
| ## API | ## API | ||||||
| 
 | 
 | ||||||
| ``` | ``` | ||||||
| var ACME = require('acme-v2.js').ACME.create({ | var ACME = require('acme-v2').ACME.create({ | ||||||
|   RSA: require('rsa-compat').RSA |   RSA: require('rsa-compat').RSA | ||||||
|  | 
 | ||||||
|  |   // other overrides | ||||||
|  | , request: require('request') | ||||||
|  | , promisify: require('util').promisify | ||||||
|  | 
 | ||||||
|  |   // used for constructing user-agent | ||||||
|  | , os: require('os') | ||||||
|  | , process: require('process') | ||||||
|  | 
 | ||||||
|  |   // used for overriding the default user-agent | ||||||
|  | , userAgent: 'My custom UA String' | ||||||
|  | , getUserAgentString: function (deps) { return 'My custom UA String'; } | ||||||
| }); | }); | ||||||
| ``` | ``` | ||||||
| 
 | 
 | ||||||
| ```javascript | ```javascript | ||||||
| // Accounts | // Accounts | ||||||
| ACME.registerNewAccount(options, cb)        // returns "regr" registration data | ACME.accounts.create(options)                 // returns Promise<regr> registration data | ||||||
| 
 | 
 | ||||||
|     { email: '<email>'                        //    valid email (server checks MX records) |     { email: '<email>'                        //    valid email (server checks MX records) | ||||||
|     , accountKeypair: {                       //    privateKeyPem or privateKeyJwt |     , accountKeypair: {                       //    privateKeyPem or privateKeyJwt | ||||||
|         privateKeyPem: '<ASCII PEM>' |         privateKeyPem: '<ASCII PEM>' | ||||||
|       } |       } | ||||||
|     , agreeToTerms: fn (tosUrl, cb) {}        //    must specify agree=tosUrl to continue (or falsey to end) |     , agreeToTerms: fn (tosUrl) {}            //    returns Promise with tosUrl | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  | 
 | ||||||
| // Registration | // Registration | ||||||
| ACME.getCertificate(options, cb)            // returns (err, pems={ privkey (key), cert, chain (ca) }) | ACME.certificates.create(options)             // returns Promise<pems={ privkey (key), cert, chain (ca) }> | ||||||
| 
 | 
 | ||||||
|     { newAuthzUrl: '<url>'                    //    specify acmeUrls.newAuthz |     { newAuthzUrl: '<url>'                    //    specify acmeUrls.newAuthz | ||||||
|     , newCertUrl: '<url>'                     //    specify acmeUrls.newCert |     , newCertUrl: '<url>'                     //    specify acmeUrls.newCert | ||||||
| @ -64,20 +89,19 @@ ACME.getCertificate(options, cb)            // returns (err, pems={ privkey (key | |||||||
|       } |       } | ||||||
|     , domains: [ 'example.com' ] |     , domains: [ 'example.com' ] | ||||||
| 
 | 
 | ||||||
|     , setChallenge: fn (hostname, key, val, cb) |     , setChallenge: fn (hostname, key, val)   // return Promise | ||||||
|     , removeChallenge: fn (hostname, key, cb) |     , removeChallenge: fn (hostname, key)     // return Promise | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  | 
 | ||||||
| // Discovery URLs | // Discovery URLs | ||||||
| ACME.getAcmeUrls(acmeDiscoveryUrl, cb)      // returns (err, acmeUrls={newReg,newAuthz,newCert,revokeCert}) | ACME.init(acmeDirectoryUrl)                   // returns Promise<acmeUrls={keyChange,meta,newAccount,newNonce,newOrder,revokeCert}> | ||||||
| ``` | ``` | ||||||
| 
 | 
 | ||||||
| Helpers & Stuff | Helpers & Stuff | ||||||
| 
 | 
 | ||||||
| ```javascript | ```javascript | ||||||
| // Constants | // Constants | ||||||
| ACME.productionServerUrl                // https://acme-v02.api.letsencrypt.org/directory |  | ||||||
| ACME.stagingServerUrl                   // https://acme-staging-v02.api.letsencrypt.org/directory |  | ||||||
| ACME.acmeChallengePrefix                // /.well-known/acme-challenge/ | ACME.acmeChallengePrefix                // /.well-known/acme-challenge/ | ||||||
| ``` | ``` | ||||||
| 
 | 
 | ||||||
|  | |||||||
							
								
								
									
										53
									
								
								compat.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										53
									
								
								compat.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,53 @@ | |||||||
|  | 'use strict'; | ||||||
|  | 
 | ||||||
|  | var ACME2 = require('./').ACME; | ||||||
|  | 
 | ||||||
|  | function resolveFn(cb) { | ||||||
|  |   return function (val) { | ||||||
|  |     // nextTick to get out of Promise chain
 | ||||||
|  |     process.nextTick(function () { cb(null, val); }); | ||||||
|  |   }; | ||||||
|  | } | ||||||
|  | function rejectFn(cb) { | ||||||
|  |   return function (err) { | ||||||
|  |     console.log('reject something or other:'); | ||||||
|  |     console.log(err.stack); | ||||||
|  |     // nextTick to get out of Promise chain
 | ||||||
|  |     process.nextTick(function () { cb(err); }); | ||||||
|  |   }; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | function create(deps) { | ||||||
|  |   deps.LeCore = {}; | ||||||
|  |   var acme2 = ACME2.create(deps); | ||||||
|  |   acme2.registerNewAccount = function (options, cb) { | ||||||
|  |     acme2.accounts.create(options).then(resolveFn(cb), rejectFn(cb)); | ||||||
|  |   }; | ||||||
|  |   acme2.getCertificate = function (options, cb) { | ||||||
|  |     acme2.certificates.create(options).then(resolveFn(cb), rejectFn(cb)); | ||||||
|  |   }; | ||||||
|  |   acme2.getAcmeUrls = function (options, cb) { | ||||||
|  |     acme2.init(options).then(resolveFn(cb), rejectFn(cb)); | ||||||
|  |   }; | ||||||
|  |   acme2.stagingServerUrl = module.exports.defaults.stagingServerUrl; | ||||||
|  |   acme2.productionServerUrl = module.exports.defaults.productionServerUrl; | ||||||
|  |   return acme2; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | module.exports.ACME = { }; | ||||||
|  | module.exports.defaults = { | ||||||
|  |   productionServerUrl:    'https://acme-v02.api.letsencrypt.org/directory' | ||||||
|  | , stagingServerUrl:       'https://acme-staging-v02.api.letsencrypt.org/directory' | ||||||
|  | , knownEndpoints:         [ 'keyChange', 'meta', 'newAccount', 'newNonce', 'newOrder', 'revokeCert' ] | ||||||
|  | , challengeTypes:         [ 'http-01', 'dns-01' ] | ||||||
|  | , challengeType:          'http-01' | ||||||
|  | , keyType:                'rsa' // ecdsa
 | ||||||
|  | , keySize:                2048 // 256
 | ||||||
|  | }; | ||||||
|  | Object.keys(module.exports.defaults).forEach(function (key) { | ||||||
|  |   module.exports.ACME[key] = module.exports.defaults[key]; | ||||||
|  | }); | ||||||
|  | Object.keys(ACME2).forEach(function (key) { | ||||||
|  |   module.exports.ACME[key] = ACME2[key]; | ||||||
|  |   module.exports.ACME.create = create; | ||||||
|  | }); | ||||||
							
								
								
									
										860
									
								
								node.js
									
									
									
									
									
								
							
							
						
						
									
										860
									
								
								node.js
									
									
									
									
									
								
							| @ -6,463 +6,487 @@ | |||||||
| 'use strict'; | 'use strict'; | ||||||
| /* globals Promise */ | /* globals Promise */ | ||||||
| 
 | 
 | ||||||
| var defaults = { | var ACME = module.exports.ACME = {}; | ||||||
|   productionServerUrl:    'https://acme-v02.api.letsencrypt.org/directory' |  | ||||||
| , stagingServerUrl:       'https://acme-staging-v02.api.letsencrypt.org/directory' |  | ||||||
| , acmeChallengePrefix:    '/.well-known/acme-challenge/' |  | ||||||
| , knownEndpoints:         [ 'keyChange', 'meta', 'newAccount', 'newNonce', 'newOrder', 'revokeCert' ] |  | ||||||
| , challengeType:          'http-01' // dns-01
 |  | ||||||
| , keyType:                'rsa' // ecdsa
 |  | ||||||
| , keySize:                2048 // 256
 |  | ||||||
| }; |  | ||||||
| 
 | 
 | ||||||
| function create(deps) { | ACME.acmeChallengePrefix = '/.well-known/acme-challenge/'; | ||||||
|   if (!deps) { deps = {}; } |  | ||||||
|   deps.LeCore = {}; |  | ||||||
|   deps.pkg = deps.pkg || require('./package.json'); |  | ||||||
|   deps.os = deps.os || require('os'); |  | ||||||
|   deps.process = deps.process || require('process'); |  | ||||||
| 
 | 
 | ||||||
|  | ACME._getUserAgentString = function (deps) { | ||||||
|   var uaDefaults = { |   var uaDefaults = { | ||||||
|       pkg: "Greenlock/" + deps.pkg.version |       pkg: "Greenlock/" + deps.pkg.version | ||||||
|     , os: " (" + deps.os.type() + "; " + deps.process.arch + " " + deps.os.platform() + " " + deps.os.release() + ")" |     , os: "(" + deps.os.type() + "; " + deps.process.arch + " " + deps.os.platform() + " " + deps.os.release() + ")" | ||||||
|     , node: " Node.js/" + deps.process.version |     , node: "Node.js/" + deps.process.version | ||||||
|     , user: '' |     , user: '' | ||||||
|   }; |   }; | ||||||
|   //var currentUAProps;
 |  | ||||||
| 
 | 
 | ||||||
|   function getUaString() { |   var userAgent = []; | ||||||
|     var userAgent = ''; |  | ||||||
| 
 | 
 | ||||||
|     //Object.keys(currentUAProps)
 |   //Object.keys(currentUAProps)
 | ||||||
|     Object.keys(uaDefaults).forEach(function (key) { |   Object.keys(uaDefaults).forEach(function (key) { | ||||||
|       userAgent += uaDefaults[key]; |     if (uaDefaults[key]) { | ||||||
|       //userAgent += currentUAProps[key];
 |       userAgent.push(uaDefaults[key]); | ||||||
|     }); |  | ||||||
| 
 |  | ||||||
|     return userAgent.trim(); |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   function getRequest(opts) { |  | ||||||
|     if (!opts) { opts = {}; } |  | ||||||
| 
 |  | ||||||
|     return deps.request.defaults({ |  | ||||||
|       headers: { |  | ||||||
|         'User-Agent': opts.userAgent || getUaString() |  | ||||||
|       } |  | ||||||
|     }); |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   var RSA = deps.RSA || require('rsa-compat').RSA; |  | ||||||
|   deps.request = deps.request || require('request'); |  | ||||||
|   deps.promisify = deps.promisify || require('util').promisify; |  | ||||||
| 
 |  | ||||||
|   var directoryUrl = deps.directoryUrl || defaults.stagingServerUrl; |  | ||||||
|   var request = deps.promisify(getRequest({})); |  | ||||||
| 
 |  | ||||||
|   var acme2 = { |  | ||||||
|     getAcmeUrls: function (_directoryUrl) { |  | ||||||
|       var me = this; |  | ||||||
|       return request({ url: _directoryUrl || directoryUrl, json: true }).then(function (resp) { |  | ||||||
|         me._directoryUrls = resp.body; |  | ||||||
|         me._tos = me._directoryUrls.meta.termsOfService; |  | ||||||
|         return me._directoryUrls; |  | ||||||
|       }); |  | ||||||
|     } |     } | ||||||
|   , getNonce: function () { |   }); | ||||||
|       var me = this; |  | ||||||
|       if (me._nonce) { return new Promise(function (resolve) { resolve(me._nonce); return; }); } |  | ||||||
|       return request({ method: 'HEAD', url: me._directoryUrls.newNonce }).then(function (resp) { |  | ||||||
|         me._nonce = resp.toJSON().headers['replay-nonce']; |  | ||||||
|         return me._nonce; |  | ||||||
|       }); |  | ||||||
|     } |  | ||||||
|     // ACME RFC Section 7.3 Account Creation
 |  | ||||||
|     /* |  | ||||||
|      { |  | ||||||
|        "protected": base64url({ |  | ||||||
|          "alg": "ES256", |  | ||||||
|          "jwk": {...}, |  | ||||||
|          "nonce": "6S8IqOGY7eL2lsGoTZYifg", |  | ||||||
|          "url": "https://example.com/acme/new-account" |  | ||||||
|        }), |  | ||||||
|        "payload": base64url({ |  | ||||||
|          "termsOfServiceAgreed": true, |  | ||||||
|          "onlyReturnExisting": false, |  | ||||||
|          "contact": [ |  | ||||||
|            "mailto:cert-admin@example.com", |  | ||||||
|            "mailto:admin@example.com" |  | ||||||
|          ] |  | ||||||
|        }), |  | ||||||
|        "signature": "RZPOnYoPs1PhjszF...-nh6X1qtOFPB519I" |  | ||||||
|      } |  | ||||||
|     */ |  | ||||||
|   , registerNewAccount: function (options) { |  | ||||||
|       var me = this; |  | ||||||
| 
 | 
 | ||||||
|       console.log('[acme-v2] registerNewAccount'); |   return userAgent.join(' ').trim(); | ||||||
|  | }; | ||||||
|  | ACME._directory = function (me) { | ||||||
|  |   return me._request({ url: me.directoryUrl, json: true }); | ||||||
|  | }; | ||||||
|  | ACME._getNonce = function (me) { | ||||||
|  |   if (me._nonce) { return new Promise(function (resolve) { resolve(me._nonce); return; }); } | ||||||
|  |   return me._request({ method: 'HEAD', url: me._directoryUrls.newNonce }).then(function (resp) { | ||||||
|  |     me._nonce = resp.toJSON().headers['replay-nonce']; | ||||||
|  |     return me._nonce; | ||||||
|  |   }); | ||||||
|  | }; | ||||||
|  | // ACME RFC Section 7.3 Account Creation
 | ||||||
|  | /* | ||||||
|  |  { | ||||||
|  |    "protected": base64url({ | ||||||
|  |      "alg": "ES256", | ||||||
|  |      "jwk": {...}, | ||||||
|  |      "nonce": "6S8IqOGY7eL2lsGoTZYifg", | ||||||
|  |      "url": "https://example.com/acme/new-account" | ||||||
|  |    }), | ||||||
|  |    "payload": base64url({ | ||||||
|  |      "termsOfServiceAgreed": true, | ||||||
|  |      "onlyReturnExisting": false, | ||||||
|  |      "contact": [ | ||||||
|  |        "mailto:cert-admin@example.com", | ||||||
|  |        "mailto:admin@example.com" | ||||||
|  |      ] | ||||||
|  |    }), | ||||||
|  |    "signature": "RZPOnYoPs1PhjszF...-nh6X1qtOFPB519I" | ||||||
|  |  } | ||||||
|  | */ | ||||||
|  | ACME._registerAccount = function (me, options) { | ||||||
|  |   console.log('[acme-v2] accounts.create'); | ||||||
| 
 | 
 | ||||||
|       return me.getNonce().then(function () { |   return ACME._getNonce(me).then(function () { | ||||||
|         return new Promise(function (resolve, reject) { |     return new Promise(function (resolve, reject) { | ||||||
| 
 | 
 | ||||||
|           function agree(err, tosUrl) { |       function agree(tosUrl) { | ||||||
|             if (err) { reject(err); return; } |         var err; | ||||||
|             if (me._tos !== tosUrl) { |         if (me._tos !== tosUrl) { | ||||||
|               err = new Error("You must agree to the ToS at '" + me._tos + "'"); |           err = new Error("You must agree to the ToS at '" + me._tos + "'"); | ||||||
|               err.code = "E_AGREE_TOS"; |           err.code = "E_AGREE_TOS"; | ||||||
|               reject(err); |           reject(err); | ||||||
|               return; |  | ||||||
|             } |  | ||||||
| 
 |  | ||||||
|             var jwk = RSA.exportPublicJwk(options.accountKeypair); |  | ||||||
|             var body = { |  | ||||||
|               termsOfServiceAgreed: tosUrl === me._tos |  | ||||||
|             , onlyReturnExisting: false |  | ||||||
|             , contact: [ 'mailto:' + options.email ] |  | ||||||
|             }; |  | ||||||
|             if (options.externalAccount) { |  | ||||||
|               body.externalAccountBinding = RSA.signJws( |  | ||||||
|                 options.externalAccount.secret |  | ||||||
|               , undefined |  | ||||||
|               , { alg: "HS256" |  | ||||||
|                 , kid: options.externalAccount.id |  | ||||||
|                 , url: me._directoryUrls.newAccount |  | ||||||
|                 } |  | ||||||
|               , new Buffer(JSON.stringify(jwk)) |  | ||||||
|               ); |  | ||||||
|             } |  | ||||||
|             var payload = JSON.stringify(body); |  | ||||||
|             var jws = RSA.signJws( |  | ||||||
|               options.accountKeypair |  | ||||||
|             , undefined |  | ||||||
|             , { nonce: me._nonce |  | ||||||
|               , alg: 'RS256' |  | ||||||
|               , url: me._directoryUrls.newAccount |  | ||||||
|               , jwk: jwk |  | ||||||
|               } |  | ||||||
|             , new Buffer(payload) |  | ||||||
|             ); |  | ||||||
| 
 |  | ||||||
|             console.log('[acme-v2] registerNewAccount JSON body:'); |  | ||||||
|             delete jws.header; |  | ||||||
|             console.log(jws); |  | ||||||
|             me._nonce = null; |  | ||||||
|             return request({ |  | ||||||
|               method: 'POST' |  | ||||||
|             , url: me._directoryUrls.newAccount |  | ||||||
|             , headers: { 'Content-Type': 'application/jose+json' } |  | ||||||
|             , json: jws |  | ||||||
|             }).then(function (resp) { |  | ||||||
|               me._nonce = resp.toJSON().headers['replay-nonce']; |  | ||||||
|               var location = resp.toJSON().headers.location; |  | ||||||
|               console.log('[DEBUG] new account location:'); // the account id url
 |  | ||||||
|               console.log(location); // the account id url
 |  | ||||||
|               console.log(resp.toJSON()); |  | ||||||
|               me._kid = location; |  | ||||||
|               return resp.body; |  | ||||||
|             }).then(resolve); |  | ||||||
|           } |  | ||||||
| 
 |  | ||||||
|           console.log('[acme-v2] agreeToTerms'); |  | ||||||
|           options.agreeToTerms(me._tos, agree); |  | ||||||
|         }); |  | ||||||
|       }); |  | ||||||
|     } |  | ||||||
|     /* |  | ||||||
|      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" |  | ||||||
|      } |  | ||||||
|     */ |  | ||||||
|   , _getChallenges: function (options, auth) { |  | ||||||
|       console.log('\n[DEBUG] getChallenges\n'); |  | ||||||
|       return request({ method: 'GET', url: auth, json: true }).then(function (resp) { |  | ||||||
|         return resp.body; |  | ||||||
|       }); |  | ||||||
|     } |  | ||||||
|     // https://tools.ietf.org/html/draft-ietf-acme-acme-10#section-7.5.1
 |  | ||||||
|   , _postChallenge: function (options, identifier, ch) { |  | ||||||
|       var me = this; |  | ||||||
| 
 |  | ||||||
|       var body = { }; |  | ||||||
| 
 |  | ||||||
|       var payload = JSON.stringify(body); |  | ||||||
| 
 |  | ||||||
|       var thumbprint = RSA.thumbprint(options.accountKeypair); |  | ||||||
|       var keyAuthorization = ch.token + '.' + thumbprint; |  | ||||||
|       //   keyAuthorization = token || '.' || base64url(JWK_Thumbprint(accountKey))
 |  | ||||||
|       //   /.well-known/acme-challenge/:token
 |  | ||||||
| 
 |  | ||||||
|       return new Promise(function (resolve, reject) { |  | ||||||
|         if (options.setupChallenge) { |  | ||||||
|           options.setupChallenge( |  | ||||||
|             { identifier: identifier |  | ||||||
|             , hostname: identifier.value |  | ||||||
|             , type: ch.type |  | ||||||
|             , token: ch.token |  | ||||||
|             , thumbprint: thumbprint |  | ||||||
|             , keyAuthorization: keyAuthorization |  | ||||||
|             , dnsAuthorization: RSA.utils.toWebsafeBase64( |  | ||||||
|                 require('crypto').createHash('sha256').update(keyAuthorization).digest('base64') |  | ||||||
|               ) |  | ||||||
|             } |  | ||||||
|           , testChallenge |  | ||||||
|           ); |  | ||||||
|         } else { |  | ||||||
|           options.setChallenge(identifier.value, ch.token, keyAuthorization, testChallenge); |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         function testChallenge(err) { |  | ||||||
|           if (err) { reject(err); return; } |  | ||||||
| 
 |  | ||||||
|           // TODO put check dns / http checks here?
 |  | ||||||
|           // http-01: GET https://example.org/.well-known/acme-challenge/{{token}} => {{keyAuth}}
 |  | ||||||
|           // dns-01: TXT _acme-challenge.example.org. => "{{urlSafeBase64(sha256(keyAuth))}}"
 |  | ||||||
| 
 |  | ||||||
|           function wait(ms) { |  | ||||||
|             return new Promise(function (resolve) { |  | ||||||
|               setTimeout(resolve, (ms || 1100)); |  | ||||||
|             }); |  | ||||||
|           } |  | ||||||
| 
 |  | ||||||
|           function pollStatus() { |  | ||||||
|             console.log('\n[DEBUG] statusChallenge\n'); |  | ||||||
|             return request({ method: 'GET', url: ch.url, json: true }).then(function (resp) { |  | ||||||
|               console.error('poll: resp.body:'); |  | ||||||
|               console.error(resp.body); |  | ||||||
| 
 |  | ||||||
|               if ('pending' === resp.body.status) { |  | ||||||
|                 console.log('poll: again'); |  | ||||||
|                 return wait().then(pollStatus); |  | ||||||
|               } |  | ||||||
| 
 |  | ||||||
|               if ('valid' === resp.body.status) { |  | ||||||
|                 console.log('poll: valid'); |  | ||||||
|                 try { |  | ||||||
|                   if (options.teardownChallenge) { |  | ||||||
|                     options.teardownChallenge( |  | ||||||
|                       { identifier: identifier |  | ||||||
|                       , type: ch.type |  | ||||||
|                       , token: ch.token |  | ||||||
|                       } |  | ||||||
|                     , function () {} |  | ||||||
|                     ); |  | ||||||
|                   } else { |  | ||||||
|                     options.removeChallenge(identifier.value, ch.token, function () {}); |  | ||||||
|                   } |  | ||||||
|                 } catch(e) {} |  | ||||||
|                 return resp.body; |  | ||||||
|               } |  | ||||||
| 
 |  | ||||||
|               if (!resp.body.status) { |  | ||||||
|                 console.error("[acme-v2] (y) bad challenge state:"); |  | ||||||
|               } |  | ||||||
|               else if ('invalid' === resp.body.status) { |  | ||||||
|                 console.error("[acme-v2] (x) invalid challenge state:"); |  | ||||||
|               } |  | ||||||
|               else { |  | ||||||
|                 console.error("[acme-v2] (z) bad challenge state:"); |  | ||||||
|               } |  | ||||||
| 
 |  | ||||||
|               return Promise.reject(new Error("[acme-v2] bad challenge state")); |  | ||||||
|             }); |  | ||||||
|           } |  | ||||||
| 
 |  | ||||||
|           console.log('\n[DEBUG] postChallenge\n'); |  | ||||||
|           //console.log('\n[DEBUG] stop to fix things\n'); return;
 |  | ||||||
| 
 |  | ||||||
|           function post() { |  | ||||||
|             var jws = RSA.signJws( |  | ||||||
|               options.accountKeypair |  | ||||||
|             , undefined |  | ||||||
|             , { nonce: me._nonce, alg: 'RS256', url: ch.url, kid: me._kid } |  | ||||||
|             , new Buffer(payload) |  | ||||||
|             ); |  | ||||||
|             me._nonce = null; |  | ||||||
|             return request({ |  | ||||||
|               method: 'POST' |  | ||||||
|             , url: ch.url |  | ||||||
|             , headers: { 'Content-Type': 'application/jose+json' } |  | ||||||
|             , json: jws |  | ||||||
|             }).then(function (resp) { |  | ||||||
|               me._nonce = resp.toJSON().headers['replay-nonce']; |  | ||||||
|               console.log('respond to challenge: resp.body:'); |  | ||||||
|               console.log(resp.body); |  | ||||||
|               return wait().then(pollStatus).then(resolve, reject); |  | ||||||
|             }); |  | ||||||
|           } |  | ||||||
| 
 |  | ||||||
|           return wait(20 * 1000).then(post); |  | ||||||
|         } |  | ||||||
|       }); |  | ||||||
|     } |  | ||||||
|   , _finalizeOrder: function (options, validatedDomains) { |  | ||||||
|       console.log('finalizeOrder:'); |  | ||||||
|       var me = this; |  | ||||||
| 
 |  | ||||||
|       var csr = RSA.generateCsrWeb64(options.domainKeypair, validatedDomains); |  | ||||||
|       var body = { csr: csr }; |  | ||||||
|       var payload = JSON.stringify(body); |  | ||||||
| 
 |  | ||||||
|       function wait(ms) { |  | ||||||
|         return new Promise(function (resolve) { |  | ||||||
|           setTimeout(resolve, (ms || 1100)); |  | ||||||
|         }); |  | ||||||
|       } |  | ||||||
| 
 |  | ||||||
|       function pollCert() { |  | ||||||
|         var jws = RSA.signJws( |  | ||||||
|           options.accountKeypair |  | ||||||
|         , undefined |  | ||||||
|         , { nonce: me._nonce, alg: 'RS256', url: me._finalize, kid: me._kid } |  | ||||||
|         , new Buffer(payload) |  | ||||||
|         ); |  | ||||||
| 
 |  | ||||||
|         console.log('finalize:', me._finalize); |  | ||||||
|         me._nonce = null; |  | ||||||
|         return request({ |  | ||||||
|           method: 'POST' |  | ||||||
|         , url: me._finalize |  | ||||||
|         , headers: { 'Content-Type': 'application/jose+json' } |  | ||||||
|         , json: jws |  | ||||||
|         }).then(function (resp) { |  | ||||||
|           me._nonce = resp.toJSON().headers['replay-nonce']; |  | ||||||
| 
 |  | ||||||
|           console.log('order finalized: resp.body:'); |  | ||||||
|           console.log(resp.body); |  | ||||||
| 
 |  | ||||||
|           if ('processing' === resp.body.status) { |  | ||||||
|             return wait().then(pollCert); |  | ||||||
|           } |  | ||||||
| 
 |  | ||||||
|           if ('valid' === resp.body.status) { |  | ||||||
|             me._expires = resp.body.expires; |  | ||||||
|             me._certificate = resp.body.certificate; |  | ||||||
| 
 |  | ||||||
|             return resp.body; |  | ||||||
|           } |  | ||||||
| 
 |  | ||||||
|           if ('invalid' === resp.body.status) { |  | ||||||
|             console.error('cannot finalize: badness'); |  | ||||||
|             return; |  | ||||||
|           } |  | ||||||
| 
 |  | ||||||
|           console.error('(x) cannot finalize: badness'); |  | ||||||
|           return; |           return; | ||||||
|         }); |  | ||||||
|       } |  | ||||||
| 
 |  | ||||||
|       return pollCert(); |  | ||||||
|     } |  | ||||||
|   , _getCertificate: function () { |  | ||||||
|       var me = this; |  | ||||||
|       return request({ method: 'GET', url: me._certificate, json: true }).then(function (resp) { |  | ||||||
|         console.log('Certificate:'); |  | ||||||
|         console.log(resp.body); |  | ||||||
|         return resp.body; |  | ||||||
|       }); |  | ||||||
|     } |  | ||||||
|   , getCertificate: function (options, cb) { |  | ||||||
|       console.log('[acme-v2] DEBUG get cert 1'); |  | ||||||
|       var me = this; |  | ||||||
| 
 |  | ||||||
|       if (!options.challengeTypes) { |  | ||||||
|         if (!options.challengeType) { |  | ||||||
|           cb(new Error("challenge type must be specified")); |  | ||||||
|           return Promise.reject(new Error("challenge type must be specified")); |  | ||||||
|         } |         } | ||||||
|         options.challengeTypes = [ options.challengeType ]; |  | ||||||
|       } |  | ||||||
| 
 | 
 | ||||||
|       console.log('[acme-v2] getCertificate'); |         var jwk = me.RSA.exportPublicJwk(options.accountKeypair); | ||||||
|       return me.getNonce().then(function () { |  | ||||||
|         var body = { |         var body = { | ||||||
|           identifiers: options.domains.map(function (hostname) { |           termsOfServiceAgreed: tosUrl === me._tos | ||||||
|             return { type: "dns" , value: hostname }; |         , onlyReturnExisting: false | ||||||
|           }) |         , contact: [ 'mailto:' + options.email ] | ||||||
|           //, "notBefore": "2016-01-01T00:00:00Z"
 |  | ||||||
|           //, "notAfter": "2016-01-08T00:00:00Z"
 |  | ||||||
|         }; |         }; | ||||||
| 
 |         if (options.externalAccount) { | ||||||
|  |           body.externalAccountBinding = me.RSA.signJws( | ||||||
|  |             options.externalAccount.secret | ||||||
|  |           , undefined | ||||||
|  |           , { alg: "HS256" | ||||||
|  |             , kid: options.externalAccount.id | ||||||
|  |             , url: me._directoryUrls.newAccount | ||||||
|  |             } | ||||||
|  |           , new Buffer(JSON.stringify(jwk)) | ||||||
|  |           ); | ||||||
|  |         } | ||||||
|         var payload = JSON.stringify(body); |         var payload = JSON.stringify(body); | ||||||
|         var jws = RSA.signJws( |         var jws = me.RSA.signJws( | ||||||
|           options.accountKeypair |           options.accountKeypair | ||||||
|         , undefined |         , undefined | ||||||
|         , { nonce: me._nonce, alg: 'RS256', url: me._directoryUrls.newOrder, kid: me._kid } |         , { nonce: me._nonce | ||||||
|  |           , alg: 'RS256' | ||||||
|  |           , url: me._directoryUrls.newAccount | ||||||
|  |           , jwk: jwk | ||||||
|  |           } | ||||||
|         , new Buffer(payload) |         , new Buffer(payload) | ||||||
|         ); |         ); | ||||||
| 
 | 
 | ||||||
|         console.log('\n[DEBUG] newOrder\n'); |         console.log('[acme-v2] accounts.create JSON body:'); | ||||||
|  |         delete jws.header; | ||||||
|  |         console.log(jws); | ||||||
|         me._nonce = null; |         me._nonce = null; | ||||||
|         return request({ |         return me._request({ | ||||||
|           method: 'POST' |           method: 'POST' | ||||||
|         , url: me._directoryUrls.newOrder |         , url: me._directoryUrls.newAccount | ||||||
|         , headers: { 'Content-Type': 'application/jose+json' } |         , headers: { 'Content-Type': 'application/jose+json' } | ||||||
|         , json: jws |         , json: jws | ||||||
|         }).then(function (resp) { |         }).then(function (resp) { | ||||||
|           me._nonce = resp.toJSON().headers['replay-nonce']; |           me._nonce = resp.toJSON().headers['replay-nonce']; | ||||||
|           var location = resp.toJSON().headers.location; |           var location = resp.toJSON().headers.location; | ||||||
|  |           console.log('[DEBUG] new account location:'); // the account id url
 | ||||||
|           console.log(location); // the account id url
 |           console.log(location); // the account id url
 | ||||||
|           console.log(resp.toJSON()); |           console.log(resp.toJSON()); | ||||||
|           me._authorizations = resp.body.authorizations; |           me._kid = location; | ||||||
|           me._order = location; |           return resp.body; | ||||||
|           me._finalize = resp.body.finalize; |         }).then(resolve, reject); | ||||||
|           //console.log('[DEBUG] finalize:', me._finalize); return;
 |       } | ||||||
| 
 | 
 | ||||||
|           //return resp.body;
 |       console.log('[acme-v2] agreeToTerms'); | ||||||
|           return Promise.all(me._authorizations.map(function (authUrl) { |       if (1 === options.agreeToTerms.length) { | ||||||
|             return me._getChallenges(options, authUrl).then(function (results) { |         return options.agreeToTerms(me._tos).then(agree, reject); | ||||||
|               // var domain = options.domains[i]; // results.identifier.value
 |       } | ||||||
|               var chType = options.challengeTypes.filter(function (chType) { |       else if (2 === options.agreeToTerms.length) { | ||||||
|                 return results.challenges.some(function (ch) { |         return options.agreeToTerms(me._tos, function (err, tosUrl) { | ||||||
|                   return ch.type === chType; |           if (!err) { agree(tosUrl); return; } | ||||||
|                 }); |           reject(err); | ||||||
|               })[0]; |         }); | ||||||
|               var challenge = results.challenges.filter(function (ch) { |       } | ||||||
|                 if (chType === ch.type) { |       else { | ||||||
|                   return ch; |         reject(new Error('agreeToTerms has incorrect function signature.' | ||||||
|                 } |           + ' Should be fn(tos) { return Promise<tos>; }')); | ||||||
|               })[0]; |       } | ||||||
|  |     }); | ||||||
|  |   }); | ||||||
|  | }; | ||||||
|  | /* | ||||||
|  |  POST /acme/new-order HTTP/1.1 | ||||||
|  |  Host: example.com | ||||||
|  |  Content-Type: application/jose+json | ||||||
| 
 | 
 | ||||||
|               if (!challenge) { |  { | ||||||
|                 return Promise.reject(new Error("Server didn't offer any challenge we can handle.")); |    "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" | ||||||
|  |  } | ||||||
|  | */ | ||||||
|  | ACME._getChallenges = function (me, options, auth) { | ||||||
|  |   console.log('\n[DEBUG] getChallenges\n'); | ||||||
|  |   return me._request({ method: 'GET', url: auth, json: true }).then(function (resp) { | ||||||
|  |     return resp.body; | ||||||
|  |   }); | ||||||
|  | }; | ||||||
|  | ACME._wait = function wait(ms) { | ||||||
|  |   return new Promise(function (resolve) { | ||||||
|  |     setTimeout(resolve, (ms || 1100)); | ||||||
|  |   }); | ||||||
|  | }; | ||||||
|  | // https://tools.ietf.org/html/draft-ietf-acme-acme-10#section-7.5.1
 | ||||||
|  | ACME._postChallenge = function (me, options, identifier, ch) { | ||||||
|  |   var body = { }; | ||||||
|  | 
 | ||||||
|  |   var payload = JSON.stringify(body); | ||||||
|  | 
 | ||||||
|  |   var thumbprint = me.RSA.thumbprint(options.accountKeypair); | ||||||
|  |   var keyAuthorization = ch.token + '.' + thumbprint; | ||||||
|  |   //   keyAuthorization = token || '.' || base64url(JWK_Thumbprint(accountKey))
 | ||||||
|  |   //   /.well-known/acme-challenge/:token
 | ||||||
|  | 
 | ||||||
|  |   return new Promise(function (resolve, reject) { | ||||||
|  |     function failChallenge(err) { | ||||||
|  |       if (err) { reject(err); return; } | ||||||
|  |       testChallenge(); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     function testChallenge() { | ||||||
|  |       // TODO put check dns / http checks here?
 | ||||||
|  |       // http-01: GET https://example.org/.well-known/acme-challenge/{{token}} => {{keyAuth}}
 | ||||||
|  |       // dns-01: TXT _acme-challenge.example.org. => "{{urlSafeBase64(sha256(keyAuth))}}"
 | ||||||
|  | 
 | ||||||
|  |       function pollStatus() { | ||||||
|  |         console.log('\n[DEBUG] statusChallenge\n'); | ||||||
|  |         return me._request({ method: 'GET', url: ch.url, json: true }).then(function (resp) { | ||||||
|  |           console.error('poll: resp.body:'); | ||||||
|  |           console.error(resp.body); | ||||||
|  | 
 | ||||||
|  |           if ('pending' === resp.body.status) { | ||||||
|  |             console.log('poll: again'); | ||||||
|  |             return ACME._wait(1 * 1000).then(pollStatus); | ||||||
|  |           } | ||||||
|  | 
 | ||||||
|  |           if ('valid' === resp.body.status) { | ||||||
|  |             console.log('poll: valid'); | ||||||
|  |             try { | ||||||
|  |               if (1 === options.removeChallenge.length) { | ||||||
|  |                 options.removeChallenge( | ||||||
|  |                   { identifier: identifier | ||||||
|  |                   , type: ch.type | ||||||
|  |                   , token: ch.token | ||||||
|  |                   } | ||||||
|  |                 ).then(function () {}, function () {}); | ||||||
|  |               } else if (2 === options.removeChallenge.length) { | ||||||
|  |                 options.removeChallenge( | ||||||
|  |                   { identifier: identifier | ||||||
|  |                   , type: ch.type | ||||||
|  |                   , token: ch.token | ||||||
|  |                   } | ||||||
|  |                 , function (err) { return err; } | ||||||
|  |                 ); | ||||||
|  |               } else { | ||||||
|  |                 options.removeChallenge(identifier.value, ch.token, function () {}); | ||||||
|               } |               } | ||||||
|  |             } catch(e) {} | ||||||
|  |             return resp.body; | ||||||
|  |           } | ||||||
| 
 | 
 | ||||||
|               return me._postChallenge(options, results.identifier, challenge); |           if (!resp.body.status) { | ||||||
|             }); |             console.error("[acme-v2] (y) bad challenge state:"); | ||||||
|           })).then(function () { |           } | ||||||
|             var validatedDomains = body.identifiers.map(function (ident) { |           else if ('invalid' === resp.body.status) { | ||||||
|               return ident.value; |             console.error("[acme-v2] (x) invalid challenge state:"); | ||||||
|             }); |           } | ||||||
|  |           else { | ||||||
|  |             console.error("[acme-v2] (z) bad challenge state:"); | ||||||
|  |           } | ||||||
| 
 | 
 | ||||||
|             return me._finalizeOrder(options, validatedDomains); |           return Promise.reject(new Error("[acme-v2] bad challenge state")); | ||||||
|           }).then(function () { |         }); | ||||||
|             return me._getCertificate().then(function (result) { cb(null, result); return result; }, cb); |       } | ||||||
|           }); | 
 | ||||||
|  |       console.log('\n[DEBUG] postChallenge\n'); | ||||||
|  |       //console.log('\n[DEBUG] stop to fix things\n'); return;
 | ||||||
|  | 
 | ||||||
|  |       function post() { | ||||||
|  |         var jws = me.RSA.signJws( | ||||||
|  |           options.accountKeypair | ||||||
|  |         , undefined | ||||||
|  |         , { nonce: me._nonce, alg: 'RS256', url: ch.url, kid: me._kid } | ||||||
|  |         , new Buffer(payload) | ||||||
|  |         ); | ||||||
|  |         me._nonce = null; | ||||||
|  |         return me._request({ | ||||||
|  |           method: 'POST' | ||||||
|  |         , url: ch.url | ||||||
|  |         , headers: { 'Content-Type': 'application/jose+json' } | ||||||
|  |         , json: jws | ||||||
|  |         }).then(function (resp) { | ||||||
|  |           me._nonce = resp.toJSON().headers['replay-nonce']; | ||||||
|  |           console.log('respond to challenge: resp.body:'); | ||||||
|  |           console.log(resp.body); | ||||||
|  |           return ACME._wait(1 * 1000).then(pollStatus).then(resolve, reject); | ||||||
|  |         }); | ||||||
|  |       } | ||||||
|  | 
 | ||||||
|  |       return ACME._wait(1 * 1000).then(post); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     try { | ||||||
|  |       if (1 === options.setChallenge.length) { | ||||||
|  |         options.setChallenge( | ||||||
|  |           { identifier: identifier | ||||||
|  |           , hostname: identifier.value | ||||||
|  |           , type: ch.type | ||||||
|  |           , token: ch.token | ||||||
|  |           , thumbprint: thumbprint | ||||||
|  |           , keyAuthorization: keyAuthorization | ||||||
|  |           , dnsAuthorization: me.RSA.utils.toWebsafeBase64( | ||||||
|  |               require('crypto').createHash('sha256').update(keyAuthorization).digest('base64') | ||||||
|  |             ) | ||||||
|  |           } | ||||||
|  |         ).then(testChallenge, reject); | ||||||
|  |       } else if (2 === options.setChallenge.length) { | ||||||
|  |         options.setChallenge( | ||||||
|  |           { identifier: identifier | ||||||
|  |           , hostname: identifier.value | ||||||
|  |           , type: ch.type | ||||||
|  |           , token: ch.token | ||||||
|  |           , thumbprint: thumbprint | ||||||
|  |           , keyAuthorization: keyAuthorization | ||||||
|  |           , dnsAuthorization: me.RSA.utils.toWebsafeBase64( | ||||||
|  |               require('crypto').createHash('sha256').update(keyAuthorization).digest('base64') | ||||||
|  |             ) | ||||||
|  |           } | ||||||
|  |         , failChallenge | ||||||
|  |         ); | ||||||
|  |       } else { | ||||||
|  |         options.setChallenge(identifier.value, ch.token, keyAuthorization, failChallenge); | ||||||
|  |       } | ||||||
|  |     } catch(e) { | ||||||
|  |       reject(e); | ||||||
|  |     } | ||||||
|  |   }); | ||||||
|  | }; | ||||||
|  | ACME._finalizeOrder = function (me, options, validatedDomains) { | ||||||
|  |   console.log('finalizeOrder:'); | ||||||
|  |   var csr = me.RSA.generateCsrWeb64(options.domainKeypair, validatedDomains); | ||||||
|  |   var body = { csr: csr }; | ||||||
|  |   var payload = JSON.stringify(body); | ||||||
|  | 
 | ||||||
|  |   function pollCert() { | ||||||
|  |     var jws = me.RSA.signJws( | ||||||
|  |       options.accountKeypair | ||||||
|  |     , undefined | ||||||
|  |     , { nonce: me._nonce, alg: 'RS256', url: me._finalize, kid: me._kid } | ||||||
|  |     , new Buffer(payload) | ||||||
|  |     ); | ||||||
|  | 
 | ||||||
|  |     console.log('finalize:', me._finalize); | ||||||
|  |     me._nonce = null; | ||||||
|  |     return me._request({ | ||||||
|  |       method: 'POST' | ||||||
|  |     , url: me._finalize | ||||||
|  |     , headers: { 'Content-Type': 'application/jose+json' } | ||||||
|  |     , json: jws | ||||||
|  |     }).then(function (resp) { | ||||||
|  |       me._nonce = resp.toJSON().headers['replay-nonce']; | ||||||
|  | 
 | ||||||
|  |       console.log('order finalized: resp.body:'); | ||||||
|  |       console.log(resp.body); | ||||||
|  | 
 | ||||||
|  |       if ('processing' === resp.body.status) { | ||||||
|  |         return ACME._wait().then(pollCert); | ||||||
|  |       } | ||||||
|  | 
 | ||||||
|  |       if ('valid' === resp.body.status) { | ||||||
|  |         me._expires = resp.body.expires; | ||||||
|  |         me._certificate = resp.body.certificate; | ||||||
|  | 
 | ||||||
|  |         return resp.body; | ||||||
|  |       } | ||||||
|  | 
 | ||||||
|  |       if ('invalid' === resp.body.status) { | ||||||
|  |         console.error('cannot finalize: badness'); | ||||||
|  |         return; | ||||||
|  |       } | ||||||
|  | 
 | ||||||
|  |       console.error('(x) cannot finalize: badness'); | ||||||
|  |       return; | ||||||
|  |     }); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   return pollCert(); | ||||||
|  | }; | ||||||
|  | ACME._getCertificate = function (me, options) { | ||||||
|  |   console.log('[acme-v2] DEBUG get cert 1'); | ||||||
|  | 
 | ||||||
|  |   if (!options.challengeTypes) { | ||||||
|  |     if (!options.challengeType) { | ||||||
|  |       return Promise.reject(new Error("challenge type must be specified")); | ||||||
|  |     } | ||||||
|  |     options.challengeTypes = [ options.challengeType ]; | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   console.log('[acme-v2] certificates.create'); | ||||||
|  |   return ACME._getNonce(me).then(function () { | ||||||
|  |     var body = { | ||||||
|  |       identifiers: options.domains.map(function (hostname) { | ||||||
|  |         return { type: "dns" , value: hostname }; | ||||||
|  |       }) | ||||||
|  |       //, "notBefore": "2016-01-01T00:00:00Z"
 | ||||||
|  |       //, "notAfter": "2016-01-08T00:00:00Z"
 | ||||||
|  |     }; | ||||||
|  | 
 | ||||||
|  |     var payload = JSON.stringify(body); | ||||||
|  |     var jws = me.RSA.signJws( | ||||||
|  |       options.accountKeypair | ||||||
|  |     , undefined | ||||||
|  |     , { nonce: me._nonce, alg: 'RS256', url: me._directoryUrls.newOrder, kid: me._kid } | ||||||
|  |     , new Buffer(payload) | ||||||
|  |     ); | ||||||
|  | 
 | ||||||
|  |     console.log('\n[DEBUG] newOrder\n'); | ||||||
|  |     me._nonce = null; | ||||||
|  |     return me._request({ | ||||||
|  |       method: 'POST' | ||||||
|  |     , url: me._directoryUrls.newOrder | ||||||
|  |     , headers: { 'Content-Type': 'application/jose+json' } | ||||||
|  |     , json: jws | ||||||
|  |     }).then(function (resp) { | ||||||
|  |       me._nonce = resp.toJSON().headers['replay-nonce']; | ||||||
|  |       var location = resp.toJSON().headers.location; | ||||||
|  |       console.log(location); // the account id url
 | ||||||
|  |       console.log(resp.toJSON()); | ||||||
|  |       me._authorizations = resp.body.authorizations; | ||||||
|  |       me._order = location; | ||||||
|  |       me._finalize = resp.body.finalize; | ||||||
|  |       //console.log('[DEBUG] finalize:', me._finalize); return;
 | ||||||
|  | 
 | ||||||
|  |       //return resp.body;
 | ||||||
|  |       return Promise.all(me._authorizations.map(function (authUrl, i) { | ||||||
|  |         console.log("Authorizations map #" + i); | ||||||
|  |         return ACME._getChallenges(me, options, authUrl).then(function (results) { | ||||||
|  |           // var domain = options.domains[i]; // results.identifier.value
 | ||||||
|  |           var chType = options.challengeTypes.filter(function (chType) { | ||||||
|  |             return results.challenges.some(function (ch) { | ||||||
|  |               return ch.type === chType; | ||||||
|  |             }); | ||||||
|  |           })[0]; | ||||||
|  | 
 | ||||||
|  |           var challenge = results.challenges.filter(function (ch) { | ||||||
|  |             if (chType === ch.type) { | ||||||
|  |               return ch; | ||||||
|  |             } | ||||||
|  |           })[0]; | ||||||
|  | 
 | ||||||
|  |           if (!challenge) { | ||||||
|  |             return Promise.reject(new Error("Server didn't offer any challenge we can handle.")); | ||||||
|  |           } | ||||||
|  | 
 | ||||||
|  |           return ACME._postChallenge(me, options, results.identifier, challenge); | ||||||
|  |         }); | ||||||
|  |       })).then(function () { | ||||||
|  |         var validatedDomains = body.identifiers.map(function (ident) { | ||||||
|  |           return ident.value; | ||||||
|  |         }); | ||||||
|  | 
 | ||||||
|  |         return ACME._finalizeOrder(me, options, validatedDomains); | ||||||
|  |       }).then(function () { | ||||||
|  |         return me._request({ method: 'GET', url: me._certificate, json: true }).then(function (resp) { | ||||||
|  |           console.log('Certificate:'); | ||||||
|  |           console.log(resp.body); | ||||||
|  |           return resp.body; | ||||||
|         }); |         }); | ||||||
|       }); |       }); | ||||||
|  |     }); | ||||||
|  |   }); | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | ACME.create = function create(me) { | ||||||
|  |   if (!me) { me = {}; } | ||||||
|  |   me.acmeChallengePrefix = ACME.acmeChallengePrefix; | ||||||
|  |   me.RSA = me.RSA || require('rsa-compat').RSA; | ||||||
|  |   me.request = me.request || require('request'); | ||||||
|  |   me.promisify = me.promisify || require('util').promisify; | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  |   if ('function' !== typeof me.getUserAgentString) { | ||||||
|  |     me.pkg = me.pkg || require('./package.json'); | ||||||
|  |     me.os = me.os || require('os'); | ||||||
|  |     me.process = me.process || require('process'); | ||||||
|  |     me.userAgent = ACME._getUserAgentString(me); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   function getRequest(opts) { | ||||||
|  |     if (!opts) { opts = {}; } | ||||||
|  | 
 | ||||||
|  |     return me.request.defaults({ | ||||||
|  |       headers: { | ||||||
|  |         'User-Agent': opts.userAgent || me.userAgent || me.getUserAgentString(me) | ||||||
|  |       } | ||||||
|  |     }); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   if ('function' !== typeof me._request) { | ||||||
|  |     me._request = me.promisify(getRequest({})); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   me.init = function (_directoryUrl) { | ||||||
|  |     me.directoryUrl = me.directoryUrl || _directoryUrl; | ||||||
|  |     return ACME._directory(me).then(function (resp) { | ||||||
|  |       me._directoryUrls = resp.body; | ||||||
|  |       me._tos = me._directoryUrls.meta.termsOfService; | ||||||
|  |       return me._directoryUrls; | ||||||
|  |     }); | ||||||
|  |   }; | ||||||
|  |   me.accounts = { | ||||||
|  |     create: function (options) { | ||||||
|  |       return ACME._registerAccount(me, options); | ||||||
|     } |     } | ||||||
|   }; |   }; | ||||||
|   return acme2; |   me.certificates = { | ||||||
| } |     create: function (options) { | ||||||
| 
 |       return ACME._getCertificate(me, options); | ||||||
| module.exports.ACME = { |     } | ||||||
|   create: create |   }; | ||||||
|  |   return me; | ||||||
| }; | }; | ||||||
| Object.keys(defaults).forEach(function (key) { |  | ||||||
|   module.exports.ACME[key] = defaults[key]; |  | ||||||
| }); |  | ||||||
|  | |||||||
							
								
								
									
										75
									
								
								test.cb.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										75
									
								
								test.cb.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,75 @@ | |||||||
|  | 'use strict'; | ||||||
|  | 
 | ||||||
|  | module.exports.run = function run(web, chType, email) { | ||||||
|  |   var RSA = require('rsa-compat').RSA; | ||||||
|  |   var directoryUrl = 'https://acme-staging-v02.api.letsencrypt.org/directory'; | ||||||
|  |   var acme2 = require('./compat').ACME.create({ RSA: RSA }); | ||||||
|  |   // [ 'test.ppl.family' ] 'coolaj86@gmail.com''http-01'
 | ||||||
|  | 	console.log(web, chType, email); | ||||||
|  | 	return; | ||||||
|  |   acme2.init(directoryUrl).then(function (body) { | ||||||
|  |     console.log(body); | ||||||
|  | 		return; | ||||||
|  | 
 | ||||||
|  |     var options = { | ||||||
|  |       agreeToTerms: function (tosUrl, agree) { | ||||||
|  |         agree(null, tosUrl); | ||||||
|  |       } | ||||||
|  |     , setChallenge: function (opts, cb) { | ||||||
|  | 
 | ||||||
|  |         console.log(""); | ||||||
|  |         console.log('identifier:'); | ||||||
|  |         console.log(opts.identifier); | ||||||
|  |         console.log('hostname:'); | ||||||
|  |         console.log(opts.hostname); | ||||||
|  |         console.log('type:'); | ||||||
|  |         console.log(opts.type); | ||||||
|  |         console.log('token:'); | ||||||
|  |         console.log(opts.token); | ||||||
|  |         console.log('thumbprint:'); | ||||||
|  |         console.log(opts.thumbprint); | ||||||
|  |         console.log('keyAuthorization:'); | ||||||
|  |         console.log(opts.keyAuthorization); | ||||||
|  |         console.log('dnsAuthorization:'); | ||||||
|  |         console.log(opts.dnsAuthorization); | ||||||
|  |         console.log(""); | ||||||
|  | 
 | ||||||
|  |         console.log("Put the string '" + opts.keyAuthorization + "' into a file at '" + opts.hostname + "/" + opts.token + "'"); | ||||||
|  |         console.log("\nThen hit the 'any' key to continue (must be specifically the 'any' key)..."); | ||||||
|  | 
 | ||||||
|  |         function onAny() { | ||||||
|  |           process.stdin.pause(); | ||||||
|  |           process.stdin.removeEventListener('data', onAny); | ||||||
|  |           process.stdin.setRawMode(false); | ||||||
|  |           cb(); | ||||||
|  |         } | ||||||
|  |         process.stdin.setRawMode(true); | ||||||
|  |         process.stdin.resume(); | ||||||
|  |         process.stdin.on('data', onAny); | ||||||
|  |       } | ||||||
|  |     , removeChallenge: function (opts, cb) { | ||||||
|  | 				// hostname, key
 | ||||||
|  |         console.log('[DEBUG] remove challenge', hostname, key); | ||||||
|  |         setTimeout(cb, 1 * 1000); | ||||||
|  |       } | ||||||
|  |     , challengeType: chType | ||||||
|  |     , email: email | ||||||
|  |     , accountKeypair: RSA.import({ privateKeyPem: require('fs').readFileSync(__dirname + '/account.privkey.pem') }) | ||||||
|  |     , domainKeypair: RSA.import({ privateKeyPem: require('fs').readFileSync(__dirname + '/privkey.pem') }) | ||||||
|  |     , domains: web | ||||||
|  |     }; | ||||||
|  | 
 | ||||||
|  |     acme2.registerNewAccount(options).then(function (account) { | ||||||
|  |       console.log('account:'); | ||||||
|  |       console.log(account); | ||||||
|  | 
 | ||||||
|  |       acme2.getCertificate(options, function (fullchainPem) { | ||||||
|  |         console.log('[acme-v2] A fullchain.pem:'); | ||||||
|  |         console.log(fullchainPem); | ||||||
|  |       }).then(function (fullchainPem) { | ||||||
|  |         console.log('[acme-v2] B fullchain.pem:'); | ||||||
|  |         console.log(fullchainPem); | ||||||
|  |       }); | ||||||
|  |     }); | ||||||
|  |   }); | ||||||
|  | }; | ||||||
							
								
								
									
										57
									
								
								test.compat.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										57
									
								
								test.compat.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,57 @@ | |||||||
|  | 'use strict'; | ||||||
|  | 
 | ||||||
|  | var RSA = require('rsa-compat').RSA; | ||||||
|  | 
 | ||||||
|  | module.exports.run = function (web, chType, email) { | ||||||
|  |   console.log('[DEBUG] run', web, chType, email); | ||||||
|  | 
 | ||||||
|  |   var acme2 = require('./compat.js').ACME.create({ RSA: RSA }); | ||||||
|  |   acme2.getAcmeUrls(acme2.stagingServerUrl, function (err, body) { | ||||||
|  |     if (err) { console.log('err 1'); throw err; } | ||||||
|  |     console.log(body); | ||||||
|  | 
 | ||||||
|  |     var options = { | ||||||
|  |       agreeToTerms: function (tosUrl, agree) { | ||||||
|  |         agree(null, tosUrl); | ||||||
|  |       } | ||||||
|  |     , setChallenge: function (hostname, token, val, cb) { | ||||||
|  |         console.log("Put the string '" + val + "' into a file at '" + hostname + "/" + acme2.acmeChallengePrefix + "/" + token + "'"); | ||||||
|  |         console.log("echo '" + val + "' > '" + hostname + "/" + acme2.acmeChallengePrefix + "/" + token + "'"); | ||||||
|  |         console.log("\nThen hit the 'any' key to continue (must be specifically the 'any' key)..."); | ||||||
|  | 
 | ||||||
|  |         function onAny() { | ||||||
|  |           console.log("'any' key was hit"); | ||||||
|  |           process.stdin.pause(); | ||||||
|  |           process.stdin.removeListener('data', onAny); | ||||||
|  |           process.stdin.setRawMode(false); | ||||||
|  |           cb(); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         process.stdin.setRawMode(true); | ||||||
|  |         process.stdin.resume(); | ||||||
|  |         process.stdin.on('data', onAny); | ||||||
|  |       } | ||||||
|  |     , removeChallenge: function (hostname, key, cb) { | ||||||
|  |         console.log('[DEBUG] remove challenge', hostname, key); | ||||||
|  |         setTimeout(cb, 1 * 1000); | ||||||
|  |       } | ||||||
|  |     , challengeType: chType | ||||||
|  |     , email: email | ||||||
|  |     , accountKeypair: RSA.import({ privateKeyPem: require('fs').readFileSync(__dirname + '/account.privkey.pem') }) | ||||||
|  |     , domainKeypair: RSA.import({ privateKeyPem: require('fs').readFileSync(__dirname + '/privkey.pem') }) | ||||||
|  |     , domains: web | ||||||
|  |     }; | ||||||
|  | 
 | ||||||
|  |     acme2.registerNewAccount(options, function (err, account) { | ||||||
|  |       if (err) { console.log('err 2'); throw err; } | ||||||
|  |       console.log('account:'); | ||||||
|  |       console.log(account); | ||||||
|  | 
 | ||||||
|  |       acme2.getCertificate(options, function (err, fullchainPem) { | ||||||
|  |         if (err) { console.log('err 3'); throw err; } | ||||||
|  |         console.log('[acme-v2] A fullchain.pem:'); | ||||||
|  |         console.log(fullchainPem); | ||||||
|  |       }); | ||||||
|  |     }); | ||||||
|  |   }); | ||||||
|  | }; | ||||||
							
								
								
									
										95
									
								
								test.js
									
									
									
									
									
								
							
							
						
						
									
										95
									
								
								test.js
									
									
									
									
									
								
							| @ -1,56 +1,45 @@ | |||||||
| 'use strict'; | 'use strict'; | ||||||
| 
 | 
 | ||||||
| var RSA = require('rsa-compat').RSA; | var readline = require('readline'); | ||||||
| var acme2 = require('./').ACME.create({ RSA: RSA }); | var rl = readline.createInterface({ | ||||||
| 
 |   input: process.stdin, | ||||||
| acme2.getAcmeUrls(acme2.stagingServerUrl).then(function (body) { |   output: process.stdout | ||||||
|   console.log(body); |  | ||||||
| 
 |  | ||||||
|   var options = { |  | ||||||
|     agreeToTerms: function (tosUrl, agree) { |  | ||||||
|       agree(null, tosUrl); |  | ||||||
|     } |  | ||||||
|     /* |  | ||||||
|   , setupChallenge: function (opts) { |  | ||||||
|       console.log('type:'); |  | ||||||
|       console.log(ch.type); |  | ||||||
|       console.log('ch.token:'); |  | ||||||
|       console.log(ch.token); |  | ||||||
|       console.log('thumbprint:'); |  | ||||||
|       console.log(thumbprint); |  | ||||||
|       console.log('keyAuthorization:'); |  | ||||||
|       console.log(keyAuthorization); |  | ||||||
|       console.log('dnsAuthorization:'); |  | ||||||
|       console.log(dnsAuthorization); |  | ||||||
|     } |  | ||||||
|     */ |  | ||||||
|     // teardownChallenge
 |  | ||||||
|   , setChallenge: function (hostname, key, val, cb) { |  | ||||||
|       console.log('[DEBUG] set challenge', hostname, key, val); |  | ||||||
|       console.log("You have 20 seconds to put the string '" + val + "' into a file at '" + hostname + "/" + key + "'"); |  | ||||||
|       setTimeout(cb, 20 * 1000); |  | ||||||
|     } |  | ||||||
|   , removeChallenge: function (hostname, key, cb) { |  | ||||||
|       console.log('[DEBUG] remove challenge', hostname, key); |  | ||||||
|       setTimeout(cb, 1 * 1000); |  | ||||||
|     } |  | ||||||
|   , challengeType: 'http-01' |  | ||||||
|   , email: 'coolaj86@gmail.com' |  | ||||||
|   , accountKeypair: RSA.import({ privateKeyPem: require('fs').readFileSync(__dirname + '/account.privkey.pem') }) |  | ||||||
|   , domainKeypair: RSA.import({ privateKeyPem: require('fs').readFileSync(__dirname + '/privkey.pem') }) |  | ||||||
|   , domains: [ 'test.ppl.family' ] |  | ||||||
|   }; |  | ||||||
| 
 |  | ||||||
|   acme2.registerNewAccount(options).then(function (account) { |  | ||||||
|     console.log('account:'); |  | ||||||
|     console.log(account); |  | ||||||
| 
 |  | ||||||
|     acme2.getCertificate(options, function (fullchainPem) { |  | ||||||
|       console.log('[acme-v2] A fullchain.pem:'); |  | ||||||
|       console.log(fullchainPem); |  | ||||||
|     }).then(function (fullchainPem) { |  | ||||||
|       console.log('[acme-v2] B fullchain.pem:'); |  | ||||||
|       console.log(fullchainPem); |  | ||||||
|     }); |  | ||||||
|   }); |  | ||||||
| }); | }); | ||||||
|  | 
 | ||||||
|  | function getWeb() { | ||||||
|  | 	rl.question('What web address(es) would you like to get certificates for? (ex: example.com,*.example.com) ', function (web) { | ||||||
|  | 		web = (web||'').trim().split(/,/g); | ||||||
|  | 		if (!web[0]) { getWeb(); return; } | ||||||
|  | 
 | ||||||
|  |     if (web.some(function (w) { return '*' === w[0]; })) { | ||||||
|  |       console.log('Wildcard domains must use dns-01'); | ||||||
|  |       getEmail(web, 'dns-01'); | ||||||
|  |     } else { | ||||||
|  |       getChallengeType(web); | ||||||
|  |     } | ||||||
|  | 	}); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | function getChallengeType(web) { | ||||||
|  | 	rl.question('What challenge will you be testing today? http-01 or dns-01? [http-01] ', function (chType) { | ||||||
|  | 		chType = (chType||'').trim(); | ||||||
|  | 		if (!chType) { chType = 'http-01'; } | ||||||
|  | 
 | ||||||
|  | 		getEmail(web, chType); | ||||||
|  | 	}); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | function getEmail(web, chType) { | ||||||
|  | 	rl.question('What email should we use? (optional) ', function (email) { | ||||||
|  | 		email = (email||'').trim(); | ||||||
|  | 		if (!email) { email = null; } | ||||||
|  | 
 | ||||||
|  |     rl.close(); | ||||||
|  |     console.log("[DEBUG] rl blah blah"); | ||||||
|  |     require('./test.compat.js').run(web, chType, email); | ||||||
|  |     //require('./test.cb.js').run(web, chType, email);
 | ||||||
|  |     //require('./test.promise.js').run(web, chType, email);
 | ||||||
|  | 	}); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | getWeb(); | ||||||
|  | |||||||
							
								
								
									
										84
									
								
								test.promise.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										84
									
								
								test.promise.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,84 @@ | |||||||
|  | 'use strict'; | ||||||
|  | 
 | ||||||
|  | /* global Promise */ | ||||||
|  | 
 | ||||||
|  | module.exports.run = function run(web, chType, email) { | ||||||
|  |   var RSA = require('rsa-compat').RSA; | ||||||
|  |   var directoryUrl = 'https://acme-staging-v02.api.letsencrypt.org/directory'; | ||||||
|  |   var acme2 = require('./compat').ACME.create({ RSA: RSA }); | ||||||
|  |   // [ 'test.ppl.family' ] 'coolaj86@gmail.com''http-01'
 | ||||||
|  | 	console.log(web, chType, email); | ||||||
|  | 	return; | ||||||
|  |   acme2.init(directoryUrl).then(function (body) { | ||||||
|  |     console.log(body); | ||||||
|  | 		return; | ||||||
|  | 
 | ||||||
|  |     var options = { | ||||||
|  |       agreeToTerms: function (tosUrl, agree) { | ||||||
|  |         agree(null, tosUrl); | ||||||
|  |       } | ||||||
|  |     , setChallenge: function (opts) { | ||||||
|  | 
 | ||||||
|  |         console.log(""); | ||||||
|  |         console.log('identifier:'); | ||||||
|  |         console.log(opts.identifier); | ||||||
|  |         console.log('hostname:'); | ||||||
|  |         console.log(opts.hostname); | ||||||
|  |         console.log('type:'); | ||||||
|  |         console.log(opts.type); | ||||||
|  |         console.log('token:'); | ||||||
|  |         console.log(opts.token); | ||||||
|  |         console.log('thumbprint:'); | ||||||
|  |         console.log(opts.thumbprint); | ||||||
|  |         console.log('keyAuthorization:'); | ||||||
|  |         console.log(opts.keyAuthorization); | ||||||
|  |         console.log('dnsAuthorization:'); | ||||||
|  |         console.log(opts.dnsAuthorization); | ||||||
|  |         console.log(""); | ||||||
|  | 
 | ||||||
|  |         console.log("Put the string '" + opts.keyAuthorization + "' into a file at '" + opts.hostname + "/" + opts.token + "'"); | ||||||
|  |         console.log("\nThen hit the 'any' key to continue (must be specifically the 'any' key)..."); | ||||||
|  | 
 | ||||||
|  |         return new Promise(function (resolve) { | ||||||
|  |           function onAny() { | ||||||
|  |             process.stdin.pause(); | ||||||
|  |             process.stdin.removeEventListener('data', onAny); | ||||||
|  |             process.stdin.setRawMode(false); | ||||||
|  | 
 | ||||||
|  |             resolve(); | ||||||
|  |           } | ||||||
|  |           process.stdin.setRawMode(true); | ||||||
|  |           process.stdin.resume(); | ||||||
|  |           process.stdin.on('data', onAny); | ||||||
|  |         }); | ||||||
|  |       } | ||||||
|  |     , removeChallenge: function (opts) { | ||||||
|  | 				// hostname, key
 | ||||||
|  |         console.log('[DEBUG] remove challenge', opts.hostname, opts.keyAuthorization); | ||||||
|  |         console.log("Remove the file '" + opts.hostname + "/" + opts.token + "'"); | ||||||
|  | 
 | ||||||
|  |         return new Promise(function (resolve) { | ||||||
|  |           setTimeout(resolve, 1 * 1000); | ||||||
|  |         }); | ||||||
|  |       } | ||||||
|  |     , challengeType: chType | ||||||
|  |     , email: email | ||||||
|  |     , accountKeypair: RSA.import({ privateKeyPem: require('fs').readFileSync(__dirname + '/account.privkey.pem') }) | ||||||
|  |     , domainKeypair: RSA.import({ privateKeyPem: require('fs').readFileSync(__dirname + '/privkey.pem') }) | ||||||
|  |     , domains: web | ||||||
|  |     }; | ||||||
|  | 
 | ||||||
|  |     acme2.registerNewAccount(options).then(function (account) { | ||||||
|  |       console.log('account:'); | ||||||
|  |       console.log(account); | ||||||
|  | 
 | ||||||
|  |       acme2.getCertificate(options, function (fullchainPem) { | ||||||
|  |         console.log('[acme-v2] A fullchain.pem:'); | ||||||
|  |         console.log(fullchainPem); | ||||||
|  |       }).then(function (fullchainPem) { | ||||||
|  |         console.log('[acme-v2] B fullchain.pem:'); | ||||||
|  |         console.log(fullchainPem); | ||||||
|  |       }); | ||||||
|  |     }); | ||||||
|  |   }); | ||||||
|  | }; | ||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user