Compare commits
	
		
			No commits in common. "master" and "master" have entirely different histories.
		
	
	
		
	
		
| @ -1,11 +0,0 @@ | |||||||
| # editorconfig.org |  | ||||||
| root = true |  | ||||||
| 
 |  | ||||||
| [*] |  | ||||||
| indent_style = space |  | ||||||
| indent_size = 2 |  | ||||||
| tab_width = 2 |  | ||||||
| end_of_line = lf |  | ||||||
| charset = utf-8 |  | ||||||
| trim_trailing_whitespace = true |  | ||||||
| insert_final_newline = true |  | ||||||
							
								
								
									
										49
									
								
								README.md
									
									
									
									
									
								
							
							
						
						
									
										49
									
								
								README.md
									
									
									
									
									
								
							| @ -1,46 +1,9 @@ | |||||||
| # Bluecrypt™ [Keypairs](https://git.rootprojects.org/root/bluecrypt-keypairs.js) | A [Root](https://rootprojects.org) Project | # Bluecrypt™ Keypairs | ||||||
| 
 | 
 | ||||||
| A port of [keypairs.js](https://git.coolaj86.com/coolaj86/keypairs.js) to the browser. | A port of [keypairs.js](https://git.coolaj86.com/coolaj86/keypairs.js) to the browser. | ||||||
| 
 | 
 | ||||||
| # Features (port in-progress) | * Keypairs | ||||||
| 
 |   * Eckles (ECDSA) | ||||||
|   * [x] Keypair generation and encoding |   * Rasha (RSA) | ||||||
|     * [x] RSA |   * X509 | ||||||
|     * [x] ECDSA (P-256, P-384) |   * ASN1 | ||||||
|     * [x] JWK-to-PEM |  | ||||||
|     * [ ] JWK-to-SSH |  | ||||||
|     * [ ] PEM-to-JWK |  | ||||||
|     * [ ] SSH-to-JWK |  | ||||||
|     * [x] ASN1, X509, PEM, DER |  | ||||||
|   * [x] SHA256 JWK Thumbprints |  | ||||||
|   * [x] Sign JWS |  | ||||||
|   * [ ] Create JWTs |  | ||||||
|   * [ ] JWK fetching. See [Keyfetch.js](https://npmjs.com/packages/keyfetch/) |  | ||||||
|     * [ ] OIDC |  | ||||||
|     * [ ] Auth0 |  | ||||||
|   * [ ] CLI (ee [keypairs-cli](https://npmjs.com/packages/keypairs-cli/)) |  | ||||||
|   * [ ] Node.js (ee [keypairs.js](https://npmjs.com/packages/keypairs.js)) |  | ||||||
|   * [ ] [CSR.js](https://git.rootprojects.org/root/bluecrypt-csr.js) |  | ||||||
|   * [ ] [ACME.js](https://git.rootprojects.org/root/bluecrypt-acme.js) (Let's Encyrpt) |  | ||||||
| 
 |  | ||||||
| # Online Demos |  | ||||||
| 
 |  | ||||||
| * Bluecrypt Keypairs.js Demo <https://rootprojects.org/keypairs/> |  | ||||||
| 
 |  | ||||||
| # QuickStart |  | ||||||
| 
 |  | ||||||
| `bluecrypt-keypairs.js` |  | ||||||
| ```html |  | ||||||
| <script src="https://rootprojects.org/keypairs/bluecrypt-keypairs.js"></script> |  | ||||||
| ``` |  | ||||||
| 
 |  | ||||||
| `bluecrypt-keypairs.min.js` |  | ||||||
| ```html |  | ||||||
| <script src="https://rootprojects.org/keypairs/bluecrypt-keypairs.min.js"></script> |  | ||||||
| ``` |  | ||||||
| 
 |  | ||||||
| You can see `index.html` and `app.js` in the repo for full example usage. |  | ||||||
| 
 |  | ||||||
| # Documentation |  | ||||||
| 
 |  | ||||||
| See [keypairs.js](https://git.coolaj86.com/coolaj86/keypairs.js) for documentation. |  | ||||||
|  | |||||||
							
								
								
									
										138
									
								
								app.js
									
									
									
									
									
								
							
							
						
						
									
										138
									
								
								app.js
									
									
									
									
									
								
							| @ -1,14 +1,7 @@ | |||||||
| /*global Promise*/ |  | ||||||
| (function () { | (function () { | ||||||
|   'use strict'; |   'use strict'; | ||||||
| 
 | 
 | ||||||
|   var Keypairs = window.Keypairs; |   var Keypairs = window.Keypairs; | ||||||
|   var Rasha = window.Rasha; |  | ||||||
|   var Eckles = window.Eckles; |  | ||||||
|   var x509 = window.x509; |  | ||||||
|   var CSR = window.CSR; |  | ||||||
|   var ACME = window.ACME; |  | ||||||
|   var accountStuff = {}; |  | ||||||
| 
 | 
 | ||||||
|   function $(sel) { |   function $(sel) { | ||||||
|     return document.querySelector(sel); |     return document.querySelector(sel); | ||||||
| @ -42,12 +35,9 @@ | |||||||
|       $('.js-loading').hidden = false; |       $('.js-loading').hidden = false; | ||||||
|       $('.js-jwk').hidden = true; |       $('.js-jwk').hidden = true; | ||||||
|       $('.js-toc-der-public').hidden = true; |       $('.js-toc-der-public').hidden = true; | ||||||
|  |       $('.js-toc-pem-public').hidden = true; | ||||||
|       $('.js-toc-der-private').hidden = true; |       $('.js-toc-der-private').hidden = true; | ||||||
|       $('.js-toc-jwk').hidden = true; |       $('.js-toc-pem-private').hidden = true; | ||||||
| 
 |  | ||||||
|       $$('.js-toc-pem').forEach(function ($el) { |  | ||||||
|         $el.hidden = true; |  | ||||||
|       }); |  | ||||||
|       $$('input').map(function ($el) { $el.disabled = true; }); |       $$('input').map(function ($el) { $el.disabled = true; }); | ||||||
|       $$('button').map(function ($el) { $el.disabled = true; }); |       $$('button').map(function ($el) { $el.disabled = true; }); | ||||||
|       var opts = { |       var opts = { | ||||||
| @ -55,51 +45,34 @@ | |||||||
|         , namedCurve: $('input[name="ec-crv"]:checked').value |         , namedCurve: $('input[name="ec-crv"]:checked').value | ||||||
|         , modulusLength: $('input[name="rsa-len"]:checked').value |         , modulusLength: $('input[name="rsa-len"]:checked').value | ||||||
|       }; |       }; | ||||||
|       var then = Date.now(); |  | ||||||
|       console.log('opts', opts); |       console.log('opts', opts); | ||||||
|       Keypairs.generate(opts).then(function (results) { |       Keypairs.generate(opts).then(function (results) { | ||||||
|         console.log("Key generation time:", (Date.now() - then) + "ms"); |         var der_public, der_private; | ||||||
|         var pubDer; |         if (opts.kty == 'EC') { | ||||||
|         var privDer; |           der_public = x509.packSpki(results.public); | ||||||
|         if (/EC/i.test(opts.kty)) { |           der_private = x509.packPkcs8(results.private); | ||||||
|           privDer = x509.packPkcs8(results.private); |           var pem_private = Eckles.export({ jwk: results.private }) | ||||||
|           pubDer = x509.packSpki(results.public); |           var pem_public = Eckles.export({ jwk: results.public, public: true }) | ||||||
|           Eckles.export({ jwk: results.private, format: 'sec1' }).then(function (pem) { |           $('.js-input-pem-public').innerText = pem_public; | ||||||
|             $('.js-input-pem-sec1-private').innerText = pem; |           $('.js-toc-pem-public').hidden = false; | ||||||
|             $('.js-toc-pem-sec1-private').hidden = false; |           $('.js-input-pem-private').innerText = pem_private; | ||||||
|           }); |           $('.js-toc-pem-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 { |         } else { | ||||||
|           privDer = x509.packPkcs8(results.private); |           der_private = x509.packPkcs8(results.private); | ||||||
|           pubDer = x509.packSpki(results.public); |           der_public = x509.packPkcs8(results.public); | ||||||
|           Rasha.export({ jwk: results.private, format: 'pkcs1' }).then(function (pem) { |           Rasha.pack({ jwk: results.private }).then(function (pem) { | ||||||
|             $('.js-input-pem-pkcs1-private').innerText = pem; |             $('.js-input-pem-private').innerText = pem; | ||||||
|             $('.js-toc-pem-pkcs1-private').hidden = false; |             $('.js-toc-pem-private').hidden = false; | ||||||
|           }); |           }) | ||||||
|           Rasha.export({ jwk: results.private, format: 'pkcs8' }).then(function (pem) { |           Rasha.pack({ jwk: results.public }).then(function (pem) { | ||||||
|             $('.js-input-pem-pkcs8-private').innerText = pem; |             $('.js-input-pem-public').innerText = pem; | ||||||
|             $('.js-toc-pem-pkcs8-private').hidden = false; |             $('.js-toc-pem-public').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-der-public').innerText = der_public; | ||||||
|         $('.js-toc-der-public').hidden = false; |         $('.js-toc-der-public').hidden = false; | ||||||
|         $('.js-der-private').innerText = privDer; |         $('.js-der-private').innerText = der_private; | ||||||
|         $('.js-toc-der-private').hidden = false; |         $('.js-toc-der-private').hidden = false; | ||||||
|         $('.js-jwk').innerText = JSON.stringify(results, null, 2); |         $('.js-jwk').innerText = JSON.stringify(results, null, 2); | ||||||
|         $('.js-loading').hidden = true; |         $('.js-loading').hidden = true; | ||||||
| @ -107,73 +80,18 @@ | |||||||
|         $$('input').map(function ($el) { $el.disabled = false; }); |         $$('input').map(function ($el) { $el.disabled = false; }); | ||||||
|         $$('button').map(function ($el) { $el.disabled = false; }); |         $$('button').map(function ($el) { $el.disabled = false; }); | ||||||
|         $('.js-toc-jwk').hidden = false; |         $('.js-toc-jwk').hidden = false; | ||||||
| 
 |  | ||||||
|       }); |       }); | ||||||
|     }); |     }); | ||||||
| 
 | 
 | ||||||
|     $('form.js-keysign').addEventListener('submit', function (ev) { |     $('form.js-acme-account').addEventListener('submit', function (ev) { | ||||||
|       ev.preventDefault(); |       ev.preventDefault(); | ||||||
|       ev.stopPropagation(); |       ev.stopPropagation(); | ||||||
|       $('.js-pem-loading').hidden = false; |       $('.js-loading').hidden = false; | ||||||
|       $('.js-toc-jws').hidden = true; |       ACME.accounts.create | ||||||
|       $('.js-toc-jwt').hidden = true; |  | ||||||
|       $$('input').map(function ($el) { $el.disabled = true; }); |  | ||||||
|       $$('button').map(function ($el) { $el.disabled = true; }); |  | ||||||
| 
 |  | ||||||
|       try { |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
|         var opts = { |  | ||||||
|           jwk: JSON.parse($('textarea[name="jwk"]').value), |  | ||||||
|           claims: { |  | ||||||
|             exp: "1h", |  | ||||||
|             iss: document.getElementById(`-acmeDomains`).value |  | ||||||
|           } |  | ||||||
|         }; |  | ||||||
| 
 |  | ||||||
|         Keypairs.signJwt(opts).then(function (msg) { |  | ||||||
|           document.getElementById(`sign-error`).innerText = null; |  | ||||||
|           $('.js-jwt').innerText = msg; |  | ||||||
|           $('.js-toc-jwt').hidden = false; |  | ||||||
|           var msgArr = msg.split(".") |  | ||||||
|           var protected64 = msgArr[0] |  | ||||||
|           var payload64 = msgArr[1] |  | ||||||
|           var signature = msgArr[2] |  | ||||||
|           var signedMsg = { |  | ||||||
|             protected: protected64 |  | ||||||
|             , payload: payload64 |  | ||||||
|             , signature |  | ||||||
|           }; |  | ||||||
|           $('.js-jws').innerText = JSON.stringify(signedMsg, null, 2); |  | ||||||
|           $('.js-toc-jws').hidden = false; |  | ||||||
|           $('.js-pem-loading').hidden = true; |  | ||||||
|           $$('input').map(function ($el) { $el.disabled = false; }); |  | ||||||
|           $$('button').map(function ($el) { $el.disabled = false; }); |  | ||||||
|         }).catch(function (error) { |  | ||||||
|           document.getElementById(`sign-error`).innerText = error.message |  | ||||||
|           $('.js-pem-loading').hidden = true; |  | ||||||
|           $$('input').map(function ($el) { $el.disabled = false; }); |  | ||||||
|           $$('button').map(function ($el) { $el.disabled = false; }); |  | ||||||
|         }) |  | ||||||
|       } catch (error) { |  | ||||||
|         document.getElementById(`sign-error`).innerText = error.message |  | ||||||
|         $('.js-pem-loading').hidden = true; |  | ||||||
|         $$('input').map(function ($el) { $el.disabled = false; }); |  | ||||||
|         $$('button').map(function ($el) { $el.disabled = false; }); |  | ||||||
|       } |  | ||||||
| 
 |  | ||||||
|     }); |     }); | ||||||
| 
 | 
 | ||||||
|     $('.js-generate').hidden = false; |     $('.js-generate').hidden = false; | ||||||
|     $('.js-sign').hidden = false; |     $('.js-create-account').hidden = false; | ||||||
|     $('textarea[name="jwk"]').value = JSON.stringify({ |  | ||||||
|       "crv": "P-256", |  | ||||||
|       "d": "LImWxqqTHbP3LHQfqscDSUzf_uNePGqf9U6ETEcO5Ho", |  | ||||||
|       "kty": "EC", |  | ||||||
|       "x": "vdjQ3T6VBX82LIKDzepYgRsz3HgRwp83yPuonu6vqos", |  | ||||||
|       "y": "IUkEXtAMnppnV1A19sE2bJhUo4WPbq6EYgWxma4oGyg", |  | ||||||
|       "kid": "MnfJYyS9W5gUjrJLdn8ePMzik8ZJz2qc-VZmKOs_oCw" |  | ||||||
|     }) |  | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   window.addEventListener('load', run); |   window.addEventListener('load', run); | ||||||
|  | |||||||
							
								
								
									
										42
									
								
								bundle.sh
									
									
									
									
									
								
							
							
						
						
									
										42
									
								
								bundle.sh
									
									
									
									
									
								
							| @ -1,42 +0,0 @@ | |||||||
| #!/bin/bash |  | ||||||
| 
 |  | ||||||
| # Development Version |  | ||||||
| cat > bluecrypt-keypairs.js << EOF |  | ||||||
| // 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/. */ |  | ||||||
| ; |  | ||||||
| EOF |  | ||||||
| cat ./lib/encoding.js \ |  | ||||||
|   ./lib/asn1-packer.js \ |  | ||||||
|   ./lib/x509.js \ |  | ||||||
|   ./lib/ecdsa.js \ |  | ||||||
|   ./lib/rsa.js \ |  | ||||||
|   ./lib/keypairs.js \ |  | ||||||
|   >> bluecrypt-keypairs.js |  | ||||||
| 
 |  | ||||||
| # Gzipped |  | ||||||
| cat > bluecrypt-keypairs.min.js << EOF |  | ||||||
| // 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/. */ |  | ||||||
| ; |  | ||||||
| EOF |  | ||||||
| uglifyjs bluecrypt-keypairs.js >> bluecrypt-keypairs.min.js |  | ||||||
| gzip -f bluecrypt-keypairs.min.js |  | ||||||
| 
 |  | ||||||
| # Minified Gzipped |  | ||||||
| cat > bluecrypt-keypairs.min.js << EOF |  | ||||||
| // 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/. */ |  | ||||||
| ; |  | ||||||
| EOF |  | ||||||
| uglifyjs bluecrypt-keypairs.js >> bluecrypt-keypairs.min.js |  | ||||||
| 
 |  | ||||||
| rsync -av ./ root@beta.therootcompany.com:~/beta.therootcompany.com/keypairs/ |  | ||||||
| rsync -av ./ root@beta.rootprojects.org:~/beta.rootprojects.org/keypairs/ |  | ||||||
| rsync -av ./ ubuntu@rootprojects.org:/srv/www/rootprojects.org/keypairs/ |  | ||||||
							
								
								
									
										117
									
								
								index.html
									
									
									
									
									
								
							
							
						
						
									
										117
									
								
								index.html
									
									
									
									
									
								
							| @ -1,6 +1,5 @@ | |||||||
| <html> | <html> | ||||||
| 
 |   <head> | ||||||
| <head> |  | ||||||
|     <title>BlueCrypt</title> |     <title>BlueCrypt</title> | ||||||
|     <style> |     <style> | ||||||
|       textarea { |       textarea { | ||||||
| @ -18,17 +17,9 @@ | |||||||
|     </style> |     </style> | ||||||
|   </head> |   </head> | ||||||
|   <body> |   <body> | ||||||
|     <h1>@bluecrypt/keypairs: Universal keygen & signing for browsers</h1> |     <h1>BlueCrypt for the Browser</h1> | ||||||
|     <p>Keypairs.js is <strong>easy-to-use browser crypto in kilobytes, not megabytes.</strong></p> |     <p>BlueCrypt is universal crypto for the browser. It's lightweight, fast, and based on native webcrypto. | ||||||
| 
 |     This means it's easy-to-use crypto in kilobytes, not megabytes.</p> | ||||||
|     <p>It's a modern alternative to larger, legacy libraries like PKI.js and rsasign, |  | ||||||
|     with more universal support for keygen, signing, and verification (including PKI, X509, JOSE, JWS, and JWT) |  | ||||||
|     at a fraction of the cost.</p> |  | ||||||
| 
 |  | ||||||
|     <p>This is intended to be explored with your JavaScript console open.</p> |  | ||||||
|     <pre><code><script src="<a href="https://rootprojects.org/keypairs/bluecrypt-keypairs.js">https://rootprojects.org/keypairs/bluecrypt-keypairs.js</a>"></script></code></pre> |  | ||||||
|     <pre><code><script src="<a href="https://rootprojects.org/keypairs/bluecrypt-keypairs.min.js">https://rootprojects.org/keypairs/bluecrypt-keypairs.min.js</a>"></script></code></pre> |  | ||||||
|     <a href="https://git.rootprojects.org/root/bluecrypt-keypairs.js">Documentation</a> |  | ||||||
| 
 | 
 | ||||||
|     <h2>Keypair Generation</h2> |     <h2>Keypair Generation</h2> | ||||||
|     <form class="js-keygen"> |     <form class="js-keygen"> | ||||||
| @ -43,25 +34,38 @@ | |||||||
|       </div> |       </div> | ||||||
|       <div class="js-ec-opts"> |       <div class="js-ec-opts"> | ||||||
|         <p>EC Options:</p> |         <p>EC Options:</p> | ||||||
|         <label for="-crv2"><input type="radio" id="-crv2" |         <input type="radio" id="-crv2" | ||||||
|          name="ec-crv" value="P-256" checked>P-256</label> |          name="ec-crv" value="P-256" checked> | ||||||
|         <label for="-crv3"><input type="radio" id="-crv3" |         <label for="-crv2">P-256</label> | ||||||
|          name="ec-crv" value="P-384">P-384</label> |         <input type="radio" id="-crv3" | ||||||
|         <!-- label for="-crv5"><input type="radio" id="-crv5" |          name="ec-crv" value="P-384"> | ||||||
|          name="ec-crv" value="P-521">P-521</label --> |         <label for="-crv3">P-384</label> | ||||||
|  |         <!-- input type="radio" id="-crv5" | ||||||
|  |          name="ec-crv" value="P-521"> | ||||||
|  |         <label for="-crv5">P-521</label --> | ||||||
|       </div> |       </div> | ||||||
|       <div class="js-rsa-opts" hidden> |       <div class="js-rsa-opts" hidden> | ||||||
|         <p>RSA Options:</p> |         <p>RSA Options:</p> | ||||||
|         <label for="-modlen2"><input type="radio" id="-modlen2" |         <input type="radio" id="-modlen2" | ||||||
|          name="rsa-len" value="2048" checked>2048</label> |          name="rsa-len" value="2048" checked> | ||||||
|         <label for="-modlen3"><input type="radio" id="-modlen3" |         <label for="-modlen2">2048</label> | ||||||
|          name="rsa-len" value="3072">3072</label> |         <input type="radio" id="-modlen3" | ||||||
|         <label for="-modlen5"><input type="radio" id="-modlen5" |          name="rsa-len" value="3072"> | ||||||
|          name="rsa-len" value="4096">4096</label> |         <label for="-modlen3">3072</label> | ||||||
|  |         <input type="radio" id="-modlen5" | ||||||
|  |          name="rsa-len" value="4096"> | ||||||
|  |         <label for="-modlen5">4096</label> | ||||||
|       </div> |       </div> | ||||||
|       <button class="js-generate" hidden>Generate</button> |       <button class="js-generate" hidden>Generate</button> | ||||||
|     </form> |     </form> | ||||||
| 
 | 
 | ||||||
|  |     <h2>ACME Account</h2> | ||||||
|  |     <form class="js-acme-account"> | ||||||
|  |       <label for="-acmeEmail">Email:</label> | ||||||
|  |       <input class="js-email" type="email" id="-acmeEmail"> | ||||||
|  |       <button class="js-create-account" hidden>Create Account</button> | ||||||
|  |     </form> | ||||||
|  | 
 | ||||||
|     <div class="js-loading" hidden>Loading</div> |     <div class="js-loading" hidden>Loading</div> | ||||||
| 
 | 
 | ||||||
|     <details class="js-toc-jwk" hidden> |     <details class="js-toc-jwk" hidden> | ||||||
| @ -76,64 +80,29 @@ | |||||||
|       <summary>DER Public Binary</summary> |       <summary>DER Public Binary</summary> | ||||||
|       <pre><code class="js-der-public"> </code></pre> |       <pre><code class="js-der-public"> </code></pre> | ||||||
|     </details> |     </details> | ||||||
|   <details class="js-toc-pem js-toc-pem-pkcs1-private" hidden> |     <details class="js-toc-pem-private" hidden> | ||||||
|     <summary>PEM Private (base64-encoded PKCS1 DER)</summary> |       <summary>PEM Private (base64-encoded DER)</summary> | ||||||
|     <pre><code  class="js-input-pem-pkcs1-private" ></code></pre> |       <pre><code  class="js-input-pem-private" ></code></pre> | ||||||
|     </details> |     </details> | ||||||
|   <details class="js-toc-pem js-toc-pem-sec1-private" hidden> |     <details class="js-toc-pem-public" hidden> | ||||||
|     <summary>PEM Private (base64-encoded SEC1 DER)</summary> |       <summary>PEM Public (base64-encoded DER)</summary> | ||||||
|     <pre><code  class="js-input-pem-sec1-private" ></code></pre> |       <pre><code  class="js-input-pem-public" ></code></pre> | ||||||
|     </details> |     </details> | ||||||
|   <details class="js-toc-pem js-toc-pem-pkcs8-private" hidden> |     <details class="js-toc-acme-account-request" hidden> | ||||||
|     <summary>PEM Private (base64-encoded PKCS8 DER)</summary> |       <summary>ACME Account Request</summary> | ||||||
|     <pre><code  class="js-input-pem-pkcs8-private" ></code></pre> |       <pre><code class="js-acme-account-request"> </code></pre> | ||||||
|     </details> |     </details> | ||||||
|   <details class="js-toc-pem js-toc-pem-pkcs1-public" hidden> |     <details class="js-toc-acme-account-response" hidden> | ||||||
|     <summary>PEM Public (base64-encoded PKCS1 DER)</summary> |       <summary>ACME Account Response</summary> | ||||||
|     <pre><code  class="js-input-pem-pkcs1-public" ></code></pre> |       <pre><code class="js-acme-account-response"> </code></pre> | ||||||
|     </details> |     </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> |  | ||||||
| 
 |  | ||||||
|   <h2>Signing</h2> |  | ||||||
|   <div class="errors" id="sign-error"></div> |  | ||||||
|   <form class="js-keysign"> |  | ||||||
|     <div> |  | ||||||
|       <label for="-acmeDomains">Domains:</label> |  | ||||||
|       <input class="js-domains" type="text" id="-acmeDomains" value="example.com www.example.com"> |  | ||||||
|     </div> |  | ||||||
|     <div> |  | ||||||
|       <label for="jwk">JWK:</label> |  | ||||||
|       <br> |  | ||||||
|       <textarea id="jwk" name="jwk"></textarea> |  | ||||||
|     </div> |  | ||||||
|     <button class="js-sign" hidden>Sign</button> |  | ||||||
|   </form> |  | ||||||
|   <div class="js-pem-loading" hidden>Loading</div> |  | ||||||
|   <details class="js-toc-jws" hidden> |  | ||||||
|     <summary>JWS </summary> |  | ||||||
|     <pre><code class="js-jws"></code></pre> |  | ||||||
|   </details> |  | ||||||
|   <details class="js-toc-jwt" hidden> |  | ||||||
|     <summary>JWT </summary> |  | ||||||
|     <pre><code class="js-jwt"></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.</p> |  | ||||||
|     <br> |  | ||||||
|     <footer>View (git) source |  | ||||||
|       <a href="https://git.rootprojects.org/root/bluecrypt-keypairs.js">@bluecrypt/keypairs</a></footer> |  | ||||||
| 
 |  | ||||||
|     <script src="./lib/bluecrypt-encoding.js"></script> |     <script src="./lib/bluecrypt-encoding.js"></script> | ||||||
|  |     <script src="./lib/ecdsa.js"></script> | ||||||
|     <script src="./lib/asn1-packer.js"></script> |     <script src="./lib/asn1-packer.js"></script> | ||||||
|     <script src="./lib/x509.js"></script> |     <script src="./lib/x509.js"></script> | ||||||
|     <script src="./lib/ecdsa.js"></script> |  | ||||||
|     <script src="./lib/rsa.js"></script> |     <script src="./lib/rsa.js"></script> | ||||||
|     <script src="./lib/keypairs.js"></script> |     <script src="./lib/keypairs.js"></script> | ||||||
|  |     <script src="./lib/acme.js"></script> | ||||||
|     <script src="./app.js"></script> |     <script src="./app.js"></script> | ||||||
|   </body> |   </body> | ||||||
| </html> | </html> | ||||||
|  | |||||||
							
								
								
									
										951
									
								
								lib/acme.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										951
									
								
								lib/acme.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,951 @@ | |||||||
|  | // Copyright 2018-present 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/. */
 | ||||||
|  | (function (exports) { | ||||||
|  | 'use strict'; | ||||||
|  | /* globals Promise */ | ||||||
|  | 
 | ||||||
|  | var ACME = exports.ACME = {}; | ||||||
|  | var Keypairs = exports.Keypairs || {}; | ||||||
|  | var Enc = exports.Enc || {}; | ||||||
|  | var Crypto = exports.Crypto || {}; | ||||||
|  | 
 | ||||||
|  | ACME.formatPemChain = function formatPemChain(str) { | ||||||
|  |   return str.trim().replace(/[\r\n]+/g, '\n').replace(/\-\n\-/g, '-\n\n-') + '\n'; | ||||||
|  | }; | ||||||
|  | ACME.splitPemChain = function splitPemChain(str) { | ||||||
|  |   return str.trim().split(/[\r\n]{2,}/g).map(function (str) { | ||||||
|  |     return str + '\n'; | ||||||
|  |   }); | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | // http-01: GET https://example.org/.well-known/acme-challenge/{{token}} => {{keyAuth}}
 | ||||||
|  | // dns-01: TXT _acme-challenge.example.org. => "{{urlSafeBase64(sha256(keyAuth))}}"
 | ||||||
|  | ACME.challengePrefixes = { | ||||||
|  |   'http-01': '/.well-known/acme-challenge' | ||||||
|  | , 'dns-01': '_acme-challenge' | ||||||
|  | }; | ||||||
|  | ACME.challengeTests = { | ||||||
|  |   'http-01': function (me, auth) { | ||||||
|  |     var url = 'http://' + auth.hostname + ACME.challengePrefixes['http-01'] + '/' + auth.token; | ||||||
|  |     return me._request({ method: 'GET', url: url }).then(function (resp) { | ||||||
|  |       var err; | ||||||
|  | 
 | ||||||
|  |       // TODO limit the number of bytes that are allowed to be downloaded
 | ||||||
|  |       if (auth.keyAuthorization === resp.body.toString('utf8').trim()) { | ||||||
|  |         return true; | ||||||
|  |       } | ||||||
|  | 
 | ||||||
|  |       err = new Error( | ||||||
|  |         "Error: Failed HTTP-01 Pre-Flight / Dry Run.\n" | ||||||
|  |       + "curl '" + url + "'\n" | ||||||
|  |       + "Expected: '" + auth.keyAuthorization + "'\n" | ||||||
|  |       + "Got: '" + resp.body + "'\n" | ||||||
|  |       + "See https://git.coolaj86.com/coolaj86/acme-v2.js/issues/4" | ||||||
|  |       ); | ||||||
|  |       err.code = 'E_FAIL_DRY_CHALLENGE'; | ||||||
|  |       return Promise.reject(err); | ||||||
|  |     }); | ||||||
|  |   } | ||||||
|  | , 'dns-01': function (me, auth) { | ||||||
|  |     // remove leading *. on wildcard domains
 | ||||||
|  |     return me.dig({ | ||||||
|  |       type: 'TXT' | ||||||
|  |     , name: auth.dnsHost | ||||||
|  |     }).then(function (ans) { | ||||||
|  |       var err; | ||||||
|  | 
 | ||||||
|  |       if (ans.answer.some(function (txt) { | ||||||
|  |         return auth.dnsAuthorization === txt.data[0]; | ||||||
|  |       })) { | ||||||
|  |         return true; | ||||||
|  |       } | ||||||
|  | 
 | ||||||
|  |       err = new Error( | ||||||
|  |         "Error: Failed DNS-01 Pre-Flight Dry Run.\n" | ||||||
|  |       + "dig TXT '" + auth.dnsHost + "' does not return '" + auth.dnsAuthorization + "'\n" | ||||||
|  |       + "See https://git.coolaj86.com/coolaj86/acme-v2.js/issues/4" | ||||||
|  |       ); | ||||||
|  |       err.code = 'E_FAIL_DRY_CHALLENGE'; | ||||||
|  |       return Promise.reject(err); | ||||||
|  |     }); | ||||||
|  |   } | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | ACME._directory = function (me) { | ||||||
|  |   // GET-as-GET ok
 | ||||||
|  |   return me._request({ method: 'GET', url: me.directoryUrl, json: true }); | ||||||
|  | }; | ||||||
|  | ACME._getNonce = function (me) { | ||||||
|  |   // GET-as-GET, HEAD-as-HEAD ok
 | ||||||
|  |   if (me._nonce) { return new Promise(function (resolve) { resolve(me._nonce); return; }); } | ||||||
|  |   return me._request({ method: 'HEAD', url: me._directoryUrls.newNonce }).then(function (resp) { | ||||||
|  |     me._nonce = resp.toJSON().headers['replay-nonce']; | ||||||
|  |     return me._nonce; | ||||||
|  |   }); | ||||||
|  | }; | ||||||
|  | // ACME RFC Section 7.3 Account Creation
 | ||||||
|  | /* | ||||||
|  |  { | ||||||
|  |    "protected": base64url({ | ||||||
|  |      "alg": "ES256", | ||||||
|  |      "jwk": {...}, | ||||||
|  |      "nonce": "6S8IqOGY7eL2lsGoTZYifg", | ||||||
|  |      "url": "https://example.com/acme/new-account" | ||||||
|  |    }), | ||||||
|  |    "payload": base64url({ | ||||||
|  |      "termsOfServiceAgreed": true, | ||||||
|  |      "onlyReturnExisting": false, | ||||||
|  |      "contact": [ | ||||||
|  |        "mailto:cert-admin@example.com", | ||||||
|  |        "mailto:admin@example.com" | ||||||
|  |      ] | ||||||
|  |    }), | ||||||
|  |    "signature": "RZPOnYoPs1PhjszF...-nh6X1qtOFPB519I" | ||||||
|  |  } | ||||||
|  | */ | ||||||
|  | ACME._registerAccount = function (me, options) { | ||||||
|  |   if (me.debug) { console.debug('[acme-v2] accounts.create'); } | ||||||
|  | 
 | ||||||
|  |   return ACME._getNonce(me).then(function () { | ||||||
|  |     return new Promise(function (resolve, reject) { | ||||||
|  | 
 | ||||||
|  |       function agree(tosUrl) { | ||||||
|  |         var err; | ||||||
|  |         if (me._tos !== tosUrl) { | ||||||
|  |           err = new Error("You must agree to the ToS at '" + me._tos + "'"); | ||||||
|  |           err.code = "E_AGREE_TOS"; | ||||||
|  |           reject(err); | ||||||
|  |           return; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         var jwk = options.accountKeypair.privateKeyJwk; | ||||||
|  |         var p; | ||||||
|  |         if (jwk) { | ||||||
|  |           p = Promise.resolve({ private: jwk, public: Keypairs.neuter(jwk) }); | ||||||
|  |         } else { | ||||||
|  |           p = Keypairs.import({ pem: options.accountKeypair.privateKeyPem }); | ||||||
|  |         } | ||||||
|  |         return p.then(function (pair) { | ||||||
|  |           if (pair.public.kid) { | ||||||
|  |             pair = JSON.parse(JSON.stringify(pair)); | ||||||
|  |             delete pair.public.kid; | ||||||
|  |             delete pair.private.kid; | ||||||
|  |           } | ||||||
|  |           return pair; | ||||||
|  |         }).then(function (pair) { | ||||||
|  |           var contact; | ||||||
|  |           if (options.contact) { | ||||||
|  |             contact = options.contact.slice(0); | ||||||
|  |           } else if (options.email) { | ||||||
|  |             contact = [ 'mailto:' + options.email ]; | ||||||
|  |           } | ||||||
|  |           var body = { | ||||||
|  |             termsOfServiceAgreed: tosUrl === me._tos | ||||||
|  |           , onlyReturnExisting: false | ||||||
|  |           , contact: contact | ||||||
|  |           }; | ||||||
|  |           if (options.externalAccount) { | ||||||
|  |             body.externalAccountBinding = me.RSA.signJws( | ||||||
|  |               // TODO is HMAC the standard, or is this arbitrary?
 | ||||||
|  |               options.externalAccount.secret | ||||||
|  |             , undefined | ||||||
|  |             , { alg: options.externalAccount.alg || "HS256" | ||||||
|  |               , kid: options.externalAccount.id | ||||||
|  |               , url: me._directoryUrls.newAccount | ||||||
|  |               } | ||||||
|  |             , Buffer.from(JSON.stringify(pair.public)) | ||||||
|  |             ); | ||||||
|  |           } | ||||||
|  |           var payload = JSON.stringify(body); | ||||||
|  |           var jws = Keypairs.signJws( | ||||||
|  |             options.accountKeypair | ||||||
|  |           , undefined | ||||||
|  |           , { nonce: me._nonce | ||||||
|  |             , alg: (me._alg || 'RS256') | ||||||
|  |             , url: me._directoryUrls.newAccount | ||||||
|  |             , jwk: pair.public | ||||||
|  |             } | ||||||
|  |           , Buffer.from(payload) | ||||||
|  |           ); | ||||||
|  | 
 | ||||||
|  |           delete jws.header; | ||||||
|  |           if (me.debug) { console.debug('[acme-v2] accounts.create JSON body:'); } | ||||||
|  |           if (me.debug) { console.debug(jws); } | ||||||
|  |           me._nonce = null; | ||||||
|  |           return me._request({ | ||||||
|  |             method: 'POST' | ||||||
|  |           , url: me._directoryUrls.newAccount | ||||||
|  |           , headers: { 'Content-Type': 'application/jose+json' } | ||||||
|  |           , json: jws | ||||||
|  |           }).then(function (resp) { | ||||||
|  |             var account = resp.body; | ||||||
|  | 
 | ||||||
|  |             if (2 !== Math.floor(resp.statusCode / 100)) { | ||||||
|  |               throw new Error('account error: ' + JSON.stringify(body)); | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             me._nonce = resp.toJSON().headers['replay-nonce']; | ||||||
|  |             var location = resp.toJSON().headers.location; | ||||||
|  |             // the account id url
 | ||||||
|  |             me._kid = location; | ||||||
|  |             if (me.debug) { console.debug('[DEBUG] new account location:'); } | ||||||
|  |             if (me.debug) { console.debug(location); } | ||||||
|  |             if (me.debug) { console.debug(resp.toJSON()); } | ||||||
|  | 
 | ||||||
|  |             /* | ||||||
|  |             { | ||||||
|  |               contact: ["mailto:jon@example.com"], | ||||||
|  |               orders: "https://some-url", | ||||||
|  |               status: 'valid' | ||||||
|  |             } | ||||||
|  |             */ | ||||||
|  |             if (!account) { account = { _emptyResponse: true, key: {} }; } | ||||||
|  |             // https://git.coolaj86.com/coolaj86/acme-v2.js/issues/8
 | ||||||
|  |             if (!account.key) { account.key = {}; } | ||||||
|  |             account.key.kid = me._kid; | ||||||
|  |             return account; | ||||||
|  |           }).then(resolve, reject); | ||||||
|  |         }); | ||||||
|  |       } | ||||||
|  | 
 | ||||||
|  |       if (me.debug) { console.debug('[acme-v2] agreeToTerms'); } | ||||||
|  |       if (1 === options.agreeToTerms.length) { | ||||||
|  |         // newer promise API
 | ||||||
|  |         return options.agreeToTerms(me._tos).then(agree, reject); | ||||||
|  |       } | ||||||
|  |       else if (2 === options.agreeToTerms.length) { | ||||||
|  |         // backwards compat cb API
 | ||||||
|  |         return options.agreeToTerms(me._tos, function (err, tosUrl) { | ||||||
|  |           if (!err) { agree(tosUrl); return; } | ||||||
|  |           reject(err); | ||||||
|  |         }); | ||||||
|  |       } | ||||||
|  |       else { | ||||||
|  |         reject(new Error('agreeToTerms has incorrect function signature.' | ||||||
|  |           + ' Should be fn(tos) { return Promise<tos>; }')); | ||||||
|  |       } | ||||||
|  |     }); | ||||||
|  |   }); | ||||||
|  | }; | ||||||
|  | /* | ||||||
|  |  POST /acme/new-order HTTP/1.1 | ||||||
|  |  Host: example.com | ||||||
|  |  Content-Type: application/jose+json | ||||||
|  | 
 | ||||||
|  |  { | ||||||
|  |    "protected": base64url({ | ||||||
|  |      "alg": "ES256", | ||||||
|  |      "kid": "https://example.com/acme/acct/1", | ||||||
|  |      "nonce": "5XJ1L3lEkMG7tR6pA00clA", | ||||||
|  |      "url": "https://example.com/acme/new-order" | ||||||
|  |    }), | ||||||
|  |    "payload": base64url({ | ||||||
|  |      "identifiers": [{"type:"dns","value":"example.com"}], | ||||||
|  |      "notBefore": "2016-01-01T00:00:00Z", | ||||||
|  |      "notAfter": "2016-01-08T00:00:00Z" | ||||||
|  |    }), | ||||||
|  |    "signature": "H6ZXtGjTZyUnPeKn...wEA4TklBdh3e454g" | ||||||
|  |  } | ||||||
|  | */ | ||||||
|  | ACME._getChallenges = function (me, options, auth) { | ||||||
|  |   if (me.debug) { console.debug('\n[DEBUG] getChallenges\n'); } | ||||||
|  |   // TODO POST-as-GET
 | ||||||
|  |   return me._request({ method: 'GET', url: auth, json: true }).then(function (resp) { | ||||||
|  |     return resp.body; | ||||||
|  |   }); | ||||||
|  | }; | ||||||
|  | ACME._wait = function wait(ms) { | ||||||
|  |   return new Promise(function (resolve) { | ||||||
|  |     setTimeout(resolve, (ms || 1100)); | ||||||
|  |   }); | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | ACME._testChallengeOptions = function () { | ||||||
|  |   var chToken = ACME._prnd(16); | ||||||
|  |   return [ | ||||||
|  |     { | ||||||
|  |       "type": "http-01", | ||||||
|  |       "status": "pending", | ||||||
|  |       "url": "https://acme-staging-v02.example.com/0", | ||||||
|  |       "token": "test-" + chToken + "-0" | ||||||
|  |     } | ||||||
|  |   , { | ||||||
|  |       "type": "dns-01", | ||||||
|  |       "status": "pending", | ||||||
|  |       "url": "https://acme-staging-v02.example.com/1", | ||||||
|  |       "token": "test-" + chToken + "-1", | ||||||
|  |       "_wildcard": true | ||||||
|  |     } | ||||||
|  |   , { | ||||||
|  |       "type": "tls-sni-01", | ||||||
|  |       "status": "pending", | ||||||
|  |       "url": "https://acme-staging-v02.example.com/2", | ||||||
|  |       "token": "test-" + chToken + "-2" | ||||||
|  |     } | ||||||
|  |   , { | ||||||
|  |       "type": "tls-alpn-01", | ||||||
|  |       "status": "pending", | ||||||
|  |       "url": "https://acme-staging-v02.example.com/3", | ||||||
|  |       "token": "test-" + chToken + "-3" | ||||||
|  |     } | ||||||
|  |   ]; | ||||||
|  | }; | ||||||
|  | ACME._testChallenges = function (me, options) { | ||||||
|  |   if (me.skipChallengeTest) { | ||||||
|  |     return Promise.resolve(); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   var CHECK_DELAY = 0; | ||||||
|  |   return Promise.all(options.domains.map(function (identifierValue) { | ||||||
|  |     // TODO we really only need one to pass, not all to pass
 | ||||||
|  |     var challenges = ACME._testChallengeOptions(); | ||||||
|  |     if (identifierValue.includes("*")) { | ||||||
|  |       challenges = challenges.filter(function (ch) { return ch._wildcard; }); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     var challenge = ACME._chooseChallenge(options, { challenges: challenges }); | ||||||
|  |     if (!challenge) { | ||||||
|  |       // For example, wildcards require dns-01 and, if we don't have that, we have to bail
 | ||||||
|  |       var enabled = options.challengeTypes.join(', ') || 'none'; | ||||||
|  |       var suitable = challenges.map(function (r) { return r.type; }).join(', ') || 'none'; | ||||||
|  |       return Promise.reject(new Error( | ||||||
|  |         "None of the challenge types that you've enabled ( " + enabled + " )" | ||||||
|  |           + " are suitable for validating the domain you've selected (" + identifierValue + ")." | ||||||
|  |           + " You must enable one of ( " + suitable + " )." | ||||||
|  |       )); | ||||||
|  |     } | ||||||
|  |     if ('dns-01' === challenge.type) { | ||||||
|  |       // Give the nameservers a moment to propagate
 | ||||||
|  |       CHECK_DELAY = 1.5 * 1000; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     return Promise.resolve().then(function () { | ||||||
|  |       var results = { | ||||||
|  |         identifier: { | ||||||
|  |           type: "dns" | ||||||
|  |         , value: identifierValue.replace(/^\*\./, '') | ||||||
|  |         } | ||||||
|  |       , challenges: [ challenge ] | ||||||
|  |       , expires: new Date(Date.now() + (60 * 1000)).toISOString() | ||||||
|  |       , wildcard: identifierValue.includes('*.') || undefined | ||||||
|  |       }; | ||||||
|  |       var dryrun = true; | ||||||
|  |       var auth = ACME._challengeToAuth(me, options, results, challenge, dryrun); | ||||||
|  |       return ACME._setChallenge(me, options, auth).then(function () { | ||||||
|  |         return auth; | ||||||
|  |       }); | ||||||
|  |     }); | ||||||
|  |   })).then(function (auths) { | ||||||
|  |     return ACME._wait(CHECK_DELAY).then(function () { | ||||||
|  |       return Promise.all(auths.map(function (auth) { | ||||||
|  |         return ACME.challengeTests[auth.type](me, auth); | ||||||
|  |       })); | ||||||
|  |     }); | ||||||
|  |   }); | ||||||
|  | }; | ||||||
|  | ACME._chooseChallenge = function(options, results) { | ||||||
|  |   // For each of the challenge types that we support
 | ||||||
|  |   var challenge; | ||||||
|  |   options.challengeTypes.some(function (chType) { | ||||||
|  |     // And for each of the challenge types that are allowed
 | ||||||
|  |     return results.challenges.some(function (ch) { | ||||||
|  |       // Check to see if there are any matches
 | ||||||
|  |       if (ch.type === chType) { | ||||||
|  |         challenge = ch; | ||||||
|  |         return true; | ||||||
|  |       } | ||||||
|  |     }); | ||||||
|  |   }); | ||||||
|  | 
 | ||||||
|  |   return challenge; | ||||||
|  | }; | ||||||
|  | ACME._challengeToAuth = function (me, options, request, challenge, dryrun) { | ||||||
|  |   // we don't poison the dns cache with our dummy request
 | ||||||
|  |   var dnsPrefix = ACME.challengePrefixes['dns-01']; | ||||||
|  |   if (dryrun) { | ||||||
|  |     dnsPrefix = dnsPrefix.replace('acme-challenge', 'greenlock-dryrun-' + ACME._prnd(4)); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   var auth = {}; | ||||||
|  | 
 | ||||||
|  |   // straight copy from the new order response
 | ||||||
|  |   // { identifier, status, expires, challenges, wildcard }
 | ||||||
|  |   Object.keys(request).forEach(function (key) { | ||||||
|  |     auth[key] = request[key]; | ||||||
|  |   }); | ||||||
|  | 
 | ||||||
|  |   // copy from the challenge we've chosen
 | ||||||
|  |   // { type, status, url, token }
 | ||||||
|  |   // (note the duplicate status overwrites the one above, but they should be the same)
 | ||||||
|  |   Object.keys(challenge).forEach(function (key) { | ||||||
|  |     // don't confused devs with the id url
 | ||||||
|  |     auth[key] = challenge[key]; | ||||||
|  |   }); | ||||||
|  | 
 | ||||||
|  |   // batteries-included helpers
 | ||||||
|  |   auth.hostname = auth.identifier.value; | ||||||
|  |   // because I'm not 100% clear if the wildcard identifier does or doesn't have the leading *. in all cases
 | ||||||
|  |   auth.altname = ACME._untame(auth.identifier.value, auth.wildcard); | ||||||
|  |   auth.thumbprint = me.RSA.thumbprint(options.accountKeypair); | ||||||
|  |   //   keyAuthorization = token || '.' || base64url(JWK_Thumbprint(accountKey))
 | ||||||
|  |   auth.keyAuthorization = challenge.token + '.' + auth.thumbprint; | ||||||
|  |   // conflicts with ACME challenge id url is already in use, so we call this challengeUrl instead
 | ||||||
|  |   auth.challengeUrl = 'http://' + auth.identifier.value + ACME.challengePrefixes['http-01'] + '/' + auth.token; | ||||||
|  |   auth.dnsHost = dnsPrefix + '.' + auth.hostname.replace('*.', ''); | ||||||
|  | 
 | ||||||
|  |   return Crypto._sha('sha256', auth.keyAuthorization).then(function (hash) { | ||||||
|  |     auth.dnsAuthorization = hash; | ||||||
|  |     return auth; | ||||||
|  |   }); | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | ACME._untame = function (name, wild) { | ||||||
|  |   if (wild) { name = '*.' + name.replace('*.', ''); } | ||||||
|  |   return name; | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | // https://tools.ietf.org/html/draft-ietf-acme-acme-10#section-7.5.1
 | ||||||
|  | ACME._postChallenge = function (me, options, auth) { | ||||||
|  |   var RETRY_INTERVAL = me.retryInterval || 1000; | ||||||
|  |   var DEAUTH_INTERVAL = me.deauthWait || 10 * 1000; | ||||||
|  |   var MAX_POLL = me.retryPoll || 8; | ||||||
|  |   var MAX_PEND = me.retryPending || 4; | ||||||
|  |   var count = 0; | ||||||
|  | 
 | ||||||
|  |   var altname = ACME._untame(auth.identifier.value, auth.wildcard); | ||||||
|  | 
 | ||||||
|  |   /* | ||||||
|  |    POST /acme/authz/1234 HTTP/1.1 | ||||||
|  |    Host: example.com | ||||||
|  |    Content-Type: application/jose+json | ||||||
|  | 
 | ||||||
|  |    { | ||||||
|  |      "protected": base64url({ | ||||||
|  |        "alg": "ES256", | ||||||
|  |        "kid": "https://example.com/acme/acct/1", | ||||||
|  |        "nonce": "xWCM9lGbIyCgue8di6ueWQ", | ||||||
|  |        "url": "https://example.com/acme/authz/1234" | ||||||
|  |      }), | ||||||
|  |      "payload": base64url({ | ||||||
|  |        "status": "deactivated" | ||||||
|  |      }), | ||||||
|  |      "signature": "srX9Ji7Le9bjszhu...WTFdtujObzMtZcx4" | ||||||
|  |    } | ||||||
|  |    */ | ||||||
|  |   function deactivate() { | ||||||
|  |     var jws = me.RSA.signJws( | ||||||
|  |       options.accountKeypair | ||||||
|  |     , undefined | ||||||
|  |     , { nonce: me._nonce, alg: (me._alg || 'RS256'), url: auth.url, kid: me._kid } | ||||||
|  |     , Buffer.from(JSON.stringify({ "status": "deactivated" })) | ||||||
|  |     ); | ||||||
|  |     me._nonce = null; | ||||||
|  |     return me._request({ | ||||||
|  |       method: 'POST' | ||||||
|  |     , url: auth.url | ||||||
|  |     , headers: { 'Content-Type': 'application/jose+json' } | ||||||
|  |     , json: jws | ||||||
|  |     }).then(function (resp) { | ||||||
|  |       if (me.debug) { console.debug('[acme-v2.js] deactivate:'); } | ||||||
|  |       if (me.debug) { console.debug(resp.headers); } | ||||||
|  |       if (me.debug) { console.debug(resp.body); } | ||||||
|  |       if (me.debug) { console.debug(); } | ||||||
|  | 
 | ||||||
|  |       me._nonce = resp.toJSON().headers['replay-nonce']; | ||||||
|  |       if (me.debug) { console.debug('deactivate challenge: resp.body:'); } | ||||||
|  |       if (me.debug) { console.debug(resp.body); } | ||||||
|  |       return ACME._wait(DEAUTH_INTERVAL); | ||||||
|  |     }); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   function pollStatus() { | ||||||
|  |     if (count >= MAX_POLL) { | ||||||
|  |       return Promise.reject(new Error( | ||||||
|  |         "[acme-v2] stuck in bad pending/processing state for '" + altname + "'" | ||||||
|  |       )); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     count += 1; | ||||||
|  | 
 | ||||||
|  |     if (me.debug) { console.debug('\n[DEBUG] statusChallenge\n'); } | ||||||
|  |     // TODO POST-as-GET
 | ||||||
|  |     return me._request({ method: 'GET', url: auth.url, json: true }).then(function (resp) { | ||||||
|  |       if ('processing' === resp.body.status) { | ||||||
|  |         if (me.debug) { console.debug('poll: again'); } | ||||||
|  |         return ACME._wait(RETRY_INTERVAL).then(pollStatus); | ||||||
|  |       } | ||||||
|  | 
 | ||||||
|  |       // This state should never occur
 | ||||||
|  |       if ('pending' === resp.body.status) { | ||||||
|  |         if (count >= MAX_PEND) { | ||||||
|  |           return ACME._wait(RETRY_INTERVAL).then(deactivate).then(respondToChallenge); | ||||||
|  |         } | ||||||
|  |         if (me.debug) { console.debug('poll: again'); } | ||||||
|  |         return ACME._wait(RETRY_INTERVAL).then(respondToChallenge); | ||||||
|  |       } | ||||||
|  | 
 | ||||||
|  |       if ('valid' === resp.body.status) { | ||||||
|  |         if (me.debug) { console.debug('poll: valid'); } | ||||||
|  | 
 | ||||||
|  |         try { | ||||||
|  |           if (1 === options.removeChallenge.length) { | ||||||
|  |             options.removeChallenge(auth).then(function () {}, function () {}); | ||||||
|  |           } else if (2 === options.removeChallenge.length) { | ||||||
|  |             options.removeChallenge(auth, function (err) { return err; }); | ||||||
|  |           } else { | ||||||
|  |             if (!ACME._removeChallengeWarn) { | ||||||
|  |               console.warn("Please update to acme-v2 removeChallenge(options) <Promise> or removeChallenge(options, cb)."); | ||||||
|  |               console.warn("The API has been changed for compatibility with all ACME / Let's Encrypt challenge types."); | ||||||
|  |               ACME._removeChallengeWarn = true; | ||||||
|  |             } | ||||||
|  |             options.removeChallenge(auth.request.identifier, auth.token, function () {}); | ||||||
|  |           } | ||||||
|  |         } catch(e) {} | ||||||
|  |         return resp.body; | ||||||
|  |       } | ||||||
|  | 
 | ||||||
|  |       var errmsg; | ||||||
|  |       if (!resp.body.status) { | ||||||
|  |         errmsg = "[acme-v2] (E_STATE_EMPTY) empty challenge state for '" + altname + "':"; | ||||||
|  |       } | ||||||
|  |       else if ('invalid' === resp.body.status) { | ||||||
|  |         errmsg = "[acme-v2] (E_STATE_INVALID) challenge state for '" + altname + "': '" + resp.body.status + "'"; | ||||||
|  |       } | ||||||
|  |       else { | ||||||
|  |         errmsg = "[acme-v2] (E_STATE_UKN) challenge state for '" + altname + "': '" + resp.body.status + "'"; | ||||||
|  |       } | ||||||
|  | 
 | ||||||
|  |       return Promise.reject(new Error(errmsg)); | ||||||
|  |     }); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   function respondToChallenge() { | ||||||
|  |     var jws = me.RSA.signJws( | ||||||
|  |       options.accountKeypair | ||||||
|  |     , undefined | ||||||
|  |     , { nonce: me._nonce, alg: (me._alg || 'RS256'), url: auth.url, kid: me._kid } | ||||||
|  |     , Buffer.from(JSON.stringify({ })) | ||||||
|  |     ); | ||||||
|  |     me._nonce = null; | ||||||
|  |     return me._request({ | ||||||
|  |       method: 'POST' | ||||||
|  |     , url: auth.url | ||||||
|  |     , headers: { 'Content-Type': 'application/jose+json' } | ||||||
|  |     , json: jws | ||||||
|  |     }).then(function (resp) { | ||||||
|  |       if (me.debug) { console.debug('[acme-v2.js] challenge accepted!'); } | ||||||
|  |       if (me.debug) { console.debug(resp.headers); } | ||||||
|  |       if (me.debug) { console.debug(resp.body); } | ||||||
|  |       if (me.debug) { console.debug(); } | ||||||
|  | 
 | ||||||
|  |       me._nonce = resp.toJSON().headers['replay-nonce']; | ||||||
|  |       if (me.debug) { console.debug('respond to challenge: resp.body:'); } | ||||||
|  |       if (me.debug) { console.debug(resp.body); } | ||||||
|  |       return ACME._wait(RETRY_INTERVAL).then(pollStatus); | ||||||
|  |     }); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   return respondToChallenge(); | ||||||
|  | }; | ||||||
|  | ACME._setChallenge = function (me, options, auth) { | ||||||
|  |   return new Promise(function (resolve, reject) { | ||||||
|  |     try { | ||||||
|  |       if (1 === options.setChallenge.length) { | ||||||
|  |         options.setChallenge(auth).then(resolve).catch(reject); | ||||||
|  |       } else if (2 === options.setChallenge.length) { | ||||||
|  |         options.setChallenge(auth, function (err) { | ||||||
|  |           if(err) { reject(err); } else { resolve(); } | ||||||
|  |         }); | ||||||
|  |       } else { | ||||||
|  |         var challengeCb = function(err) { | ||||||
|  |           if(err) { reject(err); } else { resolve(); } | ||||||
|  |         }; | ||||||
|  |         // for backwards compat adding extra keys without changing params length
 | ||||||
|  |         Object.keys(auth).forEach(function (key) { | ||||||
|  |           challengeCb[key] = auth[key]; | ||||||
|  |         }); | ||||||
|  |         if (!ACME._setChallengeWarn) { | ||||||
|  |           console.warn("Please update to acme-v2 setChallenge(options) <Promise> or setChallenge(options, cb)."); | ||||||
|  |           console.warn("The API has been changed for compatibility with all ACME / Let's Encrypt challenge types."); | ||||||
|  |           ACME._setChallengeWarn = true; | ||||||
|  |         } | ||||||
|  |         options.setChallenge(auth.identifier.value, auth.token, auth.keyAuthorization, challengeCb); | ||||||
|  |       } | ||||||
|  |     } catch(e) { | ||||||
|  |       reject(e); | ||||||
|  |     } | ||||||
|  |   }).then(function () { | ||||||
|  |     // TODO: Do we still need this delay? Or shall we leave it to plugins to account for themselves?
 | ||||||
|  |     var DELAY = me.setChallengeWait || 500; | ||||||
|  |     if (me.debug) { console.debug('\n[DEBUG] waitChallengeDelay %s\n', DELAY); } | ||||||
|  |     return ACME._wait(DELAY); | ||||||
|  |   }); | ||||||
|  | }; | ||||||
|  | ACME._finalizeOrder = function (me, options, validatedDomains) { | ||||||
|  |   if (me.debug) { console.debug('finalizeOrder:'); } | ||||||
|  |   var csr = me.RSA.generateCsrWeb64(options.domainKeypair, validatedDomains); | ||||||
|  |   var body = { csr: csr }; | ||||||
|  |   var payload = JSON.stringify(body); | ||||||
|  | 
 | ||||||
|  |   function pollCert() { | ||||||
|  |     var jws = me.RSA.signJws( | ||||||
|  |       options.accountKeypair | ||||||
|  |     , undefined | ||||||
|  |     , { nonce: me._nonce, alg: (me._alg || 'RS256'), url: me._finalize, kid: me._kid } | ||||||
|  |     , Buffer.from(payload) | ||||||
|  |     ); | ||||||
|  | 
 | ||||||
|  |     if (me.debug) { console.debug('finalize:', me._finalize); } | ||||||
|  |     me._nonce = null; | ||||||
|  |     return me._request({ | ||||||
|  |       method: 'POST' | ||||||
|  |     , url: me._finalize | ||||||
|  |     , headers: { 'Content-Type': 'application/jose+json' } | ||||||
|  |     , json: jws | ||||||
|  |     }).then(function (resp) { | ||||||
|  |       // https://tools.ietf.org/html/draft-ietf-acme-acme-12#section-7.1.3
 | ||||||
|  |       // Possible values are: "pending" => ("invalid" || "ready") => "processing" => "valid"
 | ||||||
|  |       me._nonce = resp.toJSON().headers['replay-nonce']; | ||||||
|  | 
 | ||||||
|  |       if (me.debug) { console.debug('order finalized: resp.body:'); } | ||||||
|  |       if (me.debug) { console.debug(resp.body); } | ||||||
|  | 
 | ||||||
|  |       if ('valid' === resp.body.status) { | ||||||
|  |         me._expires = resp.body.expires; | ||||||
|  |         me._certificate = resp.body.certificate; | ||||||
|  | 
 | ||||||
|  |         return resp.body; // return order
 | ||||||
|  |       } | ||||||
|  | 
 | ||||||
|  |       if ('processing' === resp.body.status) { | ||||||
|  |         return ACME._wait().then(pollCert); | ||||||
|  |       } | ||||||
|  | 
 | ||||||
|  |       if (me.debug) { console.debug("Error: bad status:\n" + JSON.stringify(resp.body, null, 2)); } | ||||||
|  | 
 | ||||||
|  |       if ('pending' === resp.body.status) { | ||||||
|  |         return Promise.reject(new Error( | ||||||
|  |           "Did not finalize order: status 'pending'." | ||||||
|  |         + " Best guess: You have not accepted at least one challenge for each domain:\n" | ||||||
|  |         + "Requested: '" + options.domains.join(', ') + "'\n" | ||||||
|  |         + "Validated: '" + validatedDomains.join(', ') + "'\n" | ||||||
|  |         + JSON.stringify(resp.body, null, 2) | ||||||
|  |         )); | ||||||
|  |       } | ||||||
|  | 
 | ||||||
|  |       if ('invalid' === resp.body.status) { | ||||||
|  |         return Promise.reject(new Error( | ||||||
|  |           "Did not finalize order: status 'invalid'." | ||||||
|  |         + " Best guess: One or more of the domain challenges could not be verified" | ||||||
|  |         + " (or the order was canceled).\n" | ||||||
|  |         + "Requested: '" + options.domains.join(', ') + "'\n" | ||||||
|  |         + "Validated: '" + validatedDomains.join(', ') + "'\n" | ||||||
|  |         + JSON.stringify(resp.body, null, 2) | ||||||
|  |         )); | ||||||
|  |       } | ||||||
|  | 
 | ||||||
|  |       if ('ready' === resp.body.status) { | ||||||
|  |         return Promise.reject(new Error( | ||||||
|  |           "Did not finalize order: status 'ready'." | ||||||
|  |         + " Hmmm... this state shouldn't be possible here. That was the last state." | ||||||
|  |         + " This one should at least be 'processing'.\n" | ||||||
|  |         + "Requested: '" + options.domains.join(', ') + "'\n" | ||||||
|  |         + "Validated: '" + validatedDomains.join(', ') + "'\n" | ||||||
|  |         + JSON.stringify(resp.body, null, 2) + "\n\n" | ||||||
|  |         + "Please open an issue at https://git.coolaj86.com/coolaj86/acme-v2.js" | ||||||
|  |         )); | ||||||
|  |       } | ||||||
|  | 
 | ||||||
|  |       return Promise.reject(new Error( | ||||||
|  |         "Didn't finalize order: Unhandled status '" + resp.body.status + "'." | ||||||
|  |       + " This is not one of the known statuses...\n" | ||||||
|  |       + "Requested: '" + options.domains.join(', ') + "'\n" | ||||||
|  |       + "Validated: '" + validatedDomains.join(', ') + "'\n" | ||||||
|  |       + JSON.stringify(resp.body, null, 2) + "\n\n" | ||||||
|  |       + "Please open an issue at https://git.coolaj86.com/coolaj86/acme-v2.js" | ||||||
|  |       )); | ||||||
|  |     }); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   return pollCert(); | ||||||
|  | }; | ||||||
|  | ACME._getCertificate = function (me, options) { | ||||||
|  |   if (me.debug) { console.debug('[acme-v2] DEBUG get cert 1'); } | ||||||
|  | 
 | ||||||
|  |   // Lot's of error checking to inform the user of mistakes
 | ||||||
|  |   if (!(options.challengeTypes||[]).length) { | ||||||
|  |     options.challengeTypes = Object.keys(options.challenges||{}); | ||||||
|  |   } | ||||||
|  |   if (!options.challengeTypes.length) { | ||||||
|  |     options.challengeTypes = [ options.challengeType ].filter(Boolean); | ||||||
|  |   } | ||||||
|  |   if (options.challengeType) { | ||||||
|  |     options.challengeTypes.sort(function (a, b) { | ||||||
|  |       if (a === options.challengeType) { return -1; } | ||||||
|  |       if (b === options.challengeType) { return 1; } | ||||||
|  |       return 0; | ||||||
|  |     }); | ||||||
|  |     if (options.challengeType !== options.challengeTypes[0]) { | ||||||
|  |       return Promise.reject(new Error("options.challengeType is '" + options.challengeType + "'," | ||||||
|  |         + " which does not exist in the supplied types '" + options.challengeTypes.join(',') + "'")); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |   // TODO check that all challengeTypes are represented in challenges
 | ||||||
|  |   if (!options.challengeTypes.length) { | ||||||
|  |     return Promise.reject(new Error("options.challengeTypes (string array) must be specified" | ||||||
|  |       + " (and in order of preferential priority).")); | ||||||
|  |   } | ||||||
|  |   if (!(options.domains && options.domains.length)) { | ||||||
|  |     return Promise.reject(new Error("options.domains must be a list of string domain names," | ||||||
|  |     + " with the first being the subject of the domain (or options.subject must specified).")); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   // It's just fine if there's no account, we'll go get the key id we need via the public key
 | ||||||
|  |   if (!me._kid) { | ||||||
|  |     if (options.accountKid || options.account && options.account.kid) { | ||||||
|  |       me._kid = options.accountKid || options.account.kid; | ||||||
|  |     } else { | ||||||
|  |       //return Promise.reject(new Error("must include KeyID"));
 | ||||||
|  |       // This is an idempotent request. It'll return the same account for the same public key.
 | ||||||
|  |       return ACME._registerAccount(me, options).then(function () { | ||||||
|  |         // start back from the top
 | ||||||
|  |         return ACME._getCertificate(me, options); | ||||||
|  |       }); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   // Do a little dry-run / self-test
 | ||||||
|  |   return ACME._testChallenges(me, options).then(function () { | ||||||
|  |     if (me.debug) { console.debug('[acme-v2] certificates.create'); } | ||||||
|  |     return ACME._getNonce(me).then(function () { | ||||||
|  |       var body = { | ||||||
|  |         // raw wildcard syntax MUST be used here
 | ||||||
|  |         identifiers: options.domains.sort(function (a, b) { | ||||||
|  |           // the first in the list will be the subject of the certificate, I believe (and hope)
 | ||||||
|  |           if (!options.subject) { return 0; } | ||||||
|  |           if (options.subject === a) { return -1; } | ||||||
|  |           if (options.subject === b) { return 1; } | ||||||
|  |           return 0; | ||||||
|  |         }).map(function (hostname) { | ||||||
|  |           return { type: "dns", value: hostname }; | ||||||
|  |         }) | ||||||
|  |         //, "notBefore": "2016-01-01T00:00:00Z"
 | ||||||
|  |         //, "notAfter": "2016-01-08T00:00:00Z"
 | ||||||
|  |       }; | ||||||
|  | 
 | ||||||
|  |       var payload = JSON.stringify(body); | ||||||
|  |       // determine the signing algorithm to use in protected header // TODO isn't that handled by the signer?
 | ||||||
|  |       me._kty = (options.accountKeypair.privateKeyJwk && options.accountKeypair.privateKeyJwk.kty || 'RSA'); | ||||||
|  |       me._alg = ('EC' === me._kty) ? 'ES256' : 'RS256'; // TODO vary with bitwidth of key (if not handled)
 | ||||||
|  |       var jws = me.RSA.signJws( | ||||||
|  |         options.accountKeypair | ||||||
|  |       , undefined | ||||||
|  |       , { nonce: me._nonce, alg: me._alg, url: me._directoryUrls.newOrder, kid: me._kid } | ||||||
|  |       , Buffer.from(payload, 'utf8') | ||||||
|  |       ); | ||||||
|  | 
 | ||||||
|  |       if (me.debug) { console.debug('\n[DEBUG] newOrder\n'); } | ||||||
|  |       me._nonce = null; | ||||||
|  |       return me._request({ | ||||||
|  |         method: 'POST' | ||||||
|  |       , url: me._directoryUrls.newOrder | ||||||
|  |       , headers: { 'Content-Type': 'application/jose+json' } | ||||||
|  |       , json: jws | ||||||
|  |       }).then(function (resp) { | ||||||
|  |         me._nonce = resp.toJSON().headers['replay-nonce']; | ||||||
|  |         var location = resp.toJSON().headers.location; | ||||||
|  |         var setAuths; | ||||||
|  |         var auths = []; | ||||||
|  |         if (me.debug) { console.debug(location); } // the account id url
 | ||||||
|  |         if (me.debug) { console.debug(resp.toJSON()); } | ||||||
|  |         me._authorizations = resp.body.authorizations; | ||||||
|  |         me._order = location; | ||||||
|  |         me._finalize = resp.body.finalize; | ||||||
|  |         //if (me.debug) console.debug('[DEBUG] finalize:', me._finalize); return;
 | ||||||
|  | 
 | ||||||
|  |         if (!me._authorizations) { | ||||||
|  |           return Promise.reject(new Error( | ||||||
|  |             "[acme-v2.js] authorizations were not fetched for '" + options.domains.join() + "':\n" | ||||||
|  |             + JSON.stringify(resp.body) | ||||||
|  |           )); | ||||||
|  |         } | ||||||
|  |         if (me.debug) { console.debug("[acme-v2] POST newOrder has authorizations"); } | ||||||
|  |         setAuths = me._authorizations.slice(0); | ||||||
|  | 
 | ||||||
|  |         function setNext() { | ||||||
|  |           var authUrl = setAuths.shift(); | ||||||
|  |           if (!authUrl) { return; } | ||||||
|  | 
 | ||||||
|  |           return ACME._getChallenges(me, options, authUrl).then(function (results) { | ||||||
|  |             // var domain = options.domains[i]; // results.identifier.value
 | ||||||
|  | 
 | ||||||
|  |             // If it's already valid, we're golden it regardless
 | ||||||
|  |             if (results.challenges.some(function (ch) { return 'valid' === ch.status; })) { | ||||||
|  |               return setNext(); | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             var challenge = ACME._chooseChallenge(options, results); | ||||||
|  |             if (!challenge) { | ||||||
|  |               // For example, wildcards require dns-01 and, if we don't have that, we have to bail
 | ||||||
|  |               return Promise.reject(new Error( | ||||||
|  |                 "Server didn't offer any challenge we can handle for '" + options.domains.join() + "'." | ||||||
|  |               )); | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             return ACME._challengeToAuth(me, options, results, challenge, false).then(function (auth) { | ||||||
|  |               auths.push(auth); | ||||||
|  |               return ACME._setChallenge(me, options, auth).then(setNext); | ||||||
|  |             }); | ||||||
|  |           }); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         function challengeNext() { | ||||||
|  |           var auth = auths.shift(); | ||||||
|  |           if (!auth) { return; } | ||||||
|  |           return ACME._postChallenge(me, options, auth).then(challengeNext); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         // First we set every challenge
 | ||||||
|  |         // Then we ask for each challenge to be checked
 | ||||||
|  |         // Doing otherwise would potentially cause us to poison our own DNS cache with misses
 | ||||||
|  |         return setNext().then(challengeNext).then(function () { | ||||||
|  |           if (me.debug) { console.debug("[getCertificate] next.then"); } | ||||||
|  |           var validatedDomains = body.identifiers.map(function (ident) { | ||||||
|  |             return ident.value; | ||||||
|  |           }); | ||||||
|  | 
 | ||||||
|  |           return ACME._finalizeOrder(me, options, validatedDomains); | ||||||
|  |         }).then(function (order) { | ||||||
|  |           if (me.debug) { console.debug('acme-v2: order was finalized'); } | ||||||
|  |           // TODO POST-as-GET
 | ||||||
|  |           return me._request({ method: 'GET', url: me._certificate, json: true }).then(function (resp) { | ||||||
|  |             if (me.debug) { console.debug('acme-v2: csr submitted and cert received:'); } | ||||||
|  |             // https://github.com/certbot/certbot/issues/5721
 | ||||||
|  |             var certsarr = ACME.splitPemChain(ACME.formatPemChain((resp.body||''))); | ||||||
|  |             //  cert, chain, fullchain, privkey, /*TODO, subject, altnames, issuedAt, expiresAt */
 | ||||||
|  |             var certs = { | ||||||
|  |               expires: order.expires | ||||||
|  |             , identifiers: order.identifiers | ||||||
|  |             //, authorizations: order.authorizations
 | ||||||
|  |             , cert: certsarr.shift() | ||||||
|  |             //, privkey: privkeyPem
 | ||||||
|  |             , chain: certsarr.join('\n') | ||||||
|  |             }; | ||||||
|  |             if (me.debug) { console.debug(certs); } | ||||||
|  |             return certs; | ||||||
|  |           }); | ||||||
|  |         }); | ||||||
|  |       }); | ||||||
|  |     }); | ||||||
|  |   }); | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | ACME.create = function create(me) { | ||||||
|  |   if (!me) { me = {}; } | ||||||
|  |   // me.debug = true;
 | ||||||
|  |   me.challengePrefixes = ACME.challengePrefixes; | ||||||
|  |   me.RSA = me.RSA || require('rsa-compat').RSA; | ||||||
|  |   //me.Keypairs = me.Keypairs || require('keypairs');
 | ||||||
|  |   me.request = me.request || require('@coolaj86/urequest'); | ||||||
|  |   if (!me.dig) { | ||||||
|  |     me.dig = function (query) { | ||||||
|  |       // TODO use digd.js
 | ||||||
|  |       return new Promise(function (resolve, reject) { | ||||||
|  |         var dns = require('dns'); | ||||||
|  |         dns.resolveTxt(query.name, function (err, records) { | ||||||
|  |           if (err) { reject(err); return; } | ||||||
|  | 
 | ||||||
|  |           resolve({ | ||||||
|  |             answer: records.map(function (rr) { | ||||||
|  |               return { | ||||||
|  |                 data: rr | ||||||
|  |               }; | ||||||
|  |             }) | ||||||
|  |           }); | ||||||
|  |         }); | ||||||
|  |       }); | ||||||
|  |     }; | ||||||
|  |   } | ||||||
|  |   me.promisify = me.promisify || require('util').promisify /*node v8+*/ || require('bluebird').promisify /*node v6*/; | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  |   if ('function' !== typeof me._request) { | ||||||
|  |     // MUST have a User-Agent string (see node.js version)
 | ||||||
|  |     me._request = function (opts) { | ||||||
|  |       return window.fetch(opts.url, opts).then(function (resp) { | ||||||
|  |         return resp.json().then(function (json) { | ||||||
|  |           var headers = {}; | ||||||
|  |           Array.from(resp.headers.entries()).forEach(function (h) { headers[h[0]] = h[1]; }); | ||||||
|  |           return { headers: headers , body: json }; | ||||||
|  |         }); | ||||||
|  |       }); | ||||||
|  |     }; | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   me.init = function (_directoryUrl) { | ||||||
|  |     me.directoryUrl = me.directoryUrl || _directoryUrl; | ||||||
|  |     return ACME._directory(me).then(function (resp) { | ||||||
|  |       me._directoryUrls = resp.body; | ||||||
|  |       me._tos = me._directoryUrls.meta.termsOfService; | ||||||
|  |       return me._directoryUrls; | ||||||
|  |     }); | ||||||
|  |   }; | ||||||
|  |   me.accounts = { | ||||||
|  |     create: function (options) { | ||||||
|  |       return ACME._registerAccount(me, options); | ||||||
|  |     } | ||||||
|  |   }; | ||||||
|  |   me.certificates = { | ||||||
|  |     create: function (options) { | ||||||
|  |       return ACME._getCertificate(me, options); | ||||||
|  |     } | ||||||
|  |   }; | ||||||
|  |   return me; | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | ACME._toWebsafeBase64 = function (b64) { | ||||||
|  |   return b64.replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g,""); | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | // In v8 this is crypto random, but we're just using it for pseudorandom
 | ||||||
|  | ACME._prnd = function (n) { | ||||||
|  |   var rnd = ''; | ||||||
|  |   while (rnd.length / 2 < n) { | ||||||
|  |     var num = Math.random().toString().substr(2); | ||||||
|  |     if (num.length % 2) { | ||||||
|  |       num = '0' + num; | ||||||
|  |     } | ||||||
|  |     var pairs = num.match(/(..?)/g); | ||||||
|  |     rnd += pairs.map(ACME._toHex).join(''); | ||||||
|  |   } | ||||||
|  |   return rnd.substr(0, n*2); | ||||||
|  | }; | ||||||
|  | ACME._toHex = function (pair) { | ||||||
|  |   return parseInt(pair, 10).toString(16); | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | Enc.bufToUrlBase64 = function (u8) { | ||||||
|  |   return Enc.bufToBase64(u8) | ||||||
|  |     .replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, ''); | ||||||
|  | }; | ||||||
|  | Enc.bufToBase64 = function (u8) { | ||||||
|  |   var bin = ''; | ||||||
|  |   u8.forEach(function (i) { | ||||||
|  |     bin += String.fromCharCode(i); | ||||||
|  |   }); | ||||||
|  |   return btoa(bin); | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | Crypto._sha = function (sha, str) { | ||||||
|  |   var encoder = new TextEncoder(); | ||||||
|  |   var data = encoder.encode(str); | ||||||
|  |   sha = 'SHA-' + sha.replace(/^sha-?/i, ''); | ||||||
|  |   return window.crypto.subtle.digest(sha, data).then(function (hash) { | ||||||
|  |     return Enc.bufToUrlBase64(new Uint8Array(hash)); | ||||||
|  |   }); | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | }('undefined' === typeof window ? module.exports : window)); | ||||||
| @ -125,7 +125,7 @@ PEM.parseBlock = PEM.parseBlock || function (str) { | |||||||
|   var der = str.split(/\n/).filter(function (line) { |   var der = str.split(/\n/).filter(function (line) { | ||||||
|     return !/-----/.test(line); |     return !/-----/.test(line); | ||||||
|   }).join(''); |   }).join(''); | ||||||
|   return { bytes: Enc.base64ToBuf(der) }; |   return { der: Enc.base64ToBuf(der) }; | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| Enc.base64ToBuf = function (b64) { | Enc.base64ToBuf = function (b64) { | ||||||
|  | |||||||
| @ -66,11 +66,8 @@ Enc.numToHex = function (d) { | |||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| Enc.bufToUrlBase64 = function (u8) { | Enc.bufToUrlBase64 = function (u8) { | ||||||
|   return Enc.base64ToUrlBase64(Enc.bufToBase64(u8)); |   return Enc.bufToBase64(u8) | ||||||
| }; |     .replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, ''); | ||||||
| 
 |  | ||||||
| Enc.base64ToUrlBase64 = function (str) { |  | ||||||
|   return str.replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, ''); |  | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| Enc.bufToBase64 = function (u8) { | Enc.bufToBase64 = function (u8) { | ||||||
| @ -113,8 +110,6 @@ Enc.binToHex = function (bin) { | |||||||
|     return h; |     return h; | ||||||
|   }).join(''); |   }).join(''); | ||||||
| }; | }; | ||||||
| // TODO are there any nuance differences here?
 |  | ||||||
| Enc.utf8ToHex = Enc.binToHex; |  | ||||||
| 
 | 
 | ||||||
| Enc.hexToBase64 = function (hex) { | Enc.hexToBase64 = function (hex) { | ||||||
|   return btoa(Enc.hexToBin(hex)); |   return btoa(Enc.hexToBin(hex)); | ||||||
| @ -3,10 +3,7 @@ | |||||||
| 'use strict'; | 'use strict'; | ||||||
| 
 | 
 | ||||||
| var EC = exports.Eckles = {}; | var EC = exports.Eckles = {}; | ||||||
| var x509 = exports.x509; |  | ||||||
| if ('undefined' !== typeof module) { module.exports = EC; } | if ('undefined' !== typeof module) { module.exports = EC; } | ||||||
| var PEM = exports.PEM; |  | ||||||
| var SSH = exports.SSH; |  | ||||||
| var Enc = {}; | var Enc = {}; | ||||||
| var textEncoder = new TextEncoder(); | var textEncoder = new TextEncoder(); | ||||||
| 
 | 
 | ||||||
| @ -46,8 +43,6 @@ EC.generate = function (opts) { | |||||||
|       "jwk" |       "jwk" | ||||||
|     , result.privateKey |     , result.privateKey | ||||||
|     ).then(function (privJwk) { |     ).then(function (privJwk) { | ||||||
|       privJwk.key_ops = undefined; |  | ||||||
|       privJwk.ext = undefined; |  | ||||||
|       return { |       return { | ||||||
|         private: privJwk |         private: privJwk | ||||||
|       , public: EC.neuter({ jwk: privJwk }) |       , public: EC.neuter({ jwk: privJwk }) | ||||||
| @ -57,7 +52,6 @@ EC.generate = function (opts) { | |||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| EC.export = function (opts) { | EC.export = function (opts) { | ||||||
|   return Promise.resolve().then(function () { |  | ||||||
|   if (!opts || !opts.jwk || 'object' !== typeof opts.jwk) { |   if (!opts || !opts.jwk || 'object' !== typeof opts.jwk) { | ||||||
|     throw new Error("must pass { jwk: jwk } as a JSON object"); |     throw new Error("must pass { jwk: jwk } as a JSON object"); | ||||||
|   } |   } | ||||||
| @ -103,7 +97,6 @@ EC.export = function (opts) { | |||||||
|   } else { |   } else { | ||||||
|     throw new Error("Sanity Error: reached unreachable code block with format: " + format); |     throw new Error("Sanity Error: reached unreachable code block with format: " + format); | ||||||
|   } |   } | ||||||
|   }); |  | ||||||
| }; | }; | ||||||
| EC.pack = function (opts) { | EC.pack = function (opts) { | ||||||
|   return Promise.resolve().then(function () { |   return Promise.resolve().then(function () { | ||||||
|  | |||||||
							
								
								
									
										194
									
								
								lib/keypairs.js
									
									
									
									
									
								
							
							
						
						
									
										194
									
								
								lib/keypairs.js
									
									
									
									
									
								
							| @ -33,20 +33,12 @@ Keypairs.generate = function (opts) { | |||||||
|   }); |   }); | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| Keypairs.export = function (opts) { |  | ||||||
|   return Eckles.export(opts).catch(function (err) { |  | ||||||
|     return Rasha.export(opts).catch(function () { |  | ||||||
|       return Promise.reject(err); |  | ||||||
|     }); |  | ||||||
|   }); |  | ||||||
| }; |  | ||||||
| 
 |  | ||||||
| 
 | 
 | ||||||
| /** | /** | ||||||
|  * Chopping off the private parts is now part of the public API. |  * Chopping off the private parts is now part of the public API. | ||||||
|  * I thought it sounded a little too crude at first, but it really is the best name in every possible way. |  * I thought it sounded a little too crude at first, but it really is the best name in every possible way. | ||||||
|  */ |  */ | ||||||
| Keypairs.neuter = function (opts) { | Keypairs.neuter = Keypairs._neuter = function (opts) { | ||||||
|   /** trying to find the best balance of an immutable copy with custom attributes */ |   /** trying to find the best balance of an immutable copy with custom attributes */ | ||||||
|   var jwk = {}; |   var jwk = {}; | ||||||
|   Object.keys(opts.jwk).forEach(function (k) { |   Object.keys(opts.jwk).forEach(function (k) { | ||||||
| @ -136,12 +128,11 @@ Keypairs.signJws = function (opts) { | |||||||
|       if (!opts.jwk) { |       if (!opts.jwk) { | ||||||
|         throw new Error("opts.jwk must exist and must declare 'typ'"); |         throw new Error("opts.jwk must exist and must declare 'typ'"); | ||||||
|       } |       } | ||||||
|       if (opts.jwk.alg) { return opts.jwk.alg; } |       return ('RSA' === opts.jwk.kty) ? "RS256" : "ES256"; | ||||||
|       var typ = ('RSA' === opts.jwk.kty) ? "RS" : "ES"; |  | ||||||
|       return typ + Keypairs._getBits(opts); |  | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     function sign() { |     function sign(pem) { | ||||||
|  |       var header = opts.header; | ||||||
|       var protect = opts.protected; |       var protect = opts.protected; | ||||||
|       var payload = opts.payload; |       var payload = opts.payload; | ||||||
| 
 | 
 | ||||||
| @ -152,9 +143,8 @@ Keypairs.signJws = function (opts) { | |||||||
|       if (false !== protect) { |       if (false !== protect) { | ||||||
|         if (!protect) { protect = {}; } |         if (!protect) { protect = {}; } | ||||||
|         if (!protect.alg) { protect.alg = alg(); } |         if (!protect.alg) { protect.alg = alg(); } | ||||||
|         // There's a particular request where ACME / Let's Encrypt explicitly doesn't use a kid
 |         // There's a particular request where Let's Encrypt explicitly doesn't use a kid
 | ||||||
|         if (false === protect.kid) { protect.kid = undefined; } |         if (!protect.kid && false !== protect.kid) { protect.kid = thumb; } | ||||||
|         else if (!protect.kid) { protect.kid = thumb; } |  | ||||||
|         protectedHeader = JSON.stringify(protect); |         protectedHeader = JSON.stringify(protect); | ||||||
|       } |       } | ||||||
| 
 | 
 | ||||||
| @ -165,7 +155,7 @@ Keypairs.signJws = function (opts) { | |||||||
|       // Trying to detect if it's a plain object (not Buffer, ArrayBuffer, Array, Uint8Array, etc)
 |       // Trying to detect if it's a plain object (not Buffer, ArrayBuffer, Array, Uint8Array, etc)
 | ||||||
|       if (payload && ('string' !== typeof payload) |       if (payload && ('string' !== typeof payload) | ||||||
|         && ('undefined' === typeof payload.byteLength) |         && ('undefined' === typeof payload.byteLength) | ||||||
|         && ('undefined' === typeof payload.buffer) |         && ('undefined' === typeof payload.byteLength) | ||||||
|       ) { |       ) { | ||||||
|         payload = JSON.stringify(payload); |         payload = JSON.stringify(payload); | ||||||
|       } |       } | ||||||
| @ -175,129 +165,77 @@ Keypairs.signJws = function (opts) { | |||||||
|       } |       } | ||||||
| 
 | 
 | ||||||
|       // node specifies RSA-SHAxxx even when it's actually ecdsa (it's all encoded x509 shasums anyway)
 |       // node specifies RSA-SHAxxx even when it's actually ecdsa (it's all encoded x509 shasums anyway)
 | ||||||
|  |       var nodeAlg = "SHA" + (((protect||header).alg||'').replace(/^[^\d]+/, '')||'256'); | ||||||
|       var protected64 = Enc.strToUrlBase64(protectedHeader); |       var protected64 = Enc.strToUrlBase64(protectedHeader); | ||||||
|       var payload64 = Enc.bufToUrlBase64(payload); |       var payload64 = Enc.bufToUrlBase64(payload); | ||||||
|       var msg = protected64 + '.' + payload64; |       var binsig = require('crypto') | ||||||
|  |         .createSign(nodeAlg) | ||||||
|  |         .update(protect ? (protected64 + "." + payload64) : payload64) | ||||||
|  |         .sign(pem) | ||||||
|  |       ; | ||||||
|  |       if ('EC' === opts.jwk.kty) { | ||||||
|  |         // ECDSA JWT signatures differ from "normal" ECDSA signatures
 | ||||||
|  |         // https://tools.ietf.org/html/rfc7518#section-3.4
 | ||||||
|  |         binsig = convertIfEcdsa(binsig); | ||||||
|  |       } | ||||||
| 
 | 
 | ||||||
|       return Keypairs._sign(opts, msg).then(function (buf) { |       var sig = binsig.toString('base64') | ||||||
|         var signedMsg = { |         .replace(/\+/g, '-') | ||||||
|           protected: protected64 |         .replace(/\//g, '_') | ||||||
|  |         .replace(/=/g, '') | ||||||
|  |       ; | ||||||
|  | 
 | ||||||
|  |       return { | ||||||
|  |         header: header | ||||||
|  |       , protected: protected64 || undefined | ||||||
|       , payload: payload64 |       , payload: payload64 | ||||||
|         , signature: Enc.bufToUrlBase64(buf) |       , signature: sig | ||||||
|       }; |       }; | ||||||
| 
 |  | ||||||
|         return signedMsg; |  | ||||||
|       }); |  | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     if (opts.jwk) { |     function convertIfEcdsa(binsig) { | ||||||
|       return sign(); |       // should have asn1 sequence header of 0x30
 | ||||||
|  |       if (0x30 !== binsig[0]) { throw new Error("Impossible EC SHA head marker"); } | ||||||
|  |       var index = 2; // first ecdsa "R" header byte
 | ||||||
|  |       var len = binsig[1]; | ||||||
|  |       var lenlen = 0; | ||||||
|  |       // Seek length of length if length is greater than 127 (i.e. two 512-bit / 64-byte R and S values)
 | ||||||
|  |       if (0x80 & len) { | ||||||
|  |         lenlen = len - 0x80; // should be exactly 1
 | ||||||
|  |         len = binsig[2]; // should be <= 130 (two 64-bit SHA-512s, plus padding)
 | ||||||
|  |         index += lenlen; | ||||||
|  |       } | ||||||
|  |       // should be of BigInt type
 | ||||||
|  |       if (0x02 !== binsig[index]) { throw new Error("Impossible EC SHA R marker"); } | ||||||
|  |       index += 1; | ||||||
|  | 
 | ||||||
|  |       var rlen = binsig[index]; | ||||||
|  |       var bits = 32; | ||||||
|  |       if (rlen > 49) { | ||||||
|  |         bits = 64; | ||||||
|  |       } else if (rlen > 33) { | ||||||
|  |         bits = 48; | ||||||
|  |       } | ||||||
|  |       var r = binsig.slice(index + 1, index + 1 + rlen).toString('hex'); | ||||||
|  |       var slen = binsig[index + 1 + rlen + 1]; // skip header and read length
 | ||||||
|  |       var s = binsig.slice(index + 1 + rlen + 1 + 1).toString('hex'); | ||||||
|  |       if (2 *slen !== s.length) { throw new Error("Impossible EC SHA S length"); } | ||||||
|  |       // There may be one byte of padding on either
 | ||||||
|  |       while (r.length < 2*bits) { r = '00' + r; } | ||||||
|  |       while (s.length < 2*bits) { s = '00' + s; } | ||||||
|  |       if (2*(bits+1) === r.length) { r = r.slice(2); } | ||||||
|  |       if (2*(bits+1) === s.length) { s = s.slice(2); } | ||||||
|  |       return Enc.hexToBuf(r + s); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     if (opts.pem && opts.jwk) { | ||||||
|  |       return sign(opts.pem); | ||||||
|     } else { |     } else { | ||||||
|       return Keypairs.import({ pem: opts.pem }).then(function (pair) { |       return Keypairs.export({ jwk: opts.jwk }).then(sign); | ||||||
|         opts.jwk = pair.private; |  | ||||||
|         return sign(); |  | ||||||
|       }); |  | ||||||
|     } |     } | ||||||
|   }); |   }); | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| Keypairs._sign = function (opts, payload) { |  | ||||||
|   return Keypairs._import(opts).then(function (privkey) { |  | ||||||
|     if ('string' === typeof payload) { |  | ||||||
|       payload = (new TextEncoder()).encode(payload); |  | ||||||
|     } |  | ||||||
|     return window.crypto.subtle.sign( |  | ||||||
|       { name: Keypairs._getName(opts) |  | ||||||
|       , hash: { name: 'SHA-' + Keypairs._getBits(opts) } |  | ||||||
|       } |  | ||||||
|     , privkey |  | ||||||
|     , payload |  | ||||||
|     ).then(function (signature) { |  | ||||||
|       signature = new Uint8Array(signature); // ArrayBuffer -> u8
 |  | ||||||
|       // This will come back into play for CSRs, but not for JOSE
 |  | ||||||
|       if ('EC' === opts.jwk.kty && /x509|asn1/i.test(opts.format)) { |  | ||||||
|         return Keypairs._ecdsaJoseSigToAsn1Sig(signature); |  | ||||||
|       } else { |  | ||||||
|         // jose/jws/jwt
 |  | ||||||
|         return signature; |  | ||||||
|       } |  | ||||||
|     }); |  | ||||||
|   }); |  | ||||||
| }; |  | ||||||
| Keypairs._getBits = function (opts) { |  | ||||||
|   if (opts.alg) { return opts.alg.replace(/[a-z\-]/ig, ''); } |  | ||||||
|   // base64 len to byte len
 |  | ||||||
|   var len = Math.floor((opts.jwk.n||'').length * 0.75); |  | ||||||
| 
 |  | ||||||
|   // TODO this may be a bug
 |  | ||||||
|   // need to confirm that the padding is no more or less than 1 byte
 |  | ||||||
|   if (/521/.test(opts.jwk.crv) || len >= 511) { |  | ||||||
|     return '512'; |  | ||||||
|   } else if (/384/.test(opts.jwk.crv) || len >= 383) { |  | ||||||
|     return '384'; |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   return '256'; |  | ||||||
| }; |  | ||||||
| Keypairs._getName = function (opts) { |  | ||||||
|   if (/EC/i.test(opts.jwk.kty)) { |  | ||||||
|     return 'ECDSA'; |  | ||||||
|   } else { |  | ||||||
|     return 'RSASSA-PKCS1-v1_5'; |  | ||||||
|   } |  | ||||||
| }; |  | ||||||
| Keypairs._import = function (opts) { |  | ||||||
|   return Promise.resolve().then(function () { |  | ||||||
|     var ops; |  | ||||||
|     // all private keys just happen to have a 'd'
 |  | ||||||
|     if (opts.jwk.d) { |  | ||||||
|       ops = [ 'sign' ]; |  | ||||||
|     } else { |  | ||||||
|       ops = [ 'verify' ]; |  | ||||||
|     } |  | ||||||
|     // gotta mark it as extractable, as if it matters
 |  | ||||||
|     opts.jwk.ext = true; |  | ||||||
|     opts.jwk.key_ops = ops; |  | ||||||
| 
 |  | ||||||
|     return window.crypto.subtle.importKey( |  | ||||||
|       "jwk" |  | ||||||
|     , opts.jwk |  | ||||||
|     , { name: Keypairs._getName(opts) |  | ||||||
|       , namedCurve: opts.jwk.crv |  | ||||||
|       , hash: { name: 'SHA-' + Keypairs._getBits(opts) } } |  | ||||||
|     , true |  | ||||||
|     , ops |  | ||||||
|     ).then(function (privkey) { |  | ||||||
|       delete opts.jwk.ext; |  | ||||||
|       return privkey; |  | ||||||
|     }); |  | ||||||
|   }); |  | ||||||
| }; |  | ||||||
| // ECDSA JOSE / JWS / JWT signatures differ from "normal" ASN1/X509 ECDSA signatures
 |  | ||||||
| // https://tools.ietf.org/html/rfc7518#section-3.4
 |  | ||||||
| Keypairs._ecdsaJoseSigToAsn1Sig = function (bufsig) { |  | ||||||
|   // it's easier to do the manipulation in the browser with an array
 |  | ||||||
|   bufsig = Array.from(bufsig); |  | ||||||
|   var hlen = bufsig.length / 2; // should be even
 |  | ||||||
|   var r = bufsig.slice(0, hlen); |  | ||||||
|   var s = bufsig.slice(hlen); |  | ||||||
|   // unpad positive ints less than 32 bytes wide
 |  | ||||||
|   while (!r[0]) { r = r.slice(1); } |  | ||||||
|   while (!s[0]) { s = s.slice(1); } |  | ||||||
|   // pad (or re-pad) ambiguously non-negative BigInts, up to 33 bytes wide
 |  | ||||||
|   if (0x80 & r[0]) { r.unshift(0); } |  | ||||||
|   if (0x80 & s[0]) { s.unshift(0); } |  | ||||||
| 
 |  | ||||||
|   var len = 2 + r.length + 2 + s.length; |  | ||||||
|   var head = [0x30]; |  | ||||||
|   // hard code 0x80 + 1 because it won't be longer than
 |  | ||||||
|   // two SHA512 plus two pad bytes (130 bytes <= 256)
 |  | ||||||
|   if (len >= 0x80) { head.push(0x81); } |  | ||||||
|   head.push(len); |  | ||||||
| 
 |  | ||||||
|   return Uint8Array.from(head.concat([0x02, r.length], r, [0x02, s.length], s)); |  | ||||||
| }; |  | ||||||
| 
 |  | ||||||
| function setTime(time) { | function setTime(time) { | ||||||
|   if ('number' === typeof time) { return time; } |   if ('number' === typeof time) { return time; } | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -5,8 +5,6 @@ | |||||||
| var RSA = exports.Rasha = {}; | var RSA = exports.Rasha = {}; | ||||||
| var x509 = exports.x509; | var x509 = exports.x509; | ||||||
| if ('undefined' !== typeof module) { module.exports = RSA; } | if ('undefined' !== typeof module) { module.exports = RSA; } | ||||||
| var PEM = exports.PEM; |  | ||||||
| var SSH = exports.SSH; |  | ||||||
| var Enc = {}; | var Enc = {}; | ||||||
| var textEncoder = new TextEncoder(); | var textEncoder = new TextEncoder(); | ||||||
| 
 | 
 | ||||||
| @ -110,7 +108,6 @@ RSA.thumbprint = function (opts) { | |||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| RSA.export = function (opts) { | RSA.export = function (opts) { | ||||||
|   return Promise.resolve().then(function () { |  | ||||||
|   if (!opts || !opts.jwk || 'object' !== typeof opts.jwk) { |   if (!opts || !opts.jwk || 'object' !== typeof opts.jwk) { | ||||||
|     throw new Error("must pass { jwk: jwk }"); |     throw new Error("must pass { jwk: jwk }"); | ||||||
|   } |   } | ||||||
| @ -118,7 +115,7 @@ RSA.export = function (opts) { | |||||||
|   var format = opts.format; |   var format = opts.format; | ||||||
|   var pub = opts.public; |   var pub = opts.public; | ||||||
|   if (pub || -1 !== [ 'spki', 'pkix', 'ssh', 'rfc4716' ].indexOf(format)) { |   if (pub || -1 !== [ 'spki', 'pkix', 'ssh', 'rfc4716' ].indexOf(format)) { | ||||||
|       jwk = RSA.neuter({ jwk: jwk }); |     jwk = RSA.nueter(jwk); | ||||||
|   } |   } | ||||||
|   if ('RSA' !== jwk.kty) { |   if ('RSA' !== jwk.kty) { | ||||||
|     throw new Error("options.jwk.kty must be 'RSA' for RSA keys"); |     throw new Error("options.jwk.kty must be 'RSA' for RSA keys"); | ||||||
| @ -160,7 +157,6 @@ RSA.export = function (opts) { | |||||||
|   } else { |   } else { | ||||||
|     throw new Error("Sanity Error: reached unreachable code block with format: " + format); |     throw new Error("Sanity Error: reached unreachable code block with format: " + format); | ||||||
|   } |   } | ||||||
|   }); |  | ||||||
| }; | }; | ||||||
| RSA.pack = function (opts) { | RSA.pack = function (opts) { | ||||||
|   // wrapped in a promise for API compatibility
 |   // wrapped in a promise for API compatibility
 | ||||||
|  | |||||||
							
								
								
									
										47
									
								
								lib/x509.js
									
									
									
									
									
								
							
							
						
						
									
										47
									
								
								lib/x509.js
									
									
									
									
									
								
							| @ -1,6 +1,6 @@ | |||||||
|  | 'use strict'; | ||||||
| (function (exports) { | (function (exports) { | ||||||
|   'use strict'; |   'use strict'; | ||||||
| 
 |  | ||||||
|   var x509 = exports.x509 = {}; |   var x509 = exports.x509 = {}; | ||||||
|   var ASN1 = exports.ASN1; |   var ASN1 = exports.ASN1; | ||||||
|   var Enc = exports.Enc; |   var Enc = exports.Enc; | ||||||
| @ -162,7 +162,7 @@ | |||||||
|    * @param {*} jwk  |    * @param {*} jwk  | ||||||
|    */ |    */ | ||||||
|   x509.packPkcs8 = function (jwk) { |   x509.packPkcs8 = function (jwk) { | ||||||
|     if ('RSA' === jwk.kty) { |     if (jwk.kty == 'RSA') { | ||||||
|       if (!jwk.d) { |       if (!jwk.d) { | ||||||
|         // Public RSA
 |         // Public RSA
 | ||||||
|         return Enc.hexToBuf(ASN1('30' |         return Enc.hexToBuf(ASN1('30' | ||||||
| @ -219,49 +219,6 @@ | |||||||
|     ); |     ); | ||||||
|   }; |   }; | ||||||
|   x509.packSpki = function (jwk) { |   x509.packSpki = function (jwk) { | ||||||
|     if (/EC/i.test(jwk.kty)) { |  | ||||||
|       return x509.packSpkiEc(jwk); |  | ||||||
|     } |  | ||||||
|     return x509.packSpkiRsa(jwk); |  | ||||||
|   }; |  | ||||||
|   x509.packSpkiRsa = function (jwk) { |  | ||||||
|   if (!jwk.d) { |  | ||||||
|     // Public RSA
 |  | ||||||
|     return Enc.hexToBuf(ASN1('30' |  | ||||||
|       , ASN1('30' |  | ||||||
|         , ASN1('06', '2a864886f70d010101') |  | ||||||
|         , ASN1('05') |  | ||||||
|       ) |  | ||||||
|       , ASN1.BitStr(ASN1('30' |  | ||||||
|         , ASN1.UInt(Enc.base64ToHex(jwk.n)) |  | ||||||
|         , ASN1.UInt(Enc.base64ToHex(jwk.e)) |  | ||||||
|       )) |  | ||||||
|     )); |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   // Private RSA
 |  | ||||||
|   return Enc.hexToBuf(ASN1('30' |  | ||||||
|     , ASN1.UInt('00') |  | ||||||
|     , ASN1('30' |  | ||||||
|       , ASN1('06', '2a864886f70d010101') |  | ||||||
|       , ASN1('05') |  | ||||||
|     ) |  | ||||||
|     , ASN1('04' |  | ||||||
|       , ASN1('30' |  | ||||||
|         , ASN1.UInt('00') |  | ||||||
|         , ASN1.UInt(Enc.base64ToHex(jwk.n)) |  | ||||||
|         , ASN1.UInt(Enc.base64ToHex(jwk.e)) |  | ||||||
|         , ASN1.UInt(Enc.base64ToHex(jwk.d)) |  | ||||||
|         , ASN1.UInt(Enc.base64ToHex(jwk.p)) |  | ||||||
|         , ASN1.UInt(Enc.base64ToHex(jwk.q)) |  | ||||||
|         , ASN1.UInt(Enc.base64ToHex(jwk.dp)) |  | ||||||
|         , ASN1.UInt(Enc.base64ToHex(jwk.dq)) |  | ||||||
|         , ASN1.UInt(Enc.base64ToHex(jwk.qi)) |  | ||||||
|       ) |  | ||||||
|     ) |  | ||||||
|   )); |  | ||||||
| }; |  | ||||||
|   x509.packSpkiEc = function (jwk) { |  | ||||||
|     var x = Enc.base64ToHex(jwk.x); |     var x = Enc.base64ToHex(jwk.x); | ||||||
|     var y = Enc.base64ToHex(jwk.y); |     var y = Enc.base64ToHex(jwk.y); | ||||||
|     var objId = ('P-256' === jwk.crv) ? OBJ_ID_EC : OBJ_ID_EC_384; |     var objId = ('P-256' === jwk.crv) ? OBJ_ID_EC : OBJ_ID_EC_384; | ||||||
|  | |||||||
							
								
								
									
										37
									
								
								package.json
									
									
									
									
									
								
							
							
						
						
									
										37
									
								
								package.json
									
									
									
									
									
								
							| @ -1,37 +0,0 @@ | |||||||
| { |  | ||||||
|   "name": "@bluecrypt/keypairs", |  | ||||||
|   "version": "0.1.1", |  | ||||||
|   "description": "Zero-Dependency Native Browser support for ECDSA P-256 and P-384, and RSA 2048/3072/4096 written in VanillaJS", |  | ||||||
|   "homepage": "https://rootprojects.org/keypairs/", |  | ||||||
|   "files": [ |  | ||||||
|     "lib", |  | ||||||
|     "bluecrypt-keypairs.js", |  | ||||||
|     "bluecrypt-keypairs.min.js" |  | ||||||
|   ], |  | ||||||
|   "directories": { |  | ||||||
|     "lib": "lib" |  | ||||||
|   }, |  | ||||||
|   "scripts": { |  | ||||||
|     "test": "node test.js" |  | ||||||
|   }, |  | ||||||
|   "repository": { |  | ||||||
|     "type": "git", |  | ||||||
|     "url": "https://git.coolaj86.com/coolaj86/bluecrypt-keypairs.js.git" |  | ||||||
|   }, |  | ||||||
|   "keywords": [ |  | ||||||
|     "browser", |  | ||||||
|     "EC", |  | ||||||
|     "RSA", |  | ||||||
|     "ECDSA", |  | ||||||
|     "P-256", |  | ||||||
|     "P-384", |  | ||||||
|     "bluecrypt", |  | ||||||
|     "keypairs", |  | ||||||
|     "greenlock", |  | ||||||
|     "VanillaJS" |  | ||||||
|   ], |  | ||||||
|   "author": "AJ ONeal <coolaj86@gmail.com> (https://coolaj86.com/)", |  | ||||||
|   "license": "MPL-2.0", |  | ||||||
|   "devDependencies": { |  | ||||||
|   } |  | ||||||
| } |  | ||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user