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. | ||||
| 
 | ||||
| # Features (port in-progress) | ||||
| 
 | ||||
|   * [x] Keypair generation and encoding | ||||
|     * [x] RSA | ||||
|     * [x] ECDSA (P-256, P-384) | ||||
|     * [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. | ||||
| * Keypairs | ||||
|   * Eckles (ECDSA) | ||||
|   * Rasha (RSA) | ||||
|   * X509 | ||||
|   * ASN1 | ||||
|  | ||||
							
								
								
									
										138
									
								
								app.js
									
									
									
									
									
								
							
							
						
						
									
										138
									
								
								app.js
									
									
									
									
									
								
							| @ -1,14 +1,7 @@ | ||||
| /*global Promise*/ | ||||
| (function () { | ||||
|   'use strict'; | ||||
| 
 | ||||
|   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) { | ||||
|     return document.querySelector(sel); | ||||
| @ -42,12 +35,9 @@ | ||||
|       $('.js-loading').hidden = false; | ||||
|       $('.js-jwk').hidden = true; | ||||
|       $('.js-toc-der-public').hidden = true; | ||||
|       $('.js-toc-pem-public').hidden = true; | ||||
|       $('.js-toc-der-private').hidden = true; | ||||
|       $('.js-toc-jwk').hidden = true; | ||||
| 
 | ||||
|       $$('.js-toc-pem').forEach(function ($el) { | ||||
|         $el.hidden = true; | ||||
|       }); | ||||
|       $('.js-toc-pem-private').hidden = true; | ||||
|       $$('input').map(function ($el) { $el.disabled = true; }); | ||||
|       $$('button').map(function ($el) { $el.disabled = true; }); | ||||
|       var opts = { | ||||
| @ -55,51 +45,34 @@ | ||||
|         , namedCurve: $('input[name="ec-crv"]:checked').value | ||||
|         , modulusLength: $('input[name="rsa-len"]:checked').value | ||||
|       }; | ||||
|       var then = Date.now(); | ||||
|       console.log('opts', opts); | ||||
|       Keypairs.generate(opts).then(function (results) { | ||||
|         console.log("Key generation time:", (Date.now() - then) + "ms"); | ||||
|         var pubDer; | ||||
|         var privDer; | ||||
|         if (/EC/i.test(opts.kty)) { | ||||
|           privDer = x509.packPkcs8(results.private); | ||||
|           pubDer = x509.packSpki(results.public); | ||||
|           Eckles.export({ jwk: results.private, format: 'sec1' }).then(function (pem) { | ||||
|             $('.js-input-pem-sec1-private').innerText = pem; | ||||
|             $('.js-toc-pem-sec1-private').hidden = false; | ||||
|           }); | ||||
|           Eckles.export({ jwk: results.private, format: 'pkcs8' }).then(function (pem) { | ||||
|             $('.js-input-pem-pkcs8-private').innerText = pem; | ||||
|             $('.js-toc-pem-pkcs8-private').hidden = false; | ||||
|           }); | ||||
|           Eckles.export({ jwk: results.public, public: true }).then(function (pem) { | ||||
|             $('.js-input-pem-spki-public').innerText = pem; | ||||
|             $('.js-toc-pem-spki-public').hidden = false; | ||||
|           }); | ||||
|         var der_public, der_private; | ||||
|         if (opts.kty == 'EC') { | ||||
|           der_public = x509.packSpki(results.public); | ||||
|           der_private = x509.packPkcs8(results.private); | ||||
|           var pem_private = Eckles.export({ jwk: results.private }) | ||||
|           var pem_public = Eckles.export({ jwk: results.public, public: true }) | ||||
|           $('.js-input-pem-public').innerText = pem_public; | ||||
|           $('.js-toc-pem-public').hidden = false; | ||||
|           $('.js-input-pem-private').innerText = pem_private; | ||||
|           $('.js-toc-pem-private').hidden = false; | ||||
|         } else { | ||||
|           privDer = x509.packPkcs8(results.private); | ||||
|           pubDer = x509.packSpki(results.public); | ||||
|           Rasha.export({ jwk: results.private, format: 'pkcs1' }).then(function (pem) { | ||||
|             $('.js-input-pem-pkcs1-private').innerText = pem; | ||||
|             $('.js-toc-pem-pkcs1-private').hidden = false; | ||||
|           }); | ||||
|           Rasha.export({ jwk: results.private, format: 'pkcs8' }).then(function (pem) { | ||||
|             $('.js-input-pem-pkcs8-private').innerText = pem; | ||||
|             $('.js-toc-pem-pkcs8-private').hidden = false; | ||||
|           }); | ||||
|           Rasha.export({ jwk: results.public, format: 'pkcs1' }).then(function (pem) { | ||||
|             $('.js-input-pem-pkcs1-public').innerText = pem; | ||||
|             $('.js-toc-pem-pkcs1-public').hidden = false; | ||||
|           }); | ||||
|           Rasha.export({ jwk: results.public, format: 'spki' }).then(function (pem) { | ||||
|             $('.js-input-pem-spki-public').innerText = pem; | ||||
|             $('.js-toc-pem-spki-public').hidden = false; | ||||
|           }); | ||||
|           der_private = x509.packPkcs8(results.private); | ||||
|           der_public = x509.packPkcs8(results.public); | ||||
|           Rasha.pack({ jwk: results.private }).then(function (pem) { | ||||
|             $('.js-input-pem-private').innerText = pem; | ||||
|             $('.js-toc-pem-private').hidden = false; | ||||
|           }) | ||||
|           Rasha.pack({ jwk: results.public }).then(function (pem) { | ||||
|             $('.js-input-pem-public').innerText = pem; | ||||
|             $('.js-toc-pem-public').hidden = false; | ||||
|           }) | ||||
|         } | ||||
| 
 | ||||
|         $('.js-der-public').innerText = pubDer; | ||||
|         $('.js-der-public').innerText = der_public; | ||||
|         $('.js-toc-der-public').hidden = false; | ||||
|         $('.js-der-private').innerText = privDer; | ||||
|         $('.js-der-private').innerText = der_private; | ||||
|         $('.js-toc-der-private').hidden = false; | ||||
|         $('.js-jwk').innerText = JSON.stringify(results, null, 2); | ||||
|         $('.js-loading').hidden = true; | ||||
| @ -107,73 +80,18 @@ | ||||
|         $$('input').map(function ($el) { $el.disabled = false; }); | ||||
|         $$('button').map(function ($el) { $el.disabled = false; }); | ||||
|         $('.js-toc-jwk').hidden = false; | ||||
| 
 | ||||
|       }); | ||||
|     }); | ||||
| 
 | ||||
|     $('form.js-keysign').addEventListener('submit', function (ev) { | ||||
|     $('form.js-acme-account').addEventListener('submit', function (ev) { | ||||
|       ev.preventDefault(); | ||||
|       ev.stopPropagation(); | ||||
|       $('.js-pem-loading').hidden = false; | ||||
|       $('.js-toc-jws').hidden = true; | ||||
|       $('.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-loading').hidden = false; | ||||
|       ACME.accounts.create | ||||
|     }); | ||||
| 
 | ||||
|     $('.js-generate').hidden = false; | ||||
|     $('.js-sign').hidden = false; | ||||
|     $('textarea[name="jwk"]').value = JSON.stringify({ | ||||
|       "crv": "P-256", | ||||
|       "d": "LImWxqqTHbP3LHQfqscDSUzf_uNePGqf9U6ETEcO5Ho", | ||||
|       "kty": "EC", | ||||
|       "x": "vdjQ3T6VBX82LIKDzepYgRsz3HgRwp83yPuonu6vqos", | ||||
|       "y": "IUkEXtAMnppnV1A19sE2bJhUo4WPbq6EYgWxma4oGyg", | ||||
|       "kid": "MnfJYyS9W5gUjrJLdn8ePMzik8ZJz2qc-VZmKOs_oCw" | ||||
|     }) | ||||
|     $('.js-create-account').hidden = false; | ||||
|   } | ||||
| 
 | ||||
|   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> | ||||
| 
 | ||||
| <head> | ||||
|   <head> | ||||
|     <title>BlueCrypt</title> | ||||
|     <style> | ||||
|       textarea { | ||||
| @ -18,17 +17,9 @@ | ||||
|     </style> | ||||
|   </head> | ||||
|   <body> | ||||
|     <h1>@bluecrypt/keypairs: Universal keygen & signing for browsers</h1> | ||||
|     <p>Keypairs.js is <strong>easy-to-use browser crypto in kilobytes, not megabytes.</strong></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> | ||||
|     <h1>BlueCrypt for the Browser</h1> | ||||
|     <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> | ||||
| 
 | ||||
|     <h2>Keypair Generation</h2> | ||||
|     <form class="js-keygen"> | ||||
| @ -43,25 +34,38 @@ | ||||
|       </div> | ||||
|       <div class="js-ec-opts"> | ||||
|         <p>EC Options:</p> | ||||
|         <label for="-crv2"><input type="radio" id="-crv2" | ||||
|          name="ec-crv" value="P-256" checked>P-256</label> | ||||
|         <label for="-crv3"><input type="radio" id="-crv3" | ||||
|          name="ec-crv" value="P-384">P-384</label> | ||||
|         <!-- label for="-crv5"><input type="radio" id="-crv5" | ||||
|          name="ec-crv" value="P-521">P-521</label --> | ||||
|         <input type="radio" id="-crv2" | ||||
|          name="ec-crv" value="P-256" checked> | ||||
|         <label for="-crv2">P-256</label> | ||||
|         <input type="radio" id="-crv3" | ||||
|          name="ec-crv" value="P-384"> | ||||
|         <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 class="js-rsa-opts" hidden> | ||||
|         <p>RSA Options:</p> | ||||
|         <label for="-modlen2"><input type="radio" id="-modlen2" | ||||
|          name="rsa-len" value="2048" checked>2048</label> | ||||
|         <label for="-modlen3"><input type="radio" id="-modlen3" | ||||
|          name="rsa-len" value="3072">3072</label> | ||||
|         <label for="-modlen5"><input type="radio" id="-modlen5" | ||||
|          name="rsa-len" value="4096">4096</label> | ||||
|         <input type="radio" id="-modlen2" | ||||
|          name="rsa-len" value="2048" checked> | ||||
|         <label for="-modlen2">2048</label> | ||||
|         <input type="radio" id="-modlen3" | ||||
|          name="rsa-len" value="3072"> | ||||
|         <label for="-modlen3">3072</label> | ||||
|         <input type="radio" id="-modlen5" | ||||
|          name="rsa-len" value="4096"> | ||||
|         <label for="-modlen5">4096</label> | ||||
|       </div> | ||||
|       <button class="js-generate" hidden>Generate</button> | ||||
|     </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> | ||||
| 
 | ||||
|     <details class="js-toc-jwk" hidden> | ||||
| @ -76,64 +80,29 @@ | ||||
|       <summary>DER Public Binary</summary> | ||||
|       <pre><code class="js-der-public"> </code></pre> | ||||
|     </details> | ||||
|   <details class="js-toc-pem js-toc-pem-pkcs1-private" hidden> | ||||
|     <summary>PEM Private (base64-encoded PKCS1 DER)</summary> | ||||
|     <pre><code  class="js-input-pem-pkcs1-private" ></code></pre> | ||||
|     <details class="js-toc-pem-private" hidden> | ||||
|       <summary>PEM Private (base64-encoded DER)</summary> | ||||
|       <pre><code  class="js-input-pem-private" ></code></pre> | ||||
|     </details> | ||||
|   <details class="js-toc-pem js-toc-pem-sec1-private" hidden> | ||||
|     <summary>PEM Private (base64-encoded SEC1 DER)</summary> | ||||
|     <pre><code  class="js-input-pem-sec1-private" ></code></pre> | ||||
|     <details class="js-toc-pem-public" hidden> | ||||
|       <summary>PEM Public (base64-encoded DER)</summary> | ||||
|       <pre><code  class="js-input-pem-public" ></code></pre> | ||||
|     </details> | ||||
|   <details class="js-toc-pem js-toc-pem-pkcs8-private" hidden> | ||||
|     <summary>PEM Private (base64-encoded PKCS8 DER)</summary> | ||||
|     <pre><code  class="js-input-pem-pkcs8-private" ></code></pre> | ||||
|     <details class="js-toc-acme-account-request" hidden> | ||||
|       <summary>ACME Account Request</summary> | ||||
|       <pre><code class="js-acme-account-request"> </code></pre> | ||||
|     </details> | ||||
|   <details class="js-toc-pem js-toc-pem-pkcs1-public" hidden> | ||||
|     <summary>PEM Public (base64-encoded PKCS1 DER)</summary> | ||||
|     <pre><code  class="js-input-pem-pkcs1-public" ></code></pre> | ||||
|     <details class="js-toc-acme-account-response" hidden> | ||||
|       <summary>ACME Account Response</summary> | ||||
|       <pre><code class="js-acme-account-response"> </code></pre> | ||||
|     </details> | ||||
|   <details class="js-toc-pem js-toc-pem-spki-public" hidden> | ||||
|     <summary>PEM Public (base64-encoded SPKI/PKIX DER)</summary> | ||||
|     <pre><code  class="js-input-pem-spki-public" ></code></pre> | ||||
|   </details> | ||||
| 
 | ||||
|   <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/ecdsa.js"></script> | ||||
|     <script src="./lib/asn1-packer.js"></script> | ||||
|     <script src="./lib/x509.js"></script> | ||||
|     <script src="./lib/ecdsa.js"></script> | ||||
|     <script src="./lib/rsa.js"></script> | ||||
|     <script src="./lib/keypairs.js"></script> | ||||
|     <script src="./lib/acme.js"></script> | ||||
|     <script src="./app.js"></script> | ||||
|   </body> | ||||
| </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) { | ||||
|     return !/-----/.test(line); | ||||
|   }).join(''); | ||||
|   return { bytes: Enc.base64ToBuf(der) }; | ||||
|   return { der: Enc.base64ToBuf(der) }; | ||||
| }; | ||||
| 
 | ||||
| Enc.base64ToBuf = function (b64) { | ||||
|  | ||||
| @ -66,11 +66,8 @@ Enc.numToHex = function (d) { | ||||
| }; | ||||
| 
 | ||||
| Enc.bufToUrlBase64 = function (u8) { | ||||
|   return Enc.base64ToUrlBase64(Enc.bufToBase64(u8)); | ||||
| }; | ||||
| 
 | ||||
| Enc.base64ToUrlBase64 = function (str) { | ||||
|   return str.replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, ''); | ||||
|   return Enc.bufToBase64(u8) | ||||
|     .replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, ''); | ||||
| }; | ||||
| 
 | ||||
| Enc.bufToBase64 = function (u8) { | ||||
| @ -113,8 +110,6 @@ Enc.binToHex = function (bin) { | ||||
|     return h; | ||||
|   }).join(''); | ||||
| }; | ||||
| // TODO are there any nuance differences here?
 | ||||
| Enc.utf8ToHex = Enc.binToHex; | ||||
| 
 | ||||
| Enc.hexToBase64 = function (hex) { | ||||
|   return btoa(Enc.hexToBin(hex)); | ||||
| @ -3,10 +3,7 @@ | ||||
| 'use strict'; | ||||
| 
 | ||||
| var EC = exports.Eckles = {}; | ||||
| var x509 = exports.x509; | ||||
| if ('undefined' !== typeof module) { module.exports = EC; } | ||||
| var PEM = exports.PEM; | ||||
| var SSH = exports.SSH; | ||||
| var Enc = {}; | ||||
| var textEncoder = new TextEncoder(); | ||||
| 
 | ||||
| @ -46,8 +43,6 @@ EC.generate = function (opts) { | ||||
|       "jwk" | ||||
|     , result.privateKey | ||||
|     ).then(function (privJwk) { | ||||
|       privJwk.key_ops = undefined; | ||||
|       privJwk.ext = undefined; | ||||
|       return { | ||||
|         private: privJwk | ||||
|       , public: EC.neuter({ jwk: privJwk }) | ||||
| @ -57,7 +52,6 @@ EC.generate = function (opts) { | ||||
| }; | ||||
| 
 | ||||
| EC.export = function (opts) { | ||||
|   return Promise.resolve().then(function () { | ||||
|   if (!opts || !opts.jwk || 'object' !== typeof opts.jwk) { | ||||
|     throw new Error("must pass { jwk: jwk } as a JSON object"); | ||||
|   } | ||||
| @ -103,7 +97,6 @@ EC.export = function (opts) { | ||||
|   } else { | ||||
|     throw new Error("Sanity Error: reached unreachable code block with format: " + format); | ||||
|   } | ||||
|   }); | ||||
| }; | ||||
| EC.pack = function (opts) { | ||||
|   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. | ||||
|  * 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 */ | ||||
|   var jwk = {}; | ||||
|   Object.keys(opts.jwk).forEach(function (k) { | ||||
| @ -136,12 +128,11 @@ Keypairs.signJws = function (opts) { | ||||
|       if (!opts.jwk) { | ||||
|         throw new Error("opts.jwk must exist and must declare 'typ'"); | ||||
|       } | ||||
|       if (opts.jwk.alg) { return opts.jwk.alg; } | ||||
|       var typ = ('RSA' === opts.jwk.kty) ? "RS" : "ES"; | ||||
|       return typ + Keypairs._getBits(opts); | ||||
|       return ('RSA' === opts.jwk.kty) ? "RS256" : "ES256"; | ||||
|     } | ||||
| 
 | ||||
|     function sign() { | ||||
|     function sign(pem) { | ||||
|       var header = opts.header; | ||||
|       var protect = opts.protected; | ||||
|       var payload = opts.payload; | ||||
| 
 | ||||
| @ -152,9 +143,8 @@ Keypairs.signJws = function (opts) { | ||||
|       if (false !== protect) { | ||||
|         if (!protect) { protect = {}; } | ||||
|         if (!protect.alg) { protect.alg = alg(); } | ||||
|         // There's a particular request where ACME / Let's Encrypt explicitly doesn't use a kid
 | ||||
|         if (false === protect.kid) { protect.kid = undefined; } | ||||
|         else if (!protect.kid) { protect.kid = thumb; } | ||||
|         // There's a particular request where Let's Encrypt explicitly doesn't use a kid
 | ||||
|         if (!protect.kid && false !== protect.kid) { protect.kid = thumb; } | ||||
|         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)
 | ||||
|       if (payload && ('string' !== typeof payload) | ||||
|         && ('undefined' === typeof payload.byteLength) | ||||
|         && ('undefined' === typeof payload.buffer) | ||||
|         && ('undefined' === typeof payload.byteLength) | ||||
|       ) { | ||||
|         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)
 | ||||
|       var nodeAlg = "SHA" + (((protect||header).alg||'').replace(/^[^\d]+/, '')||'256'); | ||||
|       var protected64 = Enc.strToUrlBase64(protectedHeader); | ||||
|       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 signedMsg = { | ||||
|           protected: protected64 | ||||
|       var sig = binsig.toString('base64') | ||||
|         .replace(/\+/g, '-') | ||||
|         .replace(/\//g, '_') | ||||
|         .replace(/=/g, '') | ||||
|       ; | ||||
| 
 | ||||
|       return { | ||||
|         header: header | ||||
|       , protected: protected64 || undefined | ||||
|       , payload: payload64 | ||||
|         , signature: Enc.bufToUrlBase64(buf) | ||||
|       , signature: sig | ||||
|       }; | ||||
| 
 | ||||
|         return signedMsg; | ||||
|       }); | ||||
|     } | ||||
| 
 | ||||
|     if (opts.jwk) { | ||||
|       return sign(); | ||||
|     function convertIfEcdsa(binsig) { | ||||
|       // 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 { | ||||
|       return Keypairs.import({ pem: opts.pem }).then(function (pair) { | ||||
|         opts.jwk = pair.private; | ||||
|         return sign(); | ||||
|       }); | ||||
|       return Keypairs.export({ jwk: opts.jwk }).then(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) { | ||||
|   if ('number' === typeof time) { return time; } | ||||
| 
 | ||||
|  | ||||
| @ -5,8 +5,6 @@ | ||||
| var RSA = exports.Rasha = {}; | ||||
| var x509 = exports.x509; | ||||
| if ('undefined' !== typeof module) { module.exports = RSA; } | ||||
| var PEM = exports.PEM; | ||||
| var SSH = exports.SSH; | ||||
| var Enc = {}; | ||||
| var textEncoder = new TextEncoder(); | ||||
| 
 | ||||
| @ -110,7 +108,6 @@ RSA.thumbprint = function (opts) { | ||||
| }; | ||||
| 
 | ||||
| RSA.export = function (opts) { | ||||
|   return Promise.resolve().then(function () { | ||||
|   if (!opts || !opts.jwk || 'object' !== typeof opts.jwk) { | ||||
|     throw new Error("must pass { jwk: jwk }"); | ||||
|   } | ||||
| @ -118,7 +115,7 @@ RSA.export = function (opts) { | ||||
|   var format = opts.format; | ||||
|   var pub = opts.public; | ||||
|   if (pub || -1 !== [ 'spki', 'pkix', 'ssh', 'rfc4716' ].indexOf(format)) { | ||||
|       jwk = RSA.neuter({ jwk: jwk }); | ||||
|     jwk = RSA.nueter(jwk); | ||||
|   } | ||||
|   if ('RSA' !== jwk.kty) { | ||||
|     throw new Error("options.jwk.kty must be 'RSA' for RSA keys"); | ||||
| @ -160,7 +157,6 @@ RSA.export = function (opts) { | ||||
|   } else { | ||||
|     throw new Error("Sanity Error: reached unreachable code block with format: " + format); | ||||
|   } | ||||
|   }); | ||||
| }; | ||||
| RSA.pack = function (opts) { | ||||
|   // wrapped in a promise for API compatibility
 | ||||
|  | ||||
							
								
								
									
										47
									
								
								lib/x509.js
									
									
									
									
									
								
							
							
						
						
									
										47
									
								
								lib/x509.js
									
									
									
									
									
								
							| @ -1,6 +1,6 @@ | ||||
| 'use strict'; | ||||
| (function (exports) { | ||||
|   'use strict'; | ||||
| 
 | ||||
|   var x509 = exports.x509 = {}; | ||||
|   var ASN1 = exports.ASN1; | ||||
|   var Enc = exports.Enc; | ||||
| @ -162,7 +162,7 @@ | ||||
|    * @param {*} jwk  | ||||
|    */ | ||||
|   x509.packPkcs8 = function (jwk) { | ||||
|     if ('RSA' === jwk.kty) { | ||||
|     if (jwk.kty == 'RSA') { | ||||
|       if (!jwk.d) { | ||||
|         // Public RSA
 | ||||
|         return Enc.hexToBuf(ASN1('30' | ||||
| @ -219,49 +219,6 @@ | ||||
|     ); | ||||
|   }; | ||||
|   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 y = Enc.base64ToHex(jwk.y); | ||||
|     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