mirror of
				https://github.com/therootcompany/acme.js.git
				synced 2024-11-16 17:29:00 +00:00 
			
		
		
		
	v1.8: transitional support for v2.0
This commit is contained in:
		
							parent
							
								
									dfbee8aa79
								
							
						
					
					
						commit
						e6497fe34b
					
				
							
								
								
									
										19
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										19
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							| @ -1,32 +1,17 @@ | ||||
| .env | ||||
| 
 | ||||
| *.pem | ||||
| letsencrypt.work | ||||
| letsencrypt.logs | ||||
| letsencrypt.config | ||||
| 
 | ||||
| # Logs | ||||
| logs | ||||
| *.log | ||||
| 
 | ||||
| # Runtime data | ||||
| pids | ||||
| *.pid | ||||
| *.seed | ||||
| 
 | ||||
| # Directory for instrumented libs generated by jscoverage/JSCover | ||||
| lib-cov | ||||
| 
 | ||||
| # Coverage directory used by tools like istanbul | ||||
| coverage | ||||
| 
 | ||||
| # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) | ||||
| .grunt | ||||
| 
 | ||||
| # node-waf configuration | ||||
| .lock-wscript | ||||
| 
 | ||||
| # Compiled binary addons (http://nodejs.org/api/addons.html) | ||||
| build/Release | ||||
| 
 | ||||
| # Dependency directory | ||||
| # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git | ||||
| node_modules | ||||
|  | ||||
							
								
								
									
										224
									
								
								README.md
									
									
									
									
									
								
							
							
						
						
									
										224
									
								
								README.md
									
									
									
									
									
								
							| @ -2,28 +2,77 @@ | ||||
| | [acme-v2-cli.js](https://git.coolaj86.com/coolaj86/acme-v2-cli.js) | ||||
| | [greenlock.js](https://git.coolaj86.com/coolaj86/greenlock.js) | ||||
| | [goldilocks.js](https://git.coolaj86.com/coolaj86/goldilocks.js) | ||||
| | | ||||
| 
 | ||||
| | A [Root](https://therootcompany.com) Project | ||||
| # [acme-v2.js](https://git.coolaj86.com/coolaj86/acme-v2.js) | a [Root](https://therootcompany.com) project | ||||
| 
 | ||||
| # [acme-v2.js](https://git.coolaj86.com/coolaj86/acme-v2.js) | ||||
| A **Zero (External) Dependency**\* library for building | ||||
| Let's Encrypt v2 (ACME draft 18) clients and getting Free SSL certificates. | ||||
| 
 | ||||
| A lightweight, **Low Dependency**\* framework for building | ||||
| Let's Encrypt v2 (ACME draft 12) clients, successor to `le-acme-core.js`. | ||||
| Built [by request](https://git.coolaj86.com/coolaj86/greenlock.js/issues/5#issuecomment-8). | ||||
| The primary goal of this library is to make it easy to | ||||
| get Accounts and Certificates through Let's Encrypt. | ||||
| 
 | ||||
| \* <small>although `node-forge` and `ursa` are included as `optionalDependencies` | ||||
| for backwards compatibility with older versions of node, there are no other | ||||
| dependencies except those that I wrote for this (and related) projects.</small> | ||||
| # Features | ||||
| 
 | ||||
| - [x] Let's Encrypt™ v2 / ACME Draft 12 | ||||
|   - [ ] (in-progress) Let's Encrypt™ v2.1 / ACME Draft 18 | ||||
|   - [ ] (in-progress) StartTLS Everywhere™ | ||||
| - [x] Works with any [generic ACME challenge handler](https://git.rootprojects.org/root/acme-challenge-test.js) | ||||
|   - [x] **http-01** for single or multiple domains per certificate | ||||
|   - [x] **dns-01** for wildcards, localhost, private networks, etc | ||||
| - [x] VanillaJS | ||||
|   - [x] Zero External Dependencies | ||||
|   - [x] Safe, Efficient, Maintained | ||||
|   - [x] Works in Node v6+ | ||||
|   - [ ] (v2) Works in Web Browsers (See [Demo](https://greenlock.domains)) | ||||
| 
 | ||||
| \* <small>The only required dependencies were built by us, specifically for this and related libraries. | ||||
| There are some, truly optional, backwards-compatibility dependencies for node v6.</small> | ||||
| 
 | ||||
| ## Looking for Quick 'n' Easy™? | ||||
| 
 | ||||
| If you're looking to _build a webserver_, try [greenlock.js](https://git.coolaj86.com/coolaj86/greenlock.js). | ||||
| If you're looking for an _ACME-enabled webserver_, try [goldilocks.js](https://git.coolaj86.com/coolaj86/goldilocks.js). | ||||
| If you want something that's more "batteries included" give | ||||
| [greenlock.js](https://git.coolaj86.com/coolaj86/greenlock.js) | ||||
| a try. | ||||
| 
 | ||||
| - [greenlock.js](https://git.coolaj86.com/coolaj86/greenlock.js) | ||||
| - [goldilocks.js](https://git.coolaj86.com/coolaj86/goldilocks.js) | ||||
| 
 | ||||
| ## v1.7+: Transitional v2 Support | ||||
| 
 | ||||
| By the end of June 2019 we expect to have completed the migration to Let's Encrypt v2.1 (ACME draft 18). | ||||
| 
 | ||||
| Although the draft 18 changes themselves don't requiring breaking the API, | ||||
| we've been keeping backwards compatibility for a long time and the API has become messy. | ||||
| 
 | ||||
| We're taking this **mandatory ACME update** as an opportunity to **clean up** and **greatly simplify** | ||||
| the code with a fresh new release. | ||||
| 
 | ||||
| As of **v1.7** we started adding **transitional support** for the **next major version**, v2.0 of acme-v2.js. | ||||
| We've been really good about backwards compatibility for | ||||
| 
 | ||||
| ## Recommended Example | ||||
| 
 | ||||
| Due to the upcoming changes we've removed the old documentation. | ||||
| 
 | ||||
| Instead we recommend that you take a look at the | ||||
| [Digital Ocean DNS-01 Example](https://git.rootprojects.org/root/acme-v2.js/src/branch/master/examples/dns-01-digitalocean.js) | ||||
| 
 | ||||
| - [examples/dns-01-digitalocean.js](https://git.rootprojects.org/root/acme-v2.js/src/branch/master/examples/dns-01-digitalocean.js) | ||||
| 
 | ||||
| That's not exactly the new API, but it's close. | ||||
| 
 | ||||
| ## Let's Encrypt v02 Directory URLs | ||||
| 
 | ||||
| ``` | ||||
| # Production URL | ||||
| https://acme-v02.api.letsencrypt.org/directory | ||||
| ``` | ||||
| 
 | ||||
| ``` | ||||
| # Staging URL | ||||
| https://acme-staging-v02.api.letsencrypt.org/directory | ||||
| ``` | ||||
| 
 | ||||
| <!-- | ||||
| ## How to build ACME clients | ||||
| 
 | ||||
| As this is intended to build ACME clients, there is not a simple 2-line example | ||||
| @ -63,136 +112,75 @@ examples/https-server.js | ||||
| examples/http-server.js | ||||
| ``` | ||||
| 
 | ||||
| ## Let's Encrypt Directory URLs | ||||
| --> | ||||
| 
 | ||||
| ``` | ||||
| # Production URL | ||||
| https://acme-v02.api.letsencrypt.org/directory | ||||
| ``` | ||||
| ## API | ||||
| 
 | ||||
| ``` | ||||
| # Staging URL | ||||
| https://acme-staging-v02.api.letsencrypt.org/directory | ||||
| ``` | ||||
| 
 | ||||
| ## Two API versions, Two Implementations | ||||
| 
 | ||||
| This library (acme-v2.js) supports ACME [_draft 11_](https://tools.ietf.org/html/draft-ietf-acme-acme-11), | ||||
| otherwise known as Let's Encrypt v2 (or v02). | ||||
| 
 | ||||
| - ACME draft 11 | ||||
| - Let's Encrypt v2 | ||||
| - Let's Encrypt v02 | ||||
| 
 | ||||
| The predecessor (le-acme-core) supports Let's Encrypt v1 (or v01), which was a | ||||
| [hodge-podge of various drafts](https://github.com/letsencrypt/boulder/blob/master/docs/acme-divergences.md) | ||||
| of the ACME spec early on. | ||||
| 
 | ||||
| - ACME early draft | ||||
| - Let's Encrypt v1 | ||||
| - Let's Encrypt v01 | ||||
| 
 | ||||
| This library maintains compatibility with le-acme-core so that it can be used as a **drop-in replacement** | ||||
| and requires **no changes to existing code**, | ||||
| but also provides an updated API more congruent with draft 11. | ||||
| 
 | ||||
| ## le-acme-core-compatible API (recommended) | ||||
| 
 | ||||
| Status: Stable, Locked, Bugfix-only | ||||
| 
 | ||||
| See Full Documentation at <https://git.coolaj86.com/coolaj86/le-acme-core.js> | ||||
| 
 | ||||
| ```js | ||||
| var RSA = require('rsa-compat').RSA; | ||||
| var acme = require('acme-v2/compat.js').ACME.create({ RSA: RSA }); | ||||
| 
 | ||||
| // | ||||
| // Use exactly the same as le-acme-core | ||||
| // | ||||
| ``` | ||||
| 
 | ||||
| ## Promise API (dev) | ||||
| 
 | ||||
| Status: Almost stable, but **not semver locked** | ||||
| Status: Small, but breaking changes coming in v2 | ||||
| 
 | ||||
| This API is a simple evolution of le-acme-core, | ||||
| but tries to provide a better mapping to the new draft 11 APIs. | ||||
| 
 | ||||
| ```js | ||||
| // Create Instance (Dependency Injection) | ||||
| var ACME = require('acme-v2').ACME.create({ | ||||
|   RSA: require('rsa-compat').RSA | ||||
| 	// used for overriding the default user-agent | ||||
| 	userAgent: 'My custom UA String', | ||||
| 	getUserAgentString: function(deps) { | ||||
| 		return 'My custom UA String'; | ||||
| 	}, | ||||
| 
 | ||||
|   // other overrides | ||||
| , request: require('request') | ||||
| , promisify: require('util').promisify | ||||
| 	// don't try to validate challenges locally | ||||
| 	skipChallengeTest: false, | ||||
| 	skipDryRun: false, | ||||
| 
 | ||||
|   // 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'; } | ||||
| 
 | ||||
| 
 | ||||
|   // don't try to validate challenges locally | ||||
| , skipChallengeTest: false | ||||
|   // ask if the certificate can be issued up to 10 times before failing | ||||
| , retryPoll: 8 | ||||
|   // ask if the certificate has been validated up to 6 times before cancelling | ||||
| , retryPending: 4 | ||||
|   // Wait 1000ms between retries | ||||
| , retryInterval: 1000 | ||||
|   // Wait 10,000ms after deauthorizing a challenge before retrying | ||||
| , deauthWait: 10 * 1000 | ||||
| 	// ask if the certificate can be issued up to 10 times before failing | ||||
| 	retryPoll: 8, | ||||
| 	// ask if the certificate has been validated up to 6 times before cancelling | ||||
| 	retryPending: 4, | ||||
| 	// Wait 1000ms between retries | ||||
| 	retryInterval: 1000, | ||||
| 	// Wait 10,000ms after deauthorizing a challenge before retrying | ||||
| 	deauthWait: 10 * 1000 | ||||
| }); | ||||
| 
 | ||||
| 
 | ||||
| // Discover Directory URLs | ||||
| ACME.init(acmeDirectoryUrl)                   // returns Promise<acmeUrls={keyChange,meta,newAccount,newNonce,newOrder,revokeCert}> | ||||
| 
 | ||||
| ACME.init(acmeDirectoryUrl); // returns Promise<acmeUrls={keyChange,meta,newAccount,newNonce,newOrder,revokeCert}> | ||||
| 
 | ||||
| // Accounts | ||||
| ACME.accounts.create(options)                 // returns Promise<regr> registration data | ||||
| 
 | ||||
|     { email: '<email>'                        //    valid email (server checks MX records) | ||||
|     , accountKeypair: {                       //    privateKeyPem or privateKeyJwt | ||||
|         privateKeyPem: '<ASCII PEM>' | ||||
|       } | ||||
|     , agreeToTerms: fn (tosUrl) {}            //    returns Promise with tosUrl | ||||
|     } | ||||
| ACME.accounts.create(options); // returns Promise<regr> registration data | ||||
| 
 | ||||
| options = { | ||||
| 	email: '<email>', // valid email (server checks MX records) | ||||
| 	accountKeypair: { | ||||
| 		//    privateKeyPem or privateKeyJwt | ||||
| 		privateKeyPem: '<ASCII PEM>' | ||||
| 	}, | ||||
| 	agreeToTerms: function(tosUrl) {} //    should Promise the same `tosUrl` back | ||||
| }; | ||||
| 
 | ||||
| // Registration | ||||
| ACME.certificates.create(options)             // returns Promise<pems={ privkey (key), cert, chain (ca) }> | ||||
| ACME.certificates.create(options); // returns Promise<pems={ privkey (key), cert, chain (ca) }> | ||||
| 
 | ||||
|     { newAuthzUrl: '<url>'                    //    specify acmeUrls.newAuthz | ||||
|     , newCertUrl: '<url>'                     //    specify acmeUrls.newCert | ||||
| options = { | ||||
| 	domainKeypair: { | ||||
| 		privateKeyPem: '<ASCII PEM>' | ||||
| 	}, | ||||
| 	accountKeypair: { | ||||
| 		privateKeyPem: '<ASCII PEM>' | ||||
| 	}, | ||||
| 	domains: ['example.com'], | ||||
| 
 | ||||
|     , domainKeypair: { | ||||
|         privateKeyPem: '<ASCII PEM>' | ||||
|       } | ||||
|     , accountKeypair: { | ||||
|         privateKeyPem: '<ASCII PEM>' | ||||
|       } | ||||
|     , domains: [ 'example.com' ] | ||||
| 
 | ||||
|     , setChallenge: fn (hostname, key, val)   // return Promise | ||||
|     , removeChallenge: fn (hostname, key)     // return Promise | ||||
|     } | ||||
| ``` | ||||
| 
 | ||||
| Helpers & Stuff | ||||
| 
 | ||||
| ```javascript | ||||
| // Constants | ||||
| ACME.challengePrefixes['http-01']; // '/.well-known/acme-challenge' | ||||
| ACME.challengePrefixes['dns-01']; // '_acme-challenge' | ||||
| 	getZones: function(opts) {}, // should Promise an array of domain zone names | ||||
| 	setChallenge: function(opts) {}, // should Promise the record id, or name | ||||
| 	removeChallenge: function(opts) {} // should Promise null | ||||
| }; | ||||
| ``` | ||||
| 
 | ||||
| # Changelog | ||||
| 
 | ||||
| - v1.8 | ||||
|   - more transitional prepwork for new v2 API | ||||
|   - support newer (simpler) dns-01 and http-01 libraries | ||||
| - v1.5 | ||||
|   - perform full test challenge first (even before nonce) | ||||
| - v1.3 | ||||
|  | ||||
							
								
								
									
										69
									
								
								examples/dns-01-digitalocean.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										69
									
								
								examples/dns-01-digitalocean.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,69 @@ | ||||
| (function(exports) { | ||||
| 	'use strict'; | ||||
| 
 | ||||
| 	// node[0] ./test.js[1] jon.doe@gmail.com[2] example.com,*.example.com[3] xxxxxx[4]
 | ||||
| 	var email = process.argv[2] || process.env.ACME_EMAIL; | ||||
| 	var domains = (process.argv[3] || process.env.ACME_DOMAINS).split(/[,\s]+/); | ||||
| 	var token = process.argv[4] || process.env.DIGITALOCEAN_API_KEY; | ||||
| 
 | ||||
| 	// git clone https://git.rootprojects.org/root/acme-dns-01-digitalocean.js node_modules/acme-dns-01-digitalocean
 | ||||
| 	var dns01 = require('acme-dns-01-digitalocean').create({ | ||||
| 		//baseUrl: 'https://api.digitalocean.com/v2/domains',
 | ||||
| 		token: token | ||||
| 	}); | ||||
| 
 | ||||
| 	// This will be replaced with Keypairs.js in the next version
 | ||||
| 	var promisify = require('util').promisify; | ||||
| 	var generateKeypair = promisify(require('rsa-compat').RSA.generateKeypair); | ||||
| 
 | ||||
| 	//var ACME = exports.ACME || require('acme').ACME;
 | ||||
| 	var ACME = exports.ACME || require('../').ACME; | ||||
| 	var acme = ACME.create({}); | ||||
| 	acme | ||||
| 		.init({ | ||||
| 			//directoryUrl: 'https://acme-staging-v02.api.letsencrypt.org/directory'
 | ||||
| 		}) | ||||
| 		.then(function() { | ||||
| 			return generateKeypair(null).then(function(accountPair) { | ||||
| 				return generateKeypair(null).then(function(serverPair) { | ||||
| 					return acme.accounts | ||||
| 						.create({ | ||||
| 							// valid email (server checks MX records)
 | ||||
| 							email: email, | ||||
| 							accountKeypair: accountPair, | ||||
| 							agreeToTerms: function(tosUrl) { | ||||
| 								// ask user (if user is the host)
 | ||||
| 								return tosUrl; | ||||
| 							} | ||||
| 						}) | ||||
| 						.then(function(account) { | ||||
| 							console.info('Created Account:'); | ||||
| 							console.info(account); | ||||
| 
 | ||||
| 							return acme.certificates | ||||
| 								.create({ | ||||
| 									domains: domains, | ||||
| 									challenges: { 'dns-01': dns01 }, | ||||
| 									domainKeypair: serverPair, | ||||
| 									accountKeypair: accountPair, | ||||
| 
 | ||||
| 									// v2 will be directly compatible with the new ACME modules,
 | ||||
| 									// whereas this version needs a shim
 | ||||
| 									getZones: dns01.zones, | ||||
| 									setChallenge: dns01.set, | ||||
| 									removeChallenge: dns01.remove | ||||
| 								}) | ||||
| 								.then(function(certs) { | ||||
| 									console.info('Secured SSL Certificates'); | ||||
| 									console.info(certs); | ||||
| 								}); | ||||
| 						}); | ||||
| 				}); | ||||
| 			}); | ||||
| 		}) | ||||
| 		.catch(function(e) { | ||||
| 			console.error('Something went wrong:'); | ||||
| 			console.error(e); | ||||
| 			process.exit(500); | ||||
| 		}); | ||||
| })('undefined' === typeof module ? window : module.exports); | ||||
							
								
								
									
										3
									
								
								examples/example.env
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								examples/example.env
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,3 @@ | ||||
| ACME_EMAIL=jon.doe@gmail.com | ||||
| ACME_DOMAINS=example.com,foo.example.com,*.foo.example.com | ||||
| DIGITALOCEAN_API_KEY=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx | ||||
							
								
								
									
										451
									
								
								node.js
									
									
									
									
									
								
							
							
						
						
									
										451
									
								
								node.js
									
									
									
									
									
								
							| @ -276,7 +276,10 @@ ACME._registerAccount = function(me, options) { | ||||
| 			} | ||||
| 			if (1 === options.agreeToTerms.length) { | ||||
| 				// newer promise API
 | ||||
| 				return options.agreeToTerms(me._tos).then(agree, reject); | ||||
| 				return Promise.resolve(options.agreeToTerms(me._tos)).then( | ||||
| 					agree, | ||||
| 					reject | ||||
| 				); | ||||
| 			} else if (2 === options.agreeToTerms.length) { | ||||
| 				// backwards compat cb API
 | ||||
| 				return options.agreeToTerms(me._tos, function(err, tosUrl) { | ||||
| @ -461,6 +464,58 @@ ACME._chooseChallenge = function(options, results) { | ||||
| 
 | ||||
| 	return challenge; | ||||
| }; | ||||
| ACME._getZones = function(me, options, dnsHosts) { | ||||
| 	if ('function' !== typeof options.getZones) { | ||||
| 		options.getZones = function() { | ||||
| 			return Promise.resolve([]); | ||||
| 		}; | ||||
| 	} | ||||
| 	return new Promise(function(resolve, reject) { | ||||
| 		try { | ||||
| 			if (options.getZones.length <= 1) { | ||||
| 				options | ||||
| 					.getZones({ dnsHosts: dnsHosts }) | ||||
| 					.then(resolve) | ||||
| 					.catch(reject); | ||||
| 			} else if (2 === options.getZones.length) { | ||||
| 				options.getZones({ dnsHosts: dnsHosts }, function(err, zonenames) { | ||||
| 					if (err) { | ||||
| 						reject(err); | ||||
| 					} else { | ||||
| 						resolve(zonenames); | ||||
| 					} | ||||
| 				}); | ||||
| 			} else { | ||||
| 				throw new Error( | ||||
| 					'options.getZones should accept opts and Promise an array of zone names' | ||||
| 				); | ||||
| 			} | ||||
| 		} catch (e) { | ||||
| 			reject(e); | ||||
| 		} | ||||
| 	}); | ||||
| }; | ||||
| 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]; | ||||
| } | ||||
| ACME._challengeToAuth = function(me, options, request, challenge, dryrun) { | ||||
| 	// we don't poison the dns cache with our dummy request
 | ||||
| 	var dnsPrefix = ACME.challengePrefixes['dns-01']; | ||||
| @ -490,6 +545,7 @@ ACME._challengeToAuth = function(me, options, request, challenge, dryrun) { | ||||
| 		auth[key] = challenge[key]; | ||||
| 	}); | ||||
| 
 | ||||
| 	var zone = pluckZone(options.zonenames || [], auth.identifier.value); | ||||
| 	// 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
 | ||||
| @ -511,7 +567,15 @@ ACME._challengeToAuth = function(me, options, request, challenge, dryrun) { | ||||
| 			.update(auth.keyAuthorization) | ||||
| 			.digest('base64') | ||||
| 	); | ||||
| 	if (zone) { | ||||
| 		auth.dnsZone = zone; | ||||
| 		auth.dnsPrefix = auth.dnsHost | ||||
| 			.replace(newZoneRegExp(zone), '') | ||||
| 			.replace(/\.$/, ''); | ||||
| 	} | ||||
| 
 | ||||
| 	// for backwards compat
 | ||||
| 	auth.challenge = auth; | ||||
| 	return auth; | ||||
| }; | ||||
| 
 | ||||
| @ -997,187 +1061,204 @@ ACME._getCertificate = function(me, options) { | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	// Do a little dry-run / self-test
 | ||||
| 	return ACME._testChallenges(me, options).then(function() { | ||||
| 		if (me.debug) { | ||||
| 			console.debug('[acme-v2] certificates.create'); | ||||
| 		} | ||||
| 		return ACME._getNonce(me).then(function() { | ||||
| 			var body = { | ||||
| 				// raw wildcard syntax MUST be used here
 | ||||
| 				identifiers: options.domains | ||||
| 					.sort(function(a, b) { | ||||
| 						// the first in the list will be the subject of the certificate, I believe (and hope)
 | ||||
| 						if (!options.subject) { | ||||
| 							return 0; | ||||
| 						} | ||||
| 						if (options.subject === a) { | ||||
| 							return -1; | ||||
| 						} | ||||
| 						if (options.subject === b) { | ||||
| 							return 1; | ||||
| 						} | ||||
| 						return 0; | ||||
| 					}) | ||||
| 					.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); | ||||
| 			// determine the signing algorithm to use in protected header // TODO isn't that handled by the signer?
 | ||||
| 			me._kty = | ||||
| 				(options.accountKeypair.privateKeyJwk && | ||||
| 					options.accountKeypair.privateKeyJwk.kty) || | ||||
| 				'RSA'; | ||||
| 			me._alg = 'EC' === me._kty ? 'ES256' : 'RS256'; // TODO vary with bitwidth of key (if not handled)
 | ||||
| 			var jws = me.RSA.signJws( | ||||
| 				options.accountKeypair, | ||||
| 				undefined, | ||||
| 				{ | ||||
| 					nonce: me._nonce, | ||||
| 					alg: me._alg, | ||||
| 					url: me._directoryUrls.newOrder, | ||||
| 					kid: me._kid | ||||
| 				}, | ||||
| 				Buffer.from(payload, 'utf8') | ||||
| 			); | ||||
| 
 | ||||
| 	var dnsHosts = options.domains.map(function(d) { | ||||
| 		return ( | ||||
| 			require('crypto') | ||||
| 				.randomBytes(2) | ||||
| 				.toString('hex') + d | ||||
| 		); | ||||
| 	}); | ||||
| 	return ACME._getZones(me, options, dnsHosts).then(function(zonenames) { | ||||
| 		options.zonenames = zonenames; | ||||
| 		// Do a little dry-run / self-test
 | ||||
| 		return ACME._testChallenges(me, options).then(function() { | ||||
| 			if (me.debug) { | ||||
| 				console.debug('\n[DEBUG] newOrder\n'); | ||||
| 				console.debug('[acme-v2] certificates.create'); | ||||
| 			} | ||||
| 			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; | ||||
| 					var setAuths; | ||||
| 					var auths = []; | ||||
| 					if (me.debug) { | ||||
| 						console.debug(location); | ||||
| 					} // the account id url
 | ||||
| 					if (me.debug) { | ||||
| 						console.debug(resp.toJSON()); | ||||
| 					} | ||||
| 					me._authorizations = resp.body.authorizations; | ||||
| 					me._order = location; | ||||
| 					me._finalize = resp.body.finalize; | ||||
| 					//if (me.debug) console.debug('[DEBUG] finalize:', me._finalize); return;
 | ||||
| 
 | ||||
| 					if (!me._authorizations) { | ||||
| 						return Promise.reject( | ||||
| 							new Error( | ||||
| 								"[acme-v2.js] authorizations were not fetched for '" + | ||||
| 									options.domains.join() + | ||||
| 									"':\n" + | ||||
| 									JSON.stringify(resp.body) | ||||
| 							) | ||||
| 						); | ||||
| 					} | ||||
| 					if (me.debug) { | ||||
| 						console.debug('[acme-v2] POST newOrder has authorizations'); | ||||
| 					} | ||||
| 					setAuths = me._authorizations.slice(0); | ||||
| 
 | ||||
| 					function setNext() { | ||||
| 						var authUrl = setAuths.shift(); | ||||
| 						if (!authUrl) { | ||||
| 							return; | ||||
| 						} | ||||
| 
 | ||||
| 						return ACME._getChallenges(me, options, authUrl).then(function( | ||||
| 							results | ||||
| 						) { | ||||
| 							// var domain = options.domains[i]; // results.identifier.value
 | ||||
| 
 | ||||
| 							// If it's already valid, we're golden it regardless
 | ||||
| 							if ( | ||||
| 								results.challenges.some(function(ch) { | ||||
| 									return 'valid' === ch.status; | ||||
| 								}) | ||||
| 							) { | ||||
| 								return setNext(); | ||||
| 			return ACME._getNonce(me).then(function() { | ||||
| 				var body = { | ||||
| 					// raw wildcard syntax MUST be used here
 | ||||
| 					identifiers: options.domains | ||||
| 						.sort(function(a, b) { | ||||
| 							// the first in the list will be the subject of the certificate, I believe (and hope)
 | ||||
| 							if (!options.subject) { | ||||
| 								return 0; | ||||
| 							} | ||||
| 
 | ||||
| 							var challenge = ACME._chooseChallenge(options, results); | ||||
| 							if (!challenge) { | ||||
| 								// For example, wildcards require dns-01 and, if we don't have that, we have to bail
 | ||||
| 								return Promise.reject( | ||||
| 									new Error( | ||||
| 										"Server didn't offer any challenge we can handle for '" + | ||||
| 											options.domains.join() + | ||||
| 											"'." | ||||
| 									) | ||||
| 								); | ||||
| 							if (options.subject === a) { | ||||
| 								return -1; | ||||
| 							} | ||||
| 
 | ||||
| 							var auth = ACME._challengeToAuth(me, options, results, challenge); | ||||
| 							auths.push(auth); | ||||
| 							return ACME._setChallenge(me, options, auth).then(setNext); | ||||
| 						}); | ||||
| 					} | ||||
| 
 | ||||
| 					function challengeNext() { | ||||
| 						var auth = auths.shift(); | ||||
| 						if (!auth) { | ||||
| 							return; | ||||
| 						} | ||||
| 						return ACME._postChallenge(me, options, auth).then(challengeNext); | ||||
| 					} | ||||
| 
 | ||||
| 					// First we set every challenge
 | ||||
| 					// Then we ask for each challenge to be checked
 | ||||
| 					// Doing otherwise would potentially cause us to poison our own DNS cache with misses
 | ||||
| 					return setNext() | ||||
| 						.then(challengeNext) | ||||
| 						.then(function() { | ||||
| 							if (me.debug) { | ||||
| 								console.debug('[getCertificate] next.then'); | ||||
| 							if (options.subject === b) { | ||||
| 								return 1; | ||||
| 							} | ||||
| 							var validatedDomains = body.identifiers.map(function(ident) { | ||||
| 								return ident.value; | ||||
| 							}); | ||||
| 
 | ||||
| 							return ACME._finalizeOrder(me, options, validatedDomains); | ||||
| 							return 0; | ||||
| 						}) | ||||
| 						.then(function(order) { | ||||
| 							if (me.debug) { | ||||
| 								console.debug('acme-v2: order was finalized'); | ||||
| 						.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); | ||||
| 				// determine the signing algorithm to use in protected header // TODO isn't that handled by the signer?
 | ||||
| 				me._kty = | ||||
| 					(options.accountKeypair.privateKeyJwk && | ||||
| 						options.accountKeypair.privateKeyJwk.kty) || | ||||
| 					'RSA'; | ||||
| 				me._alg = 'EC' === me._kty ? 'ES256' : 'RS256'; // TODO vary with bitwidth of key (if not handled)
 | ||||
| 				var jws = me.RSA.signJws( | ||||
| 					options.accountKeypair, | ||||
| 					undefined, | ||||
| 					{ | ||||
| 						nonce: me._nonce, | ||||
| 						alg: me._alg, | ||||
| 						url: me._directoryUrls.newOrder, | ||||
| 						kid: me._kid | ||||
| 					}, | ||||
| 					Buffer.from(payload, 'utf8') | ||||
| 				); | ||||
| 
 | ||||
| 				if (me.debug) { | ||||
| 					console.debug('\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; | ||||
| 						var setAuths; | ||||
| 						var auths = []; | ||||
| 						if (me.debug) { | ||||
| 							console.debug(location); | ||||
| 						} // the account id url
 | ||||
| 						if (me.debug) { | ||||
| 							console.debug(resp.toJSON()); | ||||
| 						} | ||||
| 						me._authorizations = resp.body.authorizations; | ||||
| 						me._order = location; | ||||
| 						me._finalize = resp.body.finalize; | ||||
| 						//if (me.debug) console.debug('[DEBUG] finalize:', me._finalize); return;
 | ||||
| 
 | ||||
| 						if (!me._authorizations) { | ||||
| 							return Promise.reject( | ||||
| 								new Error( | ||||
| 									"[acme-v2.js] authorizations were not fetched for '" + | ||||
| 										options.domains.join() + | ||||
| 										"':\n" + | ||||
| 										JSON.stringify(resp.body) | ||||
| 								) | ||||
| 							); | ||||
| 						} | ||||
| 						if (me.debug) { | ||||
| 							console.debug('[acme-v2] POST newOrder has authorizations'); | ||||
| 						} | ||||
| 						setAuths = me._authorizations.slice(0); | ||||
| 
 | ||||
| 						function setNext() { | ||||
| 							var authUrl = setAuths.shift(); | ||||
| 							if (!authUrl) { | ||||
| 								return; | ||||
| 							} | ||||
| 							return me | ||||
| 								._request({ method: 'GET', url: me._certificate, json: true }) | ||||
| 								.then(function(resp) { | ||||
| 									if (me.debug) { | ||||
| 										console.debug('acme-v2: csr submitted and cert received:'); | ||||
| 									} | ||||
| 									// https://github.com/certbot/certbot/issues/5721
 | ||||
| 									var certsarr = ACME.splitPemChain( | ||||
| 										ACME.formatPemChain(resp.body || '') | ||||
| 
 | ||||
| 							return ACME._getChallenges(me, options, authUrl).then(function( | ||||
| 								results | ||||
| 							) { | ||||
| 								// var domain = options.domains[i]; // results.identifier.value
 | ||||
| 
 | ||||
| 								// If it's already valid, we're golden it regardless
 | ||||
| 								if ( | ||||
| 									results.challenges.some(function(ch) { | ||||
| 										return 'valid' === ch.status; | ||||
| 									}) | ||||
| 								) { | ||||
| 									return setNext(); | ||||
| 								} | ||||
| 
 | ||||
| 								var challenge = ACME._chooseChallenge(options, results); | ||||
| 								if (!challenge) { | ||||
| 									// For example, wildcards require dns-01 and, if we don't have that, we have to bail
 | ||||
| 									return Promise.reject( | ||||
| 										new Error( | ||||
| 											"Server didn't offer any challenge we can handle for '" + | ||||
| 												options.domains.join() + | ||||
| 												"'." | ||||
| 										) | ||||
| 									); | ||||
| 									//  cert, chain, fullchain, privkey, /*TODO, subject, altnames, issuedAt, expiresAt */
 | ||||
| 									var certs = { | ||||
| 										expires: order.expires, | ||||
| 										identifiers: order.identifiers, | ||||
| 										//, authorizations: order.authorizations
 | ||||
| 										cert: certsarr.shift(), | ||||
| 										//, privkey: privkeyPem
 | ||||
| 										chain: certsarr.join('\n') | ||||
| 									}; | ||||
| 									if (me.debug) { | ||||
| 										console.debug(certs); | ||||
| 									} | ||||
| 									return certs; | ||||
| 								} | ||||
| 
 | ||||
| 								var auth = ACME._challengeToAuth( | ||||
| 									me, | ||||
| 									options, | ||||
| 									results, | ||||
| 									challenge | ||||
| 								); | ||||
| 								auths.push(auth); | ||||
| 								return ACME._setChallenge(me, options, auth).then(setNext); | ||||
| 							}); | ||||
| 						} | ||||
| 
 | ||||
| 						function challengeNext() { | ||||
| 							var auth = auths.shift(); | ||||
| 							if (!auth) { | ||||
| 								return; | ||||
| 							} | ||||
| 							return ACME._postChallenge(me, options, auth).then(challengeNext); | ||||
| 						} | ||||
| 
 | ||||
| 						// First we set every challenge
 | ||||
| 						// Then we ask for each challenge to be checked
 | ||||
| 						// Doing otherwise would potentially cause us to poison our own DNS cache with misses
 | ||||
| 						return setNext() | ||||
| 							.then(challengeNext) | ||||
| 							.then(function() { | ||||
| 								if (me.debug) { | ||||
| 									console.debug('[getCertificate] next.then'); | ||||
| 								} | ||||
| 								var validatedDomains = body.identifiers.map(function(ident) { | ||||
| 									return ident.value; | ||||
| 								}); | ||||
| 						}); | ||||
| 				}); | ||||
| 
 | ||||
| 								return ACME._finalizeOrder(me, options, validatedDomains); | ||||
| 							}) | ||||
| 							.then(function(order) { | ||||
| 								if (me.debug) { | ||||
| 									console.debug('acme-v2: order was finalized'); | ||||
| 								} | ||||
| 								return me | ||||
| 									._request({ method: 'GET', url: me._certificate, json: true }) | ||||
| 									.then(function(resp) { | ||||
| 										if (me.debug) { | ||||
| 											console.debug( | ||||
| 												'acme-v2: 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: order.expires, | ||||
| 											identifiers: order.identifiers, | ||||
| 											//, authorizations: order.authorizations
 | ||||
| 											cert: certsarr.shift(), | ||||
| 											//, privkey: privkeyPem
 | ||||
| 											chain: certsarr.join('\n') | ||||
| 										}; | ||||
| 										if (me.debug) { | ||||
| 											console.debug(certs); | ||||
| 										} | ||||
| 										return certs; | ||||
| 									}); | ||||
| 							}); | ||||
| 					}); | ||||
| 			}); | ||||
| 		}); | ||||
| 	}); | ||||
| }; | ||||
| @ -1190,7 +1271,7 @@ ACME.create = function create(me) { | ||||
| 	me.challengePrefixes = ACME.challengePrefixes; | ||||
| 	me.RSA = me.RSA || require('rsa-compat').RSA; | ||||
| 	//me.Keypairs = me.Keypairs || require('keypairs');
 | ||||
| 	me.request = me.request || require('@coolaj86/urequest'); | ||||
| 	me.request = me.request || require('@root/request'); | ||||
| 	me._dig = function(query) { | ||||
| 		// TODO use digd.js
 | ||||
| 		return new Promise(function(resolve, reject) { | ||||
| @ -1241,7 +1322,27 @@ ACME.create = function create(me) { | ||||
| 	} | ||||
| 
 | ||||
| 	me.init = function(_directoryUrl) { | ||||
| 		me.directoryUrl = me.directoryUrl || _directoryUrl; | ||||
| 		if (_directoryUrl) { | ||||
| 			_directoryUrl = _directoryUrl.directoryUrl || _directoryUrl; | ||||
| 		} | ||||
| 		if ('string' === typeof _directoryUrl) { | ||||
| 			me.directoryUrl = _directoryUrl; | ||||
| 		} | ||||
| 		if (!me.directoryUrl) { | ||||
| 			me.directoryUrl = | ||||
| 				'https://acme-staging-v02.api.letsencrypt.org/directory'; | ||||
| 			console.warn(); | ||||
| 			console.warn( | ||||
| 				"No ACME `directoryUrl` was specified. Using Let's Encrypt's staging environment as the default, which will issue invalid certs." | ||||
| 			); | ||||
| 			console.warn('\t' + me.directoryUrl); | ||||
| 			console.warn(); | ||||
| 			console.warn( | ||||
| 				"To get valid certificates you will need to switch to a production URL. You might like Let's Encrypt v2:" | ||||
| 			); | ||||
| 			console.warn('\t' + me.directoryUrl.replace('-staging', '')); | ||||
| 			console.warn(); | ||||
| 		} | ||||
| 		return ACME._directory(me).then(function(resp) { | ||||
| 			me._directoryUrls = resp.body; | ||||
| 			me._tos = me._directoryUrls.meta.termsOfService; | ||||
|  | ||||
							
								
								
									
										22
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										22
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							| @ -1,13 +1,19 @@ | ||||
| { | ||||
| 	"name": "acme-v2", | ||||
| 	"version": "1.7.6", | ||||
| 	"version": "1.8.0", | ||||
| 	"lockfileVersion": 1, | ||||
| 	"requires": true, | ||||
| 	"dependencies": { | ||||
| 		"@coolaj86/urequest": { | ||||
| 			"version": "1.3.7", | ||||
| 			"resolved": "https://registry.npmjs.org/@coolaj86/urequest/-/urequest-1.3.7.tgz", | ||||
| 			"integrity": "sha512-PPrVYra9aWvZjSCKl/x1pJ9ZpXda1652oJrPBYy5rQumJJMkmTBN3ux+sK2xAUwVvv2wnewDlaQaHLxLwSHnIA==" | ||||
| 		"@root/request": { | ||||
| 			"version": "1.3.11", | ||||
| 			"resolved": "https://registry.npmjs.org/@root/request/-/request-1.3.11.tgz", | ||||
| 			"integrity": "sha512-3a4Eeghcjsfe6zh7EJ+ni1l8OK9Fz2wL1OjP4UCa0YdvtH39kdXB9RGWuzyNv7dZi0+Ffkc83KfH0WbPMiuJFw==" | ||||
| 		}, | ||||
| 		"dotenv": { | ||||
| 			"version": "8.0.0", | ||||
| 			"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-8.0.0.tgz", | ||||
| 			"integrity": "sha512-30xVGqjLjiUOArT4+M5q9sYdvuR4riM6yK9wMcas9Vbp6zZa+ocC9dp6QoftuhTPhFAiLK/0C5Ni2nou/Bk8lg==", | ||||
| 			"dev": true | ||||
| 		}, | ||||
| 		"eckles": { | ||||
| 			"version": "1.4.1", | ||||
| @ -29,9 +35,9 @@ | ||||
| 			"integrity": "sha512-KxtX+/fBk+wM7O3CNgwjSh5elwFilLvqWajhr6wFr2Hd63JnKTTi43Tw+Jb1hxJQWOwoya+NZWR2xztn3hCrTw==" | ||||
| 		}, | ||||
| 		"rsa-compat": { | ||||
| 			"version": "2.0.6", | ||||
| 			"resolved": "https://registry.npmjs.org/rsa-compat/-/rsa-compat-2.0.6.tgz", | ||||
| 			"integrity": "sha512-bQmpscAQec9442RaghDybrHMy1twQ3nUZOgTlqntio1yru+rMnDV64uGRzKp7dJ4VVhNv3mLh3X4MNON+YM0dA==", | ||||
| 			"version": "2.0.8", | ||||
| 			"resolved": "https://registry.npmjs.org/rsa-compat/-/rsa-compat-2.0.8.tgz", | ||||
| 			"integrity": "sha512-BFiiSEbuxzsVdaxpejbxfX07qs+rtous49Y6mL/zw6YHh9cranDvm2BvBmqT3rso84IsxNlP5BXnuNvm1Wn3Tw==", | ||||
| 			"requires": { | ||||
| 				"keypairs": "^1.2.14" | ||||
| 			} | ||||
|  | ||||
							
								
								
									
										15
									
								
								package.json
									
									
									
									
									
								
							
							
						
						
									
										15
									
								
								package.json
									
									
									
									
									
								
							| @ -1,11 +1,11 @@ | ||||
| { | ||||
| 	"name": "acme-v2", | ||||
| 	"version": "1.7.7", | ||||
| 	"description": "Free SSL. A framework for building Let's Encrypt v2 clients, and other ACME v2 (draft 11) clients. Successor to le-acme-core.js", | ||||
| 	"version": "1.8.0", | ||||
| 	"description": "A lightweight library for getting Free SSL certifications through Let's Encrypt, using the ACME protocol.", | ||||
| 	"homepage": "https://git.coolaj86.com/coolaj86/acme-v2.js", | ||||
| 	"main": "node.js", | ||||
| 	"scripts": { | ||||
| 		"test": "echo \"Error: no test specified\" && exit 1" | ||||
| 		"test": "node ./test.js" | ||||
| 	}, | ||||
| 	"repository": { | ||||
| 		"type": "git", | ||||
| @ -23,10 +23,13 @@ | ||||
| 		"automated https", | ||||
| 		"letsencrypt" | ||||
| 	], | ||||
| 	"author": "AJ ONeal <coolaj86@gmail.com> (https://coolaj86.com/)", | ||||
| 	"author": "AJ ONeal <coolaj86@gmail.com> (https://solderjs.com/)", | ||||
| 	"license": "MPL-2.0", | ||||
| 	"dependencies": { | ||||
| 		"@coolaj86/urequest": "^1.3.6", | ||||
| 		"rsa-compat": "^2.0.6" | ||||
| 		"@root/request": "^1.3.11", | ||||
| 		"rsa-compat": "^2.0.8" | ||||
| 	}, | ||||
| 	"devDependencies": { | ||||
| 		"dotenv": "^8.0.0" | ||||
| 	} | ||||
| } | ||||
|  | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user