WIP Building out all features necessary for Let's Encrypt #6
							
								
								
									
										181
									
								
								app.js
									
									
									
									
									
								
							
							
						
						
									
										181
									
								
								app.js
									
									
									
									
									
								
							| @ -6,7 +6,9 @@ | |||||||
|   var Rasha = window.Rasha; |   var Rasha = window.Rasha; | ||||||
|   var Eckles = window.Eckles; |   var Eckles = window.Eckles; | ||||||
|   var x509 = window.x509; |   var x509 = window.x509; | ||||||
|  |   var CSR = window.CSR; | ||||||
|   var ACME = window.ACME; |   var ACME = window.ACME; | ||||||
|  |   var accountStuff = {}; | ||||||
| 
 | 
 | ||||||
|   function $(sel) { |   function $(sel) { | ||||||
|     return document.querySelector(sel); |     return document.querySelector(sel); | ||||||
| @ -15,6 +17,14 @@ | |||||||
|     return Array.prototype.slice.call(document.querySelectorAll(sel)); |     return Array.prototype.slice.call(document.querySelectorAll(sel)); | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|  |   function checkTos(tos) { | ||||||
|  |     if ($('input[name="tos"]:checked')) { | ||||||
|  |       return tos; | ||||||
|  |     } else { | ||||||
|  |       return ''; | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|   function run() { |   function run() { | ||||||
|     console.log('hello'); |     console.log('hello'); | ||||||
| 
 | 
 | ||||||
| @ -51,8 +61,10 @@ | |||||||
|       , 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 pubDer; |         var pubDer; | ||||||
|         var privDer; |         var privDer; | ||||||
|         if (/EC/i.test(opts.kty)) { |         if (/EC/i.test(opts.kty)) { | ||||||
| @ -101,6 +113,9 @@ | |||||||
|         $$('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; | ||||||
|  | 
 | ||||||
|  |         $('.js-create-account').hidden = false; | ||||||
|  |         $('.js-create-csr').hidden = false; | ||||||
|       }); |       }); | ||||||
|     }); |     }); | ||||||
| 
 | 
 | ||||||
| @ -110,56 +125,25 @@ | |||||||
|       $('.js-loading').hidden = false; |       $('.js-loading').hidden = false; | ||||||
|       var acme = ACME.create({ |       var acme = ACME.create({ | ||||||
|         Keypairs: Keypairs |         Keypairs: Keypairs | ||||||
|  |       , CSR: CSR | ||||||
|       }); |       }); | ||||||
|       acme.init('https://acme-staging-v02.api.letsencrypt.org/directory').then(function (result) { |       acme.init('https://acme-staging-v02.api.letsencrypt.org/directory').then(function (result) { | ||||||
|         console.log('acme result', result); |         console.log('acme result', result); | ||||||
|         var privJwk = JSON.parse($('.js-jwk').innerText).private; |         var privJwk = JSON.parse($('.js-jwk').innerText).private; | ||||||
|         var email = $('.js-email').innerText; |         var email = $('.js-email').value; | ||||||
|         function checkTos(tos) { |  | ||||||
|           console.log("TODO checkbox for agree to terms"); |  | ||||||
|           return tos; |  | ||||||
|         } |  | ||||||
|         return acme.accounts.create({ |         return acme.accounts.create({ | ||||||
|           email: email |           email: email | ||||||
|         , agreeToTerms: checkTos |         , agreeToTerms: checkTos | ||||||
|         , accountKeypair: { privateKeyJwk: privJwk } |         , accountKeypair: { privateKeyJwk: privJwk } | ||||||
|         }).then(function (account) { |         }).then(function (account) { | ||||||
|           console.log("account created result:", account); |           console.log("account created result:", account); | ||||||
|           return Keypairs.generate({ |           accountStuff.account = account; | ||||||
|             kty: 'RSA' |           accountStuff.privateJwk = privJwk; | ||||||
|           , modulusLength: 2048 |           accountStuff.email = email; | ||||||
|           }).then(function (pair) { |           accountStuff.acme = acme; | ||||||
|             console.log('domain keypair:', pair); |           $('.js-create-order').hidden = false; | ||||||
|             var domains = ($('.js-domains').innerText||'example.com').split(/[, ]+/g); |           $('.js-toc-acme-account-response').hidden = false; | ||||||
|             return acme.certificates.create({ |           $('.js-acme-account-response').innerText = JSON.stringify(account, null, 2); | ||||||
|               accountKeypair: { privateKeyJwk: privJwk } |  | ||||||
|             , account: account |  | ||||||
|             , domainKeypair: { privateKeyJwk: pair.private } |  | ||||||
|             , email: email |  | ||||||
|             , domains: domains |  | ||||||
|             , agreeToTerms: checkTos |  | ||||||
|             , challenges: { |  | ||||||
|                 'dns-01': { |  | ||||||
|                   set: function (opts) { |  | ||||||
|                     console.log('dns-01 set challenge:'); |  | ||||||
|                     console.log(JSON.stringify(opts, null, 2)); |  | ||||||
|                     return new Promise(function (resolve) { |  | ||||||
|                       while (!window.confirm("Did you set the challenge?")) {} |  | ||||||
|                       resolve(); |  | ||||||
|                     }); |  | ||||||
|                   } |  | ||||||
|                 , remove: function (opts) { |  | ||||||
|                     console.log('dns-01 remove challenge:'); |  | ||||||
|                     console.log(JSON.stringify(opts, null, 2)); |  | ||||||
|                     return new Promise(function (resolve) { |  | ||||||
|                       while (!window.confirm("Did you delete the challenge?")) {} |  | ||||||
|                       resolve(); |  | ||||||
|                     }); |  | ||||||
|                   } |  | ||||||
|                 } |  | ||||||
|               } |  | ||||||
|             }); |  | ||||||
|           }); |  | ||||||
|         }).catch(function (err) { |         }).catch(function (err) { | ||||||
|           console.error("A bad thing happened:"); |           console.error("A bad thing happened:"); | ||||||
|           console.error(err); |           console.error(err); | ||||||
| @ -168,8 +152,123 @@ | |||||||
|       }); |       }); | ||||||
|     }); |     }); | ||||||
| 
 | 
 | ||||||
|  |     $('form.js-csr').addEventListener('submit', function (ev) { | ||||||
|  |       ev.preventDefault(); | ||||||
|  |       ev.stopPropagation(); | ||||||
|  |       generateCsr(); | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|  |     $('form.js-acme-order').addEventListener('submit', function (ev) { | ||||||
|  |       ev.preventDefault(); | ||||||
|  |       ev.stopPropagation(); | ||||||
|  |       var account = accountStuff.account; | ||||||
|  |       var privJwk = accountStuff.privateJwk; | ||||||
|  |       var email = accountStuff.email; | ||||||
|  |       var acme = accountStuff.acme; | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  |       var domains = ($('.js-domains').value||'example.com').split(/[, ]+/g); | ||||||
|  |       return getDomainPrivkey().then(function (domainPrivJwk) { | ||||||
|  |         console.log('Has CSR already?'); | ||||||
|  |         console.log(accountStuff.csr); | ||||||
|  |         return acme.certificates.create({ | ||||||
|  |           accountKeypair: { privateKeyJwk: privJwk } | ||||||
|  |         , account: account | ||||||
|  |         , domainKeypair: { privateKeyJwk: domainPrivJwk } | ||||||
|  |         , csr: accountStuff.csr | ||||||
|  |         , email: email | ||||||
|  |         , domains: domains | ||||||
|  |         , skipDryRun: $('input[name="skip-dryrun"]:checked') && true | ||||||
|  |         , agreeToTerms: checkTos | ||||||
|  |         , challenges: { | ||||||
|  |             'dns-01': { | ||||||
|  |               set: function (opts) { | ||||||
|  |                 console.info('dns-01 set challenge:'); | ||||||
|  |                 console.info('TXT', opts.dnsHost); | ||||||
|  |                 console.info(opts.dnsAuthorization); | ||||||
|  |                 return new Promise(function (resolve) { | ||||||
|  |                   while (!window.confirm("Did you set the challenge?")) {} | ||||||
|  |                   resolve(); | ||||||
|  |                 }); | ||||||
|  |               } | ||||||
|  |             , remove: function (opts) { | ||||||
|  |                 console.log('dns-01 remove challenge:'); | ||||||
|  |                 console.info('TXT', opts.dnsHost); | ||||||
|  |                 console.info(opts.dnsAuthorization); | ||||||
|  |                 return new Promise(function (resolve) { | ||||||
|  |                   while (!window.confirm("Did you delete the challenge?")) {} | ||||||
|  |                   resolve(); | ||||||
|  |                 }); | ||||||
|  |               } | ||||||
|  |             } | ||||||
|  |           , 'http-01': { | ||||||
|  |               set: function (opts) { | ||||||
|  |                 console.info('http-01 set challenge:'); | ||||||
|  |                 console.info(opts.challengeUrl); | ||||||
|  |                 console.info(opts.keyAuthorization); | ||||||
|  |                 return new Promise(function (resolve) { | ||||||
|  |                   while (!window.confirm("Did you set the challenge?")) {} | ||||||
|  |                   resolve(); | ||||||
|  |                 }); | ||||||
|  |               } | ||||||
|  |             , remove: function (opts) { | ||||||
|  |                 console.log('http-01 remove challenge:'); | ||||||
|  |                 console.info(opts.challengeUrl); | ||||||
|  |                 console.info(opts.keyAuthorization); | ||||||
|  |                 return new Promise(function (resolve) { | ||||||
|  |                   while (!window.confirm("Did you delete the challenge?")) {} | ||||||
|  |                   resolve(); | ||||||
|  |                 }); | ||||||
|  |               } | ||||||
|  |             } | ||||||
|  |           } | ||||||
|  |         , challengeTypes: [$('input[name="acme-challenge-type"]:checked').value] | ||||||
|  |         }).then(function (results) { | ||||||
|  |           console.log('Got Certificates:'); | ||||||
|  |           console.log(results); | ||||||
|  |           $('.js-toc-acme-order-response').hidden = false; | ||||||
|  |           $('.js-acme-order-response').innerText = JSON.stringify(results, null, 2); | ||||||
|  |         }).catch(function (err) { | ||||||
|  |           console.error("challenge failed:"); | ||||||
|  |           console.error(err); | ||||||
|  |           window.alert("failed! " + err.message || JSON.stringify(err)); | ||||||
|  |         }); | ||||||
|  |       }); | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|     $('.js-generate').hidden = false; |     $('.js-generate').hidden = false; | ||||||
|     $('.js-create-account').hidden = false; |   } | ||||||
|  | 
 | ||||||
|  |   function getDomainPrivkey() { | ||||||
|  |     if (accountStuff.domainPrivateJwk) { return Promise.resolve(accountStuff.domainPrivateJwk); } | ||||||
|  |     return Keypairs.generate({ | ||||||
|  |       kty: $('input[name="kty"]:checked').value | ||||||
|  |     , namedCurve: $('input[name="ec-crv"]:checked').value | ||||||
|  |     , modulusLength: $('input[name="rsa-len"]:checked').value | ||||||
|  |     }).then(function (pair) { | ||||||
|  |       console.log('domain keypair:', pair); | ||||||
|  |       accountStuff.domainPrivateJwk = pair.private; | ||||||
|  |       return pair.private; | ||||||
|  |     }); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   function generateCsr() { | ||||||
|  |     var domains = ($('.js-domains').value||'example.com').split(/[, ]+/g); | ||||||
|  |     //var privJwk = JSON.parse($('.js-jwk').innerText).private;
 | ||||||
|  |     return getDomainPrivkey().then(function (privJwk) { | ||||||
|  |       accountStuff.domainPrivateJwk = privJwk; | ||||||
|  |       return CSR({ jwk: privJwk, domains: domains }).then(function (pem) { | ||||||
|  |         // Verify with https://www.sslshopper.com/csr-decoder.html
 | ||||||
|  |         accountStuff.csr = pem; | ||||||
|  |         console.log('Created CSR:'); | ||||||
|  |         console.log(pem); | ||||||
|  | 
 | ||||||
|  |         console.log('CSR info:'); | ||||||
|  |         console.log(CSR._info(pem)); | ||||||
|  | 
 | ||||||
|  |         return pem; | ||||||
|  |       }); | ||||||
|  |     }); | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   window.addEventListener('load', run); |   window.addEventListener('load', run); | ||||||
|  | |||||||
							
								
								
									
										70
									
								
								index.html
									
									
									
									
									
								
							
							
						
						
									
										70
									
								
								index.html
									
									
									
									
									
								
							| @ -34,27 +34,21 @@ | |||||||
|       </div> |       </div> | ||||||
|       <div class="js-ec-opts"> |       <div class="js-ec-opts"> | ||||||
|         <p>EC Options:</p> |         <p>EC Options:</p> | ||||||
|         <input type="radio" id="-crv2" |         <label for="-crv2"><input type="radio" id="-crv2" | ||||||
|          name="ec-crv" value="P-256" checked> |          name="ec-crv" value="P-256" checked>P-256</label> | ||||||
|         <label for="-crv2">P-256</label> |         <label for="-crv3"><input type="radio" id="-crv3" | ||||||
|         <input type="radio" id="-crv3" |          name="ec-crv" value="P-384">P-384</label> | ||||||
|          name="ec-crv" value="P-384"> |         <!-- label for="-crv5"><input type="radio" id="-crv5" | ||||||
|         <label for="-crv3">P-384</label> |          name="ec-crv" value="P-521">P-521</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> | ||||||
|         <input type="radio" id="-modlen2" |         <label for="-modlen2"><input type="radio" id="-modlen2" | ||||||
|          name="rsa-len" value="2048" checked> |          name="rsa-len" value="2048" checked>2048</label> | ||||||
|         <label for="-modlen2">2048</label> |         <label for="-modlen3"><input type="radio" id="-modlen3" | ||||||
|         <input type="radio" id="-modlen3" |          name="rsa-len" value="3072">3072</label> | ||||||
|          name="rsa-len" value="3072"> |         <label for="-modlen5"><input type="radio" id="-modlen5" | ||||||
|         <label for="-modlen3">3072</label> |          name="rsa-len" value="4096">4096</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> | ||||||
| @ -62,14 +56,36 @@ | |||||||
|     <h2>ACME Account</h2> |     <h2>ACME Account</h2> | ||||||
|     <form class="js-acme-account"> |     <form class="js-acme-account"> | ||||||
|       <label for="-acmeEmail">Email:</label> |       <label for="-acmeEmail">Email:</label> | ||||||
|       <input class="js-email" type="email" id="-acmeEmail"> |       <input class="js-email" type="email" id="-acmeEmail" value="john.doe@gmail.com"> | ||||||
|       <br> |       <br> | ||||||
|       <label for="-acmeDomains">Domains:</label> |       <label for="-acmeTos"><input class="js-tos" name="tos" type="checkbox" id="-acmeTos" checked> | ||||||
|       <input class="js-domains" type="text" id="-acmeDomains"> |         Agree to Let's Encrypt Terms of Service</label> | ||||||
|       <br> |       <br> | ||||||
|       <button class="js-create-account" hidden>Create Account</button> |       <button class="js-create-account" hidden>Create Account</button> | ||||||
|     </form> |     </form> | ||||||
| 
 | 
 | ||||||
|  |     <h2>Certificate Signing Request</h2> | ||||||
|  |     <form class="js-csr"> | ||||||
|  |       <label for="-acmeDomains">Domains:</label> | ||||||
|  |       <input class="js-domains" type="text" id="-acmeDomains" value="example.com www.example.com"> | ||||||
|  |       <br> | ||||||
|  |       <button class="js-create-csr" hidden>Create CSR</button> | ||||||
|  |     </form> | ||||||
|  | 
 | ||||||
|  |     <h2>ACME Certificate Order</h2> | ||||||
|  |     <form class="js-acme-order"> | ||||||
|  |       Challenge type: | ||||||
|  |       <label for="-http01"><input type="radio" id="-http01" | ||||||
|  |        name="acme-challenge-type" value="http-01" checked>http-01</label> | ||||||
|  |       <label for="-dns01"><input type="radio" id="-dns01" | ||||||
|  |        name="acme-challenge-type" value="dns-01">dns-01</label> | ||||||
|  |       <br> | ||||||
|  |       <label for="-skipDryrun"><input class="js-skip-dryrun" name="skip-dryrun" | ||||||
|  |         type="checkbox" id="-skipDryrun" checked> Skip dry-run challenge</label> | ||||||
|  |       <br> | ||||||
|  |       <button class="js-create-order" hidden>Create Order</button> | ||||||
|  |     </form> | ||||||
|  | 
 | ||||||
|     <div class="js-loading" hidden>Loading</div> |     <div class="js-loading" hidden>Loading</div> | ||||||
| 
 | 
 | ||||||
|     <details class="js-toc-jwk" hidden> |     <details class="js-toc-jwk" hidden> | ||||||
| @ -104,20 +120,22 @@ | |||||||
|       <summary>PEM Public (base64-encoded SPKI/PKIX DER)</summary> |       <summary>PEM Public (base64-encoded SPKI/PKIX DER)</summary> | ||||||
|       <pre><code  class="js-input-pem-spki-public" ></code></pre> |       <pre><code  class="js-input-pem-spki-public" ></code></pre> | ||||||
|     </details> |     </details> | ||||||
|     <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-acme-account-response" hidden> |     <details class="js-toc-acme-account-response" hidden> | ||||||
|       <summary>ACME Account Response</summary> |       <summary>ACME Account Request</summary> | ||||||
|       <pre><code class="js-acme-account-response"> </code></pre> |       <pre><code class="js-acme-account-response"> </code></pre> | ||||||
|     </details> |     </details> | ||||||
|  |     <details class="js-toc-acme-order-response" hidden> | ||||||
|  |       <summary>ACME Order Response</summary> | ||||||
|  |       <pre><code class="js-acme-order-response"> </code></pre> | ||||||
|  |     </details> | ||||||
|     <script src="./lib/bluecrypt-encoding.js"></script> |     <script src="./lib/bluecrypt-encoding.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/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/asn1-parser.js"></script> | ||||||
|  |     <script src="./lib/csr.js"></script> | ||||||
|     <script src="./lib/acme.js"></script> |     <script src="./lib/acme.js"></script> | ||||||
|     <script src="./app.js"></script> |     <script src="./app.js"></script> | ||||||
|   </body> |   </body> | ||||||
|  | |||||||
							
								
								
									
										202
									
								
								lib/acme.js
									
									
									
									
									
								
							
							
						
						
									
										202
									
								
								lib/acme.js
									
									
									
									
									
								
							| @ -8,6 +8,7 @@ | |||||||
| 
 | 
 | ||||||
| var ACME = exports.ACME = {}; | var ACME = exports.ACME = {}; | ||||||
| //var Keypairs = exports.Keypairs || {};
 | //var Keypairs = exports.Keypairs || {};
 | ||||||
|  | //var CSR = exports.CSR;
 | ||||||
| var Enc = exports.Enc || {}; | var Enc = exports.Enc || {}; | ||||||
| var Crypto = exports.Crypto || {}; | var Crypto = exports.Crypto || {}; | ||||||
| 
 | 
 | ||||||
| @ -29,20 +30,19 @@ ACME.challengePrefixes = { | |||||||
| }; | }; | ||||||
| ACME.challengeTests = { | ACME.challengeTests = { | ||||||
|   'http-01': function (me, auth) { |   'http-01': function (me, auth) { | ||||||
|     var url = 'http://' + auth.hostname + ACME.challengePrefixes['http-01'] + '/' + auth.token; |     return me.http01(auth).then(function (keyAuth) { | ||||||
|     return me.request({ method: 'GET', url: url }).then(function (resp) { |  | ||||||
|       var err; |       var err; | ||||||
| 
 | 
 | ||||||
|       // TODO limit the number of bytes that are allowed to be downloaded
 |       // TODO limit the number of bytes that are allowed to be downloaded
 | ||||||
|       if (auth.keyAuthorization === resp.body.toString('utf8').trim()) { |       if (auth.keyAuthorization === (keyAuth||'').trim()) { | ||||||
|         return true; |         return true; | ||||||
|       } |       } | ||||||
| 
 | 
 | ||||||
|       err = new Error( |       err = new Error( | ||||||
|         "Error: Failed HTTP-01 Pre-Flight / Dry Run.\n" |         "Error: Failed HTTP-01 Pre-Flight / Dry Run.\n" | ||||||
|       + "curl '" + url + "'\n" |       + "curl '" + auth.challengeUrl + "'\n" | ||||||
|       + "Expected: '" + auth.keyAuthorization + "'\n" |       + "Expected: '" + auth.keyAuthorization + "'\n" | ||||||
|       + "Got: '" + resp.body + "'\n" |       + "Got: '" + keyAuth + "'\n" | ||||||
|       + "See https://git.coolaj86.com/coolaj86/acme-v2.js/issues/4" |       + "See https://git.coolaj86.com/coolaj86/acme-v2.js/issues/4" | ||||||
|       ); |       ); | ||||||
|       err.code = 'E_FAIL_DRY_CHALLENGE'; |       err.code = 'E_FAIL_DRY_CHALLENGE'; | ||||||
| @ -51,10 +51,7 @@ ACME.challengeTests = { | |||||||
|   } |   } | ||||||
| , 'dns-01': function (me, auth) { | , 'dns-01': function (me, auth) { | ||||||
|     // remove leading *. on wildcard domains
 |     // remove leading *. on wildcard domains
 | ||||||
|     return me.dig({ |     return me.dns01(auth).then(function (ans) { | ||||||
|       type: 'TXT' |  | ||||||
|     , name: auth.dnsHost |  | ||||||
|     }).then(function (ans) { |  | ||||||
|       var err; |       var err; | ||||||
| 
 | 
 | ||||||
|       if (ans.answer.some(function (txt) { |       if (ans.answer.some(function (txt) { | ||||||
| @ -154,7 +151,7 @@ ACME._registerAccount = function (me, options) { | |||||||
|             , kid: options.externalAccount.id |             , kid: options.externalAccount.id | ||||||
|             , url: me._directoryUrls.newAccount |             , url: me._directoryUrls.newAccount | ||||||
|             } |             } | ||||||
|           , payload: Enc.strToBuf(JSON.stringify(pair.public)) |           , payload: Enc.binToBuf(JSON.stringify(pair.public)) | ||||||
|           }).then(function (jws) { |           }).then(function (jws) { | ||||||
|             body.externalAccountBinding = jws; |             body.externalAccountBinding = jws; | ||||||
|             return body; |             return body; | ||||||
| @ -288,10 +285,6 @@ ACME._testChallengeOptions = function () { | |||||||
|   ]; |   ]; | ||||||
| }; | }; | ||||||
| ACME._testChallenges = function (me, options) { | ACME._testChallenges = function (me, options) { | ||||||
|   if (me.skipChallengeTest) { |  | ||||||
|     return Promise.resolve(); |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   var CHECK_DELAY = 0; |   var CHECK_DELAY = 0; | ||||||
|   return Promise.all(options.domains.map(function (identifierValue) { |   return Promise.all(options.domains.map(function (identifierValue) { | ||||||
|     // TODO we really only need one to pass, not all to pass
 |     // TODO we really only need one to pass, not all to pass
 | ||||||
| @ -311,6 +304,12 @@ ACME._testChallenges = function (me, options) { | |||||||
|           + " You must enable one of ( " + suitable + " )." |           + " You must enable one of ( " + suitable + " )." | ||||||
|       )); |       )); | ||||||
|     } |     } | ||||||
|  | 
 | ||||||
|  |     // TODO remove skipChallengeTest
 | ||||||
|  |     if (me.skipDryRun || me.skipChallengeTest) { | ||||||
|  |       return null; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|     if ('dns-01' === challenge.type) { |     if ('dns-01' === challenge.type) { | ||||||
|       // Give the nameservers a moment to propagate
 |       // Give the nameservers a moment to propagate
 | ||||||
|       CHECK_DELAY = 1.5 * 1000; |       CHECK_DELAY = 1.5 * 1000; | ||||||
| @ -326,17 +325,27 @@ ACME._testChallenges = function (me, options) { | |||||||
|       , expires: new Date(Date.now() + (60 * 1000)).toISOString() |       , expires: new Date(Date.now() + (60 * 1000)).toISOString() | ||||||
|       , wildcard: identifierValue.includes('*.') || undefined |       , wildcard: identifierValue.includes('*.') || undefined | ||||||
|       }; |       }; | ||||||
|  | 
 | ||||||
|  |       // The dry-run comes first in the spirit of "fail fast"
 | ||||||
|  |       // (and protecting against challenge failure rate limits)
 | ||||||
|       var dryrun = true; |       var dryrun = true; | ||||||
|       return ACME._challengeToAuth(me, options, results, challenge, dryrun).then(function (auth) { |       return ACME._challengeToAuth(me, options, results, challenge, dryrun).then(function (auth) { | ||||||
|  |         if (!me._canUse[auth.type]) { return; } | ||||||
|         return ACME._setChallenge(me, options, auth).then(function () { |         return ACME._setChallenge(me, options, auth).then(function () { | ||||||
|           return auth; |           return auth; | ||||||
|         }); |         }); | ||||||
|       }); |       }); | ||||||
|     }); |     }); | ||||||
|   })).then(function (auths) { |   })).then(function (auths) { | ||||||
|  |     auths = auths.filter(Boolean); | ||||||
|  |     if (!auths.length) { /*skip actual test*/ return; } | ||||||
|     return ACME._wait(CHECK_DELAY).then(function () { |     return ACME._wait(CHECK_DELAY).then(function () { | ||||||
|       return Promise.all(auths.map(function (auth) { |       return Promise.all(auths.map(function (auth) { | ||||||
|         return ACME.challengeTests[auth.type](me, auth); |         return ACME.challengeTests[auth.type](me, auth).then(function (result) { | ||||||
|  |           // not a blocker
 | ||||||
|  |           ACME._removeChallenge(me, options, auth); | ||||||
|  |           return result; | ||||||
|  |         }); | ||||||
|       })); |       })); | ||||||
|     }); |     }); | ||||||
|   }); |   }); | ||||||
| @ -390,6 +399,7 @@ ACME._challengeToAuth = function (me, options, request, challenge, dryrun) { | |||||||
|       //   keyAuthorization = token || '.' || base64url(JWK_Thumbprint(accountKey))
 |       //   keyAuthorization = token || '.' || base64url(JWK_Thumbprint(accountKey))
 | ||||||
|       auth.keyAuthorization = challenge.token + '.' + auth.thumbprint; |       auth.keyAuthorization = challenge.token + '.' + auth.thumbprint; | ||||||
|       // conflicts with ACME challenge id url is already in use, so we call this challengeUrl instead
 |       // conflicts with ACME challenge id url is already in use, so we call this challengeUrl instead
 | ||||||
|  |       // TODO auth.http01Url ?
 | ||||||
|       auth.challengeUrl = 'http://' + auth.identifier.value + ACME.challengePrefixes['http-01'] + '/' + auth.token; |       auth.challengeUrl = 'http://' + auth.identifier.value + ACME.challengePrefixes['http-01'] + '/' + auth.token; | ||||||
|       auth.dnsHost = dnsPrefix + '.' + auth.hostname.replace('*.', ''); |       auth.dnsHost = dnsPrefix + '.' + auth.hostname.replace('*.', ''); | ||||||
| 
 | 
 | ||||||
| @ -436,11 +446,11 @@ ACME._postChallenge = function (me, options, auth) { | |||||||
|    */ |    */ | ||||||
|   function deactivate() { |   function deactivate() { | ||||||
|     if (me.debug) { console.debug('[acme-v2.js] deactivate:'); } |     if (me.debug) { console.debug('[acme-v2.js] deactivate:'); } | ||||||
|     return ACME._jwsRequest({ |     return ACME._jwsRequest(me, { | ||||||
|       options: options |       options: options | ||||||
|     , url: auth.url |     , url: auth.url | ||||||
|     , protected: { kid: options._kid } |     , protected: { kid: options._kid } | ||||||
|     , payload: Enc.strToBuf(JSON.stringify({ "status": "deactivated" })) |     , payload: Enc.binToBuf(JSON.stringify({ "status": "deactivated" })) | ||||||
|     }).then(function (resp) { |     }).then(function (resp) { | ||||||
|       if (me.debug) { console.debug('deactivate challenge: resp.body:'); } |       if (me.debug) { console.debug('deactivate challenge: resp.body:'); } | ||||||
|       if (me.debug) { console.debug(resp.body); } |       if (me.debug) { console.debug(resp.body); } | ||||||
| @ -478,18 +488,7 @@ ACME._postChallenge = function (me, options, auth) { | |||||||
|         if (me.debug) { console.debug('poll: valid'); } |         if (me.debug) { console.debug('poll: valid'); } | ||||||
| 
 | 
 | ||||||
|         try { |         try { | ||||||
|           if (1 === options.removeChallenge.length) { |           ACME._removeChallenge(me, options, auth); | ||||||
|             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) {} |         } catch(e) {} | ||||||
|         return resp.body; |         return resp.body; | ||||||
|       } |       } | ||||||
| @ -511,11 +510,11 @@ ACME._postChallenge = function (me, options, auth) { | |||||||
| 
 | 
 | ||||||
|   function respondToChallenge() { |   function respondToChallenge() { | ||||||
|     if (me.debug) { console.debug('[acme-v2.js] responding to accept challenge:'); } |     if (me.debug) { console.debug('[acme-v2.js] responding to accept challenge:'); } | ||||||
|     return ACME._jwsRequest({ |     return ACME._jwsRequest(me, { | ||||||
|       options: options |       options: options | ||||||
|     , url: auth.url |     , url: auth.url | ||||||
|     , protected: { kid: options._kid } |     , protected: { kid: options._kid } | ||||||
|     , payload: Enc.strToBuf(JSON.stringify({})) |     , payload: Enc.binToBuf(JSON.stringify({})) | ||||||
|     }).then(function (resp) { |     }).then(function (resp) { | ||||||
|       if (me.debug) { console.debug('respond to challenge: resp.body:'); } |       if (me.debug) { console.debug('respond to challenge: resp.body:'); } | ||||||
|       if (me.debug) { console.debug(resp.body); } |       if (me.debug) { console.debug(resp.body); } | ||||||
| @ -526,8 +525,6 @@ ACME._postChallenge = function (me, options, auth) { | |||||||
|   return respondToChallenge(); |   return respondToChallenge(); | ||||||
| }; | }; | ||||||
| ACME._setChallenge = function (me, options, auth) { | ACME._setChallenge = function (me, options, auth) { | ||||||
|   console.log('challenge auth:', auth); |  | ||||||
|   console.log('challenges:', options.challenges); |  | ||||||
|   return new Promise(function (resolve, reject) { |   return new Promise(function (resolve, reject) { | ||||||
|     var challengers = options.challenges || {}; |     var challengers = options.challenges || {}; | ||||||
|     var challenger = (challengers[auth.type] && challengers[auth.type].set) || options.setChallenge; |     var challenger = (challengers[auth.type] && challengers[auth.type].set) || options.setChallenge; | ||||||
| @ -572,11 +569,11 @@ ACME._finalizeOrder = function (me, options, validatedDomains) { | |||||||
| 
 | 
 | ||||||
|     function pollCert() { |     function pollCert() { | ||||||
|       if (me.debug) { console.debug('[acme-v2.js] pollCert:'); } |       if (me.debug) { console.debug('[acme-v2.js] pollCert:'); } | ||||||
|       return ACME._jwsRequest({ |       return ACME._jwsRequest(me, { | ||||||
|         options: options |         options: options | ||||||
|       , url: options._finalize |       , url: options._finalize | ||||||
|       , protected: { kid: options._kid } |       , protected: { kid: options._kid } | ||||||
|       , payload: Enc.strToBuf(payload) |       , payload: Enc.binToBuf(payload) | ||||||
|       }).then(function (resp) { |       }).then(function (resp) { | ||||||
|         if (me.debug) { console.debug('order finalized: resp.body:'); } |         if (me.debug) { console.debug('order finalized: resp.body:'); } | ||||||
|         if (me.debug) { console.debug(resp.body); } |         if (me.debug) { console.debug(resp.body); } | ||||||
| @ -674,6 +671,14 @@ ACME._getCertificate = function (me, options) { | |||||||
|     return Promise.reject(new Error("options.challengeTypes (string array) must be specified" |     return Promise.reject(new Error("options.challengeTypes (string array) must be specified" | ||||||
|       + " (and in order of preferential priority).")); |       + " (and in order of preferential priority).")); | ||||||
|   } |   } | ||||||
|  |   if (options.csr) { | ||||||
|  |     // TODO validate csr signature
 | ||||||
|  |     options._csr = me.CSR._info(options.csr); | ||||||
|  |     options.domains = options._csr.altnames; | ||||||
|  |     if (options._csr.subject !== options.domains[0]) { | ||||||
|  |       return Promise.reject(new Error("certificate subject (commonName) does not match first altname (SAN)")); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|   if (!(options.domains && options.domains.length)) { |   if (!(options.domains && options.domains.length)) { | ||||||
|     return Promise.reject(new Error("options.domains must be a list of string domain names," |     return Promise.reject(new Error("options.domains must be a list of string domain names," | ||||||
|     + " with the first being the subject of the certificate (or options.subject must specified).")); |     + " with the first being the subject of the certificate (or options.subject must specified).")); | ||||||
| @ -713,14 +718,15 @@ ACME._getCertificate = function (me, options) { | |||||||
| 
 | 
 | ||||||
|     var payload = JSON.stringify(body); |     var payload = JSON.stringify(body); | ||||||
|     if (me.debug) { console.debug('\n[DEBUG] newOrder\n'); } |     if (me.debug) { console.debug('\n[DEBUG] newOrder\n'); } | ||||||
|     return ACME._jwsRequest({ |     return ACME._jwsRequest(me, { | ||||||
|       options: options |       options: options | ||||||
|     , url: me._directoryUrls.newOrder |     , url: me._directoryUrls.newOrder | ||||||
|     , protected: { kid: options._kid } |     , protected: { kid: options._kid } | ||||||
|     , payload: Enc.strToBuf(payload) |     , payload: Enc.binToBuf(payload) | ||||||
|     }).then(function (resp) { |     }).then(function (resp) { | ||||||
|       var location = resp.headers.location; |       var location = resp.headers.location; | ||||||
|       var setAuths; |       var setAuths; | ||||||
|  |       var validAuths = []; | ||||||
|       var auths = []; |       var auths = []; | ||||||
|       if (me.debug) { console.debug('[ordered]', location); } // the account id url
 |       if (me.debug) { console.debug('[ordered]', location); } // the account id url
 | ||||||
|       if (me.debug) { console.debug(resp); } |       if (me.debug) { console.debug(resp); } | ||||||
| @ -765,16 +771,32 @@ ACME._getCertificate = function (me, options) { | |||||||
|         }); |         }); | ||||||
|       } |       } | ||||||
| 
 | 
 | ||||||
|       function challengeNext() { |       function checkNext() { | ||||||
|         var auth = auths.shift(); |         var auth = auths.shift(); | ||||||
|         if (!auth) { return; } |         if (!auth) { return; } | ||||||
|  | 
 | ||||||
|  |         if (!me._canUse[auth.type] || me.skipChallengeTest) { | ||||||
|  |           // not so much "valid" as "not invalid"
 | ||||||
|  |           // but in this case we can't confirm either way
 | ||||||
|  |           validAuths.push(auth); | ||||||
|  |           return Promise.resolve(); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         return ACME.challengeTests[auth.type](me, auth).then(function () { | ||||||
|  |           validAuths.push(auth); | ||||||
|  |         }).then(checkNext); | ||||||
|  |       } | ||||||
|  | 
 | ||||||
|  |       function challengeNext() { | ||||||
|  |         var auth = validAuths.shift(); | ||||||
|  |         if (!auth) { return; } | ||||||
|         return ACME._postChallenge(me, options, auth).then(challengeNext); |         return ACME._postChallenge(me, options, auth).then(challengeNext); | ||||||
|       } |       } | ||||||
| 
 | 
 | ||||||
|       // First we set every challenge
 |       // First we set every challenge
 | ||||||
|       // Then we ask for each challenge to be checked
 |       // Then we ask for each challenge to be checked
 | ||||||
|       // Doing otherwise would potentially cause us to poison our own DNS cache with misses
 |       // Doing otherwise would potentially cause us to poison our own DNS cache with misses
 | ||||||
|       return setNext().then(challengeNext).then(function () { |       return setNext().then(checkNext).then(challengeNext).then(function () { | ||||||
|         if (me.debug) { console.debug("[getCertificate] next.then"); } |         if (me.debug) { console.debug("[getCertificate] next.then"); } | ||||||
|         var validatedDomains = body.identifiers.map(function (ident) { |         var validatedDomains = body.identifiers.map(function (ident) { | ||||||
|           return ident.value; |           return ident.value; | ||||||
| @ -805,8 +827,19 @@ ACME._getCertificate = function (me, options) { | |||||||
|   }); |   }); | ||||||
| }; | }; | ||||||
| ACME._generateCsrWeb64 = function (me, options, validatedDomains) { | ACME._generateCsrWeb64 = function (me, options, validatedDomains) { | ||||||
|   return ACME._importKeypair(me, options.domainKeypair).then(function (/*pair*/) { |   var csr; | ||||||
|     return me.Keypairs.generateCsr(options.domainKeypair, validatedDomains).then(function (der) { |   if (options.csr) { | ||||||
|  |     csr = options.csr; | ||||||
|  |     // if der, convert to base64
 | ||||||
|  |     if ('string' !== typeof csr) { csr = Enc.bufToUrlBase64(csr); } | ||||||
|  |     // nix PEM headers, if any
 | ||||||
|  |     if ('-' === csr[0]) { csr = csr.split(/\n+/).slice(1, -1).join(''); } | ||||||
|  |     csr = Enc.base64ToUrlBase64(csr.trim().replace(/\s+/g, '')); | ||||||
|  |     return Promise.resolve(csr); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   return ACME._importKeypair(me, options.domainKeypair).then(function (pair) { | ||||||
|  |     return me.CSR({ jwk: pair.private, domains: validatedDomains, encoding: 'der' }).then(function (der) { | ||||||
|       return Enc.bufToUrlBase64(der); |       return Enc.bufToUrlBase64(der); | ||||||
|     }); |     }); | ||||||
|   }); |   }); | ||||||
| @ -816,23 +849,25 @@ ACME.create = function create(me) { | |||||||
|   if (!me) { me = {}; } |   if (!me) { me = {}; } | ||||||
|   // me.debug = true;
 |   // me.debug = true;
 | ||||||
|   me.challengePrefixes = ACME.challengePrefixes; |   me.challengePrefixes = ACME.challengePrefixes; | ||||||
|   me.Keypairs = me.Keypairs || me.RSA || require('rsa-compat').RSA; |   me.Keypairs = me.Keypairs || exports.Keypairs || require('keypairs').Keypairs; | ||||||
|  |   me.CSR = me.CSR || exports.cSR || require('CSR').CSR; | ||||||
|   me._nonces = []; |   me._nonces = []; | ||||||
|  |   me._canUse = {}; | ||||||
|  |   if (!me._baseUrl) { | ||||||
|  |     me._baseUrl = ""; | ||||||
|  |   } | ||||||
|   //me.Keypairs = me.Keypairs || require('keypairs');
 |   //me.Keypairs = me.Keypairs || require('keypairs');
 | ||||||
|   //me.request = me.request || require('@root/request');
 |   //me.request = me.request || require('@root/request');
 | ||||||
|   if (!me.dig) { |   if (!me.dns01) { | ||||||
|     me.dig = function (query) { |     me.dns01 = function (auth) { | ||||||
|       // TODO use digd.js
 |       return ACME._dns01(me, auth); | ||||||
|       return new me.request({ url: "/api/dns/" + query.name + "?type=" + query.type }).then(function (resp) { |  | ||||||
|         if (!resp.body || !Array.isArray(resp.body.answer)) { |  | ||||||
|           throw new Error("failed to get DNS response"); |  | ||||||
|         } |  | ||||||
|         return { |  | ||||||
|           answer: resp.body.answer.map(function (ans) { |  | ||||||
|             return { data: ans.data, ttl: ans.ttl }; |  | ||||||
|           }) |  | ||||||
|     }; |     }; | ||||||
|       }); |   } | ||||||
|  |   // backwards compat
 | ||||||
|  |   if (!me.dig) { me.dig = me.dns01; } | ||||||
|  |   if (!me.http01) { | ||||||
|  |     me.http01 = function (auth) { | ||||||
|  |       return ACME._http01(me, auth); | ||||||
|     }; |     }; | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
| @ -853,9 +888,22 @@ ACME.create = function create(me) { | |||||||
|     if ('string' !== typeof me.directoryUrl) { |     if ('string' !== typeof me.directoryUrl) { | ||||||
|       throw new Error("you must supply either the ACME directory url as a string or an object of the ACME urls"); |       throw new Error("you must supply either the ACME directory url as a string or an object of the ACME urls"); | ||||||
|     } |     } | ||||||
|  |     var p = Promise.resolve(); | ||||||
|  |     if (!me.skipChallengeTest) { | ||||||
|  |       p = me.request({ url: me._baseUrl + "/api/_acme_api_/" }).then(function (resp) { | ||||||
|  |         if (resp.body.success) { | ||||||
|  |           me._canCheck['http-01'] = true; | ||||||
|  |           me._canCheck['dns-01'] = true; | ||||||
|  |         } | ||||||
|  |       }).catch(function () { | ||||||
|  |         // ignore
 | ||||||
|  |       }); | ||||||
|  |     } | ||||||
|  |     return p.then(function () { | ||||||
|       return ACME._directory(me).then(function (resp) { |       return ACME._directory(me).then(function (resp) { | ||||||
|         return fin(resp.body); |         return fin(resp.body); | ||||||
|       }); |       }); | ||||||
|  |     }); | ||||||
|   }; |   }; | ||||||
|   me.accounts = { |   me.accounts = { | ||||||
|     create: function (options) { |     create: function (options) { | ||||||
| @ -876,6 +924,10 @@ ACME._jwsRequest = function (me, bigopts) { | |||||||
|     bigopts.protected.nonce = nonce; |     bigopts.protected.nonce = nonce; | ||||||
|     bigopts.protected.url = bigopts.url; |     bigopts.protected.url = bigopts.url; | ||||||
|     // protected.alg: added by Keypairs.signJws
 |     // protected.alg: added by Keypairs.signJws
 | ||||||
|  |     if (!bigopts.protected.jwk) { | ||||||
|  |       // protected.kid must be overwritten due to ACME's interpretation of the spec
 | ||||||
|  |       if (!bigopts.protected.kid) { bigopts.protected.kid = bigopts.options._kid; } | ||||||
|  |     } | ||||||
|     return me.Keypairs.signJws( |     return me.Keypairs.signJws( | ||||||
|       { jwk: bigopts.options.accountKeypair.privateKeyJwk |       { jwk: bigopts.options.accountKeypair.privateKeyJwk | ||||||
|       , protected: bigopts.protected |       , protected: bigopts.protected | ||||||
| @ -992,6 +1044,48 @@ ACME._prnd = function (n) { | |||||||
| ACME._toHex = function (pair) { | ACME._toHex = function (pair) { | ||||||
|   return parseInt(pair, 10).toString(16); |   return parseInt(pair, 10).toString(16); | ||||||
| }; | }; | ||||||
|  | ACME._dns01 = function (me, auth) { | ||||||
|  |   return new me.request({ url: me._baseUrl + "/api/dns/" + auth.dnsHost + "?type=TXT" }).then(function (resp) { | ||||||
|  |     var err; | ||||||
|  |     if (!resp.body || !Array.isArray(resp.body.answer)) { | ||||||
|  |       err = new Error("failed to get DNS response"); | ||||||
|  |       console.error(err); | ||||||
|  |       throw err; | ||||||
|  |     } | ||||||
|  |     if (!resp.body.answer.length) { | ||||||
|  |       err = new Error("failed to get DNS answer record in response"); | ||||||
|  |       console.error(err); | ||||||
|  |       throw err; | ||||||
|  |     } | ||||||
|  |     return { | ||||||
|  |       answer: resp.body.answer.map(function (ans) { | ||||||
|  |         return { data: ans.data, ttl: ans.ttl }; | ||||||
|  |       }) | ||||||
|  |     }; | ||||||
|  |   }); | ||||||
|  | }; | ||||||
|  | ACME._http01 = function (me, auth) { | ||||||
|  |   var url = encodeURIComponent(auth.challengeUrl); | ||||||
|  |   return new me.request({ url: me._baseUrl + "/api/http?url=" + url }).then(function (resp) { | ||||||
|  |     return resp.body; | ||||||
|  |   }); | ||||||
|  | }; | ||||||
|  | ACME._removeChallenge = function (me, options, auth) { | ||||||
|  |   var challengers = options.challenges || {}; | ||||||
|  |   var removeChallenge = (challengers[auth.type] && challengers[auth.type].remove) || options.removeChallenge; | ||||||
|  |   if (1 === removeChallenge.length) { | ||||||
|  |     removeChallenge(auth).then(function () {}, function () {}); | ||||||
|  |   } else if (2 === removeChallenge.length) { | ||||||
|  |     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; | ||||||
|  |     } | ||||||
|  |     removeChallenge(auth.request.identifier, auth.token, function () {}); | ||||||
|  |   } | ||||||
|  | }; | ||||||
| 
 | 
 | ||||||
| Enc.bufToUrlBase64 = function (u8) { | Enc.bufToUrlBase64 = function (u8) { | ||||||
|   return Enc.bufToBase64(u8) |   return Enc.bufToBase64(u8) | ||||||
|  | |||||||
| @ -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 { der: Enc.base64ToBuf(der) }; |   return { bytes: Enc.base64ToBuf(der) }; | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| Enc.base64ToBuf = function (b64) { | Enc.base64ToBuf = function (b64) { | ||||||
|  | |||||||
| @ -66,8 +66,11 @@ Enc.numToHex = function (d) { | |||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| Enc.bufToUrlBase64 = function (u8) { | Enc.bufToUrlBase64 = function (u8) { | ||||||
|   return Enc.bufToBase64(u8) |   return Enc.base64ToUrlBase64(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) { | ||||||
| @ -110,6 +113,8 @@ 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)); | ||||||
|  | |||||||
| @ -1,699 +0,0 @@ | |||||||
| /*global CSR*/ |  | ||||||
| // CSR takes a while to load after the page load
 |  | ||||||
| (function (exports) { |  | ||||||
| 'use strict'; |  | ||||||
| 
 |  | ||||||
| var BACME = exports.ACME = {}; |  | ||||||
| var webFetch = exports.fetch; |  | ||||||
| var Keypairs = exports.Keypairs; |  | ||||||
| var Promise = exports.Promise; |  | ||||||
| 
 |  | ||||||
| var directoryUrl = 'https://acme-staging-v02.api.letsencrypt.org/directory'; |  | ||||||
| var directory; |  | ||||||
| 
 |  | ||||||
| var nonceUrl; |  | ||||||
| var nonce; |  | ||||||
| 
 |  | ||||||
| var accountKeypair; |  | ||||||
| var accountJwk; |  | ||||||
| 
 |  | ||||||
| var accountUrl; |  | ||||||
| 
 |  | ||||||
| BACME.challengePrefixes = { |  | ||||||
|   'http-01': '/.well-known/acme-challenge' |  | ||||||
| , 'dns-01': '_acme-challenge' |  | ||||||
| }; |  | ||||||
| 
 |  | ||||||
| BACME._logHeaders = function (resp) { |  | ||||||
|   console.log('Headers:'); |  | ||||||
|   Array.from(resp.headers.entries()).forEach(function (h) { console.log(h[0] + ': ' + h[1]); }); |  | ||||||
| }; |  | ||||||
| 
 |  | ||||||
| BACME._logBody = function (body) { |  | ||||||
|   console.log('Body:'); |  | ||||||
|   console.log(JSON.stringify(body, null, 2)); |  | ||||||
|   console.log(''); |  | ||||||
| }; |  | ||||||
| 
 |  | ||||||
| BACME.directory = function (opts) { |  | ||||||
|   return webFetch(opts.directoryUrl || directoryUrl, { mode: 'cors' }).then(function (resp) { |  | ||||||
|     BACME._logHeaders(resp); |  | ||||||
|     return resp.json().then(function (reply) { |  | ||||||
|       if (/error/.test(reply.type)) { |  | ||||||
|         return Promise.reject(new Error(reply.detail || reply.type)); |  | ||||||
|       } |  | ||||||
|       directory = reply; |  | ||||||
|       nonceUrl = directory.newNonce || 'https://acme-staging-v02.api.letsencrypt.org/acme/new-nonce'; |  | ||||||
|       accountUrl = directory.newAccount || 'https://acme-staging-v02.api.letsencrypt.org/acme/new-account'; |  | ||||||
|       orderUrl = directory.newOrder || "https://acme-staging-v02.api.letsencrypt.org/acme/new-order"; |  | ||||||
|       BACME._logBody(reply); |  | ||||||
|       return reply; |  | ||||||
|     }); |  | ||||||
|   }); |  | ||||||
| }; |  | ||||||
| 
 |  | ||||||
| BACME.nonce = function () { |  | ||||||
|   return webFetch(nonceUrl, { mode: 'cors' }).then(function (resp) { |  | ||||||
|     BACME._logHeaders(resp); |  | ||||||
|     nonce = resp.headers.get('replay-nonce'); |  | ||||||
|     console.log('Nonce:', nonce); |  | ||||||
|     // resp.body is empty
 |  | ||||||
|     return resp.headers.get('replay-nonce'); |  | ||||||
|   }); |  | ||||||
| }; |  | ||||||
| 
 |  | ||||||
| BACME.accounts = {}; |  | ||||||
| 
 |  | ||||||
| // type = ECDSA
 |  | ||||||
| // bitlength = 256
 |  | ||||||
| BACME.accounts.generateKeypair = function (opts) { |  | ||||||
|   return BACME.generateKeypair(opts).then(function (result) { |  | ||||||
|     accountKeypair = result; |  | ||||||
| 
 |  | ||||||
|     return webCrypto.subtle.exportKey( |  | ||||||
|       "jwk" |  | ||||||
|     , result.privateKey |  | ||||||
|     ).then(function (privJwk) { |  | ||||||
| 
 |  | ||||||
|       accountJwk = privJwk; |  | ||||||
|       console.log('private jwk:'); |  | ||||||
|       console.log(JSON.stringify(privJwk, null, 2)); |  | ||||||
| 
 |  | ||||||
|       return privJwk; |  | ||||||
|       /* |  | ||||||
|       return webCrypto.subtle.exportKey( |  | ||||||
|         "pkcs8" |  | ||||||
|       , result.privateKey |  | ||||||
|       ).then(function (keydata) { |  | ||||||
|         console.log('pkcs8:'); |  | ||||||
|         console.log(Array.from(new Uint8Array(keydata))); |  | ||||||
| 
 |  | ||||||
|         return privJwk; |  | ||||||
|         //return accountKeypair;
 |  | ||||||
|       }); |  | ||||||
|       */ |  | ||||||
|     }); |  | ||||||
|   }); |  | ||||||
| }; |  | ||||||
| 
 |  | ||||||
| // json to url-safe base64
 |  | ||||||
| BACME._jsto64 = function (json) { |  | ||||||
|   return btoa(JSON.stringify(json)).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/g, ''); |  | ||||||
| }; |  | ||||||
| 
 |  | ||||||
| var textEncoder = new TextEncoder(); |  | ||||||
| 
 |  | ||||||
| BACME._importKey = function (jwk) { |  | ||||||
|   var alg; // I think the 256 refers to the hash
 |  | ||||||
|   var wcOpts = {}; |  | ||||||
|   var extractable = true; // TODO make optionally false?
 |  | ||||||
|   var priv = jwk; |  | ||||||
|   var pub; |  | ||||||
| 
 |  | ||||||
|   // ECDSA
 |  | ||||||
|   if (/^EC/i.test(jwk.kty)) { |  | ||||||
|     wcOpts.name = 'ECDSA'; |  | ||||||
|     wcOpts.namedCurve = jwk.crv; |  | ||||||
|     alg = 'ES256'; |  | ||||||
|     pub = { |  | ||||||
|       crv: priv.crv |  | ||||||
|     , kty: priv.kty |  | ||||||
|     , x: priv.x |  | ||||||
|     , y: priv.y |  | ||||||
|     }; |  | ||||||
|     if (!priv.d) { |  | ||||||
|       priv = null; |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   // RSA
 |  | ||||||
|   if (/^RS/i.test(jwk.kty)) { |  | ||||||
|     wcOpts.name = 'RSASSA-PKCS1-v1_5'; |  | ||||||
|     wcOpts.hash = { name: "SHA-256" }; |  | ||||||
|     alg = 'RS256'; |  | ||||||
|     pub = { |  | ||||||
|       e: priv.e |  | ||||||
|     , kty: priv.kty |  | ||||||
|     , n: priv.n |  | ||||||
|     }; |  | ||||||
|     if (!priv.p) { |  | ||||||
|       priv = null; |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   return window.crypto.subtle.importKey( |  | ||||||
|     "jwk" |  | ||||||
|   , pub |  | ||||||
|   , wcOpts |  | ||||||
|   , extractable |  | ||||||
|   , [ "verify" ] |  | ||||||
|   ).then(function (publicKey) { |  | ||||||
|     function give(privateKey) { |  | ||||||
|       return { |  | ||||||
|         wcPub: publicKey |  | ||||||
|       , wcKey: privateKey |  | ||||||
|       , wcKeypair: { publicKey: publicKey, privateKey: privateKey } |  | ||||||
|       , meta: { |  | ||||||
|           alg: alg |  | ||||||
|         , name: wcOpts.name |  | ||||||
|         , hash: wcOpts.hash |  | ||||||
|         } |  | ||||||
|       , jwk: jwk |  | ||||||
|       }; |  | ||||||
|     } |  | ||||||
|     if (!priv) { |  | ||||||
|       return give(); |  | ||||||
|     } |  | ||||||
|     return window.crypto.subtle.importKey( |  | ||||||
|       "jwk" |  | ||||||
|     , priv |  | ||||||
|     , wcOpts |  | ||||||
|     , extractable |  | ||||||
|     , [ "sign"/*, "verify"*/ ] |  | ||||||
|     ).then(give); |  | ||||||
|   }); |  | ||||||
| }; |  | ||||||
| BACME._sign = function (opts) { |  | ||||||
|   var wcPrivKey = opts.abstractKey.wcKeypair.privateKey; |  | ||||||
|   var wcOpts = opts.abstractKey.meta; |  | ||||||
|   var alg = opts.abstractKey.meta.alg; // I think the 256 refers to the hash
 |  | ||||||
|   var signHash; |  | ||||||
| 
 |  | ||||||
|   console.log('kty', opts.abstractKey.jwk.kty); |  | ||||||
|   signHash = { name: "SHA-" + alg.replace(/[a-z]+/ig, '') }; |  | ||||||
| 
 |  | ||||||
|   var msg = textEncoder.encode(opts.protected64 + '.' + opts.payload64); |  | ||||||
|   console.log('msg:', msg); |  | ||||||
|   return window.crypto.subtle.sign( |  | ||||||
|     { name: wcOpts.name, hash: signHash } |  | ||||||
|   , wcPrivKey |  | ||||||
|   , msg |  | ||||||
|   ).then(function (signature) { |  | ||||||
|     //console.log('sig1:', signature);
 |  | ||||||
|     //console.log('sig2:', new Uint8Array(signature));
 |  | ||||||
|     //console.log('sig3:', Array.prototype.slice.call(new Uint8Array(signature)));
 |  | ||||||
|     // convert buffer to urlsafe base64
 |  | ||||||
|     var sig64 = btoa(Array.prototype.map.call(new Uint8Array(signature), function (ch) { |  | ||||||
|       return String.fromCharCode(ch); |  | ||||||
|     }).join('')).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/g, ''); |  | ||||||
| 
 |  | ||||||
|     console.log('[1] URL-safe Base64 Signature:'); |  | ||||||
|     console.log(sig64); |  | ||||||
| 
 |  | ||||||
|     var signedMsg = { |  | ||||||
|       protected: opts.protected64 |  | ||||||
|     , payload: opts.payload64 |  | ||||||
|     , signature: sig64 |  | ||||||
|     }; |  | ||||||
| 
 |  | ||||||
|     console.log('Signed Base64 Msg:'); |  | ||||||
|     console.log(JSON.stringify(signedMsg, null, 2)); |  | ||||||
| 
 |  | ||||||
|     return signedMsg; |  | ||||||
|   }); |  | ||||||
| }; |  | ||||||
| // email = john.doe@gmail.com
 |  | ||||||
| // jwk = { ... }
 |  | ||||||
| // agree = true
 |  | ||||||
| BACME.accounts.sign = function (opts) { |  | ||||||
| 
 |  | ||||||
|   return BACME._importKey(opts.jwk).then(function (abstractKey) { |  | ||||||
| 
 |  | ||||||
|     var payloadJson = |  | ||||||
|       { termsOfServiceAgreed: opts.agree |  | ||||||
|       , onlyReturnExisting: false |  | ||||||
|       , contact: opts.contacts || [ 'mailto:' + opts.email ] |  | ||||||
|       }; |  | ||||||
|     console.log('payload:'); |  | ||||||
|     console.log(payloadJson); |  | ||||||
|     var payload64 = BACME._jsto64( |  | ||||||
|       payloadJson |  | ||||||
|     ); |  | ||||||
| 
 |  | ||||||
|     var protectedJson = |  | ||||||
|       { nonce: opts.nonce |  | ||||||
|       , url: accountUrl |  | ||||||
|       , alg: abstractKey.meta.alg |  | ||||||
|       , jwk: null |  | ||||||
|       }; |  | ||||||
| 
 |  | ||||||
|     if (/EC/i.test(opts.jwk.kty)) { |  | ||||||
|       protectedJson.jwk = { |  | ||||||
|         crv: opts.jwk.crv |  | ||||||
|       , kty: opts.jwk.kty |  | ||||||
|       , x: opts.jwk.x |  | ||||||
|       , y: opts.jwk.y |  | ||||||
|       }; |  | ||||||
|     } else if (/RS/i.test(opts.jwk.kty)) { |  | ||||||
|       protectedJson.jwk = { |  | ||||||
|         e: opts.jwk.e |  | ||||||
|       , kty: opts.jwk.kty |  | ||||||
|       , n: opts.jwk.n |  | ||||||
|       }; |  | ||||||
|     } else { |  | ||||||
|       return Promise.reject(new Error("[acme.accounts.sign] unsupported key type '" + opts.jwk.kty + "'")); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     console.log('protected:'); |  | ||||||
|     console.log(protectedJson); |  | ||||||
|     var protected64 = BACME._jsto64( |  | ||||||
|       protectedJson |  | ||||||
|     ); |  | ||||||
| 
 |  | ||||||
|     // Note: this function hashes before signing so send data, not the hash
 |  | ||||||
|     return BACME._sign({ |  | ||||||
|       abstractKey: abstractKey |  | ||||||
|     , payload64: payload64 |  | ||||||
|     , protected64: protected64 |  | ||||||
|     }); |  | ||||||
|   }); |  | ||||||
| }; |  | ||||||
| 
 |  | ||||||
| var accountId; |  | ||||||
| 
 |  | ||||||
| BACME.accounts.set = function (opts) { |  | ||||||
|   nonce = null; |  | ||||||
|   return window.fetch(accountUrl, { |  | ||||||
|     mode: 'cors' |  | ||||||
|   , method: 'POST' |  | ||||||
|   , headers: { 'Content-Type': 'application/jose+json' } |  | ||||||
|   , body: JSON.stringify(opts.signedAccount) |  | ||||||
|   }).then(function (resp) { |  | ||||||
|     BACME._logHeaders(resp); |  | ||||||
|     nonce = resp.headers.get('replay-nonce'); |  | ||||||
|     accountId = resp.headers.get('location'); |  | ||||||
|     console.log('Next nonce:', nonce); |  | ||||||
|     console.log('Location/kid:', accountId); |  | ||||||
| 
 |  | ||||||
|     if (!resp.headers.get('content-type')) { |  | ||||||
|      console.log('Body: <none>'); |  | ||||||
| 
 |  | ||||||
|      return { kid: accountId }; |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     return resp.json().then(function (result) { |  | ||||||
|       if (/^Error/i.test(result.detail)) { |  | ||||||
|         return Promise.reject(new Error(result.detail)); |  | ||||||
|       } |  | ||||||
|       result.kid = accountId; |  | ||||||
|       BACME._logBody(result); |  | ||||||
| 
 |  | ||||||
|       return result; |  | ||||||
|     }); |  | ||||||
|   }); |  | ||||||
| }; |  | ||||||
| 
 |  | ||||||
| var orderUrl; |  | ||||||
| 
 |  | ||||||
| BACME.orders = {}; |  | ||||||
| 
 |  | ||||||
| // identifiers = [ { type: 'dns', value: 'example.com' }, { type: 'dns', value: '*.example.com' } ]
 |  | ||||||
| // signedAccount
 |  | ||||||
| BACME.orders.sign = function (opts) { |  | ||||||
|   var payload64 = BACME._jsto64({ identifiers: opts.identifiers }); |  | ||||||
| 
 |  | ||||||
|   return BACME._importKey(opts.jwk).then(function (abstractKey) { |  | ||||||
|     var protected64 = BACME._jsto64( |  | ||||||
|       { nonce: nonce, alg: abstractKey.meta.alg/*'ES256'*/, url: orderUrl, kid: opts.kid } |  | ||||||
|     ); |  | ||||||
|     console.log('abstractKey:'); |  | ||||||
|     console.log(abstractKey); |  | ||||||
|     return BACME._sign({ |  | ||||||
|       abstractKey: abstractKey |  | ||||||
|     , payload64: payload64 |  | ||||||
|     , protected64: protected64 |  | ||||||
|     }).then(function (sig) { |  | ||||||
|       if (!sig) { |  | ||||||
|         throw new Error('sig is undefined... nonsense!'); |  | ||||||
|       } |  | ||||||
|       console.log('newsig', sig); |  | ||||||
|       return sig; |  | ||||||
|     }); |  | ||||||
|   }); |  | ||||||
| }; |  | ||||||
| 
 |  | ||||||
| var currentOrderUrl; |  | ||||||
| var authorizationUrls; |  | ||||||
| var finalizeUrl; |  | ||||||
| 
 |  | ||||||
| BACME.orders.create = function (opts) { |  | ||||||
|   nonce = null; |  | ||||||
|   return window.fetch(orderUrl, { |  | ||||||
|     mode: 'cors' |  | ||||||
|   , method: 'POST' |  | ||||||
|   , headers: { 'Content-Type': 'application/jose+json' } |  | ||||||
|   , body: JSON.stringify(opts.signedOrder) |  | ||||||
|   }).then(function (resp) { |  | ||||||
|     BACME._logHeaders(resp); |  | ||||||
|     currentOrderUrl = resp.headers.get('location'); |  | ||||||
|     nonce = resp.headers.get('replay-nonce'); |  | ||||||
|     console.log('Next nonce:', nonce); |  | ||||||
| 
 |  | ||||||
|     return resp.json().then(function (result) { |  | ||||||
|       if (/^Error/i.test(result.detail)) { |  | ||||||
|         return Promise.reject(new Error(result.detail)); |  | ||||||
|       } |  | ||||||
|       authorizationUrls = result.authorizations; |  | ||||||
|       finalizeUrl = result.finalize; |  | ||||||
|       BACME._logBody(result); |  | ||||||
| 
 |  | ||||||
|       result.url = currentOrderUrl; |  | ||||||
|       return result; |  | ||||||
|     }); |  | ||||||
|   }); |  | ||||||
| }; |  | ||||||
| 
 |  | ||||||
| BACME.challenges = {}; |  | ||||||
| BACME.challenges.all = function () { |  | ||||||
|   var challenges = []; |  | ||||||
| 
 |  | ||||||
|   function next() { |  | ||||||
|     if (!authorizationUrls.length) { |  | ||||||
|       return challenges; |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     return BACME.challenges.view().then(function (challenge) { |  | ||||||
|       challenges.push(challenge); |  | ||||||
|       return next(); |  | ||||||
|     }); |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   return next(); |  | ||||||
| }; |  | ||||||
| BACME.challenges.view = function () { |  | ||||||
|   var authzUrl = authorizationUrls.pop(); |  | ||||||
|   var token; |  | ||||||
|   var challengeDomain; |  | ||||||
|   var challengeUrl; |  | ||||||
| 
 |  | ||||||
|   return window.fetch(authzUrl, { |  | ||||||
|     mode: 'cors' |  | ||||||
|   }).then(function (resp) { |  | ||||||
|     BACME._logHeaders(resp); |  | ||||||
| 
 |  | ||||||
|     return resp.json().then(function (result) { |  | ||||||
|       // Note: select the challenge you wish to use
 |  | ||||||
|       var challenge = result.challenges.slice(0).pop(); |  | ||||||
|       token = challenge.token; |  | ||||||
|       challengeUrl = challenge.url; |  | ||||||
|       challengeDomain = result.identifier.value; |  | ||||||
| 
 |  | ||||||
|       BACME._logBody(result); |  | ||||||
| 
 |  | ||||||
|       return { |  | ||||||
|         challenges: result.challenges |  | ||||||
|       , expires: result.expires |  | ||||||
|       , identifier: result.identifier |  | ||||||
|       , status: result.status |  | ||||||
|       , wildcard: result.wildcard |  | ||||||
|       //, token: challenge.token
 |  | ||||||
|       //, url: challenge.url
 |  | ||||||
|       //, domain: result.identifier.value,
 |  | ||||||
|       }; |  | ||||||
|     }); |  | ||||||
|   }); |  | ||||||
| }; |  | ||||||
| 
 |  | ||||||
| var thumbprint; |  | ||||||
| var keyAuth; |  | ||||||
| var httpPath; |  | ||||||
| var dnsAuth; |  | ||||||
| var dnsRecord; |  | ||||||
| 
 |  | ||||||
| BACME.thumbprint = function (opts) { |  | ||||||
|   // https://stackoverflow.com/questions/42588786/how-to-fingerprint-a-jwk
 |  | ||||||
| 
 |  | ||||||
|   var accountJwk = opts.jwk; |  | ||||||
|   var keys; |  | ||||||
| 
 |  | ||||||
|   if (/^EC/i.test(opts.jwk.kty)) { |  | ||||||
|     keys = [ 'crv', 'kty', 'x', 'y' ]; |  | ||||||
|   } else if (/^RS/i.test(opts.jwk.kty)) { |  | ||||||
|     keys = [ 'e', 'kty', 'n' ]; |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   var accountPublicStr = '{' + keys.map(function (key) { |  | ||||||
|     return '"' + key + '":"' + accountJwk[key] + '"'; |  | ||||||
|   }).join(',') + '}'; |  | ||||||
| 
 |  | ||||||
|   return window.crypto.subtle.digest( |  | ||||||
|     { name: "SHA-256" } // SHA-256 is spec'd, non-optional
 |  | ||||||
|   , textEncoder.encode(accountPublicStr) |  | ||||||
|   ).then(function (hash) { |  | ||||||
|     thumbprint = btoa(Array.prototype.map.call(new Uint8Array(hash), function (ch) { |  | ||||||
|       return String.fromCharCode(ch); |  | ||||||
|     }).join('')).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/g, ''); |  | ||||||
| 
 |  | ||||||
|     console.log('Thumbprint:'); |  | ||||||
|     console.log(opts); |  | ||||||
|     console.log(accountPublicStr); |  | ||||||
|     console.log(thumbprint); |  | ||||||
| 
 |  | ||||||
|     return thumbprint; |  | ||||||
|   }); |  | ||||||
| }; |  | ||||||
| 
 |  | ||||||
| // { token, thumbprint, challengeDomain }
 |  | ||||||
| BACME.challenges['http-01'] = function (opts) { |  | ||||||
|   // The contents of the key authorization file
 |  | ||||||
|   keyAuth = opts.token + '.' + opts.thumbprint; |  | ||||||
| 
 |  | ||||||
|   // Where the key authorization file goes
 |  | ||||||
|   httpPath = 'http://' + opts.challengeDomain + '/.well-known/acme-challenge/' + opts.token; |  | ||||||
| 
 |  | ||||||
|   console.log("echo '" + keyAuth + "' > '" + httpPath + "'"); |  | ||||||
| 
 |  | ||||||
|   return { |  | ||||||
|     path: httpPath |  | ||||||
|   , value: keyAuth |  | ||||||
|   }; |  | ||||||
| }; |  | ||||||
| 
 |  | ||||||
| // { keyAuth }
 |  | ||||||
| BACME.challenges['dns-01'] = function (opts) { |  | ||||||
|   console.log('opts.keyAuth for DNS:'); |  | ||||||
|   console.log(opts.keyAuth); |  | ||||||
|   return window.crypto.subtle.digest( |  | ||||||
|     { name: "SHA-256", } |  | ||||||
|   , textEncoder.encode(opts.keyAuth) |  | ||||||
|   ).then(function (hash) { |  | ||||||
|     dnsAuth = btoa(Array.prototype.map.call(new Uint8Array(hash), function (ch) { |  | ||||||
|       return String.fromCharCode(ch); |  | ||||||
|     }).join('')).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/g, ''); |  | ||||||
| 
 |  | ||||||
|     dnsRecord = '_acme-challenge.' + opts.challengeDomain; |  | ||||||
| 
 |  | ||||||
|     console.log('DNS TXT Auth:'); |  | ||||||
|     // The name of the record
 |  | ||||||
|     console.log(dnsRecord); |  | ||||||
|     // The TXT record value
 |  | ||||||
|     console.log(dnsAuth); |  | ||||||
| 
 |  | ||||||
|     return { |  | ||||||
|       type: 'TXT' |  | ||||||
|     , host: dnsRecord |  | ||||||
|     , answer: dnsAuth |  | ||||||
|     }; |  | ||||||
|   }); |  | ||||||
| }; |  | ||||||
| 
 |  | ||||||
| var challengePollUrl; |  | ||||||
| 
 |  | ||||||
| // { jwk, challengeUrl, accountId (kid) }
 |  | ||||||
| BACME.challenges.accept = function (opts) { |  | ||||||
|   var payload64 = BACME._jsto64({}); |  | ||||||
| 
 |  | ||||||
|   return BACME._importKey(opts.jwk).then(function (abstractKey) { |  | ||||||
|     var protected64 = BACME._jsto64( |  | ||||||
|       { nonce: nonce, alg: abstractKey.meta.alg/*'ES256'*/, url: opts.challengeUrl, kid: opts.accountId } |  | ||||||
|     ); |  | ||||||
|     return BACME._sign({ |  | ||||||
|       abstractKey: abstractKey |  | ||||||
|     , payload64: payload64 |  | ||||||
|     , protected64: protected64 |  | ||||||
|     }); |  | ||||||
|   }).then(function (signedAccept) { |  | ||||||
| 
 |  | ||||||
|     nonce = null; |  | ||||||
|     return window.fetch( |  | ||||||
|       opts.challengeUrl |  | ||||||
|     , { mode: 'cors' |  | ||||||
|       , method: 'POST' |  | ||||||
|       , headers: { 'Content-Type': 'application/jose+json' } |  | ||||||
|       , body: JSON.stringify(signedAccept) |  | ||||||
|       } |  | ||||||
|     ).then(function (resp) { |  | ||||||
|       BACME._logHeaders(resp); |  | ||||||
|       nonce = resp.headers.get('replay-nonce'); |  | ||||||
|       console.log("ACCEPT NONCE:", nonce); |  | ||||||
| 
 |  | ||||||
|       return resp.json().then(function (reply) { |  | ||||||
|         challengePollUrl = reply.url; |  | ||||||
| 
 |  | ||||||
|         console.log('Challenge ACK:'); |  | ||||||
|         console.log(JSON.stringify(reply)); |  | ||||||
|         return reply; |  | ||||||
|       }); |  | ||||||
|     }); |  | ||||||
|   }); |  | ||||||
| }; |  | ||||||
| 
 |  | ||||||
| BACME.challenges.check = function (opts) { |  | ||||||
|   return window.fetch(opts.challengePollUrl, { mode: 'cors' }).then(function (resp) { |  | ||||||
|     BACME._logHeaders(resp); |  | ||||||
| 
 |  | ||||||
|     return resp.json().then(function (reply) { |  | ||||||
|       if (/error/.test(reply.type)) { |  | ||||||
|         return Promise.reject(new Error(reply.detail || reply.type)); |  | ||||||
|       } |  | ||||||
|       challengePollUrl = reply.url; |  | ||||||
| 
 |  | ||||||
|       BACME._logBody(reply); |  | ||||||
| 
 |  | ||||||
|       return reply; |  | ||||||
|     }); |  | ||||||
|   }); |  | ||||||
| }; |  | ||||||
| 
 |  | ||||||
| var domainKeypair; |  | ||||||
| var domainJwk; |  | ||||||
| 
 |  | ||||||
| BACME.generateKeypair = function (opts) { |  | ||||||
|   var wcOpts = {}; |  | ||||||
| 
 |  | ||||||
|   // ECDSA has only the P curves and an associated bitlength
 |  | ||||||
|   if (/^EC/i.test(opts.type)) { |  | ||||||
|     wcOpts.name = 'ECDSA'; |  | ||||||
|     if (/256/.test(opts.bitlength)) { |  | ||||||
|       wcOpts.namedCurve = 'P-256'; |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   // RSA-PSS is another option, but I don't think it's used for Let's Encrypt
 |  | ||||||
|   // I think the hash is only necessary for signing, not generation or import
 |  | ||||||
|   if (/^RS/i.test(opts.type)) { |  | ||||||
|     wcOpts.name = 'RSASSA-PKCS1-v1_5'; |  | ||||||
|     wcOpts.modulusLength = opts.bitlength; |  | ||||||
|     if (opts.bitlength < 2048) { |  | ||||||
|       wcOpts.modulusLength = opts.bitlength * 8; |  | ||||||
|     } |  | ||||||
|     wcOpts.publicExponent = new Uint8Array([0x01, 0x00, 0x01]); |  | ||||||
|     wcOpts.hash = { name: "SHA-256" }; |  | ||||||
|   } |  | ||||||
|   var extractable = true; |  | ||||||
|   return window.crypto.subtle.generateKey( |  | ||||||
|     wcOpts |  | ||||||
|   , extractable |  | ||||||
|   , [ 'sign', 'verify' ] |  | ||||||
|   ); |  | ||||||
| }; |  | ||||||
| BACME.domains = {}; |  | ||||||
| // TODO factor out from BACME.accounts.generateKeypair even more
 |  | ||||||
| BACME.domains.generateKeypair = function (opts) { |  | ||||||
|   return BACME.generateKeypair(opts).then(function (result) { |  | ||||||
|     domainKeypair = result; |  | ||||||
| 
 |  | ||||||
|     return window.crypto.subtle.exportKey( |  | ||||||
|       "jwk" |  | ||||||
|     , result.privateKey |  | ||||||
|     ).then(function (privJwk) { |  | ||||||
| 
 |  | ||||||
|       domainJwk = privJwk; |  | ||||||
|       console.log('private jwk:'); |  | ||||||
|       console.log(JSON.stringify(privJwk, null, 2)); |  | ||||||
| 
 |  | ||||||
|       return privJwk; |  | ||||||
|     }); |  | ||||||
|   }); |  | ||||||
| }; |  | ||||||
| 
 |  | ||||||
| // { serverJwk, domains }
 |  | ||||||
| BACME.orders.generateCsr = function (opts) { |  | ||||||
|   return BACME._importKey(opts.serverJwk).then(function (abstractKey) { |  | ||||||
|     return Promise.resolve(CSR.generate({ keypair: abstractKey.wcKeypair, domains: opts.domains })); |  | ||||||
|   }); |  | ||||||
| }; |  | ||||||
| 
 |  | ||||||
| var certificateUrl; |  | ||||||
| 
 |  | ||||||
| // { csr, jwk, finalizeUrl, accountId }
 |  | ||||||
| BACME.orders.finalize = function (opts) { |  | ||||||
|   var payload64 = BACME._jsto64( |  | ||||||
|     { csr: opts.csr } |  | ||||||
|   ); |  | ||||||
| 
 |  | ||||||
|   return BACME._importKey(opts.jwk).then(function (abstractKey) { |  | ||||||
|     var protected64 = BACME._jsto64( |  | ||||||
|       { nonce: nonce, alg: abstractKey.meta.alg/*'ES256'*/, url: opts.finalizeUrl, kid: opts.accountId } |  | ||||||
|     ); |  | ||||||
|     return BACME._sign({ |  | ||||||
|       abstractKey: abstractKey |  | ||||||
|     , payload64: payload64 |  | ||||||
|     , protected64: protected64 |  | ||||||
|     }); |  | ||||||
|   }).then(function (signedFinal) { |  | ||||||
| 
 |  | ||||||
|     nonce = null; |  | ||||||
|     return window.fetch( |  | ||||||
|       opts.finalizeUrl |  | ||||||
|     , { mode: 'cors' |  | ||||||
|       , method: 'POST' |  | ||||||
|       , headers: { 'Content-Type': 'application/jose+json' } |  | ||||||
|       , body: JSON.stringify(signedFinal) |  | ||||||
|       } |  | ||||||
|     ).then(function (resp) { |  | ||||||
|       BACME._logHeaders(resp); |  | ||||||
|       nonce = resp.headers.get('replay-nonce'); |  | ||||||
| 
 |  | ||||||
|       return resp.json().then(function (reply) { |  | ||||||
|         if (/error/.test(reply.type)) { |  | ||||||
|           return Promise.reject(new Error(reply.detail || reply.type)); |  | ||||||
|         } |  | ||||||
|         certificateUrl = reply.certificate; |  | ||||||
|         BACME._logBody(reply); |  | ||||||
| 
 |  | ||||||
|         return reply; |  | ||||||
|       }); |  | ||||||
|     }); |  | ||||||
|   }); |  | ||||||
| }; |  | ||||||
| 
 |  | ||||||
| BACME.orders.receive = function (opts) { |  | ||||||
|   return window.fetch( |  | ||||||
|     opts.certificateUrl |  | ||||||
|   , { mode: 'cors' |  | ||||||
|     , method: 'GET' |  | ||||||
|     } |  | ||||||
|   ).then(function (resp) { |  | ||||||
|     BACME._logHeaders(resp); |  | ||||||
|     nonce = resp.headers.get('replay-nonce'); |  | ||||||
| 
 |  | ||||||
|     return resp.text().then(function (reply) { |  | ||||||
|       BACME._logBody(reply); |  | ||||||
| 
 |  | ||||||
|       return reply; |  | ||||||
|     }); |  | ||||||
|   }); |  | ||||||
| }; |  | ||||||
| 
 |  | ||||||
| BACME.orders.check = function (opts) { |  | ||||||
|   return window.fetch( |  | ||||||
|     opts.orderUrl |  | ||||||
|   , { mode: 'cors' |  | ||||||
|     , method: 'GET' |  | ||||||
|     } |  | ||||||
|   ).then(function (resp) { |  | ||||||
|     BACME._logHeaders(resp); |  | ||||||
| 
 |  | ||||||
|     return resp.json().then(function (reply) { |  | ||||||
|       if (/error/.test(reply.type)) { |  | ||||||
|         return Promise.reject(new Error(reply.detail || reply.type)); |  | ||||||
|       } |  | ||||||
|       BACME._logBody(reply); |  | ||||||
| 
 |  | ||||||
|       return reply; |  | ||||||
|     }); |  | ||||||
|   }); |  | ||||||
| }; |  | ||||||
| 
 |  | ||||||
| }(window)); |  | ||||||
							
								
								
									
										298
									
								
								lib/csr.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										298
									
								
								lib/csr.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,298 @@ | |||||||
|  | // 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'; | ||||||
|  | /*global Promise*/ | ||||||
|  | 
 | ||||||
|  | var ASN1 = exports.ASN1; | ||||||
|  | var Enc = exports.Enc; | ||||||
|  | var PEM = exports.PEM; | ||||||
|  | var X509 = exports.x509; | ||||||
|  | var Keypairs = exports.Keypairs; | ||||||
|  | 
 | ||||||
|  | // TODO find a way that the prior node-ish way of `module.exports = function () {}` isn't broken
 | ||||||
|  | var CSR = exports.CSR = function (opts) { | ||||||
|  |   // We're using a Promise here to be compatible with the browser version
 | ||||||
|  |   // which will probably use the webcrypto API for some of the conversions
 | ||||||
|  |   return CSR._prepare(opts).then(function (opts) { | ||||||
|  |     return CSR.create(opts).then(function (bytes) { | ||||||
|  |       return CSR._encode(opts, bytes); | ||||||
|  |     }); | ||||||
|  |   }); | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | CSR._prepare = function (opts) { | ||||||
|  |   return Promise.resolve().then(function () { | ||||||
|  |     var Keypairs; | ||||||
|  |     opts = JSON.parse(JSON.stringify(opts)); | ||||||
|  | 
 | ||||||
|  |     // We do a bit of extra error checking for user convenience
 | ||||||
|  |     if (!opts) { throw new Error("You must pass options with key and domains to rsacsr"); } | ||||||
|  |     if (!Array.isArray(opts.domains) || 0 === opts.domains.length) { | ||||||
|  |       new Error("You must pass options.domains as a non-empty array"); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     // I need to check that 例.中国 is a valid domain name
 | ||||||
|  |     if (!opts.domains.every(function (d) { | ||||||
|  |       // allow punycode? xn--
 | ||||||
|  |       if ('string' === typeof d /*&& /\./.test(d) && !/--/.test(d)*/) { | ||||||
|  |         return true; | ||||||
|  |       } | ||||||
|  |     })) { | ||||||
|  |       throw new Error("You must pass options.domains as strings"); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     if (opts.jwk) { return opts; } | ||||||
|  |     if (opts.key && opts.key.kty) { | ||||||
|  |       opts.jwk = opts.key; | ||||||
|  |       return opts; | ||||||
|  |     } | ||||||
|  |     if (!opts.pem && !opts.key) { | ||||||
|  |       throw new Error("You must pass options.key as a JSON web key"); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     Keypairs = exports.Keypairs; | ||||||
|  |     if (!exports.Keypairs) { | ||||||
|  |       throw new Error("Keypairs.js is an optional dependency for PEM-to-JWK.\n" | ||||||
|  |         + "Install it if you'd like to use it:\n" | ||||||
|  |         + "\tnpm install --save rasha\n" | ||||||
|  |         + "Otherwise supply a jwk as the private key." | ||||||
|  |       ); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     return Keypairs.import({ pem: opts.pem || opts.key }).then(function (pair) { | ||||||
|  |       opts.jwk = pair.private; | ||||||
|  |       return opts; | ||||||
|  |     }); | ||||||
|  |   }); | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | CSR._encode = function (opts, bytes) { | ||||||
|  |   if ('der' === (opts.encoding||'').toLowerCase()) { | ||||||
|  |     return bytes; | ||||||
|  |   } | ||||||
|  |   return PEM.packBlock({ | ||||||
|  |     type: "CERTIFICATE REQUEST" | ||||||
|  |   , bytes: bytes /* { jwk: jwk, domains: opts.domains } */ | ||||||
|  |   }); | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | CSR.create = function createCsr(opts) { | ||||||
|  |   var hex = CSR.request(opts.jwk, opts.domains); | ||||||
|  |   return CSR._sign(opts.jwk, hex).then(function (csr) { | ||||||
|  |     return Enc.hexToBuf(csr); | ||||||
|  |   }); | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | //
 | ||||||
|  | // EC / RSA
 | ||||||
|  | //
 | ||||||
|  | CSR.request = function createCsrBodyEc(jwk, domains) { | ||||||
|  |   var asn1pub; | ||||||
|  |   if (/^EC/i.test(jwk.kty)) { | ||||||
|  |     asn1pub = X509.packCsrEcPublicKey(jwk); | ||||||
|  |   } else { | ||||||
|  |     asn1pub = X509.packCsrRsaPublicKey(jwk); | ||||||
|  |   } | ||||||
|  |   return X509.packCsr(asn1pub, domains); | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | CSR._sign = function csrEcSig(jwk, request) { | ||||||
|  |   // Took some tips from https://gist.github.com/codermapuche/da4f96cdb6d5ff53b7ebc156ec46a10a
 | ||||||
|  |   // TODO will have to convert web ECDSA signatures to PEM ECDSA signatures (but RSA should be the same)
 | ||||||
|  |   // TODO have a consistent non-private way to sign
 | ||||||
|  |   return Keypairs._sign({ jwk: jwk, format: 'x509' }, Enc.hexToBuf(request)).then(function (sig) { | ||||||
|  |     return CSR._toDer({ request: request, signature: sig, kty: jwk.kty }); | ||||||
|  |   }); | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | CSR._toDer = function encode(opts) { | ||||||
|  |   var sty; | ||||||
|  |   if (/^EC/i.test(opts.kty)) { | ||||||
|  |     // 1.2.840.10045.4.3.2 ecdsaWithSHA256 (ANSI X9.62 ECDSA algorithm with SHA256)
 | ||||||
|  |     sty = ASN1('30', ASN1('06', '2a8648ce3d040302')); | ||||||
|  |   } else { | ||||||
|  |     // 1.2.840.113549.1.1.11 sha256WithRSAEncryption (PKCS #1)
 | ||||||
|  |     sty = ASN1('30', ASN1('06', '2a864886f70d01010b'), ASN1('05')); | ||||||
|  |   } | ||||||
|  |   return ASN1('30' | ||||||
|  |     // The Full CSR Request Body
 | ||||||
|  |   , opts.request | ||||||
|  |     // The Signature Type
 | ||||||
|  |   , sty | ||||||
|  |     // The Signature
 | ||||||
|  |   , ASN1.BitStr(Enc.bufToHex(opts.signature)) | ||||||
|  |   ); | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | X509.packCsr = function (asn1pubkey, domains) { | ||||||
|  |   return ASN1('30' | ||||||
|  |     // Version (0)
 | ||||||
|  |   , ASN1.UInt('00') | ||||||
|  | 
 | ||||||
|  |     // 2.5.4.3 commonName (X.520 DN component)
 | ||||||
|  |   , ASN1('30', ASN1('31', ASN1('30', ASN1('06', '550403'), ASN1('0c', Enc.utf8ToHex(domains[0]))))) | ||||||
|  | 
 | ||||||
|  |     // Public Key (RSA or EC)
 | ||||||
|  |   , asn1pubkey | ||||||
|  | 
 | ||||||
|  |     // Request Body
 | ||||||
|  |   , ASN1('a0' | ||||||
|  |     , ASN1('30' | ||||||
|  |         // 1.2.840.113549.1.9.14 extensionRequest (PKCS #9 via CRMF)
 | ||||||
|  |       , ASN1('06', '2a864886f70d01090e') | ||||||
|  |       , ASN1('31' | ||||||
|  |         , ASN1('30' | ||||||
|  |           , ASN1('30' | ||||||
|  |               // 2.5.29.17 subjectAltName (X.509 extension)
 | ||||||
|  |             , ASN1('06', '551d11') | ||||||
|  |             , ASN1('04' | ||||||
|  |               , ASN1('30', domains.map(function (d) { | ||||||
|  |                   return ASN1('82', Enc.utf8ToHex(d)); | ||||||
|  |                 }).join('')))))))) | ||||||
|  |   ); | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | // TODO finish this later
 | ||||||
|  | // we want to parse the domains, the public key, and verify the signature
 | ||||||
|  | CSR._info = function (der) { | ||||||
|  |   // standard base64 PEM
 | ||||||
|  |   if ('string' === typeof der && '-' === der[0]) { | ||||||
|  |     der = PEM.parseBlock(der).bytes; | ||||||
|  |   } | ||||||
|  |   // jose urlBase64 not-PEM
 | ||||||
|  |   if ('string' === typeof der) { | ||||||
|  |     der = Enc.base64ToBuf(der); | ||||||
|  |   } | ||||||
|  |   // not supporting binary-encoded bas64
 | ||||||
|  |   var c = ASN1.parse(der); | ||||||
|  |   var kty; | ||||||
|  |   // A cert has 3 parts: cert, signature meta, signature
 | ||||||
|  |   if (c.children.length !== 3) { | ||||||
|  |     throw new Error("doesn't look like a certificate request: expected 3 parts of header"); | ||||||
|  |   } | ||||||
|  |   var sig = c.children[2]; | ||||||
|  |   if (sig.children.length) { | ||||||
|  |     // ASN1/X509 EC
 | ||||||
|  |     sig = sig.children[0]; | ||||||
|  |     sig = ASN1('30', ASN1.UInt(Enc.bufToHex(sig.children[0].value)), ASN1.UInt(Enc.bufToHex(sig.children[1].value))); | ||||||
|  |     sig = Enc.hexToBuf(sig); | ||||||
|  |     kty = 'EC'; | ||||||
|  |   } else { | ||||||
|  |     // Raw RSA Sig
 | ||||||
|  |     sig = sig.value; | ||||||
|  |     kty = 'RSA'; | ||||||
|  |   } | ||||||
|  |   //c.children[1]; // signature type
 | ||||||
|  |   var req = c.children[0]; | ||||||
|  |   // TODO utf8
 | ||||||
|  |   if (4 !== req.children.length) { | ||||||
|  |     throw new Error("doesn't look like a certificate request: expected 4 parts to request"); | ||||||
|  |   } | ||||||
|  |   // 0 null
 | ||||||
|  |   // 1 commonName / subject
 | ||||||
|  |   var sub = Enc.bufToBin(req.children[1].children[0].children[0].children[1].value); | ||||||
|  |   // 3 public key (type, key)
 | ||||||
|  |   //console.log('oid', Enc.bufToHex(req.children[2].children[0].children[0].value));
 | ||||||
|  |   var pub; | ||||||
|  |   // TODO reuse ASN1 parser for these?
 | ||||||
|  |   if ('EC' === kty) { | ||||||
|  |     // throw away compression byte
 | ||||||
|  |     pub = req.children[2].children[1].value.slice(1); | ||||||
|  |     pub = { kty: kty, x: pub.slice(0, 32), y: pub.slice(32) }; | ||||||
|  |     while (0 === pub.x[0]) { pub.x = pub.x.slice(1); } | ||||||
|  |     while (0 === pub.y[0]) { pub.y = pub.y.slice(1); } | ||||||
|  |     if ((pub.x.length || pub.x.byteLength) > 48) { | ||||||
|  |       pub.crv = 'P-521'; | ||||||
|  |     } else if ((pub.x.length || pub.x.byteLength) > 32) { | ||||||
|  |       pub.crv = 'P-384'; | ||||||
|  |     } else { | ||||||
|  |       pub.crv = 'P-256'; | ||||||
|  |     } | ||||||
|  |     pub.x = Enc.bufToUrlBase64(pub.x); | ||||||
|  |     pub.y = Enc.bufToUrlBase64(pub.y); | ||||||
|  |   } else { | ||||||
|  |     pub = req.children[2].children[1].children[0]; | ||||||
|  |     pub = { kty: kty, n: pub.children[0].value, e: pub.children[1].value }; | ||||||
|  |     while (0 === pub.n[0]) { pub.n = pub.n.slice(1); } | ||||||
|  |     while (0 === pub.e[0]) { pub.e = pub.e.slice(1); } | ||||||
|  |     pub.n = Enc.bufToUrlBase64(pub.n); | ||||||
|  |     pub.e = Enc.bufToUrlBase64(pub.e); | ||||||
|  |   } | ||||||
|  |   // 4 extensions
 | ||||||
|  |   var domains = req.children[3].children.filter(function (seq) { | ||||||
|  |     //  1.2.840.113549.1.9.14 extensionRequest (PKCS #9 via CRMF)
 | ||||||
|  |     if ('2a864886f70d01090e' === Enc.bufToHex(seq.children[0].value)) { | ||||||
|  |       return true; | ||||||
|  |     } | ||||||
|  |   }).map(function (seq) { | ||||||
|  |     return seq.children[1].children[0].children.filter(function (seq2) { | ||||||
|  |       // subjectAltName (X.509 extension)
 | ||||||
|  |       if ('551d11' === Enc.bufToHex(seq2.children[0].value)) { | ||||||
|  |         return true; | ||||||
|  |       } | ||||||
|  |     }).map(function (seq2) { | ||||||
|  |       return seq2.children[1].children[0].children.map(function (name) { | ||||||
|  |         // TODO utf8
 | ||||||
|  |         return Enc.bufToBin(name.value); | ||||||
|  |       }); | ||||||
|  |     })[0]; | ||||||
|  |   })[0]; | ||||||
|  | 
 | ||||||
|  |   return { | ||||||
|  |     subject: sub | ||||||
|  |   , altnames: domains | ||||||
|  |   , jwk: pub | ||||||
|  |   , signature: sig | ||||||
|  |   }; | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | X509.packCsrRsaPublicKey = function (jwk) { | ||||||
|  |   // Sequence the key
 | ||||||
|  |   var n = ASN1.UInt(Enc.base64ToHex(jwk.n)); | ||||||
|  |   var e = ASN1.UInt(Enc.base64ToHex(jwk.e)); | ||||||
|  |   var asn1pub = ASN1('30', n, e); | ||||||
|  | 
 | ||||||
|  |   // Add the CSR pub key header
 | ||||||
|  |   return ASN1('30', ASN1('30', ASN1('06', '2a864886f70d010101'), ASN1('05')), ASN1.BitStr(asn1pub)); | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | X509.packCsrEcPublicKey = function (jwk) { | ||||||
|  |   var ecOid = X509._oids[jwk.crv]; | ||||||
|  |   if (!ecOid) { | ||||||
|  |     throw new Error("Unsupported namedCurve '" + jwk.crv + "'. Supported types are " + Object.keys(X509._oids)); | ||||||
|  |   } | ||||||
|  |   var cmp = '04'; // 04 == x+y, 02 == x-only
 | ||||||
|  |   var hxy = ''; | ||||||
|  |   // Placeholder. I'm not even sure if compression should be supported.
 | ||||||
|  |   if (!jwk.y) { cmp = '02'; } | ||||||
|  |   hxy += Enc.base64ToHex(jwk.x); | ||||||
|  |   if (jwk.y) { hxy += Enc.base64ToHex(jwk.y); } | ||||||
|  | 
 | ||||||
|  |   // 1.2.840.10045.2.1 ecPublicKey
 | ||||||
|  |   return ASN1('30', ASN1('30', ASN1('06', '2a8648ce3d0201'), ASN1('06', ecOid)), ASN1.BitStr(cmp + hxy)); | ||||||
|  | }; | ||||||
|  | X509._oids = { | ||||||
|  |   // 1.2.840.10045.3.1.7 prime256v1
 | ||||||
|  |   // (ANSI X9.62 named elliptic curve) (06 08 - 2A 86 48 CE 3D 03 01 07)
 | ||||||
|  |   'P-256': '2a8648ce3d030107' | ||||||
|  |   // 1.3.132.0.34 P-384 (06 05 - 2B 81 04 00 22)
 | ||||||
|  |   // (SEC 2 recommended EC domain secp256r1)
 | ||||||
|  | , 'P-384': '2b81040022' | ||||||
|  |   // requires more logic and isn't a recommended standard
 | ||||||
|  |   // 1.3.132.0.35 P-521 (06 05 - 2B 81 04 00 23)
 | ||||||
|  |   // (SEC 2 alternate P-521)
 | ||||||
|  | //, 'P-521': '2B 81 04 00 23'
 | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | // don't replace the full parseBlock, if it exists
 | ||||||
|  | PEM.parseBlock = PEM.parseBlock || function (str) { | ||||||
|  |   var der = str.split(/\n/).filter(function (line) { | ||||||
|  |     return !/-----/.test(line); | ||||||
|  |   }).join(''); | ||||||
|  |   return { bytes: Enc.base64ToBuf(der) }; | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | }('undefined' === typeof window ? module.exports : window)); | ||||||
| @ -180,24 +180,12 @@ Keypairs.signJws = function (opts) { | |||||||
|       var msg = protected64 + '.' + payload64; |       var msg = protected64 + '.' + payload64; | ||||||
| 
 | 
 | ||||||
|       return Keypairs._sign(opts, msg).then(function (buf) { |       return Keypairs._sign(opts, msg).then(function (buf) { | ||||||
|         /* |  | ||||||
|          * This will come back into play for CSRs, but not for JOSE |  | ||||||
|         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); |  | ||||||
|         } |  | ||||||
|         */ |  | ||||||
|         var signedMsg = { |         var signedMsg = { | ||||||
|           protected: protected64 |           protected: protected64 | ||||||
|         , payload: payload64 |         , payload: payload64 | ||||||
|         , signature: Enc.bufToUrlBase64(buf) |         , signature: Enc.bufToUrlBase64(buf) | ||||||
|         }; |         }; | ||||||
| 
 | 
 | ||||||
|         console.log('Signed Base64 Msg:'); |  | ||||||
|         console.log(JSON.stringify(signedMsg, null, 2)); |  | ||||||
| 
 |  | ||||||
|         console.log('msg:', msg); |  | ||||||
|         return signedMsg; |         return signedMsg; | ||||||
|       }); |       }); | ||||||
|     } |     } | ||||||
| @ -212,40 +200,6 @@ Keypairs.signJws = function (opts) { | |||||||
|     } |     } | ||||||
|   }); |   }); | ||||||
| }; | }; | ||||||
| Keypairs._convertIfEcdsa = function (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); |  | ||||||
| }; |  | ||||||
| 
 | 
 | ||||||
| Keypairs._sign = function (opts, payload) { | Keypairs._sign = function (opts, payload) { | ||||||
|   return Keypairs._import(opts).then(function (privkey) { |   return Keypairs._import(opts).then(function (privkey) { | ||||||
| @ -259,9 +213,14 @@ Keypairs._sign = function (opts, payload) { | |||||||
|     , privkey |     , privkey | ||||||
|     , payload |     , payload | ||||||
|     ).then(function (signature) { |     ).then(function (signature) { | ||||||
|       // convert buffer to urlsafe base64
 |       signature = new Uint8Array(signature); // ArrayBuffer -> u8
 | ||||||
|       //return Enc.bufToUrlBase64(new Uint8Array(signature));
 |       // This will come back into play for CSRs, but not for JOSE
 | ||||||
|       return new Uint8Array(signature); |       if ('EC' === opts.jwk.kty && /x509|asn1/i.test(opts.format)) { | ||||||
|  |         return Keypairs._ecdsaJoseSigToAsn1Sig(signature); | ||||||
|  |       } else { | ||||||
|  |         // jose/jws/jwt
 | ||||||
|  |         return signature; | ||||||
|  |       } | ||||||
|     }); |     }); | ||||||
|   }); |   }); | ||||||
| }; | }; | ||||||
| @ -287,7 +246,6 @@ Keypairs._getName = function (opts) { | |||||||
|     return 'RSASSA-PKCS1-v1_5'; |     return 'RSASSA-PKCS1-v1_5'; | ||||||
|   } |   } | ||||||
| }; | }; | ||||||
| 
 |  | ||||||
| Keypairs._import = function (opts) { | Keypairs._import = function (opts) { | ||||||
|   return Promise.resolve().then(function () { |   return Promise.resolve().then(function () { | ||||||
|     var ops; |     var ops; | ||||||
| @ -301,7 +259,6 @@ Keypairs._import = function (opts) { | |||||||
|     opts.jwk.ext = true; |     opts.jwk.ext = true; | ||||||
|     opts.jwk.key_ops = ops; |     opts.jwk.key_ops = ops; | ||||||
| 
 | 
 | ||||||
|     console.log('jwk', opts.jwk); |  | ||||||
|     return window.crypto.subtle.importKey( |     return window.crypto.subtle.importKey( | ||||||
|       "jwk" |       "jwk" | ||||||
|     , opts.jwk |     , opts.jwk | ||||||
| @ -316,6 +273,30 @@ Keypairs._import = function (opts) { | |||||||
|     }); |     }); | ||||||
|   }); |   }); | ||||||
| }; | }; | ||||||
|  | // 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; } | ||||||
|  | |||||||
| @ -1,86 +0,0 @@ | |||||||
| /*global Promise*/ |  | ||||||
| (function (exports) { |  | ||||||
| 'use strict'; |  | ||||||
| 
 |  | ||||||
| var Keypairs = exports.Keypairs = {}; |  | ||||||
| 
 |  | ||||||
| Keypairs._stance = "We take the stance that if you're knowledgeable enough to" |  | ||||||
|   + " properly and securely use non-standard crypto then you shouldn't need Bluecrypt anyway."; |  | ||||||
| Keypairs._universal = "Bluecrypt only supports crypto with standard cross-browser and cross-platform support."; |  | ||||||
| Keypairs.generate = function (opts) { |  | ||||||
|   var wcOpts = {}; |  | ||||||
|   if (!opts) { |  | ||||||
|     opts = {}; |  | ||||||
|   } |  | ||||||
|   if (!opts.kty) { |  | ||||||
|     opts.kty = 'EC'; |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   // ECDSA has only the P curves and an associated bitlength |  | ||||||
|   if (/^EC/i.test(opts.kty)) { |  | ||||||
|     wcOpts.name = 'ECDSA'; |  | ||||||
|     if (!opts.namedCurve) { |  | ||||||
|       opts.namedCurve = 'P-256'; |  | ||||||
|     } |  | ||||||
|     wcOpts.namedCurve = opts.namedCurve; // true for supported curves |  | ||||||
|     if (/256/.test(wcOpts.namedCurve)) { |  | ||||||
|       wcOpts.namedCurve = 'P-256'; |  | ||||||
|       wcOpts.hash = { name: "SHA-256" }; |  | ||||||
|     } else if (/384/.test(wcOpts.namedCurve)) { |  | ||||||
|       wcOpts.namedCurve = 'P-384'; |  | ||||||
|       wcOpts.hash = { name: "SHA-384" }; |  | ||||||
|     } else { |  | ||||||
|       return Promise.Reject(new Error("'" + wcOpts.namedCurve + "' is not an NIST approved ECDSA namedCurve. " |  | ||||||
|         + " Please choose either 'P-256' or 'P-384'. " |  | ||||||
|         + Keypairs._stance)); |  | ||||||
|     } |  | ||||||
|   } else if (/^RSA$/i.test(opts.kty)) { |  | ||||||
|     // Support PSS? I don't think it's used for Let's Encrypt |  | ||||||
|     wcOpts.name = 'RSASSA-PKCS1-v1_5'; |  | ||||||
|     if (!opts.modulusLength) { |  | ||||||
|       opts.modulusLength = 2048; |  | ||||||
|     } |  | ||||||
|     wcOpts.modulusLength = opts.modulusLength; |  | ||||||
|     if (wcOpts.modulusLength >= 2048 && wcOpts.modulusLength < 3072) { |  | ||||||
|       // erring on the small side... for no good reason |  | ||||||
|       wcOpts.hash = { name: "SHA-256" }; |  | ||||||
|     } else if (wcOpts.modulusLength >= 3072 && wcOpts.modulusLength < 4096) { |  | ||||||
|       wcOpts.hash = { name: "SHA-384" }; |  | ||||||
|     } else if (wcOpts.modulusLength < 4097) { |  | ||||||
|       wcOpts.hash = { name: "SHA-512" }; |  | ||||||
|     } else { |  | ||||||
|       // Public key thumbprints should be paired with a hash of similar length, |  | ||||||
|       // so anything above SHA-512's keyspace would be left under-represented anyway. |  | ||||||
|       return Promise.Reject(new Error("'" + wcOpts.modulusLength + "' is not within the safe and universally" |  | ||||||
|         + " acceptable range of 2048-4096. Typically you should pick 2048, 3072, or 4096, though other values" |  | ||||||
|         + " divisible by 8 are allowed. " + Keypairs._stance)); |  | ||||||
|     } |  | ||||||
|     // TODO maybe allow this to be set to any of the standard values? |  | ||||||
|     wcOpts.publicExponent = new Uint8Array([0x01, 0x00, 0x01]); |  | ||||||
|   } else { |  | ||||||
|     return Promise.Reject(new Error("'" + opts.kty + "' is not a well-supported key type." |  | ||||||
|       + Keypairs._universal |  | ||||||
|       + " Please choose either 'EC' or 'RSA' keys.")); |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   var extractable = true; |  | ||||||
|   return window.crypto.subtle.generateKey( |  | ||||||
|     wcOpts |  | ||||||
|   , extractable |  | ||||||
|   , [ 'sign', 'verify' ] |  | ||||||
|   ).then(function (result) { |  | ||||||
|     return window.crypto.subtle.exportKey( |  | ||||||
|       "jwk" |  | ||||||
|     , result.privateKey |  | ||||||
|     ).then(function (privJwk) { |  | ||||||
|       // TODO remove |  | ||||||
|       console.log('private jwk:'); |  | ||||||
|       console.log(JSON.stringify(privJwk, null, 2)); |  | ||||||
|       return { |  | ||||||
|         privateKey: privJwk |  | ||||||
|       }; |  | ||||||
|     }); |  | ||||||
|   }); |  | ||||||
| }; |  | ||||||
| 
 |  | ||||||
| }(window)); |  | ||||||
| @ -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; | ||||||
|  | |||||||
							
								
								
									
										6
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										6
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							| @ -4,6 +4,12 @@ | |||||||
|   "lockfileVersion": 1, |   "lockfileVersion": 1, | ||||||
|   "requires": true, |   "requires": true, | ||||||
|   "dependencies": { |   "dependencies": { | ||||||
|  |     "@root/request": { | ||||||
|  |       "version": "1.3.10", | ||||||
|  |       "resolved": "https://registry.npmjs.org/@root/request/-/request-1.3.10.tgz", | ||||||
|  |       "integrity": "sha512-GSn8dfsGp0juJyXS9k7B/DjYm7Axe85wiCHfPs30eQ+/V6p2aqey45e1czb3ZwP+iPmzWCKXahhWnZhSDIil6w==", | ||||||
|  |       "dev": true | ||||||
|  |     }, | ||||||
|     "accepts": { |     "accepts": { | ||||||
|       "version": "1.3.6", |       "version": "1.3.6", | ||||||
|       "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.6.tgz", |       "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.6.tgz", | ||||||
|  | |||||||
| @ -29,6 +29,7 @@ | |||||||
|   "author": "AJ ONeal <coolaj86@gmail.com> (https://coolaj86.com/)", |   "author": "AJ ONeal <coolaj86@gmail.com> (https://coolaj86.com/)", | ||||||
|   "license": "MPL-2.0", |   "license": "MPL-2.0", | ||||||
|   "devDependencies": { |   "devDependencies": { | ||||||
|  |     "@root/request": "^1.3.10", | ||||||
|     "dig.js": "^1.3.9", |     "dig.js": "^1.3.9", | ||||||
|     "dns-suite": "^1.2.12", |     "dns-suite": "^1.2.12", | ||||||
|     "express": "^4.16.4" |     "express": "^4.16.4" | ||||||
|  | |||||||
							
								
								
									
										28
									
								
								server.js
									
									
									
									
									
								
							
							
						
						
									
										28
									
								
								server.js
									
									
									
									
									
								
							| @ -3,6 +3,7 @@ | |||||||
| var crypto = require('crypto'); | var crypto = require('crypto'); | ||||||
| //var dnsjs = require('dns-suite');
 | //var dnsjs = require('dns-suite');
 | ||||||
| var dig = require('dig.js/dns-request'); | var dig = require('dig.js/dns-request'); | ||||||
|  | var request = require('util').promisify(require('@root/request')); | ||||||
| var express = require('express'); | var express = require('express'); | ||||||
| var app = express(); | var app = express(); | ||||||
| 
 | 
 | ||||||
| @ -10,10 +11,9 @@ var nameservers = require('dns').getServers(); | |||||||
| var index = crypto.randomBytes(2).readUInt16BE(0) % nameservers.length; | var index = crypto.randomBytes(2).readUInt16BE(0) % nameservers.length; | ||||||
| var nameserver = nameservers[index]; | var nameserver = nameservers[index]; | ||||||
| 
 | 
 | ||||||
| app.use('/', express.static('./')); | app.use('/', express.static(__dirname)); | ||||||
| app.use('/api', express.json()); | app.use('/api', express.json()); | ||||||
| app.get('/api/dns/:domain', function (req, res, next) { | app.get('/api/dns/:domain', function (req, res, next) { | ||||||
|   console.log(req.params); |  | ||||||
|   var domain = req.params.domain; |   var domain = req.params.domain; | ||||||
|   var casedDomain = domain.toLowerCase().split('').map(function (ch) { |   var casedDomain = domain.toLowerCase().split('').map(function (ch) { | ||||||
|     // dns0x20 takes advantage of the fact that the binary operation for toUpperCase is
 |     // dns0x20 takes advantage of the fact that the binary operation for toUpperCase is
 | ||||||
| @ -117,9 +117,23 @@ app.get('/api/dns/:domain', function (req, res, next) { | |||||||
| 
 | 
 | ||||||
|   dig.resolveJson(query, opts); |   dig.resolveJson(query, opts); | ||||||
| }); | }); | ||||||
|  | app.get('/api/http', function (req, res) { | ||||||
|  |   var url = req.query.url; | ||||||
|  |   return request({ method: 'GET', url: url }).then(function (resp) { | ||||||
|  |     res.send(resp.body); | ||||||
|  |   }); | ||||||
|  | }); | ||||||
|  | app.get('/api/_acme_api_', function (req, res) { | ||||||
|  |   res.send({ success: true }); | ||||||
|  | }); | ||||||
| 
 | 
 | ||||||
| // curl -L http://localhost:3000/api/dns/example.com?type=A
 | module.exports = app; | ||||||
| console.log("Listening on localhost:3000"); | if (require.main === module) { | ||||||
| app.listen(3000); |   // curl -L http://localhost:3000/api/dns/example.com?type=A
 | ||||||
| console.log("Try this:"); |   console.info("Listening on localhost:3000"); | ||||||
| console.log("\tcurl -L 'http://localhost:3000/api/dns/example.com?type=A'"); |   app.listen(3000); | ||||||
|  |   console.info("Try this:"); | ||||||
|  |   console.info("\tcurl -L 'http://localhost:3000/api/_acme_api_/'"); | ||||||
|  |   console.info("\tcurl -L 'http://localhost:3000/api/dns/example.com?type=A'"); | ||||||
|  |   console.info("\tcurl -L 'http://localhost:3000/api/http/?url=https://example.com'"); | ||||||
|  | } | ||||||
|  | |||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user