mirror of
				https://github.com/therootcompany/acme.js.git
				synced 2024-11-16 17:29:00 +00:00 
			
		
		
		
	merge unrelated v2 (historical) and v3 (new from scratch)
This commit is contained in:
		
						commit
						ad42d34587
					
				
							
								
								
									
										4
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										4
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							| @ -1,4 +1,7 @@ | ||||
| .env | ||||
| *.gz | ||||
| .*.sw* | ||||
| .ignore | ||||
| 
 | ||||
| *.pem | ||||
| 
 | ||||
| @ -14,4 +17,5 @@ coverage | ||||
| 
 | ||||
| # Dependency directory | ||||
| # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git | ||||
| 
 | ||||
| node_modules | ||||
|  | ||||
| @ -2,7 +2,7 @@ | ||||
|   "bracketSpacing": true, | ||||
|   "printWidth": 80, | ||||
|   "singleQuote": true, | ||||
|   "tabWidth": 2, | ||||
|   "tabWidth": 4, | ||||
|   "trailingComma": "none", | ||||
|   "useTabs": true | ||||
| } | ||||
|  | ||||
							
								
								
									
										53
									
								
								CHANGELOG.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										53
									
								
								CHANGELOG.md
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,53 @@ | ||||
| # Changelog | ||||
| 
 | ||||
| -   v3 (Oct 2019) | ||||
|     -   Add POST-as-GET for Let's Encrypt v2 release 2 (ACME / RFC 8555) | ||||
|     -   Jump to v3 for parity with Greenlock | ||||
|     -   Merge browser and node.js versions in one | ||||
|     -   Drop all backwards-compat complexity | ||||
|     -   Move to zero-external deps, using @root packages only | ||||
| -   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 | ||||
|     -   Use node RSA keygen by default | ||||
|     -   No non-optional external deps! | ||||
| -   v1.2 | ||||
|     -   fix some API out-of-specness | ||||
|     -   doc some magic numbers (status) | ||||
|     -   updated deps | ||||
| -   v1.1.0 | ||||
|     -   reduce dependencies (use lightweight @coolaj86/request instead of request) | ||||
| -   v1.0.5 - cleanup logging | ||||
| -   v1.0.4 - v6- compat use `promisify` from node's util or bluebird | ||||
| -   v1.0.3 - documentation cleanup | ||||
| -   v1.0.2 | ||||
|     -   use `options.contact` to provide raw contact array | ||||
|     -   made `options.email` optional | ||||
|     -   file cleanup | ||||
| -   v1.0.1 | ||||
|     -   Compat API is ready for use | ||||
|     -   Eliminate debug logging | ||||
| -   Apr 10, 2018 - tested backwards-compatibility using greenlock.js | ||||
| -   Apr 5, 2018 - export http and dns challenge tests | ||||
| -   Apr 5, 2018 - test http and dns challenges (success and failure) | ||||
| -   Apr 5, 2018 - test subdomains and its wildcard | ||||
| -   Apr 5, 2018 - test two subdomains | ||||
| -   Apr 5, 2018 - test wildcard | ||||
| -   Apr 5, 2018 - completely match api for acme v1 (le-acme-core.js) | ||||
| -   Mar 21, 2018 - _mostly_ matches le-acme-core.js API | ||||
| -   Mar 21, 2018 - can now accept values (not hard coded) | ||||
| -   Mar 20, 2018 - SUCCESS - got a test certificate (hard-coded) | ||||
| -   Mar 20, 2018 - download certificate | ||||
| -   Mar 20, 2018 - poll for status | ||||
| -   Mar 20, 2018 - finalize order (submit csr) | ||||
| -   Mar 20, 2018 - generate domain keypair | ||||
| -   Mar 20, 2018 - respond to challenges | ||||
| -   Mar 16, 2018 - get challenges | ||||
| -   Mar 16, 2018 - new order | ||||
| -   Mar 15, 2018 - create account | ||||
| -   Mar 15, 2018 - generate account keypair | ||||
| -   Mar 15, 2018 - get nonce | ||||
| -   Mar 15, 2018 - get directory | ||||
							
								
								
									
										2
									
								
								LICENSE
									
									
									
									
									
								
							
							
						
						
									
										2
									
								
								LICENSE
									
									
									
									
									
								
							| @ -1,4 +1,4 @@ | ||||
| Copyright 2018 AJ ONeal | ||||
| Copyright 2015-2019 AJ ONeal | ||||
| 
 | ||||
| Mozilla Public License Version 2.0 | ||||
| ================================== | ||||
|  | ||||
							
								
								
									
										510
									
								
								README.md
									
									
									
									
									
								
							
							
						
						
									
										510
									
								
								README.md
									
									
									
									
									
								
							| @ -1,239 +1,355 @@ | ||||
| # ACME.js v3 on its way (Nov 1st, 2019) | ||||
| # [ACME.js](https://git.rootprojects.org/root/acme.js) v3 | ||||
| 
 | ||||
| ACME.js v3 is in private beta and will be available by Nov 1st. | ||||
| | Built by [Root](https://therootcompany.com) for [Greenlock](https://greenlock.domains) | ||||
| 
 | ||||
| Follow the updates on the [campaign page](https://indiegogo.com/at/greenlock), | ||||
| and contribute to support the project and get beta access now. | ||||
| Free SSL Certificates from Let's Encrypt, for Node.js and Web Browsers | ||||
| 
 | ||||
| | **acme-v2.js** ([npm](https://www.npmjs.com/package/acme-v2)) | ||||
| | [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) | ||||
| Lightweight. Fast. Modern Crypto. Zero external dependecies. | ||||
| 
 | ||||
| # [acme-v2.js](https://git.coolaj86.com/coolaj86/acme-v2.js) | a [Root](https://therootcompany.com) project | ||||
| # Features | ||||
| 
 | ||||
| A **Zero (External) Dependency**\* library for building | ||||
| Let's Encrypt v2 (ACME draft 18) clients and getting Free SSL certificates. | ||||
| | 15k gzipped | 55k minified | 88k (2,500 loc) source with comments | | ||||
| 
 | ||||
| The primary goal of this library is to make it easy to | ||||
| get Accounts and Certificates through Let's Encrypt. | ||||
| 
 | ||||
| # Features | ||||
| -   [x] Let's Encrypt v2 / ACME RFC 8555 (November 2019) | ||||
|     -   [x] POST-as-GET support | ||||
|     -   [x] Secure support for EC and RSA for account and server keys | ||||
|     -   [x] Simple and lightweight PEM, DER, ASN1, X509, and CSR implementations | ||||
|     -   [ ] (in-progress) StartTLS Everywhere™ | ||||
| -   [x] Supports International Domain Names (i.e. `.中国`) | ||||
| -   [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, Zero External Dependencies | ||||
|     -   [x] Safe, Efficient, Maintained | ||||
|     -   [x] Node.js\* (v6+) | ||||
|     -   [x] WebPack | ||||
| -   [x] Online Demo | ||||
|     -   See https://greenlock.domains | ||||
| 
 | ||||
| - [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)) | ||||
| \* Although we use `async/await` in the examples, the code is written in CommonJS, | ||||
| with Promises, so you can use it in Node.js and Browsers without transpiling. | ||||
| 
 | ||||
| \* <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> | ||||
| # Want Quick and Easy? | ||||
| 
 | ||||
| ## Looking for Quick 'n' Easy™? | ||||
| ACME.js is a low-level tool for building Let's Encrypt clients in Node and Browsers. | ||||
| 
 | ||||
| If you want something that's more "batteries included" give | ||||
| [greenlock.js](https://git.coolaj86.com/coolaj86/greenlock.js) | ||||
| a try. | ||||
| If you're looking for maximum convenience, try | ||||
| [Greenlock.js](https://git.rootprojects.org/root/greenlock-express.js). | ||||
| 
 | ||||
| - [greenlock.js](https://git.coolaj86.com/coolaj86/greenlock.js) | ||||
| -   <https://git.rootprojects.org/root/greenlock-express.js> | ||||
| 
 | ||||
| ## v1.7+: Transitional v2 Support | ||||
| # Online Demos | ||||
| 
 | ||||
| By the end of June 2019 we expect to have completed the migration to Let's Encrypt v2.1 (ACME draft 18). | ||||
| -   Greenlock for the Web <https://greenlock.domains> | ||||
| -   ACME.js Demo <https://rootprojects.org/acme/> | ||||
| 
 | ||||
| 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 expect that our hosted versions will meet all of yours needs. | ||||
| If they don't, please open an issue to let us know why. | ||||
| 
 | ||||
| We're taking this **mandatory ACME update** as an opportunity to **clean up** and **greatly simplify** | ||||
| the code with a fresh new release. | ||||
| We'd much rather improve the app than have a hundred different versions running in the wild. | ||||
| However, in keeping to our values we've made the source visible for others to inspect, improve, and modify. | ||||
| 
 | ||||
| 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 | ||||
| # QuickStart | ||||
| 
 | ||||
| ## Recommended Example | ||||
| To make it easy to generate, encode, and decode keys and certificates, | ||||
| ACME.js uses [Keypairs.js](https://git.rootprojects.org/root/keypairs.js) | ||||
| and [CSR.js](https://git.rootprojects.org/root/csr.js) | ||||
| 
 | ||||
| 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 | ||||
| (and if you want that, see [greenlock-express.js](https://git.coolaj86.com/coolaj86/greenlock-express.js)). | ||||
| 
 | ||||
| I'd recommend first running the example CLI client with a test domain and then investigating the files used for that example: | ||||
| 
 | ||||
| ```bash | ||||
| node examples/cli.js | ||||
| ``` | ||||
| 
 | ||||
| The example cli has the following prompts: | ||||
| 
 | ||||
| ``` | ||||
| What web address(es) would you like to get certificates for? (ex: example.com,*.example.com) | ||||
| What challenge will you be testing today? http-01 or dns-01? [http-01] | ||||
| What email should we use? (optional) | ||||
| What API style would you like to test? v1-compat or promise? [v1-compat] | ||||
| 
 | ||||
| Put the string 'mBfh0SqaAV3MOK3B6cAhCbIReAyDuwuxlO1Sl70x6bM.VNAzCR4THe4czVzo9piNn73B1ZXRLaB2CESwJfKkvRM' into a file at 'example.com/.well-known/acme-challenge/mBfh0SqaAV3MOK3B6cAhCbIReAyDuwuxlO1Sl70x6bM' | ||||
| 
 | ||||
| echo 'mBfh0SqaAV3MOK3B6cAhCbIReAyDuwuxlO1Sl70x6bM.VNAzCR4THe4czVzo9piNn73B1ZXRLaB2CESwJfKkvRM' > 'example.com/.well-known/acme-challenge/mBfh0SqaAV3MOK3B6cAhCbIReAyDuwuxlO1Sl70x6bM' | ||||
| 
 | ||||
| Then hit the 'any' key to continue... | ||||
| ``` | ||||
| 
 | ||||
| When you've completed the challenge you can hit a key to continue the process. | ||||
| 
 | ||||
| If you place the certificate you receive back in `tests/fullchain.pem` | ||||
| you can then test it with `examples/https-server.js`. | ||||
| 
 | ||||
| ``` | ||||
| examples/cli.js | ||||
| examples/genkeypair.js | ||||
| tests/compat.js | ||||
| examples/https-server.js | ||||
| examples/http-server.js | ||||
| ``` | ||||
| 
 | ||||
| --> | ||||
| 
 | ||||
| ## API | ||||
| 
 | ||||
| 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. | ||||
| ## Node.js | ||||
| 
 | ||||
| ```js | ||||
| var ACME = require('acme-v2').ACME.create({ | ||||
| 	// used for overriding the default user-agent | ||||
| 	userAgent: 'My custom UA String', | ||||
| 	getUserAgentString: function(deps) { | ||||
| 		return 'My custom UA String'; | ||||
| 	}, | ||||
| var ACME = require('@root/acme'); | ||||
| ``` | ||||
| 
 | ||||
| 	// don't try to validate challenges locally | ||||
| 	skipChallengeTest: false, | ||||
| 	skipDryRun: false, | ||||
| ## WebPack | ||||
| 
 | ||||
| 	// 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 | ||||
| ```html | ||||
| <meta charset="UTF-8" /> | ||||
| ``` | ||||
| 
 | ||||
| (necessary in case the webserver headers don't specify `plain/text; charset="UTF-8"`) | ||||
| 
 | ||||
| ```js | ||||
| var ACME = require('@root/acme'); | ||||
| ``` | ||||
| 
 | ||||
| ## Vanilla JS | ||||
| 
 | ||||
| ```html | ||||
| <meta charset="UTF-8" /> | ||||
| ``` | ||||
| 
 | ||||
| (necessary in case the webserver headers don't specify `plain/text; charset="UTF-8"`) | ||||
| 
 | ||||
| `acme.js` | ||||
| 
 | ||||
| ```html | ||||
| <script src="https://unpkg.com/@root/acme@3.0.0/dist/acme.js"></script> | ||||
| ``` | ||||
| 
 | ||||
| `acme.min.js` | ||||
| 
 | ||||
| ```html | ||||
| <script src="https://unpkg.com/@root/acme@3.0.0/dist/acme.min.js"></script> | ||||
| ``` | ||||
| 
 | ||||
| Use | ||||
| 
 | ||||
| ```js | ||||
| var ACME = window['@root/acme']; | ||||
| ``` | ||||
| 
 | ||||
| ## Examples | ||||
| 
 | ||||
| You can see `tests/index.js`, `examples/index.html`, `examples/app.js` in the repo for full example usage. | ||||
| 
 | ||||
| ### Emails: Maintainer vs Subscriber vs Customer | ||||
| 
 | ||||
| -   `maintainerEmail` should be the email address of the **author of the code**. | ||||
|     This person will receive critical security and API change notifications. | ||||
| -   `subscriberEmail` should be the email of the **admin of the hosting service**. | ||||
|     This person agrees to the Let's Encrypt Terms of Service and will be notified | ||||
|     when a certificate fails to renew. | ||||
| -   `customerEmail` should be the email of individual who owns the domain. | ||||
|     This is optional (not currently implemented). | ||||
| 
 | ||||
| Generally speaking **YOU** are the _maintainer_ and you **or your employer** is the _subscriber_. | ||||
| 
 | ||||
| If you (or your employer) is running any type of service | ||||
| you **SHOULD NOT** pass the _customer_ email as the subscriber email. | ||||
| 
 | ||||
| If you are not running a service (you may be building a CLI, for example), | ||||
| then you should prompt the user for their email address, and they are the subscriber. | ||||
| 
 | ||||
| ### Instantiate ACME.js | ||||
| 
 | ||||
| Although built for Let's Encrypt, ACME.js will work with any server | ||||
| that supports draft-15 of the ACME spec (includes POST-as-GET support). | ||||
| 
 | ||||
| The `init()` method takes a _directory url_ and initializes internal state according to its response. | ||||
| 
 | ||||
| ```js | ||||
| var acme = ACME.create({ | ||||
| 	maintainerEmail: 'jon@example.com' | ||||
| }); | ||||
| acme.init('https://acme-staging-v02.api.letsencrypt.org/directory').then( | ||||
| 	function() { | ||||
| 		// Ready to use, show page | ||||
| 		$('body').hidden = false; | ||||
| 	} | ||||
| ); | ||||
| ``` | ||||
| 
 | ||||
| ### Create ACME Account with Let's Encrypt | ||||
| 
 | ||||
| ACME Accounts are key and device based, with an email address as a backup identifier. | ||||
| 
 | ||||
| A public account key must be registered before an SSL certificate can be requested. | ||||
| 
 | ||||
| ```js | ||||
| var accountPrivateKey; | ||||
| var account; | ||||
| 
 | ||||
| Keypairs.generate({ kty: 'EC' }).then(function(pair) { | ||||
| 	accountPrivateKey = pair.private; | ||||
| 
 | ||||
| 	return acme.accounts | ||||
| 		.create({ | ||||
| 			agreeToTerms: function(tos) { | ||||
| 				if ( | ||||
| 					window.confirm( | ||||
| 						"Do you agree to the ACME.js and Let's Encrypt Terms of Service?" | ||||
| 					) | ||||
| 				) { | ||||
| 					return Promise.resolve(tos); | ||||
| 				} | ||||
| 			}, | ||||
| 			accountKeypair: { privateKeyJwk: pair.private }, | ||||
| 			subscriberEmail: $('.js-email-input').value | ||||
| 		}) | ||||
| 		.then(function(_account) { | ||||
| 			account = _account; | ||||
| 		}); | ||||
| }); | ||||
| ``` | ||||
| 
 | ||||
| ### Generate a Certificate Private Key | ||||
| 
 | ||||
| ```js | ||||
| var certKeypair = await Keypairs.generate({ kty: 'RSA' }); | ||||
| var pem = await Keypairs.export({ | ||||
| 	jwk: certKeypair.private, | ||||
| 	encoding: 'pem' | ||||
| }); | ||||
| 
 | ||||
| // Discover Directory URLs | ||||
| ACME.init(acmeDirectoryUrl); // returns Promise<acmeUrls={keyChange,meta,newAccount,newNonce,newOrder,revokeCert}> | ||||
| // This should be saved as `privkey.pem` | ||||
| console.log(pem); | ||||
| ``` | ||||
| 
 | ||||
| // Accounts | ||||
| ACME.accounts.create(options); // returns Promise<regr> registration data | ||||
| ### Generate a CSR | ||||
| 
 | ||||
| options = { | ||||
| 	email: '<email>', // valid email (server checks MX records) | ||||
| 	accountKeypair: { | ||||
| 		//    privateKeyPem or privateKeyJwt | ||||
| 		privateKeyPem: '<ASCII PEM>' | ||||
| The easiest way to generate a Certificate Signing Request will be either with `openssl` or with `@root/CSR`. | ||||
| 
 | ||||
| ```js | ||||
| var CSR = require('@root/csr'); | ||||
| var Enc = require('@root/encoding'); | ||||
| 
 | ||||
| // 'subject' should be first in list | ||||
| var sortedDomains = ['example.com', 'www.example.com']; | ||||
| var csr = await CSR.csr({ | ||||
| 	jwk: certKeypair.private, | ||||
| 	domains: sortedDomains, | ||||
| 	encoding: 'der' | ||||
| }).then(function(der) { | ||||
| 	return Enc.bufToUrlBase64(der); | ||||
| }); | ||||
| ``` | ||||
| 
 | ||||
| ### Get Free 90-day SSL Certificate | ||||
| 
 | ||||
| Creating an ACME "order" for a 90-day SSL certificate requires use of the account private key, | ||||
| the names of domains to be secured, and a distinctly separate server private key. | ||||
| 
 | ||||
| A domain ownership verification "challenge" (uploading a file to an unsecured HTTP url or setting a DNS record) | ||||
| is a required part of the process, which requires `set` and `remove` callbacks/promises. | ||||
| 
 | ||||
| ```js | ||||
| var certinfo = await acme.certificates.create({ | ||||
| 	agreeToTerms: function(tos) { | ||||
| 		return tos; | ||||
| 	}, | ||||
| 	agreeToTerms: function(tosUrl) {} //    should Promise the same `tosUrl` back | ||||
| }; | ||||
| 	account: account, | ||||
| 	accountKeypair: { privateKeyJwk: accountPrivateKey }, | ||||
| 	csr: csr, | ||||
| 	domains: sortedDomains, | ||||
| 	challenges: challenges, // must be implemented | ||||
| 	customerEmail: null, | ||||
| 	skipChallengeTests: false, | ||||
| 	skipDryRun: false | ||||
| }); | ||||
| 
 | ||||
| // Registration | ||||
| ACME.certificates.create(options); // returns Promise<pems={ privkey (key), cert, chain (ca) }> | ||||
| console.log('Got SSL Certificate:'); | ||||
| console.log(results.expires); | ||||
| 
 | ||||
| options = { | ||||
| 	domainKeypair: { | ||||
| 		privateKeyPem: '<ASCII PEM>' | ||||
| 	}, | ||||
| 	accountKeypair: { | ||||
| 		privateKeyPem: '<ASCII PEM>' | ||||
| 	}, | ||||
| 	domains: ['example.com'], | ||||
| // This should be saved as `fullchain.pem` | ||||
| console.log([results.cert, results.chain].join('\n')); | ||||
| ``` | ||||
| 
 | ||||
| 	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 | ||||
| ### Example "Challenge" Implementation | ||||
| 
 | ||||
| Typically here you're just presenting some sort of dialog to the user to ask them to | ||||
| upload a file or set a DNS record. | ||||
| 
 | ||||
| It may be possible to do something fancy like using OAuth2 to login to Google Domanis | ||||
| to set a DNS address, etc, but it seems like that sort of fanciness is probably best | ||||
| reserved for server-side plugins. | ||||
| 
 | ||||
| ```js | ||||
| var challenges = { | ||||
| 	'http-01': { | ||||
| 		set: function(opts) { | ||||
| 			console.info('http-01 set challenge:'); | ||||
| 			console.info(opts.challengeUrl); | ||||
| 			console.info(opts.keyAuthorization); | ||||
| 			while ( | ||||
| 				!window.confirm('Upload the challenge file before continuing.') | ||||
| 			) {} | ||||
| 			return Promise.resolve(); | ||||
| 		}, | ||||
| 		remove: function(opts) { | ||||
| 			console.log('http-01 remove challenge:', opts.challengeUrl); | ||||
| 			return Promise.resolve(); | ||||
| 		} | ||||
| 	} | ||||
| }; | ||||
| ``` | ||||
| 
 | ||||
| # Changelog | ||||
| # IDN - International Domain Names | ||||
| 
 | ||||
| - 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 | ||||
|   - Use node RSA keygen by default | ||||
|   - No non-optional external deps! | ||||
| - v1.2 | ||||
|   - fix some API out-of-specness | ||||
|   - doc some magic numbers (status) | ||||
|   - updated deps | ||||
| - v1.1.0 | ||||
|   - reduce dependencies (use lightweight @coolaj86/request instead of request) | ||||
| - v1.0.5 - cleanup logging | ||||
| - v1.0.4 - v6- compat use `promisify` from node's util or bluebird | ||||
| - v1.0.3 - documentation cleanup | ||||
| - v1.0.2 | ||||
|   - use `options.contact` to provide raw contact array | ||||
|   - made `options.email` optional | ||||
|   - file cleanup | ||||
| - v1.0.1 | ||||
|   - Compat API is ready for use | ||||
|   - Eliminate debug logging | ||||
| - Apr 10, 2018 - tested backwards-compatibility using greenlock.js | ||||
| - Apr 5, 2018 - export http and dns challenge tests | ||||
| - Apr 5, 2018 - test http and dns challenges (success and failure) | ||||
| - Apr 5, 2018 - test subdomains and its wildcard | ||||
| - Apr 5, 2018 - test two subdomains | ||||
| - Apr 5, 2018 - test wildcard | ||||
| - Apr 5, 2018 - completely match api for acme v1 (le-acme-core.js) | ||||
| - Mar 21, 2018 - _mostly_ matches le-acme-core.js API | ||||
| - Mar 21, 2018 - can now accept values (not hard coded) | ||||
| - Mar 20, 2018 - SUCCESS - got a test certificate (hard-coded) | ||||
| - Mar 20, 2018 - download certificate | ||||
| - Mar 20, 2018 - poll for status | ||||
| - Mar 20, 2018 - finalize order (submit csr) | ||||
| - Mar 20, 2018 - generate domain keypair | ||||
| - Mar 20, 2018 - respond to challenges | ||||
| - Mar 16, 2018 - get challenges | ||||
| - Mar 16, 2018 - new order | ||||
| - Mar 15, 2018 - create account | ||||
| - Mar 15, 2018 - generate account keypair | ||||
| - Mar 15, 2018 - get nonce | ||||
| - Mar 15, 2018 - get directory | ||||
| Convert domain names to `punycode` before creating the certificate: | ||||
| 
 | ||||
| # Legal | ||||
| ```js | ||||
| var punycode = require('punycode'); | ||||
| 
 | ||||
| [acme-v2.js](https://git.coolaj86.com/coolaj86/acme-v2.js) | | ||||
| acme.certificates.create({ | ||||
| 	// ... | ||||
| 	domains: ['example.com', 'www.example.com'].map(function(name) { | ||||
| 		return punycode.toASCII(name); | ||||
| 	}) | ||||
| }); | ||||
| ``` | ||||
| 
 | ||||
| The punycode library itself is lightweight and dependency-free. | ||||
| It is available both in node and for browsers. | ||||
| 
 | ||||
| # Testing | ||||
| 
 | ||||
| You will need to use one of the [`acme-dns-01-*` plugins](https://www.npmjs.com/search?q=acme-dns-01-) | ||||
| to run the test locally. | ||||
| 
 | ||||
| You'll also need a `.env` that looks something like the one in `examples/example.env`: | ||||
| 
 | ||||
| ```bash | ||||
| ENV=DEV | ||||
| SUBSCRIBER_EMAIL=letsencrypt+staging@example.com | ||||
| BASE_DOMAIN=test.example.com | ||||
| CHALLENGE_TYPE=dns-01 | ||||
| CHALLENGE_PLUGIN=acme-dns-01-digitalocean | ||||
| CHALLENGE_OPTIONS='{"token":"xxxxxxxxxxxx"}' | ||||
| ``` | ||||
| 
 | ||||
| For example: | ||||
| 
 | ||||
| ```bash | ||||
| # Get the repo and change directories into it | ||||
| git clone https://git.rootprojects.org/root/bluecrypt-acme.js | ||||
| pushd bluecrypt-acme.js/ | ||||
| 
 | ||||
| # Install the challenge plugin you'll use for the tests | ||||
| npm install --save-dev acme-dns-01-digitalocean | ||||
| 
 | ||||
| # Copy the sample .env file | ||||
| rsync -av examples/example.env .env | ||||
| 
 | ||||
| # Edit the config file to use a domain in your account, and your API token | ||||
| #vim .env | ||||
| code .env | ||||
| 
 | ||||
| # Run the tests | ||||
| node tests/index.js | ||||
| ``` | ||||
| 
 | ||||
| # Developing | ||||
| 
 | ||||
| You can see `<script>` tags in the `index.html` in the repo, which references the original | ||||
| source files. | ||||
| 
 | ||||
| Join `@rootprojects` `#general` on [Keybase](https://keybase.io) if you'd like to chat with us. | ||||
| 
 | ||||
| # Commercial Support | ||||
| 
 | ||||
| We have both commercial support and commercial licensing available. | ||||
| 
 | ||||
| You're welcome to [contact us](mailto:aj@therootcompany.com) in regards to IoT, On-Prem, | ||||
| Enterprise, and Internal installations, integrations, and deployments. | ||||
| 
 | ||||
| We also offer consulting for all-things-ACME and Let's Encrypt. | ||||
| 
 | ||||
| # Legal & Rules of the Road | ||||
| 
 | ||||
| Greenlock™ is a [trademark](https://rootprojects.org/legal/#trademark) of AJ ONeal | ||||
| 
 | ||||
| The rule of thumb is "attribute, but don't confuse". For example: | ||||
| 
 | ||||
| > Built with [ACME.js](https://git.rootprojects.org/root/bluecrypt-acme.js) (a [Root](https://rootprojects.org) project). | ||||
| 
 | ||||
| Please [contact us](mailto:aj@therootcompany.com) if have any questions in regards to our trademark, | ||||
| attribution, and/or visible source policies. We want to build great software and a great community. | ||||
| 
 | ||||
| [ACME.js](https://git.rootprojects.org/root/acme.js) | | ||||
| MPL-2.0 | | ||||
| [Terms of Use](https://therootcompany.com/legal/#terms) | | ||||
| [Privacy Policy](https://therootcompany.com/legal/#privacy) | ||||
|  | ||||
							
								
								
									
										60
									
								
								bin/bundle.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										60
									
								
								bin/bundle.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,60 @@ | ||||
| #!/usr/bin/env node
 | ||||
| (async function() { | ||||
| 	'use strict'; | ||||
| 
 | ||||
| 	var UglifyJS = require('uglify-js'); | ||||
| 	var path = require('path'); | ||||
| 	var fs = require('fs'); | ||||
| 	var promisify = require('util').promisify; | ||||
| 	var readFile = promisify(fs.readFile); | ||||
| 	var writeFile = promisify(fs.writeFile); | ||||
| 	var gzip = promisify(require('zlib').gzip); | ||||
| 
 | ||||
| 	// The order is specific, and it matters
 | ||||
| 	var files = await Promise.all( | ||||
| 		[ | ||||
| 			'../lib/encoding.js', | ||||
| 			'../lib/asn1-packer.js', | ||||
| 			'../lib/x509.js', | ||||
| 			'../lib/ecdsa.js', | ||||
| 			'../lib/rsa.js', | ||||
| 			'../lib/keypairs.js', | ||||
| 			'../lib/asn1-parser.js', | ||||
| 			'../lib/csr.js', | ||||
| 			'../lib/acme.js' | ||||
| 		].map(async function(file) { | ||||
| 			return (await readFile(path.join(__dirname, file), 'utf8')).trim(); | ||||
| 		}) | ||||
| 	); | ||||
| 
 | ||||
| 	var header = | ||||
| 		[ | ||||
| 			'// Copyright 2015-2019 AJ ONeal. All rights reserved', | ||||
| 			'/* This Source Code Form is subject to the terms of the Mozilla Public', | ||||
| 			' * License, v. 2.0. If a copy of the MPL was not distributed with this', | ||||
| 			' * file, You can obtain one at http://mozilla.org/MPL/2.0/. */' | ||||
| 		].join('\n') + '\n'; | ||||
| 
 | ||||
| 	var file = header + files.join('\n') + '\n'; | ||||
| 	await writeFile(path.join(__dirname, '../dist', 'acme.js'), file); | ||||
| 	await writeFile( | ||||
| 		path.join(__dirname, '../dist', 'acme.js.gz'), | ||||
| 		await gzip(file) | ||||
| 	); | ||||
| 
 | ||||
| 	// TODO source maps?
 | ||||
| 	var result = UglifyJS.minify(file, { | ||||
| 		compress: true, | ||||
| 		// mangling doesn't save significant
 | ||||
| 		mangle: false | ||||
| 	}); | ||||
| 	if (result.error) { | ||||
| 		throw result.error; | ||||
| 	} | ||||
| 	file = header + result.code; | ||||
| 	await writeFile(path.join(__dirname, '../dist', 'acme.min.js'), file); | ||||
| 	await writeFile( | ||||
| 		path.join(__dirname, '../dist', 'acme.min.js.gz'), | ||||
| 		await gzip(file) | ||||
| 	); | ||||
| })(); | ||||
							
								
								
									
										50
									
								
								browser.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										50
									
								
								browser.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,50 @@ | ||||
| 'use strict'; | ||||
| 
 | ||||
| var native = module.exports; | ||||
| 
 | ||||
| native._canCheck = function(me) { | ||||
| 	me._canCheck = {}; | ||||
| 	return me | ||||
| 		.request({ url: me._baseUrl + '/api/_acme_api_/' }) | ||||
| 		.then(function(resp) { | ||||
| 			if (resp.body.success) { | ||||
| 				me._canCheck['http-01'] = true; | ||||
| 				me._canCheck['dns-01'] = true; | ||||
| 			} | ||||
| 		}) | ||||
| 		.catch(function() { | ||||
| 			// ignore
 | ||||
| 		}); | ||||
| }; | ||||
| 
 | ||||
| native._dns01 = function(me, ch) { | ||||
| 	return new me.request({ | ||||
| 		url: me._baseUrl + '/api/dns/' + ch.dnsHost + '?type=TXT' | ||||
| 	}).then(function(resp) { | ||||
| 		var err; | ||||
| 		if (!resp.body || !Array.isArray(resp.body.answer)) { | ||||
| 			err = new Error('failed to get DNS response'); | ||||
| 			console.error(err); | ||||
| 			throw err; | ||||
| 		} | ||||
| 		if (!resp.body.answer.length) { | ||||
| 			err = new Error('failed to get DNS answer record in response'); | ||||
| 			console.error(err); | ||||
| 			throw err; | ||||
| 		} | ||||
| 		return { | ||||
| 			answer: resp.body.answer.map(function(ans) { | ||||
| 				return { data: ans.data, ttl: ans.ttl }; | ||||
| 			}) | ||||
| 		}; | ||||
| 	}); | ||||
| }; | ||||
| 
 | ||||
| native._http01 = function(me, ch) { | ||||
| 	var url = encodeURIComponent(ch.challengeUrl); | ||||
| 	return new me.request({ | ||||
| 		url: me._baseUrl + '/api/http?url=' + url | ||||
| 	}).then(function(resp) { | ||||
| 		return resp.body; | ||||
| 	}); | ||||
| }; | ||||
							
								
								
									
										94
									
								
								compat.js
									
									
									
									
									
								
							
							
						
						
									
										94
									
								
								compat.js
									
									
									
									
									
								
							| @ -1,94 +0,0 @@ | ||||
| // Copyright 2018 AJ ONeal. All rights reserved
 | ||||
| /* This Source Code Form is subject to the terms of the Mozilla Public | ||||
|  * License, v. 2.0. If a copy of the MPL was not distributed with this | ||||
|  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 | ||||
| 'use strict'; | ||||
| /* global Promise */ | ||||
| 
 | ||||
| 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.error('[acme-v2] handled(?) rejection as errback:'); | ||||
| 		console.error(err.stack); | ||||
| 
 | ||||
| 		// nextTick to get out of Promise chain
 | ||||
| 		process.nextTick(function() { | ||||
| 			cb(err); | ||||
| 		}); | ||||
| 
 | ||||
| 		// do not resolve promise further
 | ||||
| 		return new Promise(function() {}); | ||||
| 	}; | ||||
| } | ||||
| 
 | ||||
| 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) { | ||||
| 		options.agreeToTerms = | ||||
| 			options.agreeToTerms || | ||||
| 			function(tos) { | ||||
| 				return Promise.resolve(tos); | ||||
| 			}; | ||||
| 		acme2.certificates.create(options).then(function(certs) { | ||||
| 			var privkeyPem = acme2.RSA.exportPrivatePem(options.domainKeypair); | ||||
| 			certs.privkey = privkeyPem; | ||||
| 			resolveFn(cb)(certs); | ||||
| 		}, rejectFn(cb)); | ||||
| 	}; | ||||
| 	acme2.getAcmeUrls = function(options, cb) { | ||||
| 		acme2.init(options).then(resolveFn(cb), rejectFn(cb)); | ||||
| 	}; | ||||
| 	acme2.getOptions = function() { | ||||
| 		var defs = {}; | ||||
| 
 | ||||
| 		Object.keys(module.exports.defaults).forEach(function(key) { | ||||
| 			defs[key] = defs[deps] || module.exports.defaults[key]; | ||||
| 		}); | ||||
| 
 | ||||
| 		return defs; | ||||
| 	}; | ||||
| 	acme2.stagingServerUrl = module.exports.defaults.stagingServerUrl; | ||||
| 	acme2.productionServerUrl = module.exports.defaults.productionServerUrl; | ||||
| 	acme2.acmeChallengePrefix = module.exports.defaults.acmeChallengePrefix; | ||||
| 	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
 | ||||
| 	rsaKeySize: 2048, // 256
 | ||||
| 	acmeChallengePrefix: '/.well-known/acme-challenge/' | ||||
| }; | ||||
| 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; | ||||
							
								
								
									
										340
									
								
								examples/app.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										340
									
								
								examples/app.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,340 @@ | ||||
| /*global Promise*/ | ||||
| (function() { | ||||
| 	'use strict'; | ||||
| 
 | ||||
| 	var Keypairs = require('@root/keypairs'); | ||||
| 	var Rasha = require('@root/acme/rsa'); | ||||
| 	var Eckles = require('@root/acme/ecdsa'); | ||||
| 	var x509 = require('@root/acme/x509'); | ||||
| 	var CSR = require('@root/csr'); | ||||
| 	var ACME = require('@root/acme'); | ||||
| 	var accountStuff = {}; | ||||
| 
 | ||||
| 	function $(sel) { | ||||
| 		return document.querySelector(sel); | ||||
| 	} | ||||
| 	function $$(sel) { | ||||
| 		return Array.prototype.slice.call(document.querySelectorAll(sel)); | ||||
| 	} | ||||
| 
 | ||||
| 	function checkTos(tos) { | ||||
| 		if ($('input[name="tos"]:checked')) { | ||||
| 			return tos; | ||||
| 		} else { | ||||
| 			return ''; | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	function run() { | ||||
| 		console.log('hello'); | ||||
| 
 | ||||
| 		// Show different options for ECDSA vs RSA
 | ||||
| 		$$('input[name="kty"]').forEach(function($el) { | ||||
| 			$el.addEventListener('change', function(ev) { | ||||
| 				console.log(this); | ||||
| 				console.log(ev); | ||||
| 				if ('RSA' === ev.target.value) { | ||||
| 					$('.js-rsa-opts').hidden = false; | ||||
| 					$('.js-ec-opts').hidden = true; | ||||
| 				} else { | ||||
| 					$('.js-rsa-opts').hidden = true; | ||||
| 					$('.js-ec-opts').hidden = false; | ||||
| 				} | ||||
| 			}); | ||||
| 		}); | ||||
| 
 | ||||
| 		// Generate a key on submit
 | ||||
| 		$('form.js-keygen').addEventListener('submit', function(ev) { | ||||
| 			ev.preventDefault(); | ||||
| 			ev.stopPropagation(); | ||||
| 			$('.js-loading').hidden = false; | ||||
| 			$('.js-jwk').hidden = true; | ||||
| 			$('.js-toc-der-public').hidden = true; | ||||
| 			$('.js-toc-der-private').hidden = true; | ||||
| 			$$('.js-toc-pem').forEach(function($el) { | ||||
| 				$el.hidden = true; | ||||
| 			}); | ||||
| 			$$('input').map(function($el) { | ||||
| 				$el.disabled = true; | ||||
| 			}); | ||||
| 			$$('button').map(function($el) { | ||||
| 				$el.disabled = true; | ||||
| 			}); | ||||
| 			var opts = { | ||||
| 				kty: $('input[name="kty"]:checked').value, | ||||
| 				namedCurve: $('input[name="ec-crv"]:checked').value, | ||||
| 				modulusLength: $('input[name="rsa-len"]:checked').value | ||||
| 			}; | ||||
| 			var then = Date.now(); | ||||
| 			console.log('opts', opts); | ||||
| 			Keypairs.generate(opts).then(function(results) { | ||||
| 				console.log('Key generation time:', Date.now() - then + 'ms'); | ||||
| 				var pubDer; | ||||
| 				var privDer; | ||||
| 				if (/EC/i.test(opts.kty)) { | ||||
| 					privDer = x509.packPkcs8(results.private); | ||||
| 					pubDer = x509.packSpki(results.public); | ||||
| 					Eckles.export({ | ||||
| 						jwk: results.private, | ||||
| 						format: 'sec1' | ||||
| 					}).then(function(pem) { | ||||
| 						$('.js-input-pem-sec1-private').innerText = pem; | ||||
| 						$('.js-toc-pem-sec1-private').hidden = false; | ||||
| 					}); | ||||
| 					Eckles.export({ | ||||
| 						jwk: results.private, | ||||
| 						format: 'pkcs8' | ||||
| 					}).then(function(pem) { | ||||
| 						$('.js-input-pem-pkcs8-private').innerText = pem; | ||||
| 						$('.js-toc-pem-pkcs8-private').hidden = false; | ||||
| 					}); | ||||
| 					Eckles.export({ jwk: results.public, public: true }).then( | ||||
| 						function(pem) { | ||||
| 							$('.js-input-pem-spki-public').innerText = pem; | ||||
| 							$('.js-toc-pem-spki-public').hidden = false; | ||||
| 						} | ||||
| 					); | ||||
| 				} else { | ||||
| 					privDer = x509.packPkcs8(results.private); | ||||
| 					pubDer = x509.packSpki(results.public); | ||||
| 					Rasha.export({ | ||||
| 						jwk: results.private, | ||||
| 						format: 'pkcs1' | ||||
| 					}).then(function(pem) { | ||||
| 						$('.js-input-pem-pkcs1-private').innerText = pem; | ||||
| 						$('.js-toc-pem-pkcs1-private').hidden = false; | ||||
| 					}); | ||||
| 					Rasha.export({ | ||||
| 						jwk: results.private, | ||||
| 						format: 'pkcs8' | ||||
| 					}).then(function(pem) { | ||||
| 						$('.js-input-pem-pkcs8-private').innerText = pem; | ||||
| 						$('.js-toc-pem-pkcs8-private').hidden = false; | ||||
| 					}); | ||||
| 					Rasha.export({ jwk: results.public, format: 'pkcs1' }).then( | ||||
| 						function(pem) { | ||||
| 							$('.js-input-pem-pkcs1-public').innerText = pem; | ||||
| 							$('.js-toc-pem-pkcs1-public').hidden = false; | ||||
| 						} | ||||
| 					); | ||||
| 					Rasha.export({ jwk: results.public, format: 'spki' }).then( | ||||
| 						function(pem) { | ||||
| 							$('.js-input-pem-spki-public').innerText = pem; | ||||
| 							$('.js-toc-pem-spki-public').hidden = false; | ||||
| 						} | ||||
| 					); | ||||
| 				} | ||||
| 
 | ||||
| 				$('.js-der-public').innerText = pubDer; | ||||
| 				$('.js-toc-der-public').hidden = false; | ||||
| 				$('.js-der-private').innerText = privDer; | ||||
| 				$('.js-toc-der-private').hidden = false; | ||||
| 				$('.js-jwk').innerText = JSON.stringify(results, null, 2); | ||||
| 				$('.js-loading').hidden = true; | ||||
| 				$('.js-jwk').hidden = false; | ||||
| 				$$('input').map(function($el) { | ||||
| 					$el.disabled = false; | ||||
| 				}); | ||||
| 				$$('button').map(function($el) { | ||||
| 					$el.disabled = false; | ||||
| 				}); | ||||
| 				$('.js-toc-jwk').hidden = false; | ||||
| 
 | ||||
| 				$('.js-create-account').hidden = false; | ||||
| 				$('.js-create-csr').hidden = false; | ||||
| 			}); | ||||
| 		}); | ||||
| 
 | ||||
| 		$('form.js-acme-account').addEventListener('submit', function(ev) { | ||||
| 			ev.preventDefault(); | ||||
| 			ev.stopPropagation(); | ||||
| 			$('.js-loading').hidden = false; | ||||
| 			var acme = ACME.create({ | ||||
| 				Keypairs: Keypairs, | ||||
| 				CSR: CSR | ||||
| 			}); | ||||
| 			acme.init( | ||||
| 				'https://acme-staging-v02.api.letsencrypt.org/directory' | ||||
| 			).then(function(result) { | ||||
| 				console.log('acme result', result); | ||||
| 				var privJwk = JSON.parse($('.js-jwk').innerText).private; | ||||
| 				var email = $('.js-email').value; | ||||
| 				return acme.accounts | ||||
| 					.create({ | ||||
| 						email: email, | ||||
| 						agreeToTerms: checkTos, | ||||
| 						accountKeypair: { privateKeyJwk: privJwk } | ||||
| 					}) | ||||
| 					.then(function(account) { | ||||
| 						console.log('account created result:', account); | ||||
| 						accountStuff.account = account; | ||||
| 						accountStuff.privateJwk = privJwk; | ||||
| 						accountStuff.email = email; | ||||
| 						accountStuff.acme = acme; | ||||
| 						$('.js-create-order').hidden = false; | ||||
| 						$('.js-toc-acme-account-response').hidden = false; | ||||
| 						$( | ||||
| 							'.js-acme-account-response' | ||||
| 						).innerText = JSON.stringify(account, null, 2); | ||||
| 					}) | ||||
| 					.catch(function(err) { | ||||
| 						console.error('A bad thing happened:'); | ||||
| 						console.error(err); | ||||
| 						window.alert( | ||||
| 							err.message || JSON.stringify(err, null, 2) | ||||
| 						); | ||||
| 					}); | ||||
| 			}); | ||||
| 		}); | ||||
| 
 | ||||
| 		$('form.js-csr').addEventListener('submit', function(ev) { | ||||
| 			ev.preventDefault(); | ||||
| 			ev.stopPropagation(); | ||||
| 			generateCsr(); | ||||
| 		}); | ||||
| 
 | ||||
| 		$('form.js-acme-order').addEventListener('submit', function(ev) { | ||||
| 			ev.preventDefault(); | ||||
| 			ev.stopPropagation(); | ||||
| 			var account = accountStuff.account; | ||||
| 			var privJwk = accountStuff.privateJwk; | ||||
| 			var email = accountStuff.email; | ||||
| 			var acme = accountStuff.acme; | ||||
| 
 | ||||
| 			var domains = ($('.js-domains').value || 'example.com').split( | ||||
| 				/[, ]+/g | ||||
| 			); | ||||
| 			return getDomainPrivkey().then(function(domainPrivJwk) { | ||||
| 				console.log('Has CSR already?'); | ||||
| 				console.log(accountStuff.csr); | ||||
| 				return acme.certificates | ||||
| 					.create({ | ||||
| 						accountKeypair: { privateKeyJwk: privJwk }, | ||||
| 						account: account, | ||||
| 						serverKeypair: { privateKeyJwk: domainPrivJwk }, | ||||
| 						csr: accountStuff.csr, | ||||
| 						domains: domains, | ||||
| 						skipDryRun: | ||||
| 							$('input[name="skip-dryrun"]:checked') && true, | ||||
| 						agreeToTerms: checkTos, | ||||
| 						challenges: { | ||||
| 							'dns-01': { | ||||
| 								set: function(opts) { | ||||
| 									console.info('dns-01 set challenge:'); | ||||
| 									console.info('TXT', opts.dnsHost); | ||||
| 									console.info(opts.dnsAuthorization); | ||||
| 									return new Promise(function(resolve) { | ||||
| 										while ( | ||||
| 											!window.confirm( | ||||
| 												'Did you set the challenge?' | ||||
| 											) | ||||
| 										) {} | ||||
| 										resolve(); | ||||
| 									}); | ||||
| 								}, | ||||
| 								remove: function(opts) { | ||||
| 									console.log('dns-01 remove challenge:'); | ||||
| 									console.info('TXT', opts.dnsHost); | ||||
| 									console.info(opts.dnsAuthorization); | ||||
| 									return new Promise(function(resolve) { | ||||
| 										while ( | ||||
| 											!window.confirm( | ||||
| 												'Did you delete the challenge?' | ||||
| 											) | ||||
| 										) {} | ||||
| 										resolve(); | ||||
| 									}); | ||||
| 								} | ||||
| 							}, | ||||
| 							'http-01': { | ||||
| 								set: function(opts) { | ||||
| 									console.info('http-01 set challenge:'); | ||||
| 									console.info(opts.challengeUrl); | ||||
| 									console.info(opts.keyAuthorization); | ||||
| 									return new Promise(function(resolve) { | ||||
| 										while ( | ||||
| 											!window.confirm( | ||||
| 												'Did you set the challenge?' | ||||
| 											) | ||||
| 										) {} | ||||
| 										resolve(); | ||||
| 									}); | ||||
| 								}, | ||||
| 								remove: function(opts) { | ||||
| 									console.log('http-01 remove challenge:'); | ||||
| 									console.info(opts.challengeUrl); | ||||
| 									console.info(opts.keyAuthorization); | ||||
| 									return new Promise(function(resolve) { | ||||
| 										while ( | ||||
| 											!window.confirm( | ||||
| 												'Did you delete the challenge?' | ||||
| 											) | ||||
| 										) {} | ||||
| 										resolve(); | ||||
| 									}); | ||||
| 								} | ||||
| 							} | ||||
| 						}, | ||||
| 						challengeTypes: [ | ||||
| 							$('input[name="acme-challenge-type"]:checked').value | ||||
| 						] | ||||
| 					}) | ||||
| 					.then(function(results) { | ||||
| 						console.log('Got Certificates:'); | ||||
| 						console.log(results); | ||||
| 						$('.js-toc-acme-order-response').hidden = false; | ||||
| 						$('.js-acme-order-response').innerText = JSON.stringify( | ||||
| 							results, | ||||
| 							null, | ||||
| 							2 | ||||
| 						); | ||||
| 					}) | ||||
| 					.catch(function(err) { | ||||
| 						console.error('challenge failed:'); | ||||
| 						console.error(err); | ||||
| 						window.alert( | ||||
| 							'failed! ' + err.message || JSON.stringify(err) | ||||
| 						); | ||||
| 					}); | ||||
| 			}); | ||||
| 		}); | ||||
| 
 | ||||
| 		$('.js-generate').hidden = false; | ||||
| 	} | ||||
| 
 | ||||
| 	function getDomainPrivkey() { | ||||
| 		if (accountStuff.domainPrivateJwk) { | ||||
| 			return Promise.resolve(accountStuff.domainPrivateJwk); | ||||
| 		} | ||||
| 		return Keypairs.generate({ | ||||
| 			kty: $('input[name="kty"]:checked').value, | ||||
| 			namedCurve: $('input[name="ec-crv"]:checked').value, | ||||
| 			modulusLength: $('input[name="rsa-len"]:checked').value | ||||
| 		}).then(function(pair) { | ||||
| 			console.log('domain keypair:', pair); | ||||
| 			accountStuff.domainPrivateJwk = pair.private; | ||||
| 			return pair.private; | ||||
| 		}); | ||||
| 	} | ||||
| 
 | ||||
| 	function generateCsr() { | ||||
| 		var domains = ($('.js-domains').value || 'example.com').split(/[, ]+/g); | ||||
| 		//var privJwk = JSON.parse($('.js-jwk').innerText).private;
 | ||||
| 		return getDomainPrivkey().then(function(privJwk) { | ||||
| 			accountStuff.domainPrivateJwk = privJwk; | ||||
| 			return CSR({ jwk: privJwk, domains: domains }).then(function(pem) { | ||||
| 				// Verify with https://www.sslshopper.com/csr-decoder.html
 | ||||
| 				accountStuff.csr = pem; | ||||
| 				console.log('Created CSR:'); | ||||
| 				console.log(pem); | ||||
| 
 | ||||
| 				console.log('CSR info:'); | ||||
| 				console.log(CSR._info(pem)); | ||||
| 
 | ||||
| 				return pem; | ||||
| 			}); | ||||
| 		}); | ||||
| 	} | ||||
| 
 | ||||
| 	window.addEventListener('load', run); | ||||
| })(); | ||||
| @ -1,69 +0,0 @@ | ||||
| (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); | ||||
| @ -1,3 +1,6 @@ | ||||
| ACME_EMAIL=jon.doe@gmail.com | ||||
| ACME_DOMAINS=example.com,foo.example.com,*.foo.example.com | ||||
| DIGITALOCEAN_API_KEY=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx | ||||
| ENV=DEV | ||||
| SUBSCRIBER_EMAIL=letsencrypt+staging@example.com | ||||
| BASE_DOMAIN=test.example.com | ||||
| CHALLENGE_TYPE=dns-01 | ||||
| CHALLENGE_PLUGIN=acme-dns-01-digitalocean | ||||
| CHALLENGE_OPTIONS='{"token":"xxxxxxxxxxxx"}' | ||||
|  | ||||
| @ -11,7 +11,10 @@ if (!fs.existsSync(__dirname + '/../tests/account.privkey.pem')) { | ||||
| 		var privkeyPem = RSA.exportPrivatePem(keypair); | ||||
| 		console.log(privkeyPem); | ||||
| 
 | ||||
| 		fs.writeFileSync(__dirname + '/../tests/account.privkey.pem', privkeyPem); | ||||
| 		fs.writeFileSync( | ||||
| 			__dirname + '/../tests/account.privkey.pem', | ||||
| 			privkeyPem | ||||
| 		); | ||||
| 	}); | ||||
| } | ||||
| 
 | ||||
|  | ||||
| @ -1,13 +0,0 @@ | ||||
| // Copyright 2018 AJ ONeal. All rights reserved
 | ||||
| /* This Source Code Form is subject to the terms of the Mozilla Public | ||||
|  * License, v. 2.0. If a copy of the MPL was not distributed with this | ||||
|  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 | ||||
| 'use strict'; | ||||
| 
 | ||||
| var http = require('http'); | ||||
| var express = require('express'); | ||||
| var server = http | ||||
| 	.createServer(express.static('../tests')) | ||||
| 	.listen(80, function() { | ||||
| 		console.log('Listening on', this.address()); | ||||
| 	}); | ||||
| @ -1,20 +0,0 @@ | ||||
| // Copyright 2018 AJ ONeal. All rights reserved
 | ||||
| /* This Source Code Form is subject to the terms of the Mozilla Public | ||||
|  * License, v. 2.0. If a copy of the MPL was not distributed with this | ||||
|  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 | ||||
| 'use strict'; | ||||
| 
 | ||||
| var https = require('https'); | ||||
| var server = https | ||||
| 	.createServer( | ||||
| 		{ | ||||
| 			key: require('fs').readFileSync('../tests/privkey.pem'), | ||||
| 			cert: require('fs').readFileSync('../tests/fullchain.pem') | ||||
| 		}, | ||||
| 		function(req, res) { | ||||
| 			res.end('Hello, World!'); | ||||
| 		} | ||||
| 	) | ||||
| 	.listen(443, function() { | ||||
| 		console.log('Listening on', this.address()); | ||||
| 	}); | ||||
							
								
								
									
										231
									
								
								examples/index.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										231
									
								
								examples/index.html
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,231 @@ | ||||
| <html> | ||||
| 	<head> | ||||
| 		<title>Bluecrypt ACME - A Root Project</title> | ||||
| 		<meta charset="UTF-8" /> | ||||
| 		<style> | ||||
| 			textarea { | ||||
| 				width: 42em; | ||||
| 				height: 10em; | ||||
| 			} | ||||
| 			/* need to word wrap the binary no space der */ | ||||
| 			.js-der-public, | ||||
| 			.js-der-private { | ||||
| 				white-space: pre-wrap; /* CSS3 */ | ||||
| 				white-space: -moz-pre-wrap; /* Firefox */ | ||||
| 				white-space: -pre-wrap; /* Opera <7 */ | ||||
| 				white-space: -o-pre-wrap; /* Opera 7 */ | ||||
| 				word-wrap: break-word; /* IE */ | ||||
| 			} | ||||
| 		</style> | ||||
| 	</head> | ||||
| 	<body> | ||||
| 		<h1> | ||||
| 			@bluecrypt/acme: Let's Encrypt for the Browser | ||||
| 		</h1> | ||||
| 
 | ||||
| 		<p> | ||||
| 			This is intended to be explored with your JavaScript console open. | ||||
| 		</p> | ||||
| 		<pre><code><script src="<a href="https://rootprojects.org/acme/bluecrypt-acme.js">https://rootprojects.org/acme/bluecrypt-acme.js</a>"></script></code></pre> | ||||
| 		<pre><code><script src="<a href="https://rootprojects.org/acme/bluecrypt-acme.min.js">https://rootprojects.org/acme/bluecrypt-acme.min.js</a>"></script></code></pre> | ||||
| 		<a href="https://git.rootprojects.org/root/bluecrypt-acme.js" | ||||
| 			>Documentation</a | ||||
| 		> | ||||
| 
 | ||||
| 		<h2>1. Keypair Generation</h2> | ||||
| 		<form class="js-keygen"> | ||||
| 			<p>Key Type:</p> | ||||
| 			<div> | ||||
| 				<input type="radio" id="-ktyEC" name="kty" value="EC" checked /> | ||||
| 				<label for="-ktyEC">ECDSA</label> | ||||
| 				<input type="radio" id="-ktyRSA" name="kty" value="RSA" /> | ||||
| 				<label for="-ktyRSA">RSA</label> | ||||
| 			</div> | ||||
| 			<div class="js-ec-opts"> | ||||
| 				<p>EC Options:</p> | ||||
| 				<label for="-crv2" | ||||
| 					><input | ||||
| 						type="radio" | ||||
| 						id="-crv2" | ||||
| 						name="ec-crv" | ||||
| 						value="P-256" | ||||
| 						checked | ||||
| 					/>P-256</label | ||||
| 				> | ||||
| 				<label for="-crv3" | ||||
| 					><input | ||||
| 						type="radio" | ||||
| 						id="-crv3" | ||||
| 						name="ec-crv" | ||||
| 						value="P-384" | ||||
| 					/>P-384</label | ||||
| 				> | ||||
| 				<!-- label for="-crv5"><input type="radio" id="-crv5" | ||||
|          name="ec-crv" value="P-521">P-521</label --> | ||||
| 			</div> | ||||
| 			<div class="js-rsa-opts" hidden> | ||||
| 				<p>RSA Options:</p> | ||||
| 				<label for="-modlen2" | ||||
| 					><input | ||||
| 						type="radio" | ||||
| 						id="-modlen2" | ||||
| 						name="rsa-len" | ||||
| 						value="2048" | ||||
| 						checked | ||||
| 					/>2048</label | ||||
| 				> | ||||
| 				<label for="-modlen3" | ||||
| 					><input | ||||
| 						type="radio" | ||||
| 						id="-modlen3" | ||||
| 						name="rsa-len" | ||||
| 						value="3072" | ||||
| 					/>3072</label | ||||
| 				> | ||||
| 				<label for="-modlen5" | ||||
| 					><input | ||||
| 						type="radio" | ||||
| 						id="-modlen5" | ||||
| 						name="rsa-len" | ||||
| 						value="4096" | ||||
| 					/>4096</label | ||||
| 				> | ||||
| 			</div> | ||||
| 			<button class="js-generate" hidden>Generate</button> | ||||
| 		</form> | ||||
| 
 | ||||
| 		<h2>2. ACME Account</h2> | ||||
| 		<form class="js-acme-account"> | ||||
| 			<label for="-acmeEmail">Email:</label> | ||||
| 			<input | ||||
| 				class="js-email" | ||||
| 				type="email" | ||||
| 				id="-acmeEmail" | ||||
| 				value="john.doe@gmail.com" | ||||
| 			/> | ||||
| 			<br /> | ||||
| 			<label for="-acmeTos" | ||||
| 				><input | ||||
| 					class="js-tos" | ||||
| 					name="tos" | ||||
| 					type="checkbox" | ||||
| 					id="-acmeTos" | ||||
| 					checked | ||||
| 				/> | ||||
| 				Agree to Let's Encrypt Terms of Service</label | ||||
| 			> | ||||
| 			<br /> | ||||
| 			<button class="js-create-account" hidden>Create Account</button> | ||||
| 		</form> | ||||
| 
 | ||||
| 		<h2>3. (optional) Certificate Signing Request</h2> | ||||
| 		<form class="js-csr"> | ||||
| 			<label for="-acmeDomains">Domains:</label> | ||||
| 			<input | ||||
| 				class="js-domains" | ||||
| 				type="text" | ||||
| 				id="-acmeDomains" | ||||
| 				value="example.com www.example.com" | ||||
| 			/> | ||||
| 			<br /> | ||||
| 			<button class="js-create-csr" hidden>Create CSR</button> | ||||
| 		</form> | ||||
| 
 | ||||
| 		<h2>4. ACME Certificate Order</h2> | ||||
| 		<form class="js-acme-order"> | ||||
| 			Challenge type: | ||||
| 			<label for="-http01" | ||||
| 				><input | ||||
| 					type="radio" | ||||
| 					id="-http01" | ||||
| 					name="acme-challenge-type" | ||||
| 					value="http-01" | ||||
| 					checked | ||||
| 				/>http-01</label | ||||
| 			> | ||||
| 			<label for="-dns01" | ||||
| 				><input | ||||
| 					type="radio" | ||||
| 					id="-dns01" | ||||
| 					name="acme-challenge-type" | ||||
| 					value="dns-01" | ||||
| 				/>dns-01</label | ||||
| 			> | ||||
| 			<br /> | ||||
| 			<label for="-skipDryrun" | ||||
| 				><input | ||||
| 					class="js-skip-dryrun" | ||||
| 					name="skip-dryrun" | ||||
| 					type="checkbox" | ||||
| 					id="-skipDryrun" | ||||
| 					checked | ||||
| 				/> | ||||
| 				Skip dry-run challenge</label | ||||
| 			> | ||||
| 			<br /> | ||||
| 			<button class="js-create-order" hidden>Create Order</button> | ||||
| 		</form> | ||||
| 
 | ||||
| 		<div class="js-loading" hidden>Loading</div> | ||||
| 
 | ||||
| 		<details class="js-toc-jwk" hidden> | ||||
| 			<summary>JWK Keypair</summary> | ||||
| 			<pre><code class="js-jwk"> </code></pre> | ||||
| 		</details> | ||||
| 		<details class="js-toc-der-private" hidden> | ||||
| 			<summary>DER Private Binary</summary> | ||||
| 			<pre><code class="js-der-private"> </code></pre> | ||||
| 		</details> | ||||
| 		<details class="js-toc-der-public" hidden> | ||||
| 			<summary>DER Public Binary</summary> | ||||
| 			<pre><code class="js-der-public"> </code></pre> | ||||
| 		</details> | ||||
| 		<details class="js-toc-pem js-toc-pem-pkcs1-private" hidden> | ||||
| 			<summary>PEM Private (base64-encoded PKCS1 DER)</summary> | ||||
| 			<pre><code  class="js-input-pem-pkcs1-private" ></code></pre> | ||||
| 		</details> | ||||
| 		<details class="js-toc-pem js-toc-pem-sec1-private" hidden> | ||||
| 			<summary>PEM Private (base64-encoded SEC1 DER)</summary> | ||||
| 			<pre><code  class="js-input-pem-sec1-private" ></code></pre> | ||||
| 		</details> | ||||
| 		<details class="js-toc-pem js-toc-pem-pkcs8-private" hidden> | ||||
| 			<summary>PEM Private (base64-encoded PKCS8 DER)</summary> | ||||
| 			<pre><code  class="js-input-pem-pkcs8-private" ></code></pre> | ||||
| 		</details> | ||||
| 		<details class="js-toc-pem js-toc-pem-pkcs1-public" hidden> | ||||
| 			<summary>PEM Public (base64-encoded PKCS1 DER)</summary> | ||||
| 			<pre><code  class="js-input-pem-pkcs1-public" ></code></pre> | ||||
| 		</details> | ||||
| 		<details class="js-toc-pem js-toc-pem-spki-public" hidden> | ||||
| 			<summary>PEM Public (base64-encoded SPKI/PKIX DER)</summary> | ||||
| 			<pre><code  class="js-input-pem-spki-public" ></code></pre> | ||||
| 		</details> | ||||
| 		<details class="js-toc-acme-account-response" hidden> | ||||
| 			<summary>ACME Account Request</summary> | ||||
| 			<pre><code class="js-acme-account-response"> </code></pre> | ||||
| 		</details> | ||||
| 		<details class="js-toc-acme-order-response" hidden> | ||||
| 			<summary>ACME Order Response</summary> | ||||
| 			<pre><code class="js-acme-order-response"> </code></pre> | ||||
| 		</details> | ||||
| 
 | ||||
| 		<br /> | ||||
| 		<p> | ||||
| 			Bluecrypt™ is a collection of lightweight, zero-dependency, | ||||
| 			libraries written in VanillaJS. They are fast, tiny, and secure, | ||||
| 			using the native features of modern browsers where possible. This | ||||
| 			means it's easy-to-use crypto in kilobytes, not megabytes. | ||||
| 		</p> | ||||
| 		<br /> | ||||
| 		<footer> | ||||
| 			View (git) source | ||||
| 			<a href="https://git.rootprojects.org/root/bluecrypt-acme.js" | ||||
| 				>@bluecrypt/acme</a | ||||
| 			> | ||||
| 		</footer> | ||||
| 
 | ||||
| 		<script src="./app.js"></script> | ||||
| 		<!-- script src="../dist/acme.js"></script --> | ||||
| 		<!-- script src="../dist/app.js"></script --> | ||||
| 	</body> | ||||
| </html> | ||||
							
								
								
									
										174
									
								
								examples/server.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										174
									
								
								examples/server.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,174 @@ | ||||
| 'use strict'; | ||||
| 
 | ||||
| var crypto = require('crypto'); | ||||
| //var dnsjs = require('dns-suite');
 | ||||
| var dig = require('dig.js/dns-request'); | ||||
| var request = require('util').promisify(require('@root/request')); | ||||
| var express = require('express'); | ||||
| var app = express(); | ||||
| 
 | ||||
| var nameservers = require('dns').getServers(); | ||||
| var index = crypto.randomBytes(2).readUInt16BE(0) % nameservers.length; | ||||
| var nameserver = nameservers[index]; | ||||
| 
 | ||||
| app.use('/', express.static(__dirname)); | ||||
| app.use('/api', express.json()); | ||||
| app.get('/api/dns/:domain', function(req, res, next) { | ||||
| 	var domain = req.params.domain; | ||||
| 	var casedDomain = domain | ||||
| 		.toLowerCase() | ||||
| 		.split('') | ||||
| 		.map(function(ch) { | ||||
| 			// dns0x20 takes advantage of the fact that the binary operation for toUpperCase is
 | ||||
| 			// ch = ch | 0x20;
 | ||||
| 			return Math.round(Math.random()) % 2 ? ch : ch.toUpperCase(); | ||||
| 		}) | ||||
| 		.join(''); | ||||
| 	var typ = req.query.type; | ||||
| 	var query = { | ||||
| 		header: { | ||||
| 			id: crypto.randomBytes(2).readUInt16BE(0), | ||||
| 			qr: 0, | ||||
| 			opcode: 0, | ||||
| 			aa: 0, // Authoritative-Only
 | ||||
| 			tc: 0, // NA
 | ||||
| 			rd: 1, // Recurse
 | ||||
| 			ra: 0, // NA
 | ||||
| 			rcode: 0 // NA
 | ||||
| 		}, | ||||
| 		question: [ | ||||
| 			{ | ||||
| 				name: casedDomain, | ||||
| 				//, type: typ || 'A'
 | ||||
| 				typeName: typ || 'A', | ||||
| 				className: 'IN' | ||||
| 			} | ||||
| 		] | ||||
| 	}; | ||||
| 	var opts = { | ||||
| 		onError: function(err) { | ||||
| 			next(err); | ||||
| 		}, | ||||
| 		onMessage: function(packet) { | ||||
| 			var fail0x20; | ||||
| 
 | ||||
| 			if (packet.id !== query.id) { | ||||
| 				console.error( | ||||
| 					"[SECURITY] ignoring packet for '" + | ||||
| 						packet.question[0].name + | ||||
| 						"' due to mismatched id" | ||||
| 				); | ||||
| 				console.error(packet); | ||||
| 				return; | ||||
| 			} | ||||
| 
 | ||||
| 			packet.question.forEach(function(q) { | ||||
| 				// if (-1 === q.name.lastIndexOf(cli.casedQuery))
 | ||||
| 				if (q.name !== casedDomain) { | ||||
| 					fail0x20 = q.name; | ||||
| 				} | ||||
| 			}); | ||||
| 
 | ||||
| 			['question', 'answer', 'authority', 'additional'].forEach(function( | ||||
| 				group | ||||
| 			) { | ||||
| 				(packet[group] || []).forEach(function(a) { | ||||
| 					var an = a.name; | ||||
| 					var i = domain | ||||
| 						.toLowerCase() | ||||
| 						.lastIndexOf(a.name.toLowerCase()); // answer is something like ExAMPle.cOM and query was wWw.ExAMPle.cOM
 | ||||
| 					var j = a.name | ||||
| 						.toLowerCase() | ||||
| 						.lastIndexOf(domain.toLowerCase()); // answer is something like www.ExAMPle.cOM and query was ExAMPle.cOM
 | ||||
| 
 | ||||
| 					// it's important to note that these should only relpace changes in casing that we expected
 | ||||
| 					// any abnormalities should be left intact to go "huh?" about
 | ||||
| 					// TODO detect abnormalities?
 | ||||
| 					if (-1 !== i) { | ||||
| 						// "EXamPLE.cOm".replace("wWw.EXamPLE.cOm".substr(4), "www.example.com".substr(4))
 | ||||
| 						a.name = a.name.replace( | ||||
| 							casedDomain.substr(i), | ||||
| 							domain.substr(i) | ||||
| 						); | ||||
| 					} else if (-1 !== j) { | ||||
| 						// "www.example.com".replace("EXamPLE.cOm", "example.com")
 | ||||
| 						a.name = | ||||
| 							a.name.substr(0, j) + | ||||
| 							a.name.substr(j).replace(casedDomain, domain); | ||||
| 					} | ||||
| 
 | ||||
| 					// NOTE: right now this assumes that anything matching the query matches all the way to the end
 | ||||
| 					// it does not handle the case of a record for example.com.uk being returned in response to a query for www.example.com correctly
 | ||||
| 					// (but I don't think it should need to)
 | ||||
| 					if (a.name.length !== an.length) { | ||||
| 						console.error( | ||||
| 							"[ERROR] question / answer mismatch: '" + | ||||
| 								an + | ||||
| 								"' != '" + | ||||
| 								a.length + | ||||
| 								"'" | ||||
| 						); | ||||
| 						console.error(a); | ||||
| 					} | ||||
| 				}); | ||||
| 			}); | ||||
| 
 | ||||
| 			if (fail0x20) { | ||||
| 				console.warn( | ||||
| 					";; Warning: DNS 0x20 security not implemented (or packet spoofed). Queried '" + | ||||
| 						casedDomain + | ||||
| 						"' but got response for '" + | ||||
| 						fail0x20 + | ||||
| 						"'." | ||||
| 				); | ||||
| 				return; | ||||
| 			} | ||||
| 
 | ||||
| 			res.send({ | ||||
| 				header: packet.header, | ||||
| 				question: packet.question, | ||||
| 				answer: packet.answer, | ||||
| 				authority: packet.authority, | ||||
| 				additional: packet.additional, | ||||
| 				edns_options: packet.edns_options | ||||
| 			}); | ||||
| 		}, | ||||
| 		onListening: function() {}, | ||||
| 		onSent: function(/*res*/) {}, | ||||
| 		onTimeout: function(res) { | ||||
| 			console.error('dns timeout:', res); | ||||
| 			next(new Error('DNS timeout - no response')); | ||||
| 		}, | ||||
| 		onClose: function() {}, | ||||
| 		//, mdns: cli.mdns
 | ||||
| 		nameserver: nameserver, | ||||
| 		port: 53, | ||||
| 		timeout: 2000 | ||||
| 	}; | ||||
| 
 | ||||
| 	dig.resolveJson(query, opts); | ||||
| }); | ||||
| app.get('/api/http', function(req, res) { | ||||
| 	var url = req.query.url; | ||||
| 	return request({ method: 'GET', url: url }).then(function(resp) { | ||||
| 		res.send(resp.body); | ||||
| 	}); | ||||
| }); | ||||
| app.get('/api/_acme_api_', function(req, res) { | ||||
| 	res.send({ success: true }); | ||||
| }); | ||||
| 
 | ||||
| module.exports = app; | ||||
| if (require.main === module) { | ||||
| 	// curl -L http://localhost:3000/api/dns/example.com?type=A
 | ||||
| 	console.info('Listening on localhost:3000'); | ||||
| 	app.listen(3000); | ||||
| 	console.info('Try this:'); | ||||
| 	console.info("\tcurl -L 'http://localhost:3000/api/_acme_api_/'"); | ||||
| 	console.info( | ||||
| 		"\tcurl -L 'http://localhost:3000/api/dns/example.com?type=A'" | ||||
| 	); | ||||
| 	console.info( | ||||
| 		"\tcurl -L 'http://localhost:3000/api/http/?url=https://example.com'" | ||||
| 	); | ||||
| } | ||||
							
								
								
									
										17
									
								
								fixtures/account.jwk.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								fixtures/account.jwk.json
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,17 @@ | ||||
| { | ||||
| 	"private": { | ||||
| 		"kty": "EC", | ||||
| 		"crv": "P-256", | ||||
| 		"d": "HB1OvdHfLnIy2mYYO9cLU4BqP36CeyS8OsDf3OnYP-M", | ||||
| 		"x": "uLh0RLpAmKyyHCf2zOaF18IIuBiJEiZ8Mu3xPZ7ZxN8", | ||||
| 		"y": "vVl_cCXK0_GlCaCT5Yg750LUd8eRU6tySEdQFLM62NQ", | ||||
| 		"kid": "UuuZa_56jCM2douUq1riGyRphPtRvCPkxtkg0bP-pNs" | ||||
| 	}, | ||||
| 	"public": { | ||||
| 		"kty": "EC", | ||||
| 		"crv": "P-256", | ||||
| 		"x": "uLh0RLpAmKyyHCf2zOaF18IIuBiJEiZ8Mu3xPZ7ZxN8", | ||||
| 		"y": "vVl_cCXK0_GlCaCT5Yg750LUd8eRU6tySEdQFLM62NQ", | ||||
| 		"kid": "UuuZa_56jCM2douUq1riGyRphPtRvCPkxtkg0bP-pNs" | ||||
| 	} | ||||
| } | ||||
							
								
								
									
										13
									
								
								fixtures/account.registration.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								fixtures/account.registration.json
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,13 @@ | ||||
| { | ||||
| 	"key": { | ||||
| 		"kty": "EC", | ||||
| 		"crv": "P-256", | ||||
| 		"x": "uLh0RLpAmKyyHCf2zOaF18IIuBiJEiZ8Mu3xPZ7ZxN8", | ||||
| 		"y": "vVl_cCXK0_GlCaCT5Yg750LUd8eRU6tySEdQFLM62NQ", | ||||
| 		"kid": "https://acme-staging-v02.api.letsencrypt.org/acme/acct/11265299" | ||||
| 	}, | ||||
| 	"contact": [], | ||||
| 	"initialIp": "66.219.236.169", | ||||
| 	"createdAt": "2019-10-04T22:54:28.569489074Z", | ||||
| 	"status": "valid" | ||||
| } | ||||
							
								
								
									
										20
									
								
								fixtures/server.jwk.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										20
									
								
								fixtures/server.jwk.json
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,20 @@ | ||||
| { | ||||
| 	"private": { | ||||
| 		"kty": "RSA", | ||||
| 		"n": "ud6agEF9P6H66ciYgvZ_FakyZKossq5i6J2D4wIcJBnem5X63t7u3E7Rpc7rgVB5MElUNZmBoVO3VbaVJpiG0tS5zxkOZcj_k6C_5LXBdTHinG0bFZHtV6Wapf5fJ4PXNp71AHWv09qz4swJzz6_Rp_7ovNpivVsdVHfd8g9HqH3sjouwfIGfo-1LLm0F4NM12AJZISFt_03knhbvtd5x4ASorBiENPPnv2s7SA5kFT1Seeu-iUCq8PlKi-HMbNrLeM2E3wYySQPSSDt6UXRTvIzW_8upXRvaVThJk3wWjx-qt1CUIFoZBh2RsmiujWFFc6ORXb3GlF3U4LaMt3YEw", | ||||
| 		"e": "AQAB", | ||||
| 		"d": "YCzN9yVr4Jw5D_UK7WEMuzGUcMAZZs-TQFgY4UK7Ovbj18_QQrhKElb6Zfhepcf1HUYkO6PVjpuZ1tEl9hWgVcFa781AROyvSj04beiaVMDeSCCwjgW3MM3w6olnxTOUDaBMl9NNiqq0v9riDImkQbAQbe3To-KAH2ig4AMNlSZJAhmI2zAMiJhQE_pAcCxc-bQ5oNO-WSU0GRHWdMJSXp9mFgoBhVPDYGW-dmnoFzuNWssxlSqGXY-8a2YOuiunK6XM5_80c1eQqmy-k1InUIViR_wljskc8UiH6xa8BCznZYacgSz4PnvKsiKWKQQ1eliIucV3MC6BzMD3N8EWqQ", | ||||
| 		"p": "8NUtOIglu0dvDGmEB7QC5eC02Y2jZKnoxHSPKMAEPxQ0131_2aL49IzADWoTvae3NBPzU7ol3RwJo_GvS967OysfOr6Od699p1FSLwLfK89aql7_uVPJh4Q43H-W_NtRHKUkv0OmkDiwa4WqBQTVfREdPQ3NJT7vIY-cqH_AMRc", | ||||
| 		"q": "xZNIl9NRl3b0_V8Y-7_6_foIu9Sx5ILv2XV7WONDx2jp4vuT7byLm1UWdYPBbxLyd5TAvWqtyvaRtVNyplrD0PyyPK3NxqVJde0uzScAU-bf25DeK30V22Xo7IEZiPZoizrjtzGnS6VVNJmZ-Ictz3xmWIudw5d5XDH12fFRlmU", | ||||
| 		"dp": "F1Ld9UqiNNf_NjmF0uUpHrA7c5JXD6mw5E3Ri4XFI4LGd1QtLJuu9qgm9WWfkc-LW5zPBP3TKu3LNThz3KougdV0SdEopQi255xllC34BRso0bUvmPg3XUt94kTtD4ICAf8wZuGbYP5Mf61LQP8t2dXtefs7Me89Y4ewCVWN_HM", | ||||
| 		"dq": "oPuT35lgVtCnZ7dPrPjNMpnC-gCg_fcuJPqTiWaLuHQkdjzUWJYTDnqy9Qdo2e8PPx4mOXAtsT1clekrdp5oBOWQ-N4I172fcIXUZ3ZKzxJD_iw4yih-YajUs7exLabQoflWx9KeZIWPOm-ZRCYoznGnFqiT4GWQje1rS6xT9P0", | ||||
| 		"qi": "aXkK-w4Npw0BpUEzQ1PURVGm5y5cKIdd-CfEYwub19rronI9EEvuQHoqR7ODtZ_mlIIffHmHaM3ug50fJDB9QDOG4Ioc5S4YxVURT58Ps8at-dQAAP1UgSlV3vhXh4WZRaDECUI_728U3fxQqH78bJsy81mU8MtGU8LR_eTMXx8", | ||||
| 		"kid": "1hxSLs31DwbGo532keMUL9eY8L6gWyYlbcr0TtiV7qk" | ||||
| 	}, | ||||
| 	"public": { | ||||
| 		"kty": "RSA", | ||||
| 		"n": "ud6agEF9P6H66ciYgvZ_FakyZKossq5i6J2D4wIcJBnem5X63t7u3E7Rpc7rgVB5MElUNZmBoVO3VbaVJpiG0tS5zxkOZcj_k6C_5LXBdTHinG0bFZHtV6Wapf5fJ4PXNp71AHWv09qz4swJzz6_Rp_7ovNpivVsdVHfd8g9HqH3sjouwfIGfo-1LLm0F4NM12AJZISFt_03knhbvtd5x4ASorBiENPPnv2s7SA5kFT1Seeu-iUCq8PlKi-HMbNrLeM2E3wYySQPSSDt6UXRTvIzW_8upXRvaVThJk3wWjx-qt1CUIFoZBh2RsmiujWFFc6ORXb3GlF3U4LaMt3YEw", | ||||
| 		"e": "AQAB", | ||||
| 		"kid": "1hxSLs31DwbGo532keMUL9eY8L6gWyYlbcr0TtiV7qk" | ||||
| 	} | ||||
| } | ||||
							
								
								
									
										32
									
								
								lib/browser/http.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										32
									
								
								lib/browser/http.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,32 @@ | ||||
| 'use strict'; | ||||
| 
 | ||||
| var http = module.exports; | ||||
| 
 | ||||
| http.request = function(opts) { | ||||
| 	return window.fetch(opts.url, opts).then(function(resp) { | ||||
| 		var headers = {}; | ||||
| 		var result = { | ||||
| 			statusCode: resp.status, | ||||
| 			headers: headers, | ||||
| 			toJSON: function() { | ||||
| 				return this; | ||||
| 			} | ||||
| 		}; | ||||
| 		Array.from(resp.headers.entries()).forEach(function(h) { | ||||
| 			headers[h[0]] = h[1]; | ||||
| 		}); | ||||
| 		if (!headers['content-type']) { | ||||
| 			return result; | ||||
| 		} | ||||
| 		if (/json/.test(headers['content-type'])) { | ||||
| 			return resp.json().then(function(json) { | ||||
| 				result.body = json; | ||||
| 				return result; | ||||
| 			}); | ||||
| 		} | ||||
| 		return resp.text().then(function(txt) { | ||||
| 			result.body = txt; | ||||
| 			return result; | ||||
| 		}); | ||||
| 	}); | ||||
| }; | ||||
							
								
								
									
										13
									
								
								lib/browser/sha2.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								lib/browser/sha2.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,13 @@ | ||||
| 'use strict'; | ||||
| 
 | ||||
| var sha2 = module.exports; | ||||
| 
 | ||||
| var encoder = new TextEncoder(); | ||||
| sha2.sum = function(alg, str) { | ||||
| 	var data = str; | ||||
| 	if ('string' === typeof data) { | ||||
| 		data = encoder.encode(str); | ||||
| 	} | ||||
| 	var sha = 'SHA-' + String(alg).replace(/^sha-?/i, ''); | ||||
| 	return window.crypto.subtle.digest(sha, data); | ||||
| }; | ||||
							
								
								
									
										19
									
								
								lib/node/http.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										19
									
								
								lib/node/http.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,19 @@ | ||||
| 'use strict'; | ||||
| 
 | ||||
| var http = module.exports; | ||||
| var promisify = require('util').promisify; | ||||
| var request = promisify(require('@root/request')); | ||||
| 
 | ||||
| http.request = function(opts) { | ||||
| 	if (!opts.headers) { | ||||
| 		opts.headers = {}; | ||||
| 	} | ||||
| 	if ( | ||||
| 		!Object.keys(opts.headers).some(function(key) { | ||||
| 			return 'user-agent' === key.toLowerCase(); | ||||
| 		}) | ||||
| 	) { | ||||
| 		// TODO opts.headers['User-Agent'] = 'TODO';
 | ||||
| 	} | ||||
| 	return request(opts); | ||||
| }; | ||||
							
								
								
									
										17
									
								
								lib/node/sha2.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								lib/node/sha2.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,17 @@ | ||||
| /* global Promise */ | ||||
| 'use strict'; | ||||
| 
 | ||||
| var sha2 = module.exports; | ||||
| var crypto = require('crypto'); | ||||
| 
 | ||||
| sha2.sum = function(alg, str) { | ||||
| 	return Promise.resolve().then(function() { | ||||
| 		var sha = 'sha' + String(alg).replace(/^sha-?/i, ''); | ||||
| 		// utf8 is the default for strings
 | ||||
| 		var buf = Buffer.from(str); | ||||
| 		return crypto | ||||
| 			.createHash(sha) | ||||
| 			.update(buf) | ||||
| 			.digest(); | ||||
| 	}); | ||||
| }; | ||||
							
								
								
									
										33
									
								
								native.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										33
									
								
								native.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,33 @@ | ||||
| 'use strict'; | ||||
| 
 | ||||
| var native = module.exports; | ||||
| var promisify = require('util').promisify; | ||||
| var resolveTxt = promisify(require('dns').resolveTxt); | ||||
| 
 | ||||
| native._canCheck = function(me) { | ||||
| 	me._canCheck = {}; | ||||
| 	me._canCheck['http-01'] = true; | ||||
| 	me._canCheck['dns-01'] = true; | ||||
| 	return Promise.resolve(); | ||||
| }; | ||||
| 
 | ||||
| native._dns01 = function(me, ch) { | ||||
| 	// TODO use digd.js
 | ||||
| 	return resolveTxt(ch.dnsHost).then(function(records) { | ||||
| 		return { | ||||
| 			answer: records.map(function(rr) { | ||||
| 				return { | ||||
| 					data: rr | ||||
| 				}; | ||||
| 			}) | ||||
| 		}; | ||||
| 	}); | ||||
| }; | ||||
| 
 | ||||
| native._http01 = function(me, ch) { | ||||
| 	return new me.request({ | ||||
| 		url: ch.challengeUrl | ||||
| 	}).then(function(resp) { | ||||
| 		return resp.body; | ||||
| 	}); | ||||
| }; | ||||
							
								
								
									
										241
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										241
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							| @ -1,46 +1,227 @@ | ||||
| { | ||||
| 	"name": "acme-v2", | ||||
| 	"version": "1.8.6", | ||||
| 	"name": "@root/acme", | ||||
| 	"version": "3.0.0-wip.4", | ||||
| 	"lockfileVersion": 1, | ||||
| 	"requires": true, | ||||
| 	"dependencies": { | ||||
| 		"@root/asn1": { | ||||
| 			"version": "1.0.0", | ||||
| 			"resolved": "https://registry.npmjs.org/@root/asn1/-/asn1-1.0.0.tgz", | ||||
| 			"integrity": "sha512-0lfZNuOULKJDJmdIkP8V9RnbV3XaK6PAHD3swnFy4tZwtlMDzLKoM/dfNad7ut8Hu3r91wy9uK0WA/9zym5mig==", | ||||
| 			"requires": { | ||||
| 				"@root/encoding": "^1.0.1" | ||||
| 			} | ||||
| 		}, | ||||
| 		"@root/csr": { | ||||
| 			"version": "0.8.1", | ||||
| 			"resolved": "https://registry.npmjs.org/@root/csr/-/csr-0.8.1.tgz", | ||||
| 			"integrity": "sha512-hKl0VuE549TK6SnS2Yn9nRvKbFZXn/oAg+dZJU/tlKl/f/0yRXeuUzf8akg3JjtJq+9E592zDqeXZ7yyrg8fSQ==", | ||||
| 			"dev": true, | ||||
| 			"requires": { | ||||
| 				"@root/asn1": "^1.0.0", | ||||
| 				"@root/pem": "^1.0.4", | ||||
| 				"@root/x509": "^0.7.2" | ||||
| 			} | ||||
| 		}, | ||||
| 		"@root/encoding": { | ||||
| 			"version": "1.0.1", | ||||
| 			"resolved": "https://registry.npmjs.org/@root/encoding/-/encoding-1.0.1.tgz", | ||||
| 			"integrity": "sha512-OaEub02ufoU038gy6bsNHQOjIn8nUjGiLcaRmJ40IUykneJkIW5fxDqKxQx48cszuNflYldsJLPPXCrGfHs8yQ==" | ||||
| 		}, | ||||
| 		"@root/keypairs": { | ||||
| 			"version": "0.9.0", | ||||
| 			"resolved": "https://registry.npmjs.org/@root/keypairs/-/keypairs-0.9.0.tgz", | ||||
| 			"integrity": "sha512-NXE2L9Gv7r3iC4kB/gTPZE1vO9Ox/p14zDzAJ5cGpTpytbWOlWF7QoHSJbtVX4H7mRG/Hp7HR3jWdWdb2xaaXg==", | ||||
| 			"requires": { | ||||
| 				"@root/encoding": "^1.0.1", | ||||
| 				"@root/pem": "^1.0.4", | ||||
| 				"@root/x509": "^0.7.2" | ||||
| 			} | ||||
| 		}, | ||||
| 		"@root/pem": { | ||||
| 			"version": "1.0.4", | ||||
| 			"resolved": "https://registry.npmjs.org/@root/pem/-/pem-1.0.4.tgz", | ||||
| 			"integrity": "sha512-rEUDiUsHtild8GfIjFE9wXtcVxeS+ehCJQBwbQQ3IVfORKHK93CFnRtkr69R75lZFjcmKYVc+AXDB+AeRFOULA==" | ||||
| 		}, | ||||
| 		"@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==", | ||||
| 		"@root/x509": { | ||||
| 			"version": "0.7.2", | ||||
| 			"resolved": "https://registry.npmjs.org/@root/x509/-/x509-0.7.2.tgz", | ||||
| 			"integrity": "sha512-ENq3LGYORK5NiMFHEVeNMt+fTXaC7DTS6sQXoqV+dFdfT0vmiL5cDLjaXQhaklJQq0NiwicZegzJRl1ZOTp3WQ==", | ||||
| 			"requires": { | ||||
| 				"@root/asn1": "^1.0.0", | ||||
| 				"@root/encoding": "^1.0.1" | ||||
| 			} | ||||
| 		}, | ||||
| 		"balanced-match": { | ||||
| 			"version": "1.0.0", | ||||
| 			"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", | ||||
| 			"integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", | ||||
| 			"dev": true | ||||
| 		}, | ||||
| 		"eckles": { | ||||
| 		"bluebird": { | ||||
| 			"version": "3.7.1", | ||||
| 			"resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.1.tgz", | ||||
| 			"integrity": "sha512-DdmyoGCleJnkbp3nkbxTLJ18rjDsE4yCggEwKNXkeV123sPNfOCYeDoeuOY+F2FrSjO1YXcTU+dsy96KMy+gcg==", | ||||
| 			"dev": true | ||||
| 		}, | ||||
| 		"brace-expansion": { | ||||
| 			"version": "1.1.11", | ||||
| 			"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", | ||||
| 			"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", | ||||
| 			"dev": true, | ||||
| 			"requires": { | ||||
| 				"balanced-match": "^1.0.0", | ||||
| 				"concat-map": "0.0.1" | ||||
| 			} | ||||
| 		}, | ||||
| 		"cli": { | ||||
| 			"version": "1.0.1", | ||||
| 			"resolved": "https://registry.npmjs.org/cli/-/cli-1.0.1.tgz", | ||||
| 			"integrity": "sha1-IoF1NPJL+klQw01TLUjsvGIbjBQ=", | ||||
| 			"dev": true, | ||||
| 			"requires": { | ||||
| 				"exit": "0.1.2", | ||||
| 				"glob": "^7.1.1" | ||||
| 			} | ||||
| 		}, | ||||
| 		"concat-map": { | ||||
| 			"version": "0.0.1", | ||||
| 			"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", | ||||
| 			"integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", | ||||
| 			"dev": true | ||||
| 		}, | ||||
| 		"dig.js": { | ||||
| 			"version": "1.3.9", | ||||
| 			"resolved": "https://registry.npmjs.org/dig.js/-/dig.js-1.3.9.tgz", | ||||
| 			"integrity": "sha512-O/tSWZuW7AwpjsgePPmTanwvSDL9xF+FzLTJD9byN3C6lk79iMejC/Ahz9CERAXTW4e2TXL1vtqh3T0Ug79ocA==", | ||||
| 			"dev": true, | ||||
| 			"requires": { | ||||
| 				"cli": "^1.0.1", | ||||
| 				"dns-suite": "git+https://git.coolaj86.com/coolaj86/dns-suite.js#v1.2", | ||||
| 				"hexdump.js": "git+https://git.coolaj86.com/coolaj86/hexdump.js#v1.0.4" | ||||
| 			}, | ||||
| 			"dependencies": { | ||||
| 				"dns-suite": { | ||||
| 					"version": "git+https://git.coolaj86.com/coolaj86/dns-suite.js#092008f766540909d27c934211495c9e03705bf3", | ||||
| 					"from": "git+https://git.coolaj86.com/coolaj86/dns-suite.js#v1.2", | ||||
| 					"dev": true, | ||||
| 					"requires": { | ||||
| 						"bluebird": "^3.5.0", | ||||
| 						"hexdump.js": "git+https://git.coolaj86.com/coolaj86/hexdump.js#v1.0.4" | ||||
| 					} | ||||
| 				} | ||||
| 			} | ||||
| 		}, | ||||
| 		"dns-suite": { | ||||
| 			"version": "1.2.13", | ||||
| 			"resolved": "https://registry.npmjs.org/dns-suite/-/dns-suite-1.2.13.tgz", | ||||
| 			"integrity": "sha512-veYKPHUc2RfRCe7c4G/iKxhRv0S4InJ3JsW8tEhW6Yb7dn3ac34iozC6cNX0uzHYZUw0BG5V9Fu65L1bx1GeBg==", | ||||
| 			"dev": true, | ||||
| 			"requires": { | ||||
| 				"@root/hexdump": "^1.1.1" | ||||
| 			}, | ||||
| 			"dependencies": { | ||||
| 				"@root/hexdump": { | ||||
| 					"version": "1.1.1", | ||||
| 					"resolved": "https://registry.npmjs.org/@root/hexdump/-/hexdump-1.1.1.tgz", | ||||
| 					"integrity": "sha512-AmrmLOutlzctR599ittO06lINOco1TIqb0c1wu83fP2Eoi5iSvx7kVWC4mDufze8rxPewC+aQOx4e6Pw7izV4A==", | ||||
| 					"dev": true | ||||
| 				} | ||||
| 			} | ||||
| 		}, | ||||
| 		"dotenv": { | ||||
| 			"version": "8.2.0", | ||||
| 			"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-8.2.0.tgz", | ||||
| 			"integrity": "sha512-8sJ78ElpbDJBHNeBzUbUVLsqKdccaa/BXF1uPTw3GrvQTBgrQrtObr2mUrE38vzYd8cEv+m/JBfDLioYcfXoaw==", | ||||
| 			"dev": true | ||||
| 		}, | ||||
| 		"exit": { | ||||
| 			"version": "0.1.2", | ||||
| 			"resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz", | ||||
| 			"integrity": "sha1-BjJjj42HfMghB9MKD/8aF8uhzQw=", | ||||
| 			"dev": true | ||||
| 		}, | ||||
| 		"fs.realpath": { | ||||
| 			"version": "1.0.0", | ||||
| 			"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", | ||||
| 			"integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", | ||||
| 			"dev": true | ||||
| 		}, | ||||
| 		"glob": { | ||||
| 			"version": "7.1.5", | ||||
| 			"resolved": "https://registry.npmjs.org/glob/-/glob-7.1.5.tgz", | ||||
| 			"integrity": "sha512-J9dlskqUXK1OeTOYBEn5s8aMukWMwWfs+rPTn/jn50Ux4MNXVhubL1wu/j2t+H4NVI+cXEcCaYellqaPVGXNqQ==", | ||||
| 			"dev": true, | ||||
| 			"requires": { | ||||
| 				"fs.realpath": "^1.0.0", | ||||
| 				"inflight": "^1.0.4", | ||||
| 				"inherits": "2", | ||||
| 				"minimatch": "^3.0.4", | ||||
| 				"once": "^1.3.0", | ||||
| 				"path-is-absolute": "^1.0.0" | ||||
| 			} | ||||
| 		}, | ||||
| 		"hexdump.js": { | ||||
| 			"version": "git+https://git.coolaj86.com/coolaj86/hexdump.js#222fa7de5036a16397de2fe703c35ac54a3d8d0c", | ||||
| 			"from": "git+https://git.coolaj86.com/coolaj86/hexdump.js#v1.0.4", | ||||
| 			"dev": true | ||||
| 		}, | ||||
| 		"inflight": { | ||||
| 			"version": "1.0.6", | ||||
| 			"resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", | ||||
| 			"integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", | ||||
| 			"dev": true, | ||||
| 			"requires": { | ||||
| 				"once": "^1.3.0", | ||||
| 				"wrappy": "1" | ||||
| 			} | ||||
| 		}, | ||||
| 		"inherits": { | ||||
| 			"version": "2.0.4", | ||||
| 			"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", | ||||
| 			"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", | ||||
| 			"dev": true | ||||
| 		}, | ||||
| 		"minimatch": { | ||||
| 			"version": "3.0.4", | ||||
| 			"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", | ||||
| 			"integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", | ||||
| 			"dev": true, | ||||
| 			"requires": { | ||||
| 				"brace-expansion": "^1.1.7" | ||||
| 			} | ||||
| 		}, | ||||
| 		"once": { | ||||
| 			"version": "1.4.0", | ||||
| 			"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", | ||||
| 			"integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", | ||||
| 			"dev": true, | ||||
| 			"requires": { | ||||
| 				"wrappy": "1" | ||||
| 			} | ||||
| 		}, | ||||
| 		"path-is-absolute": { | ||||
| 			"version": "1.0.1", | ||||
| 			"resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", | ||||
| 			"integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", | ||||
| 			"dev": true | ||||
| 		}, | ||||
| 		"punycode": { | ||||
| 			"version": "1.4.1", | ||||
| 			"resolved": "https://registry.npmjs.org/eckles/-/eckles-1.4.1.tgz", | ||||
| 			"integrity": "sha512-auWyk/k8oSkVHaD4RxkPadKsLUcIwKgr/h8F7UZEueFDBO7BsE4y+H6IMUDbfqKIFPg/9MxV6KcBdJCmVVcxSA==" | ||||
| 			"resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", | ||||
| 			"integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4=", | ||||
| 			"dev": true | ||||
| 		}, | ||||
| 		"keypairs": { | ||||
| 			"version": "1.2.14", | ||||
| 			"resolved": "https://registry.npmjs.org/keypairs/-/keypairs-1.2.14.tgz", | ||||
| 			"integrity": "sha512-ZoZfZMygyB0QcjSlz7Rh6wT2CJasYEHBPETtmHZEfxuJd7bnsOG5AdtPZqHZBT+hoHvuWCp/4y8VmvTvH0Y9uA==", | ||||
| 			"requires": { | ||||
| 				"eckles": "^1.4.1", | ||||
| 				"rasha": "^1.2.4" | ||||
| 			} | ||||
| 		}, | ||||
| 		"rasha": { | ||||
| 			"version": "1.2.5", | ||||
| 			"resolved": "https://registry.npmjs.org/rasha/-/rasha-1.2.5.tgz", | ||||
| 			"integrity": "sha512-KxtX+/fBk+wM7O3CNgwjSh5elwFilLvqWajhr6wFr2Hd63JnKTTi43Tw+Jb1hxJQWOwoya+NZWR2xztn3hCrTw==" | ||||
| 		}, | ||||
| 		"rsa-compat": { | ||||
| 			"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" | ||||
| 			} | ||||
| 		"wrappy": { | ||||
| 			"version": "1.0.2", | ||||
| 			"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", | ||||
| 			"integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", | ||||
| 			"dev": true | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
|  | ||||
							
								
								
									
										61
									
								
								package.json
									
									
									
									
									
								
							
							
						
						
									
										61
									
								
								package.json
									
									
									
									
									
								
							| @ -1,41 +1,60 @@ | ||||
| { | ||||
| 	"name": "acme-v2", | ||||
| 	"version": "1.8.6", | ||||
| 	"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": "index.js", | ||||
| 	"name": "@root/acme", | ||||
| 	"version": "3.0.0-wip.4", | ||||
| 	"description": "Free SSL certificates for Node.js and Browsers. Issued via Let's Encrypt", | ||||
| 	"homepage": "https://rootprojects.org/acme/", | ||||
| 	"main": "acme.js", | ||||
| 	"browser": { | ||||
| 		"./native.js": "./browser.js", | ||||
| 		"./lib/node/sha2.js": "./lib/browser/sha2.js", | ||||
| 		"./lib/node/http.js": "./lib/browser/http.js" | ||||
| 	}, | ||||
| 	"files": [ | ||||
| 		"compat.js", | ||||
| 		"*.js", | ||||
| 		"lib", | ||||
| 		"scripts" | ||||
| 		"dist" | ||||
| 	], | ||||
| 	"scripts": { | ||||
| 		"build": "node_xxx bin/bundle.js", | ||||
| 		"lint": "jshint lib bin", | ||||
| 		"postinstall": "node scripts/postinstall", | ||||
| 		"test": "node ./test.js" | ||||
| 		"test": "node server.js", | ||||
| 		"start": "node server.js" | ||||
| 	}, | ||||
| 	"repository": { | ||||
| 		"type": "git", | ||||
| 		"url": "https://git.coolaj86.com/coolaj86/acme-v2.js.git" | ||||
| 		"url": "https://git.rootprojects.org/root/acme.js.git" | ||||
| 	}, | ||||
| 	"keywords": [ | ||||
| 		"Let's Encrypt", | ||||
| 		"ACME", | ||||
| 		"v02", | ||||
| 		"v2", | ||||
| 		"draft-11", | ||||
| 		"draft-12", | ||||
| 		"free ssl", | ||||
| 		"tls", | ||||
| 		"automated https", | ||||
| 		"letsencrypt" | ||||
| 		"Let's Encrypt", | ||||
| 		"EC", | ||||
| 		"RSA", | ||||
| 		"CSR", | ||||
| 		"browser", | ||||
| 		"greenlock", | ||||
| 		"VanillaJS", | ||||
| 		"ZeroSSL" | ||||
| 	], | ||||
| 	"author": "AJ ONeal <coolaj86@gmail.com> (https://solderjs.com/)", | ||||
| 	"author": "AJ ONeal <coolaj86@gmail.com> (https://coolaj86.com/)", | ||||
| 	"license": "MPL-2.0", | ||||
| 	"dependencies": { | ||||
| 		"@root/encoding": "^1.0.1", | ||||
| 		"@root/keypairs": "^0.9.0", | ||||
| 		"@root/pem": "^1.0.4", | ||||
| 		"@root/request": "^1.3.11", | ||||
| 		"rsa-compat": "^2.0.8" | ||||
| 		"@root/x509": "^0.7.2" | ||||
| 	}, | ||||
| 	"devDependencies": { | ||||
| 		"dotenv": "^8.0.0" | ||||
| 		"@root/csr": "^0.8.1", | ||||
| 		"dig.js": "^1.3.9", | ||||
| 		"dns-suite": "^1.2.13", | ||||
| 		"dotenv": "^8.1.0", | ||||
| 		"punycode": "^1.4.1" | ||||
| 	}, | ||||
| 	"trulyOptionalDependencies": { | ||||
| 		"eslint": "^6.5.1", | ||||
| 		"webpack": "^4.41.0", | ||||
| 		"webpack-cli": "^3.3.9" | ||||
| 	} | ||||
| } | ||||
|  | ||||
| @ -1,24 +1,4 @@ | ||||
| #!/usr/bin/env node | ||||
| 'use strict'; | ||||
| 
 | ||||
| // BG WH \u001b[47m | ||||
| // BOLD  \u001b[1m | ||||
| // RED   \u001b[31m | ||||
| // GREEN \u001b[32m | ||||
| // RESET \u001b[0m | ||||
| 
 | ||||
| setTimeout(function() { | ||||
|     [ | ||||
| 			'', | ||||
| 			'\u001b[31mGreenlock and ACME.js v3 are on the way!\u001b[0m', | ||||
| 			'Watch for updates at https://indiegogo.com/at/greenlock', | ||||
| 			'' | ||||
| 		] | ||||
| 		.forEach(function(line) { | ||||
| 			console.info(line); | ||||
| 		}); | ||||
| }, 300); | ||||
| 
 | ||||
| setTimeout(function() { | ||||
| 	// give time to read | ||||
| }, 1500); | ||||
| // TODO put postinstall back | ||||
|  | ||||
							
								
								
									
										3
									
								
								test.js
									
									
									
									
									
								
							
							
						
						
									
										3
									
								
								test.js
									
									
									
									
									
								
							| @ -1,3 +0,0 @@ | ||||
| 'use strict'; | ||||
| require('dotenv').config(); | ||||
| require('./examples/dns-01-digitalocean.js'); | ||||
							
								
								
									
										118
									
								
								tests/cb.js
									
									
									
									
									
								
							
							
						
						
									
										118
									
								
								tests/cb.js
									
									
									
									
									
								
							| @ -1,118 +0,0 @@ | ||||
| // Copyright 2018 AJ ONeal. All rights reserved
 | ||||
| /* This Source Code Form is subject to the terms of the Mozilla Public | ||||
|  * License, v. 2.0. If a copy of the MPL was not distributed with this | ||||
|  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 | ||||
| 'use strict'; | ||||
| 
 | ||||
| module.exports.run = function run( | ||||
| 	directoryUrl, | ||||
| 	RSA, | ||||
| 	web, | ||||
| 	chType, | ||||
| 	email, | ||||
| 	accountKeypair, | ||||
| 	domainKeypair | ||||
| ) { | ||||
| 	// [ 'test.ppl.family' ] 'coolaj86@gmail.com''http-01'
 | ||||
| 	var acme2 = require('../').ACME.create({ RSA: RSA }); | ||||
| 	acme2.init(directoryUrl).then(function() { | ||||
| 		var options = { | ||||
| 			agreeToTerms: function(tosUrl, agree) { | ||||
| 				agree(null, tosUrl); | ||||
| 			}, | ||||
| 			setChallenge: function(opts, cb) { | ||||
| 				var pathname; | ||||
| 
 | ||||
| 				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(''); | ||||
| 
 | ||||
| 				if ('http-01' === opts.type) { | ||||
| 					pathname = | ||||
| 						opts.hostname + | ||||
| 						acme2.challengePrefixes['http-01'] + | ||||
| 						'/' + | ||||
| 						opts.token; | ||||
| 					console.log( | ||||
| 						"Put the string '" + | ||||
| 							opts.keyAuthorization + | ||||
| 							"' into a file at '" + | ||||
| 							pathname + | ||||
| 							"'" | ||||
| 					); | ||||
| 					console.log( | ||||
| 						"echo '" + opts.keyAuthorization + "' > '" + pathname + "'" | ||||
| 					); | ||||
| 				} else if ('dns-01' === opts.type) { | ||||
| 					pathname = | ||||
| 						acme2.challengePrefixes['dns-01'] + | ||||
| 						'.' + | ||||
| 						opts.hostname.replace(/^\*\./, ''); | ||||
| 					console.log( | ||||
| 						"Put the string '" + | ||||
| 							opts.dnsAuthorization + | ||||
| 							"' into the TXT record '" + | ||||
| 							pathname + | ||||
| 							"'" | ||||
| 					); | ||||
| 					console.log( | ||||
| 						'ddig TXT ' + pathname + " '" + opts.dnsAuthorization + "'" | ||||
| 					); | ||||
| 				} else { | ||||
| 					cb(new Error('[acme-v2] unrecognized challenge type')); | ||||
| 					return; | ||||
| 				} | ||||
| 				console.log("\nThen hit the 'any' key to continue..."); | ||||
| 
 | ||||
| 				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(opts, cb) { | ||||
| 				// hostname, key
 | ||||
| 				console.log( | ||||
| 					'[acme-v2] remove challenge', | ||||
| 					opts.hostname, | ||||
| 					opts.keyAuthorization | ||||
| 				); | ||||
| 				setTimeout(cb, 1 * 1000); | ||||
| 			}, | ||||
| 			challengeType: chType, | ||||
| 			email: email, | ||||
| 			accountKeypair: accountKeypair, | ||||
| 			domainKeypair: domainKeypair, | ||||
| 			domains: web | ||||
| 		}; | ||||
| 
 | ||||
| 		acme2.accounts.create(options).then(function(account) { | ||||
| 			console.log('[acme-v2] account:'); | ||||
| 			console.log(account); | ||||
| 
 | ||||
| 			acme2.certificates.create(options).then(function(fullchainPem) { | ||||
| 				console.log('[acme-v2] fullchain.pem:'); | ||||
| 				console.log(fullchainPem); | ||||
| 			}); | ||||
| 		}); | ||||
| 	}); | ||||
| }; | ||||
							
								
								
									
										106
									
								
								tests/compat.js
									
									
									
									
									
								
							
							
						
						
									
										106
									
								
								tests/compat.js
									
									
									
									
									
								
							| @ -1,106 +0,0 @@ | ||||
| // Copyright 2018 AJ ONeal. All rights reserved
 | ||||
| /* This Source Code Form is subject to the terms of the Mozilla Public | ||||
|  * License, v. 2.0. If a copy of the MPL was not distributed with this | ||||
|  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 | ||||
| 'use strict'; | ||||
| 
 | ||||
| module.exports.run = function( | ||||
| 	directoryUrl, | ||||
| 	RSA, | ||||
| 	web, | ||||
| 	chType, | ||||
| 	email, | ||||
| 	accountKeypair, | ||||
| 	domainKeypair | ||||
| ) { | ||||
| 	console.log('[DEBUG] run', web, chType, email); | ||||
| 
 | ||||
| 	var acme2 = require('../compat.js').ACME.create({ RSA: RSA }); | ||||
| 	acme2.getAcmeUrls(acme2.stagingServerUrl, function(err /*, directoryUrls*/) { | ||||
| 		if (err) { | ||||
| 			console.log('err 1'); | ||||
| 			throw err; | ||||
| 		} | ||||
| 
 | ||||
| 		var options = { | ||||
| 			agreeToTerms: function(tosUrl, agree) { | ||||
| 				agree(null, tosUrl); | ||||
| 			}, | ||||
| 			setChallenge: function(hostname, token, val, cb) { | ||||
| 				var pathname; | ||||
| 
 | ||||
| 				if ('http-01' === cb.type) { | ||||
| 					pathname = hostname + acme2.acmeChallengePrefix + token; | ||||
| 					console.log( | ||||
| 						"Put the string '" + | ||||
| 						val /*keyAuthorization*/ + | ||||
| 							"' into a file at '" + | ||||
| 							pathname + | ||||
| 							"'" | ||||
| 					); | ||||
| 					console.log( | ||||
| 						"echo '" + val /*keyAuthorization*/ + "' > '" + pathname + "'" | ||||
| 					); | ||||
| 					console.log("\nThen hit the 'any' key to continue..."); | ||||
| 				} else if ('dns-01' === cb.type) { | ||||
| 					// forwards-backwards compat
 | ||||
| 					pathname = | ||||
| 						acme2.challengePrefixes['dns-01'] + | ||||
| 						'.' + | ||||
| 						hostname.replace(/^\*\./, ''); | ||||
| 					console.log( | ||||
| 						"Put the string '" + | ||||
| 							cb.dnsAuthorization + | ||||
| 							"' into the TXT record '" + | ||||
| 							pathname + | ||||
| 							"'" | ||||
| 					); | ||||
| 					console.log('dig TXT ' + pathname + " '" + cb.dnsAuthorization + "'"); | ||||
| 					console.log("\nThen hit the 'any' key to continue..."); | ||||
| 				} else { | ||||
| 					cb(new Error('[acme-v2] unrecognized challenge type: ' + cb.type)); | ||||
| 					return; | ||||
| 				} | ||||
| 
 | ||||
| 				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: accountKeypair, | ||||
| 			domainKeypair: domainKeypair, | ||||
| 			domains: web | ||||
| 		}; | ||||
| 
 | ||||
| 		acme2.registerNewAccount(options, function(err, account) { | ||||
| 			if (err) { | ||||
| 				console.log('err 2'); | ||||
| 				throw err; | ||||
| 			} | ||||
| 			if (options.debug) console.debug('account:'); | ||||
| 			if (options.debug) 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); | ||||
| 			}); | ||||
| 		}); | ||||
| 	}); | ||||
| }; | ||||
							
								
								
									
										15
									
								
								tests/generate-cert-key.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										15
									
								
								tests/generate-cert-key.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,15 @@ | ||||
| 'use strict'; | ||||
| 
 | ||||
| async function run() { | ||||
| 	var Keypairs = require('@root/keypairs'); | ||||
| 
 | ||||
| 	var certKeypair = await Keypairs.generate({ kty: 'RSA' }); | ||||
| 	console.log(certKeypair); | ||||
| 	var pem = await Keypairs.export({ | ||||
| 		jwk: certKeypair.private, | ||||
| 		encoding: 'pem' | ||||
| 	}); | ||||
| 	console.log(pem); | ||||
| } | ||||
| 
 | ||||
| run(); | ||||
							
								
								
									
										225
									
								
								tests/index.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										225
									
								
								tests/index.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,225 @@ | ||||
| 'use strict'; | ||||
| 
 | ||||
| require('dotenv').config(); | ||||
| 
 | ||||
| var CSR = require('@root/csr'); | ||||
| var Enc = require('@root/encoding/base64'); | ||||
| var PEM = require('@root/pem'); | ||||
| var punycode = require('punycode'); | ||||
| var ACME = require('../acme.js'); | ||||
| var Keypairs = require('@root/keypairs'); | ||||
| var acme = ACME.create({ | ||||
| 	// debug: true
 | ||||
| }); | ||||
| 
 | ||||
| // TODO exec npm install --save-dev CHALLENGE_MODULE
 | ||||
| 
 | ||||
| var config = { | ||||
| 	env: process.env.ENV, | ||||
| 	email: process.env.SUBSCRIBER_EMAIL, | ||||
| 	domain: process.env.BASE_DOMAIN, | ||||
| 	challengeType: process.env.CHALLENGE_TYPE, | ||||
| 	challengeModule: process.env.CHALLENGE_PLUGIN, | ||||
| 	challengeOptions: JSON.parse(process.env.CHALLENGE_OPTIONS) | ||||
| }; | ||||
| config.debug = !/^PROD/i.test(config.env); | ||||
| var pluginPrefix = 'acme-' + config.challengeType + '-'; | ||||
| var pluginName = config.challengeModule; | ||||
| var plugin; | ||||
| 
 | ||||
| function badPlugin(err) { | ||||
| 	if ('MODULE_NOT_FOUND' !== err.code) { | ||||
| 		console.error(err); | ||||
| 		return; | ||||
| 	} | ||||
| 	console.error("Couldn't find '" + pluginName + "'. Is it installed?"); | ||||
| 	console.error("\tnpm install --save-dev '" + pluginName + "'"); | ||||
| } | ||||
| try { | ||||
| 	plugin = require(pluginName); | ||||
| } catch (err) { | ||||
| 	if ( | ||||
| 		'MODULE_NOT_FOUND' !== err.code || | ||||
| 		0 === pluginName.indexOf(pluginPrefix) | ||||
| 	) { | ||||
| 		badPlugin(err); | ||||
| 		process.exit(1); | ||||
| 	} | ||||
| 	try { | ||||
| 		pluginName = pluginPrefix + pluginName; | ||||
| 		plugin = require(pluginName); | ||||
| 	} catch (e) { | ||||
| 		badPlugin(e); | ||||
| 		process.exit(1); | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| config.challenger = plugin.create(config.challengeOptions); | ||||
| if (!config.challengeType || !config.domain) { | ||||
| 	console.error( | ||||
| 		new Error('Missing config variables. Check you .env and the docs') | ||||
| 			.message | ||||
| 	); | ||||
| 	console.error(config); | ||||
| 	process.exit(1); | ||||
| } | ||||
| 
 | ||||
| var challenges = {}; | ||||
| challenges[config.challengeType] = config.challenger; | ||||
| 
 | ||||
| async function happyPath(accKty, srvKty, rnd) { | ||||
| 	var agreed = false; | ||||
| 	var metadata = await acme.init( | ||||
| 		'https://acme-staging-v02.api.letsencrypt.org/directory' | ||||
| 	); | ||||
| 
 | ||||
| 	// Ready to use, show page
 | ||||
| 	if (config.debug) { | ||||
| 		console.info('ACME.js initialized'); | ||||
| 		console.info(metadata); | ||||
| 		console.info(); | ||||
| 		console.info(); | ||||
| 	} | ||||
| 
 | ||||
| 	var accountKeypair = await Keypairs.generate({ kty: accKty }); | ||||
| 	if (config.debug) { | ||||
| 		console.info('Account Key Created'); | ||||
| 		console.info(JSON.stringify(accountKeypair, null, 2)); | ||||
| 		console.info(); | ||||
| 		console.info(); | ||||
| 	} | ||||
| 
 | ||||
| 	var account = await acme.accounts.create({ | ||||
| 		agreeToTerms: agree, | ||||
| 		// TODO detect jwk/pem/der?
 | ||||
| 		accountKeypair: { privateKeyJwk: accountKeypair.private }, | ||||
| 		subscriberEmail: config.email | ||||
| 	}); | ||||
| 
 | ||||
| 	// TODO top-level agree
 | ||||
| 	function agree(tos) { | ||||
| 		if (config.debug) { | ||||
| 			console.info('Agreeing to Terms of Service:'); | ||||
| 			console.info(tos); | ||||
| 			console.info(); | ||||
| 			console.info(); | ||||
| 		} | ||||
| 		agreed = true; | ||||
| 		return Promise.resolve(tos); | ||||
| 	} | ||||
| 	if (config.debug) { | ||||
| 		console.info('New Subscriber Account'); | ||||
| 		console.info(JSON.stringify(account, null, 2)); | ||||
| 		console.info(); | ||||
| 		console.info(); | ||||
| 	} | ||||
| 	if (!agreed) { | ||||
| 		throw new Error('Failed to ask the user to agree to terms'); | ||||
| 	} | ||||
| 
 | ||||
| 	var certKeypair = await Keypairs.generate({ kty: srvKty }); | ||||
| 	var pem = await Keypairs.export({ | ||||
| 		jwk: certKeypair.private, | ||||
| 		encoding: 'pem' | ||||
| 	}); | ||||
| 	if (config.debug) { | ||||
| 		console.info('Server Key Created'); | ||||
| 		console.info('privkey.jwk.json'); | ||||
| 		console.info(JSON.stringify(certKeypair, null, 2)); | ||||
| 		// This should be saved as `privkey.pem`
 | ||||
| 		console.info(); | ||||
| 		console.info('privkey.' + srvKty.toLowerCase() + '.pem:'); | ||||
| 		console.info(pem); | ||||
| 		console.info(); | ||||
| 	} | ||||
| 
 | ||||
| 	// 'subject' should be first in list
 | ||||
| 	var domains = randomDomains(rnd); | ||||
| 	if (config.debug) { | ||||
| 		console.info('Get certificates for random domains:'); | ||||
| 		console.info( | ||||
| 			domains | ||||
| 				.map(function(puny) { | ||||
| 					var uni = punycode.toUnicode(puny); | ||||
| 					if (puny !== uni) { | ||||
| 						return puny + ' (' + uni + ')'; | ||||
| 					} | ||||
| 					return puny; | ||||
| 				}) | ||||
| 				.join('\n') | ||||
| 		); | ||||
| 		console.info(); | ||||
| 	} | ||||
| 
 | ||||
| 	// Create CSR
 | ||||
| 	var csrDer = await CSR.csr({ | ||||
| 		jwk: certKeypair.private, | ||||
| 		domains: domains, | ||||
| 		encoding: 'der' | ||||
| 	}); | ||||
| 	var csr = Enc.bufToUrlBase64(csrDer); | ||||
| 	var csrPem = PEM.packBlock({ | ||||
| 		type: 'CERTIFICATE REQUEST', | ||||
| 		bytes: csrDer /* { jwk: jwk, domains: opts.domains } */ | ||||
| 	}); | ||||
| 	if (config.debug) { | ||||
| 		console.info('Certificate Signing Request'); | ||||
| 		console.info(csrPem); | ||||
| 		console.info(); | ||||
| 	} | ||||
| 
 | ||||
| 	var results = await acme.certificates.create({ | ||||
| 		account: account, | ||||
| 		accountKeypair: { privateKeyJwk: accountKeypair.private }, | ||||
| 		csr: csr, | ||||
| 		domains: domains, | ||||
| 		challenges: challenges, // must be implemented
 | ||||
| 		customerEmail: null | ||||
| 	}); | ||||
| 
 | ||||
| 	if (config.debug) { | ||||
| 		console.info('Got SSL Certificate:'); | ||||
| 		console.info(Object.keys(results)); | ||||
| 		console.info(results.expires); | ||||
| 		console.info(results.cert); | ||||
| 		console.info(results.chain); | ||||
| 		console.info(); | ||||
| 		console.info(); | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| // Try EC + RSA
 | ||||
| var rnd = random(); | ||||
| happyPath('EC', 'RSA', rnd) | ||||
| 	.then(function() { | ||||
| 		// Now try RSA + EC
 | ||||
| 		rnd = random(); | ||||
| 		return happyPath('RSA', 'EC', rnd).then(function() { | ||||
| 			console.info('success'); | ||||
| 		}); | ||||
| 	}) | ||||
| 	.catch(function(err) { | ||||
| 		console.error('Error:'); | ||||
| 		console.error(err.stack); | ||||
| 	}); | ||||
| 
 | ||||
| function randomDomains(rnd) { | ||||
| 	return ['foo-acmejs', 'bar-acmejs', '*.baz-acmejs', 'baz-acmejs'].map( | ||||
| 		function(pre) { | ||||
| 			return punycode.toASCII(pre + '-' + rnd + '.' + config.domain); | ||||
| 		} | ||||
| 	); | ||||
| } | ||||
| 
 | ||||
| function random() { | ||||
| 	return ( | ||||
| 		parseInt( | ||||
| 			Math.random() | ||||
| 				.toString() | ||||
| 				.slice(2, 99), | ||||
| 			10 | ||||
| 		) | ||||
| 			.toString(16) | ||||
| 			.slice(0, 4) + '例' | ||||
| 	); | ||||
| } | ||||
							
								
								
									
										124
									
								
								tests/promise.js
									
									
									
									
									
								
							
							
						
						
									
										124
									
								
								tests/promise.js
									
									
									
									
									
								
							| @ -1,124 +0,0 @@ | ||||
| // Copyright 2018 AJ ONeal. All rights reserved
 | ||||
| /* This Source Code Form is subject to the terms of the Mozilla Public | ||||
|  * License, v. 2.0. If a copy of the MPL was not distributed with this | ||||
|  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 | ||||
| 'use strict'; | ||||
| 
 | ||||
| /* global Promise */ | ||||
| module.exports.run = function run( | ||||
| 	directoryUrl, | ||||
| 	RSA, | ||||
| 	web, | ||||
| 	chType, | ||||
| 	email, | ||||
| 	accountKeypair, | ||||
| 	domainKeypair | ||||
| ) { | ||||
| 	var acme2 = require('../').ACME.create({ RSA: RSA }); | ||||
| 	// [ 'test.ppl.family' ] 'coolaj86@gmail.com''http-01'
 | ||||
| 	acme2.init(directoryUrl).then(function() { | ||||
| 		var options = { | ||||
| 			agreeToTerms: function(tosUrl) { | ||||
| 				return Promise.resolve(tosUrl); | ||||
| 			}, | ||||
| 			setChallenge: function(opts) { | ||||
| 				return new Promise(function(resolve, reject) { | ||||
| 					var pathname; | ||||
| 
 | ||||
| 					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(''); | ||||
| 
 | ||||
| 					if ('http-01' === opts.type) { | ||||
| 						pathname = | ||||
| 							opts.hostname + | ||||
| 							acme2.challengePrefixes['http-01'] + | ||||
| 							'/' + | ||||
| 							opts.token; | ||||
| 						console.log( | ||||
| 							"Put the string '" + | ||||
| 								opts.keyAuthorization + | ||||
| 								"' into a file at '" + | ||||
| 								pathname + | ||||
| 								"'" | ||||
| 						); | ||||
| 						console.log( | ||||
| 							"echo '" + opts.keyAuthorization + "' > '" + pathname + "'" | ||||
| 						); | ||||
| 					} else if ('dns-01' === opts.type) { | ||||
| 						pathname = | ||||
| 							acme2.challengePrefixes['dns-01'] + | ||||
| 							'.' + | ||||
| 							opts.hostname.replace(/^\*\./, ''); | ||||
| 						console.log( | ||||
| 							"Put the string '" + | ||||
| 								opts.dnsAuthorization + | ||||
| 								"' into the TXT record '" + | ||||
| 								pathname + | ||||
| 								"'" | ||||
| 						); | ||||
| 						console.log( | ||||
| 							'dig TXT ' + pathname + " '" + opts.dnsAuthorization + "'" | ||||
| 						); | ||||
| 					} else { | ||||
| 						reject(new Error('[acme-v2] unrecognized challenge type')); | ||||
| 						return; | ||||
| 					} | ||||
| 					console.log("\nThen hit the 'any' key to continue..."); | ||||
| 
 | ||||
| 					function onAny() { | ||||
| 						console.log("'any' key was hit"); | ||||
| 						process.stdin.pause(); | ||||
| 						process.stdin.removeListener('data', onAny); | ||||
| 						process.stdin.setRawMode(false); | ||||
| 						resolve(); | ||||
| 						return; | ||||
| 					} | ||||
| 
 | ||||
| 					process.stdin.setRawMode(true); | ||||
| 					process.stdin.resume(); | ||||
| 					process.stdin.on('data', onAny); | ||||
| 				}); | ||||
| 			}, | ||||
| 			removeChallenge: function(opts) { | ||||
| 				console.log( | ||||
| 					'[acme-v2] remove challenge', | ||||
| 					opts.hostname, | ||||
| 					opts.keyAuthorization | ||||
| 				); | ||||
| 				return new Promise(function(resolve) { | ||||
| 					// hostname, key
 | ||||
| 					setTimeout(resolve, 1 * 1000); | ||||
| 				}); | ||||
| 			}, | ||||
| 			challengeType: chType, | ||||
| 			email: email, | ||||
| 			accountKeypair: accountKeypair, | ||||
| 			domainKeypair: domainKeypair, | ||||
| 			domains: web | ||||
| 		}; | ||||
| 
 | ||||
| 		acme2.accounts.create(options).then(function(account) { | ||||
| 			console.log('[acme-v2] account:'); | ||||
| 			console.log(account); | ||||
| 
 | ||||
| 			acme2.certificates.create(options).then(function(fullchainPem) { | ||||
| 				console.log('[acme-v2] fullchain.pem:'); | ||||
| 				console.log(fullchainPem); | ||||
| 			}); | ||||
| 		}); | ||||
| 	}); | ||||
| }; | ||||
							
								
								
									
										20
									
								
								webpack.config.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										20
									
								
								webpack.config.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,20 @@ | ||||
| 'use strict'; | ||||
| 
 | ||||
| var path = require('path'); | ||||
| 
 | ||||
| module.exports = { | ||||
| 	entry: './examples/app.js', | ||||
| 	//entry: './acme.js',
 | ||||
| 	output: { | ||||
| 		path: path.resolve(__dirname, 'dist'), | ||||
| 		filename: 'app.js' | ||||
| 		//filename: 'acme.js',
 | ||||
| 		//library: '@root/acme',
 | ||||
| 		//libraryTarget: 'umd'
 | ||||
| 		//globalObject: "typeof self !== 'undefined' ? self : this"
 | ||||
| 	}, | ||||
| 	resolve: { | ||||
| 		aliasFields: ['webpack', 'browser'], | ||||
| 		mainFields: ['browser', 'main'] | ||||
| 	} | ||||
| }; | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user