diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..e5cdb84 --- /dev/null +++ b/.prettierrc @@ -0,0 +1,4 @@ +{ + "trailingComma": "none", + "useTabs": true +} diff --git a/README.md b/README.md index 2dbfa7b..232ef48 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,8 @@ -Greenlock™ in your Browser -========================= +# Greenlock™ in your Browser Taking greenlock™ (Let's Encrypt v2 / ACME client) where it's never been before: Your browser! -Official Site -============= +# Official Site This app is available at . @@ -14,14 +12,12 @@ If it doesn't, please open an issue to let us know why. We'd much rather improve the app than have a hundred different versions running in the wild. However, in keeping to our values we've released the source for others to inspect, improve, and modify. -Trademark Notice -================ +# Trademark Notice Greenlock™ is our trademark. If you do host your own copy of this app, please do provide attribution, but please also use your branding. -Install -======= +# Install ```bash git clone ssh://gitea@git.coolaj86.com:22042/coolaj86/greenlock.html.git @@ -30,8 +26,7 @@ pushd greenlock.html/ popd ``` -Usage -===== +# Usage Simply host from your webserver. diff --git a/app/index.html b/app/index.html index 4d657f1..133f964 100644 --- a/app/index.html +++ b/app/index.html @@ -1,262 +1,403 @@ + - - Greenlock™ - - + + Greenlock™ + + - - - + + + - - - + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - -
-
-
-
-
- - - -
-
Details
-
+ + + + + + + + + + + + + + + + + + + + +
+
+
+
+
+ + + +
+
Details
+
+
+
+ + + +
+
Verify domain
+
+
+
+ + + +
+
+
Install certificates
+
+
+ -
-
-
Greenlock
-
-
-
-

-

Certificates are valid for 90 days. Renewal is free :)

- -
- +
+
+ +
+
Greenlock
+
+
+ +

+

Certificates are valid for 90 days. Renewal is free :)

+ +
+ -
-
-
- - -
- - +
+
+
+ + +
+ + - - + + + - -
+ + +

Let's verify your domain

+
+
+ + +
+
+
+

Upload each file

+
+
+
+
+
FILENAME
+
...loading
+
+
+
+
CONTENTS
+
...loading
+
+
+ + + + + Download + +
+
+
+
LOCATION
+
..loading
+
+
+
+
+
+
+

Set each DNS Record

+
+
TXT Host
+
loading...
+
TXT Value
+
loading...
+
+
+

+ Warning: You should wait at least 30 seconds + after setting DNS records before continuing. +

+

+ Google DNS Users: You may need to wait up to + 5 minutes. +

+
+
-

Let's verify your domain

-
-
- - -
-
-
-

Upload each file

-
-
-
-
-
FILENAME
-
...loading
-
-
-
-
CONTENTS
-
...loading
-
-
- - - - - Download - -
-
-
-
LOCATION
-
..loading
-
-
-
-
-
-
-

Set each DNS Record

-
-
TXT Host
-
loading...
-
TXT Value
-
loading...
-
-
-

Warning: - You should wait at least 30 seconds after setting DNS records before continuing.

-

Google DNS Users: - You may need to wait up to 5 minutes.

-
-
+
+
+

Set each DNS Record (for wildcards)

+
+
TXT Host
+
loading...
+
TXT Value
+
loading...
+
+
+

+ Warning: You should wait at least 30 seconds + after setting DNS records before continuing. +

+

+ Google DNS: You may need to wait up to 5 + minutes. +

+
+
+
-
-
-

Set each DNS Record (for wildcards)

-
-
TXT Host
-
loading...
-
TXT Value
-
loading...
-
-
-

Warning: - You should wait at least 30 seconds after setting DNS records before continuing.

-

Google DNS: - You may need to wait up to 5 minutes.

-
-
-
+ +
- - + +
+ Verifying Domains... (give us 5 seconds or so...) + - - - Verifying Domains... (give us 5 seconds or so...) - - - -
+ - -
-
-

-
-
-
-              
-
-
- - - - - Download - -
-

-
-
-
-              
-
-
- - - - - Download - -
-
-

node.js https server example

-
    'use strict';
+				
+				
+					
+

+
+
+

+						
+
+ + + + + Download + +
+

+
+
+

+						
+
+ + + + + Download + +
+
+

node.js https server example

+
    'use strict';
 
     var https = require('https');
     var server = https.createServer({
@@ -325,9 +482,9 @@
       console.log('Listening on', this.address());
     })
             
-
+
- - -
+ -->
+ -
-
- A Root Project - | View Source (git) - | Terms of Service - | Privacy Policy -
- -
-
- + +
+
- - + + - - - + -
-
- + gtag("config", "UA-118745161-2"); + +
+ + diff --git a/app/js/bluecrypt-acme.js b/app/js/bluecrypt-acme.js index 0744c8e..3e8dd30 100644 --- a/app/js/bluecrypt-acme.js +++ b/app/js/bluecrypt-acme.js @@ -2,1814 +2,2186 @@ /* 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) { - -var Enc = exports.Enc = {}; - -Enc.bufToBin = function (buf) { - var bin = ''; - // cannot use .map() because Uint8Array would return only 0s - buf.forEach(function (ch) { - bin += String.fromCharCode(ch); - }); - return bin; -}; - -Enc.bufToHex = function toHex(u8) { - var hex = []; - var i, h; - var len = (u8.byteLength || u8.length); - - for (i = 0; i < len; i += 1) { - h = u8[i].toString(16); - if (h.length % 2) { h = '0' + h; } - hex.push(h); - } - - return hex.join('').toLowerCase(); -}; - -Enc.urlBase64ToBase64 = function urlsafeBase64ToBase64(str) { - var r = str % 4; - if (2 === r) { - str += '=='; - } else if (3 === r) { - str += '='; - } - return str.replace(/-/g, '+').replace(/_/g, '/'); -}; - -Enc.base64ToBuf = function (b64) { - return Enc.binToBuf(atob(b64)); -}; -Enc.binToBuf = function (bin) { - var arr = bin.split('').map(function (ch) { - return ch.charCodeAt(0); - }); - return 'undefined' !== typeof Uint8Array ? new Uint8Array(arr) : arr; -}; -Enc.bufToHex = function (u8) { - var hex = []; - var i, h; - var len = (u8.byteLength || u8.length); - - for (i = 0; i < len; i += 1) { - h = u8[i].toString(16); - if (h.length % 2) { h = '0' + h; } - hex.push(h); - } - - return hex.join('').toLowerCase(); -}; -Enc.numToHex = function (d) { - d = d.toString(16); - if (d.length % 2) { - return '0' + d; - } - return d; -}; - -Enc.bufToUrlBase64 = function (u8) { - return Enc.base64ToUrlBase64(Enc.bufToBase64(u8)); -}; - -Enc.base64ToUrlBase64 = function (str) { - return str.replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, ''); -}; - -Enc.bufToBase64 = function (u8) { - var bin = ''; - u8.forEach(function (i) { - bin += String.fromCharCode(i); - }); - return btoa(bin); -}; - -Enc.hexToBuf = function (hex) { - var arr = []; - hex.match(/.{2}/g).forEach(function (h) { - arr.push(parseInt(h, 16)); - }); - return 'undefined' !== typeof Uint8Array ? new Uint8Array(arr) : arr; -}; - -Enc.numToHex = function (d) { - d = d.toString(16); - if (d.length % 2) { - return '0' + d; - } - return d; -}; - - -// -// JWK to SSH (tested working) -// -Enc.base64ToHex = function (b64) { - var bin = atob(Enc.urlBase64ToBase64(b64)); - return Enc.binToHex(bin); -}; - -Enc.binToHex = function (bin) { - return bin.split('').map(function (ch) { - var h = ch.charCodeAt(0).toString(16); - if (h.length % 2) { h = '0' + h; } - return h; - }).join(''); -}; -// TODO are there any nuance differences here? -Enc.utf8ToHex = Enc.binToHex; - -Enc.hexToBase64 = function (hex) { - return btoa(Enc.hexToBin(hex)); -}; - -Enc.hexToBin = function (hex) { - return hex.match(/.{2}/g).map(function (h) { - return String.fromCharCode(parseInt(h, 16)); - }).join(''); -}; - -Enc.urlBase64ToBase64 = function urlsafeBase64ToBase64(str) { - var r = str % 4; - if (2 === r) { - str += '=='; - } else if (3 === r) { - str += '='; - } - return str.replace(/-/g, '+').replace(/_/g, '/'); -}; - - -}('undefined' !== typeof exports ? module.exports : window )); -;(function (exports) { -'use strict'; - -if (!exports.ASN1) { exports.ASN1 = {}; } -if (!exports.Enc) { exports.Enc = {}; } -if (!exports.PEM) { exports.PEM = {}; } - -var ASN1 = exports.ASN1; -var Enc = exports.Enc; -var PEM = exports.PEM; - -// -// Packer -// - -// Almost every ASN.1 type that's important for CSR -// can be represented generically with only a few rules. -exports.ASN1 = function ASN1(/*type, hexstrings...*/) { - var args = Array.prototype.slice.call(arguments); - var typ = args.shift(); - var str = args.join('').replace(/\s+/g, '').toLowerCase(); - var len = (str.length/2); - var lenlen = 0; - var hex = typ; - - // We can't have an odd number of hex chars - if (len !== Math.round(len)) { - throw new Error("invalid hex"); - } - - // The first byte of any ASN.1 sequence is the type (Sequence, Integer, etc) - // The second byte is either the size of the value, or the size of its size - - // 1. If the second byte is < 0x80 (128) it is considered the size - // 2. If it is > 0x80 then it describes the number of bytes of the size - // ex: 0x82 means the next 2 bytes describe the size of the value - // 3. The special case of exactly 0x80 is "indefinite" length (to end-of-file) - - if (len > 127) { - lenlen += 1; - while (len > 255) { - lenlen += 1; - len = len >> 8; - } - } - - if (lenlen) { hex += Enc.numToHex(0x80 + lenlen); } - return hex + Enc.numToHex(str.length/2) + str; -}; - -// The Integer type has some special rules -ASN1.UInt = function UINT() { - var str = Array.prototype.slice.call(arguments).join(''); - var first = parseInt(str.slice(0, 2), 16); - - // If the first byte is 0x80 or greater, the number is considered negative - // Therefore we add a '00' prefix if the 0x80 bit is set - if (0x80 & first) { str = '00' + str; } - - return ASN1('02', str); -}; - -// The Bit String type also has a special rule -ASN1.BitStr = function BITSTR() { - var str = Array.prototype.slice.call(arguments).join(''); - // '00' is a mask of how many bits of the next byte to ignore - return ASN1('03', '00' + str); -}; - -ASN1.pack = function (arr) { - var typ = Enc.numToHex(arr[0]); - var str = ''; - if (Array.isArray(arr[1])) { - arr[1].forEach(function (a) { - str += ASN1.pack(a); - }); - } else if ('string' === typeof arr[1]) { - str = arr[1]; - } else { - throw new Error("unexpected array"); - } - if ('03' === typ) { - return ASN1.BitStr(str); - } else if ('02' === typ) { - return ASN1.UInt(str); - } else { - return ASN1(typ, str); - } -}; -Object.keys(ASN1).forEach(function (k) { - exports.ASN1[k] = ASN1[k]; -}); -ASN1 = exports.ASN1; - -PEM.packBlock = function (opts) { - // TODO allow for headers? - return '-----BEGIN ' + opts.type + '-----\n' - + Enc.bufToBase64(opts.bytes).match(/.{1,64}/g).join('\n') + '\n' - + '-----END ' + opts.type + '-----' - ; -}; - -Enc.bufToBase64 = function (u8) { - var bin = ''; - u8.forEach(function (i) { - bin += String.fromCharCode(i); - }); - return btoa(bin); -}; - -Enc.hexToBuf = function (hex) { - var arr = []; - hex.match(/.{2}/g).forEach(function (h) { - arr.push(parseInt(h, 16)); - }); - return 'undefined' !== typeof Uint8Array ? new Uint8Array(arr) : arr; -}; - -Enc.numToHex = function (d) { - d = d.toString(16); - if (d.length % 2) { - return '0' + d; - } - return d; -}; - -}('undefined' !== typeof window ? window : module.exports)); -(function (exports) { - 'use strict'; - - var x509 = exports.x509 = {}; - var ASN1 = exports.ASN1; - var Enc = exports.Enc; - - // 1.2.840.10045.3.1.7 - // prime256v1 (ANSI X9.62 named elliptic curve) - var OBJ_ID_EC = '06 08 2A8648CE3D030107'.replace(/\s+/g, '').toLowerCase(); - // 1.3.132.0.34 - // secp384r1 (SECG (Certicom) named elliptic curve) - var OBJ_ID_EC_384 = '06 05 2B81040022'.replace(/\s+/g, '').toLowerCase(); - // 1.2.840.10045.2.1 - // ecPublicKey (ANSI X9.62 public key type) - var OBJ_ID_EC_PUB = '06 07 2A8648CE3D0201'.replace(/\s+/g, '').toLowerCase(); - - x509.parseSec1 = function parseEcOnlyPrivkey(u8, jwk) { - var index = 7; - var len = 32; - var olen = OBJ_ID_EC.length / 2; - - if ("P-384" === jwk.crv) { - olen = OBJ_ID_EC_384.length / 2; - index = 8; - len = 48; - } - if (len !== u8[index - 1]) { - throw new Error("Unexpected bitlength " + len); - } - - // private part is d - var d = u8.slice(index, index + len); - // compression bit index - var ci = index + len + 2 + olen + 2 + 3; - var c = u8[ci]; - var x, y; - - if (0x04 === c) { - y = u8.slice(ci + 1 + len, ci + 1 + len + len); - } else if (0x02 !== c) { - throw new Error("not a supported EC private key"); - } - x = u8.slice(ci + 1, ci + 1 + len); - - return { - kty: jwk.kty - , crv: jwk.crv - , d: Enc.bufToUrlBase64(d) - //, dh: Enc.bufToHex(d) - , x: Enc.bufToUrlBase64(x) - //, xh: Enc.bufToHex(x) - , y: Enc.bufToUrlBase64(y) - //, yh: Enc.bufToHex(y) - }; - }; - - x509.packPkcs1 = function (jwk) { - var n = ASN1.UInt(Enc.base64ToHex(jwk.n)); - var e = ASN1.UInt(Enc.base64ToHex(jwk.e)); - - if (!jwk.d) { - return Enc.hexToBuf(ASN1('30', n, e)); - } - - return Enc.hexToBuf(ASN1('30' - , ASN1.UInt('00') - , n - , e - , ASN1.UInt(Enc.base64ToHex(jwk.d)) - , ASN1.UInt(Enc.base64ToHex(jwk.p)) - , ASN1.UInt(Enc.base64ToHex(jwk.q)) - , ASN1.UInt(Enc.base64ToHex(jwk.dp)) - , ASN1.UInt(Enc.base64ToHex(jwk.dq)) - , ASN1.UInt(Enc.base64ToHex(jwk.qi)) - )); - }; - - x509.parsePkcs8 = function parseEcPkcs8(u8, jwk) { - var index = 24 + (OBJ_ID_EC.length / 2); - var len = 32; - if ("P-384" === jwk.crv) { - index = 24 + (OBJ_ID_EC_384.length / 2) + 2; - len = 48; - } - - //console.log(index, u8.slice(index)); - if (0x04 !== u8[index]) { - //console.log(jwk); - throw new Error("privkey not found"); - } - var d = u8.slice(index + 2, index + 2 + len); - var ci = index + 2 + len + 5; - var xi = ci + 1; - var x = u8.slice(xi, xi + len); - var yi = xi + len; - var y; - if (0x04 === u8[ci]) { - y = u8.slice(yi, yi + len); - } else if (0x02 !== u8[ci]) { - throw new Error("invalid compression bit (expected 0x04 or 0x02)"); - } - - return { - kty: jwk.kty - , crv: jwk.crv - , d: Enc.bufToUrlBase64(d) - //, dh: Enc.bufToHex(d) - , x: Enc.bufToUrlBase64(x) - //, xh: Enc.bufToHex(x) - , y: Enc.bufToUrlBase64(y) - //, yh: Enc.bufToHex(y) - }; - }; - - x509.parseSpki = function parsePem(u8, jwk) { - var ci = 16 + OBJ_ID_EC.length / 2; - var len = 32; - - if ("P-384" === jwk.crv) { - ci = 16 + OBJ_ID_EC_384.length / 2; - len = 48; - } - - var c = u8[ci]; - var xi = ci + 1; - var x = u8.slice(xi, xi + len); - var yi = xi + len; - var y; - if (0x04 === c) { - y = u8.slice(yi, yi + len); - } else if (0x02 !== c) { - throw new Error("not a supported EC private key"); - } - - return { - kty: jwk.kty - , crv: jwk.crv - , x: Enc.bufToUrlBase64(x) - //, xh: Enc.bufToHex(x) - , y: Enc.bufToUrlBase64(y) - //, yh: Enc.bufToHex(y) - }; - }; - x509.parsePkix = x509.parseSpki; - - x509.packSec1 = function (jwk) { - var d = Enc.base64ToHex(jwk.d); - var x = Enc.base64ToHex(jwk.x); - var y = Enc.base64ToHex(jwk.y); - var objId = ('P-256' === jwk.crv) ? OBJ_ID_EC : OBJ_ID_EC_384; - return Enc.hexToBuf( - ASN1('30' - , ASN1.UInt('01') - , ASN1('04', d) - , ASN1('A0', objId) - , ASN1('A1', ASN1.BitStr('04' + x + y))) - ); - }; - /** - * take a private jwk and creates a der from it - * @param {*} jwk - */ - x509.packPkcs8 = function (jwk) { - if ('RSA' === jwk.kty) { - if (!jwk.d) { - // Public RSA - return Enc.hexToBuf(ASN1('30' - , ASN1('30' - , ASN1('06', '2a864886f70d010101') - , ASN1('05') - ) - , ASN1.BitStr(ASN1('30' - , ASN1.UInt(Enc.base64ToHex(jwk.n)) - , ASN1.UInt(Enc.base64ToHex(jwk.e)) - )) - )); - } - - // Private RSA - return Enc.hexToBuf(ASN1('30' - , ASN1.UInt('00') - , ASN1('30' - , ASN1('06', '2a864886f70d010101') - , ASN1('05') - ) - , ASN1('04' - , ASN1('30' - , ASN1.UInt('00') - , ASN1.UInt(Enc.base64ToHex(jwk.n)) - , ASN1.UInt(Enc.base64ToHex(jwk.e)) - , ASN1.UInt(Enc.base64ToHex(jwk.d)) - , ASN1.UInt(Enc.base64ToHex(jwk.p)) - , ASN1.UInt(Enc.base64ToHex(jwk.q)) - , ASN1.UInt(Enc.base64ToHex(jwk.dp)) - , ASN1.UInt(Enc.base64ToHex(jwk.dq)) - , ASN1.UInt(Enc.base64ToHex(jwk.qi)) - ) - ) - )); - } - - var d = Enc.base64ToHex(jwk.d); - var x = Enc.base64ToHex(jwk.x); - var y = Enc.base64ToHex(jwk.y); - var objId = ('P-256' === jwk.crv) ? OBJ_ID_EC : OBJ_ID_EC_384; - return Enc.hexToBuf( - ASN1('30' - , ASN1.UInt('00') - , ASN1('30' - , OBJ_ID_EC_PUB - , objId - ) - , ASN1('04' - , ASN1('30' - , ASN1.UInt('01') - , ASN1('04', d) - , ASN1('A1', ASN1.BitStr('04' + x + y))))) - ); - }; - x509.packSpki = function (jwk) { - if (/EC/i.test(jwk.kty)) { - return x509.packSpkiEc(jwk); - } - return x509.packSpkiRsa(jwk); - }; - x509.packSpkiRsa = function (jwk) { - if (!jwk.d) { - // Public RSA - return Enc.hexToBuf(ASN1('30' - , ASN1('30' - , ASN1('06', '2a864886f70d010101') - , ASN1('05') - ) - , ASN1.BitStr(ASN1('30' - , ASN1.UInt(Enc.base64ToHex(jwk.n)) - , ASN1.UInt(Enc.base64ToHex(jwk.e)) - )) - )); - } - - // Private RSA - return Enc.hexToBuf(ASN1('30' - , ASN1.UInt('00') - , ASN1('30' - , ASN1('06', '2a864886f70d010101') - , ASN1('05') - ) - , ASN1('04' - , ASN1('30' - , ASN1.UInt('00') - , ASN1.UInt(Enc.base64ToHex(jwk.n)) - , ASN1.UInt(Enc.base64ToHex(jwk.e)) - , ASN1.UInt(Enc.base64ToHex(jwk.d)) - , ASN1.UInt(Enc.base64ToHex(jwk.p)) - , ASN1.UInt(Enc.base64ToHex(jwk.q)) - , ASN1.UInt(Enc.base64ToHex(jwk.dp)) - , ASN1.UInt(Enc.base64ToHex(jwk.dq)) - , ASN1.UInt(Enc.base64ToHex(jwk.qi)) - ) - ) - )); -}; - x509.packSpkiEc = function (jwk) { - var x = Enc.base64ToHex(jwk.x); - var y = Enc.base64ToHex(jwk.y); - var objId = ('P-256' === jwk.crv) ? OBJ_ID_EC : OBJ_ID_EC_384; - return Enc.hexToBuf( - ASN1('30' - , ASN1('30' - , OBJ_ID_EC_PUB - , objId - ) - , ASN1.BitStr('04' + x + y)) - ); - }; - x509.packPkix = x509.packSpki; - -}('undefined' !== typeof module ? module.exports : window)); +(function(exports) { + var Enc = (exports.Enc = {}); + + Enc.bufToBin = function(buf) { + var bin = ""; + // cannot use .map() because Uint8Array would return only 0s + buf.forEach(function(ch) { + bin += String.fromCharCode(ch); + }); + return bin; + }; + + Enc.bufToHex = function toHex(u8) { + var hex = []; + var i, h; + var len = u8.byteLength || u8.length; + + for (i = 0; i < len; i += 1) { + h = u8[i].toString(16); + if (h.length % 2) { + h = "0" + h; + } + hex.push(h); + } + + return hex.join("").toLowerCase(); + }; + + Enc.urlBase64ToBase64 = function urlsafeBase64ToBase64(str) { + var r = str % 4; + if (2 === r) { + str += "=="; + } else if (3 === r) { + str += "="; + } + return str.replace(/-/g, "+").replace(/_/g, "/"); + }; + + Enc.base64ToBuf = function(b64) { + return Enc.binToBuf(atob(b64)); + }; + Enc.binToBuf = function(bin) { + var arr = bin.split("").map(function(ch) { + return ch.charCodeAt(0); + }); + return "undefined" !== typeof Uint8Array ? new Uint8Array(arr) : arr; + }; + Enc.bufToHex = function(u8) { + var hex = []; + var i, h; + var len = u8.byteLength || u8.length; + + for (i = 0; i < len; i += 1) { + h = u8[i].toString(16); + if (h.length % 2) { + h = "0" + h; + } + hex.push(h); + } + + return hex.join("").toLowerCase(); + }; + Enc.numToHex = function(d) { + d = d.toString(16); + if (d.length % 2) { + return "0" + d; + } + return d; + }; + + Enc.bufToUrlBase64 = function(u8) { + return Enc.base64ToUrlBase64(Enc.bufToBase64(u8)); + }; + + Enc.base64ToUrlBase64 = function(str) { + return str + .replace(/\+/g, "-") + .replace(/\//g, "_") + .replace(/=/g, ""); + }; + + Enc.bufToBase64 = function(u8) { + var bin = ""; + u8.forEach(function(i) { + bin += String.fromCharCode(i); + }); + return btoa(bin); + }; + + Enc.hexToBuf = function(hex) { + var arr = []; + hex.match(/.{2}/g).forEach(function(h) { + arr.push(parseInt(h, 16)); + }); + return "undefined" !== typeof Uint8Array ? new Uint8Array(arr) : arr; + }; + + Enc.numToHex = function(d) { + d = d.toString(16); + if (d.length % 2) { + return "0" + d; + } + return d; + }; + + // + // JWK to SSH (tested working) + // + Enc.base64ToHex = function(b64) { + var bin = atob(Enc.urlBase64ToBase64(b64)); + return Enc.binToHex(bin); + }; + + Enc.binToHex = function(bin) { + return bin + .split("") + .map(function(ch) { + var h = ch.charCodeAt(0).toString(16); + if (h.length % 2) { + h = "0" + h; + } + return h; + }) + .join(""); + }; + // TODO are there any nuance differences here? + Enc.utf8ToHex = Enc.binToHex; + + Enc.hexToBase64 = function(hex) { + return btoa(Enc.hexToBin(hex)); + }; + + Enc.hexToBin = function(hex) { + return hex + .match(/.{2}/g) + .map(function(h) { + return String.fromCharCode(parseInt(h, 16)); + }) + .join(""); + }; + + Enc.urlBase64ToBase64 = function urlsafeBase64ToBase64(str) { + var r = str % 4; + if (2 === r) { + str += "=="; + } else if (3 === r) { + str += "="; + } + return str.replace(/-/g, "+").replace(/_/g, "/"); + }; +})("undefined" !== typeof exports ? module.exports : window); +(function(exports) { + "use strict"; + + if (!exports.ASN1) { + exports.ASN1 = {}; + } + if (!exports.Enc) { + exports.Enc = {}; + } + if (!exports.PEM) { + exports.PEM = {}; + } + + var ASN1 = exports.ASN1; + var Enc = exports.Enc; + var PEM = exports.PEM; + + // + // Packer + // + + // Almost every ASN.1 type that's important for CSR + // can be represented generically with only a few rules. + exports.ASN1 = function ASN1(/*type, hexstrings...*/) { + var args = Array.prototype.slice.call(arguments); + var typ = args.shift(); + var str = args + .join("") + .replace(/\s+/g, "") + .toLowerCase(); + var len = str.length / 2; + var lenlen = 0; + var hex = typ; + + // We can't have an odd number of hex chars + if (len !== Math.round(len)) { + throw new Error("invalid hex"); + } + + // The first byte of any ASN.1 sequence is the type (Sequence, Integer, etc) + // The second byte is either the size of the value, or the size of its size + + // 1. If the second byte is < 0x80 (128) it is considered the size + // 2. If it is > 0x80 then it describes the number of bytes of the size + // ex: 0x82 means the next 2 bytes describe the size of the value + // 3. The special case of exactly 0x80 is "indefinite" length (to end-of-file) + + if (len > 127) { + lenlen += 1; + while (len > 255) { + lenlen += 1; + len = len >> 8; + } + } + + if (lenlen) { + hex += Enc.numToHex(0x80 + lenlen); + } + return hex + Enc.numToHex(str.length / 2) + str; + }; + + // The Integer type has some special rules + ASN1.UInt = function UINT() { + var str = Array.prototype.slice.call(arguments).join(""); + var first = parseInt(str.slice(0, 2), 16); + + // If the first byte is 0x80 or greater, the number is considered negative + // Therefore we add a '00' prefix if the 0x80 bit is set + if (0x80 & first) { + str = "00" + str; + } + + return ASN1("02", str); + }; + + // The Bit String type also has a special rule + ASN1.BitStr = function BITSTR() { + var str = Array.prototype.slice.call(arguments).join(""); + // '00' is a mask of how many bits of the next byte to ignore + return ASN1("03", "00" + str); + }; + + ASN1.pack = function(arr) { + var typ = Enc.numToHex(arr[0]); + var str = ""; + if (Array.isArray(arr[1])) { + arr[1].forEach(function(a) { + str += ASN1.pack(a); + }); + } else if ("string" === typeof arr[1]) { + str = arr[1]; + } else { + throw new Error("unexpected array"); + } + if ("03" === typ) { + return ASN1.BitStr(str); + } else if ("02" === typ) { + return ASN1.UInt(str); + } else { + return ASN1(typ, str); + } + }; + Object.keys(ASN1).forEach(function(k) { + exports.ASN1[k] = ASN1[k]; + }); + ASN1 = exports.ASN1; + + PEM.packBlock = function(opts) { + // TODO allow for headers? + return ( + "-----BEGIN " + + opts.type + + "-----\n" + + Enc.bufToBase64(opts.bytes) + .match(/.{1,64}/g) + .join("\n") + + "\n" + + "-----END " + + opts.type + + "-----" + ); + }; + + Enc.bufToBase64 = function(u8) { + var bin = ""; + u8.forEach(function(i) { + bin += String.fromCharCode(i); + }); + return btoa(bin); + }; + + Enc.hexToBuf = function(hex) { + var arr = []; + hex.match(/.{2}/g).forEach(function(h) { + arr.push(parseInt(h, 16)); + }); + return "undefined" !== typeof Uint8Array ? new Uint8Array(arr) : arr; + }; + + Enc.numToHex = function(d) { + d = d.toString(16); + if (d.length % 2) { + return "0" + d; + } + return d; + }; +})("undefined" !== typeof window ? window : module.exports); +(function(exports) { + "use strict"; + + var x509 = (exports.x509 = {}); + var ASN1 = exports.ASN1; + var Enc = exports.Enc; + + // 1.2.840.10045.3.1.7 + // prime256v1 (ANSI X9.62 named elliptic curve) + var OBJ_ID_EC = "06 08 2A8648CE3D030107".replace(/\s+/g, "").toLowerCase(); + // 1.3.132.0.34 + // secp384r1 (SECG (Certicom) named elliptic curve) + var OBJ_ID_EC_384 = "06 05 2B81040022".replace(/\s+/g, "").toLowerCase(); + // 1.2.840.10045.2.1 + // ecPublicKey (ANSI X9.62 public key type) + var OBJ_ID_EC_PUB = "06 07 2A8648CE3D0201".replace(/\s+/g, "").toLowerCase(); + + x509.parseSec1 = function parseEcOnlyPrivkey(u8, jwk) { + var index = 7; + var len = 32; + var olen = OBJ_ID_EC.length / 2; + + if ("P-384" === jwk.crv) { + olen = OBJ_ID_EC_384.length / 2; + index = 8; + len = 48; + } + if (len !== u8[index - 1]) { + throw new Error("Unexpected bitlength " + len); + } + + // private part is d + var d = u8.slice(index, index + len); + // compression bit index + var ci = index + len + 2 + olen + 2 + 3; + var c = u8[ci]; + var x, y; + + if (0x04 === c) { + y = u8.slice(ci + 1 + len, ci + 1 + len + len); + } else if (0x02 !== c) { + throw new Error("not a supported EC private key"); + } + x = u8.slice(ci + 1, ci + 1 + len); + + return { + kty: jwk.kty, + crv: jwk.crv, + d: Enc.bufToUrlBase64(d), + //, dh: Enc.bufToHex(d) + x: Enc.bufToUrlBase64(x), + //, xh: Enc.bufToHex(x) + y: Enc.bufToUrlBase64(y) + //, yh: Enc.bufToHex(y) + }; + }; + + x509.packPkcs1 = function(jwk) { + var n = ASN1.UInt(Enc.base64ToHex(jwk.n)); + var e = ASN1.UInt(Enc.base64ToHex(jwk.e)); + + if (!jwk.d) { + return Enc.hexToBuf(ASN1("30", n, e)); + } + + return Enc.hexToBuf( + ASN1( + "30", + ASN1.UInt("00"), + n, + e, + ASN1.UInt(Enc.base64ToHex(jwk.d)), + ASN1.UInt(Enc.base64ToHex(jwk.p)), + ASN1.UInt(Enc.base64ToHex(jwk.q)), + ASN1.UInt(Enc.base64ToHex(jwk.dp)), + ASN1.UInt(Enc.base64ToHex(jwk.dq)), + ASN1.UInt(Enc.base64ToHex(jwk.qi)) + ) + ); + }; + + x509.parsePkcs8 = function parseEcPkcs8(u8, jwk) { + var index = 24 + OBJ_ID_EC.length / 2; + var len = 32; + if ("P-384" === jwk.crv) { + index = 24 + OBJ_ID_EC_384.length / 2 + 2; + len = 48; + } + + //console.log(index, u8.slice(index)); + if (0x04 !== u8[index]) { + //console.log(jwk); + throw new Error("privkey not found"); + } + var d = u8.slice(index + 2, index + 2 + len); + var ci = index + 2 + len + 5; + var xi = ci + 1; + var x = u8.slice(xi, xi + len); + var yi = xi + len; + var y; + if (0x04 === u8[ci]) { + y = u8.slice(yi, yi + len); + } else if (0x02 !== u8[ci]) { + throw new Error("invalid compression bit (expected 0x04 or 0x02)"); + } + + return { + kty: jwk.kty, + crv: jwk.crv, + d: Enc.bufToUrlBase64(d), + //, dh: Enc.bufToHex(d) + x: Enc.bufToUrlBase64(x), + //, xh: Enc.bufToHex(x) + y: Enc.bufToUrlBase64(y) + //, yh: Enc.bufToHex(y) + }; + }; + + x509.parseSpki = function parsePem(u8, jwk) { + var ci = 16 + OBJ_ID_EC.length / 2; + var len = 32; + + if ("P-384" === jwk.crv) { + ci = 16 + OBJ_ID_EC_384.length / 2; + len = 48; + } + + var c = u8[ci]; + var xi = ci + 1; + var x = u8.slice(xi, xi + len); + var yi = xi + len; + var y; + if (0x04 === c) { + y = u8.slice(yi, yi + len); + } else if (0x02 !== c) { + throw new Error("not a supported EC private key"); + } + + return { + kty: jwk.kty, + crv: jwk.crv, + x: Enc.bufToUrlBase64(x), + //, xh: Enc.bufToHex(x) + y: Enc.bufToUrlBase64(y) + //, yh: Enc.bufToHex(y) + }; + }; + x509.parsePkix = x509.parseSpki; + + x509.packSec1 = function(jwk) { + var d = Enc.base64ToHex(jwk.d); + var x = Enc.base64ToHex(jwk.x); + var y = Enc.base64ToHex(jwk.y); + var objId = "P-256" === jwk.crv ? OBJ_ID_EC : OBJ_ID_EC_384; + return Enc.hexToBuf( + ASN1( + "30", + ASN1.UInt("01"), + ASN1("04", d), + ASN1("A0", objId), + ASN1("A1", ASN1.BitStr("04" + x + y)) + ) + ); + }; + /** + * take a private jwk and creates a der from it + * @param {*} jwk + */ + x509.packPkcs8 = function(jwk) { + if ("RSA" === jwk.kty) { + if (!jwk.d) { + // Public RSA + return Enc.hexToBuf( + ASN1( + "30", + ASN1("30", ASN1("06", "2a864886f70d010101"), ASN1("05")), + ASN1.BitStr( + ASN1( + "30", + ASN1.UInt(Enc.base64ToHex(jwk.n)), + ASN1.UInt(Enc.base64ToHex(jwk.e)) + ) + ) + ) + ); + } + + // Private RSA + return Enc.hexToBuf( + ASN1( + "30", + ASN1.UInt("00"), + ASN1("30", ASN1("06", "2a864886f70d010101"), ASN1("05")), + ASN1( + "04", + ASN1( + "30", + ASN1.UInt("00"), + ASN1.UInt(Enc.base64ToHex(jwk.n)), + ASN1.UInt(Enc.base64ToHex(jwk.e)), + ASN1.UInt(Enc.base64ToHex(jwk.d)), + ASN1.UInt(Enc.base64ToHex(jwk.p)), + ASN1.UInt(Enc.base64ToHex(jwk.q)), + ASN1.UInt(Enc.base64ToHex(jwk.dp)), + ASN1.UInt(Enc.base64ToHex(jwk.dq)), + ASN1.UInt(Enc.base64ToHex(jwk.qi)) + ) + ) + ) + ); + } + + var d = Enc.base64ToHex(jwk.d); + var x = Enc.base64ToHex(jwk.x); + var y = Enc.base64ToHex(jwk.y); + var objId = "P-256" === jwk.crv ? OBJ_ID_EC : OBJ_ID_EC_384; + return Enc.hexToBuf( + ASN1( + "30", + ASN1.UInt("00"), + ASN1("30", OBJ_ID_EC_PUB, objId), + ASN1( + "04", + ASN1( + "30", + ASN1.UInt("01"), + ASN1("04", d), + ASN1("A1", ASN1.BitStr("04" + x + y)) + ) + ) + ) + ); + }; + x509.packSpki = function(jwk) { + if (/EC/i.test(jwk.kty)) { + return x509.packSpkiEc(jwk); + } + return x509.packSpkiRsa(jwk); + }; + x509.packSpkiRsa = function(jwk) { + if (!jwk.d) { + // Public RSA + return Enc.hexToBuf( + ASN1( + "30", + ASN1("30", ASN1("06", "2a864886f70d010101"), ASN1("05")), + ASN1.BitStr( + ASN1( + "30", + ASN1.UInt(Enc.base64ToHex(jwk.n)), + ASN1.UInt(Enc.base64ToHex(jwk.e)) + ) + ) + ) + ); + } + + // Private RSA + return Enc.hexToBuf( + ASN1( + "30", + ASN1.UInt("00"), + ASN1("30", ASN1("06", "2a864886f70d010101"), ASN1("05")), + ASN1( + "04", + ASN1( + "30", + ASN1.UInt("00"), + ASN1.UInt(Enc.base64ToHex(jwk.n)), + ASN1.UInt(Enc.base64ToHex(jwk.e)), + ASN1.UInt(Enc.base64ToHex(jwk.d)), + ASN1.UInt(Enc.base64ToHex(jwk.p)), + ASN1.UInt(Enc.base64ToHex(jwk.q)), + ASN1.UInt(Enc.base64ToHex(jwk.dp)), + ASN1.UInt(Enc.base64ToHex(jwk.dq)), + ASN1.UInt(Enc.base64ToHex(jwk.qi)) + ) + ) + ) + ); + }; + x509.packSpkiEc = function(jwk) { + var x = Enc.base64ToHex(jwk.x); + var y = Enc.base64ToHex(jwk.y); + var objId = "P-256" === jwk.crv ? OBJ_ID_EC : OBJ_ID_EC_384; + return Enc.hexToBuf( + ASN1("30", ASN1("30", OBJ_ID_EC_PUB, objId), ASN1.BitStr("04" + x + y)) + ); + }; + x509.packPkix = x509.packSpki; +})("undefined" !== typeof module ? module.exports : window); /*global Promise*/ -(function (exports) { -'use strict'; - -var EC = exports.Eckles = {}; -var x509 = exports.x509; -if ('undefined' !== typeof module) { module.exports = EC; } -var PEM = exports.PEM; -var SSH = exports.SSH; -var Enc = {}; -var textEncoder = new TextEncoder(); - -EC._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."; -EC._universal = "Bluecrypt only supports crypto with standard cross-browser and cross-platform support."; -EC.generate = function (opts) { - var wcOpts = {}; - if (!opts) { opts = {}; } - if (!opts.kty) { opts.kty = 'EC'; } - - // ECDSA has only the P curves and an associated bitlength - 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'. " - + EC._stance)); - } - - 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) { - privJwk.key_ops = undefined; - privJwk.ext = undefined; - return { - private: privJwk - , public: EC.neuter({ jwk: privJwk }) - }; - }); - }); -}; - -EC.export = function (opts) { - return Promise.resolve().then(function () { - if (!opts || !opts.jwk || 'object' !== typeof opts.jwk) { - throw new Error("must pass { jwk: jwk } as a JSON object"); - } - var jwk = JSON.parse(JSON.stringify(opts.jwk)); - var format = opts.format; - if (opts.public || -1 !== [ 'spki', 'pkix', 'ssh', 'rfc4716' ].indexOf(format)) { - jwk.d = null; - } - if ('EC' !== jwk.kty) { - throw new Error("options.jwk.kty must be 'EC' for EC keys"); - } - if (!jwk.d) { - if (!format || -1 !== [ 'spki', 'pkix' ].indexOf(format)) { - format = 'spki'; - } else if (-1 !== [ 'ssh', 'rfc4716' ].indexOf(format)) { - format = 'ssh'; - } else { - throw new Error("options.format must be 'spki' or 'ssh' for public EC keys, not (" - + typeof format + ") " + format); - } - } else { - if (!format || 'sec1' === format) { - format = 'sec1'; - } else if ('pkcs8' !== format) { - throw new Error("options.format must be 'sec1' or 'pkcs8' for private EC keys, not '" + format + "'"); - } - } - if (-1 === [ 'P-256', 'P-384' ].indexOf(jwk.crv)) { - throw new Error("options.jwk.crv must be either P-256 or P-384 for EC keys, not '" + jwk.crv + "'"); - } - if (!jwk.y) { - throw new Error("options.jwk.y must be a urlsafe base64-encoded either P-256 or P-384"); - } - - if ('sec1' === format) { - return PEM.packBlock({ type: "EC PRIVATE KEY", bytes: x509.packSec1(jwk) }); - } else if ('pkcs8' === format) { - return PEM.packBlock({ type: "PRIVATE KEY", bytes: x509.packPkcs8(jwk) }); - } else if (-1 !== [ 'spki', 'pkix' ].indexOf(format)) { - return PEM.packBlock({ type: "PUBLIC KEY", bytes: x509.packSpki(jwk) }); - } else if (-1 !== [ 'ssh', 'rfc4716' ].indexOf(format)) { - return SSH.packSsh(jwk); - } else { - throw new Error("Sanity Error: reached unreachable code block with format: " + format); - } - }); -}; -EC.pack = function (opts) { - return Promise.resolve().then(function () { - return EC.exportSync(opts); - }); -}; - -// Chopping off the private parts is now part of the public API. -// I thought it sounded a little too crude at first, but it really is the best name in every possible way. -EC.neuter = function (opts) { - // trying to find the best balance of an immutable copy with custom attributes - var jwk = {}; - Object.keys(opts.jwk).forEach(function (k) { - if ('undefined' === typeof opts.jwk[k]) { return; } - // ignore EC private parts - if ('d' === k) { return; } - jwk[k] = JSON.parse(JSON.stringify(opts.jwk[k])); - }); - return jwk; -}; - -// https://stackoverflow.com/questions/42588786/how-to-fingerprint-a-jwk -EC.__thumbprint = function (jwk) { - // Use the same entropy for SHA as for key - var alg = 'SHA-256'; - if (/384/.test(jwk.crv)) { - alg = 'SHA-384'; - } - return window.crypto.subtle.digest( - { name: alg } - , textEncoder.encode('{"crv":"' + jwk.crv + '","kty":"EC","x":"' + jwk.x + '","y":"' + jwk.y + '"}') - ).then(function (hash) { - return Enc.bufToUrlBase64(new Uint8Array(hash)); - }); -}; - -EC.thumbprint = function (opts) { - return Promise.resolve().then(function () { - var jwk; - if ('EC' === opts.kty) { - jwk = opts; - } else if (opts.jwk) { - jwk = opts.jwk; - } else { - return EC.import(opts).then(function (jwk) { - return EC.__thumbprint(jwk); - }); - } - return EC.__thumbprint(jwk); - }); -}; - -Enc.bufToUrlBase64 = function (u8) { - return Enc.bufToBase64(u8) - .replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, ''); -}; - -Enc.bufToBase64 = function (u8) { - var bin = ''; - u8.forEach(function (i) { - bin += String.fromCharCode(i); - }); - return btoa(bin); -}; - -}('undefined' !== typeof module ? module.exports : window)); +(function(exports) { + "use strict"; + + var EC = (exports.Eckles = {}); + var x509 = exports.x509; + if ("undefined" !== typeof module) { + module.exports = EC; + } + var PEM = exports.PEM; + var SSH = exports.SSH; + var Enc = {}; + var textEncoder = new TextEncoder(); + + EC._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."; + EC._universal = + "Bluecrypt only supports crypto with standard cross-browser and cross-platform support."; + EC.generate = function(opts) { + var wcOpts = {}; + if (!opts) { + opts = {}; + } + if (!opts.kty) { + opts.kty = "EC"; + } + + // ECDSA has only the P curves and an associated bitlength + 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'. " + + EC._stance + ) + ); + } + + 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) { + privJwk.key_ops = undefined; + privJwk.ext = undefined; + return { + private: privJwk, + public: EC.neuter({ jwk: privJwk }) + }; + }); + }); + }; + + EC.export = function(opts) { + return Promise.resolve().then(function() { + if (!opts || !opts.jwk || "object" !== typeof opts.jwk) { + throw new Error("must pass { jwk: jwk } as a JSON object"); + } + var jwk = JSON.parse(JSON.stringify(opts.jwk)); + var format = opts.format; + if ( + opts.public || + -1 !== ["spki", "pkix", "ssh", "rfc4716"].indexOf(format) + ) { + jwk.d = null; + } + if ("EC" !== jwk.kty) { + throw new Error("options.jwk.kty must be 'EC' for EC keys"); + } + if (!jwk.d) { + if (!format || -1 !== ["spki", "pkix"].indexOf(format)) { + format = "spki"; + } else if (-1 !== ["ssh", "rfc4716"].indexOf(format)) { + format = "ssh"; + } else { + throw new Error( + "options.format must be 'spki' or 'ssh' for public EC keys, not (" + + typeof format + + ") " + + format + ); + } + } else { + if (!format || "sec1" === format) { + format = "sec1"; + } else if ("pkcs8" !== format) { + throw new Error( + "options.format must be 'sec1' or 'pkcs8' for private EC keys, not '" + + format + + "'" + ); + } + } + if (-1 === ["P-256", "P-384"].indexOf(jwk.crv)) { + throw new Error( + "options.jwk.crv must be either P-256 or P-384 for EC keys, not '" + + jwk.crv + + "'" + ); + } + if (!jwk.y) { + throw new Error( + "options.jwk.y must be a urlsafe base64-encoded either P-256 or P-384" + ); + } + + if ("sec1" === format) { + return PEM.packBlock({ + type: "EC PRIVATE KEY", + bytes: x509.packSec1(jwk) + }); + } else if ("pkcs8" === format) { + return PEM.packBlock({ + type: "PRIVATE KEY", + bytes: x509.packPkcs8(jwk) + }); + } else if (-1 !== ["spki", "pkix"].indexOf(format)) { + return PEM.packBlock({ type: "PUBLIC KEY", bytes: x509.packSpki(jwk) }); + } else if (-1 !== ["ssh", "rfc4716"].indexOf(format)) { + return SSH.packSsh(jwk); + } else { + throw new Error( + "Sanity Error: reached unreachable code block with format: " + format + ); + } + }); + }; + EC.pack = function(opts) { + return Promise.resolve().then(function() { + return EC.exportSync(opts); + }); + }; + + // Chopping off the private parts is now part of the public API. + // I thought it sounded a little too crude at first, but it really is the best name in every possible way. + EC.neuter = function(opts) { + // trying to find the best balance of an immutable copy with custom attributes + var jwk = {}; + Object.keys(opts.jwk).forEach(function(k) { + if ("undefined" === typeof opts.jwk[k]) { + return; + } + // ignore EC private parts + if ("d" === k) { + return; + } + jwk[k] = JSON.parse(JSON.stringify(opts.jwk[k])); + }); + return jwk; + }; + + // https://stackoverflow.com/questions/42588786/how-to-fingerprint-a-jwk + EC.__thumbprint = function(jwk) { + // Use the same entropy for SHA as for key + var alg = "SHA-256"; + if (/384/.test(jwk.crv)) { + alg = "SHA-384"; + } + return window.crypto.subtle + .digest( + { name: alg }, + textEncoder.encode( + '{"crv":"' + + jwk.crv + + '","kty":"EC","x":"' + + jwk.x + + '","y":"' + + jwk.y + + '"}' + ) + ) + .then(function(hash) { + return Enc.bufToUrlBase64(new Uint8Array(hash)); + }); + }; + + EC.thumbprint = function(opts) { + return Promise.resolve().then(function() { + var jwk; + if ("EC" === opts.kty) { + jwk = opts; + } else if (opts.jwk) { + jwk = opts.jwk; + } else { + return EC.import(opts).then(function(jwk) { + return EC.__thumbprint(jwk); + }); + } + return EC.__thumbprint(jwk); + }); + }; + + Enc.bufToUrlBase64 = function(u8) { + return Enc.bufToBase64(u8) + .replace(/\+/g, "-") + .replace(/\//g, "_") + .replace(/=/g, ""); + }; + + Enc.bufToBase64 = function(u8) { + var bin = ""; + u8.forEach(function(i) { + bin += String.fromCharCode(i); + }); + return btoa(bin); + }; +})("undefined" !== typeof module ? module.exports : window); /*global Promise*/ -(function (exports) { -'use strict'; - -var RSA = exports.Rasha = {}; -var x509 = exports.x509; -if ('undefined' !== typeof module) { module.exports = RSA; } -var PEM = exports.PEM; -var SSH = exports.SSH; -var Enc = {}; -var textEncoder = new TextEncoder(); - -RSA._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."; -RSA._universal = "Bluecrypt only supports crypto with standard cross-browser and cross-platform support."; -RSA.generate = function (opts) { - var wcOpts = {}; - if (!opts) { opts = {}; } - if (!opts.kty) { opts.kty = 'RSA'; } - - // 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. " + RSA._stance)); - } - // TODO maybe allow this to be set to any of the standard values? - wcOpts.publicExponent = new Uint8Array([0x01, 0x00, 0x01]); - - 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) { - return { - private: privJwk - , public: RSA.neuter({ jwk: privJwk }) - }; - }); - }); -}; - -// Chopping off the private parts is now part of the public API. -// I thought it sounded a little too crude at first, but it really is the best name in every possible way. -RSA.neuter = function (opts) { - // trying to find the best balance of an immutable copy with custom attributes - var jwk = {}; - Object.keys(opts.jwk).forEach(function (k) { - if ('undefined' === typeof opts.jwk[k]) { return; } - // ignore RSA private parts - if (-1 !== ['d', 'p', 'q', 'dp', 'dq', 'qi'].indexOf(k)) { return; } - jwk[k] = JSON.parse(JSON.stringify(opts.jwk[k])); - }); - return jwk; -}; - -// https://stackoverflow.com/questions/42588786/how-to-fingerprint-a-jwk -RSA.__thumbprint = function (jwk) { - // Use the same entropy for SHA as for key - var len = Math.floor(jwk.n.length * 0.75); - var alg = 'SHA-256'; - // TODO this may be a bug - // need to confirm that the padding is no more or less than 1 byte - if (len >= 511) { - alg = 'SHA-512'; - } else if (len >= 383) { - alg = 'SHA-384'; - } - return window.crypto.subtle.digest( - { name: alg } - , textEncoder.encode('{"e":"' + jwk.e + '","kty":"RSA","n":"' + jwk.n + '"}') - ).then(function (hash) { - return Enc.bufToUrlBase64(new Uint8Array(hash)); - }); -}; - -RSA.thumbprint = function (opts) { - return Promise.resolve().then(function () { - var jwk; - if ('EC' === opts.kty) { - jwk = opts; - } else if (opts.jwk) { - jwk = opts.jwk; - } else { - return RSA.import(opts).then(function (jwk) { - return RSA.__thumbprint(jwk); - }); - } - return RSA.__thumbprint(jwk); - }); -}; - -RSA.export = function (opts) { - return Promise.resolve().then(function () { - if (!opts || !opts.jwk || 'object' !== typeof opts.jwk) { - throw new Error("must pass { jwk: jwk }"); - } - var jwk = JSON.parse(JSON.stringify(opts.jwk)); - var format = opts.format; - var pub = opts.public; - if (pub || -1 !== [ 'spki', 'pkix', 'ssh', 'rfc4716' ].indexOf(format)) { - jwk = RSA.neuter({ jwk: jwk }); - } - if ('RSA' !== jwk.kty) { - throw new Error("options.jwk.kty must be 'RSA' for RSA keys"); - } - if (!jwk.p) { - // TODO test for n and e - pub = true; - if (!format || 'pkcs1' === format) { - format = 'pkcs1'; - } else if (-1 !== [ 'spki', 'pkix' ].indexOf(format)) { - format = 'spki'; - } else if (-1 !== [ 'ssh', 'rfc4716' ].indexOf(format)) { - format = 'ssh'; - } else { - throw new Error("options.format must be 'spki', 'pkcs1', or 'ssh' for public RSA keys, not (" - + typeof format + ") " + format); - } - } else { - // TODO test for all necessary keys (d, p, q ...) - if (!format || 'pkcs1' === format) { - format = 'pkcs1'; - } else if ('pkcs8' !== format) { - throw new Error("options.format must be 'pkcs1' or 'pkcs8' for private RSA keys"); - } - } - - if ('pkcs1' === format) { - if (jwk.d) { - return PEM.packBlock({ type: "RSA PRIVATE KEY", bytes: x509.packPkcs1(jwk) }); - } else { - return PEM.packBlock({ type: "RSA PUBLIC KEY", bytes: x509.packPkcs1(jwk) }); - } - } else if ('pkcs8' === format) { - return PEM.packBlock({ type: "PRIVATE KEY", bytes: x509.packPkcs8(jwk) }); - } else if (-1 !== [ 'spki', 'pkix' ].indexOf(format)) { - return PEM.packBlock({ type: "PUBLIC KEY", bytes: x509.packSpki(jwk) }); - } else if (-1 !== [ 'ssh', 'rfc4716' ].indexOf(format)) { - return SSH.pack({ jwk: jwk, comment: opts.comment }); - } else { - throw new Error("Sanity Error: reached unreachable code block with format: " + format); - } - }); -}; -RSA.pack = function (opts) { - // wrapped in a promise for API compatibility - // with the forthcoming browser version - // (and potential future native node capability) - return Promise.resolve().then(function () { - return RSA.export(opts); - }); -}; - -Enc.bufToUrlBase64 = function (u8) { - return Enc.bufToBase64(u8) - .replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, ''); -}; - -Enc.bufToBase64 = function (u8) { - var bin = ''; - u8.forEach(function (i) { - bin += String.fromCharCode(i); - }); - return btoa(bin); -}; - -}('undefined' !== typeof module ? module.exports : window)); +(function(exports) { + "use strict"; + + var RSA = (exports.Rasha = {}); + var x509 = exports.x509; + if ("undefined" !== typeof module) { + module.exports = RSA; + } + var PEM = exports.PEM; + var SSH = exports.SSH; + var Enc = {}; + var textEncoder = new TextEncoder(); + + RSA._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."; + RSA._universal = + "Bluecrypt only supports crypto with standard cross-browser and cross-platform support."; + RSA.generate = function(opts) { + var wcOpts = {}; + if (!opts) { + opts = {}; + } + if (!opts.kty) { + opts.kty = "RSA"; + } + + // 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. " + + RSA._stance + ) + ); + } + // TODO maybe allow this to be set to any of the standard values? + wcOpts.publicExponent = new Uint8Array([0x01, 0x00, 0x01]); + + 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) { + return { + private: privJwk, + public: RSA.neuter({ jwk: privJwk }) + }; + }); + }); + }; + + // Chopping off the private parts is now part of the public API. + // I thought it sounded a little too crude at first, but it really is the best name in every possible way. + RSA.neuter = function(opts) { + // trying to find the best balance of an immutable copy with custom attributes + var jwk = {}; + Object.keys(opts.jwk).forEach(function(k) { + if ("undefined" === typeof opts.jwk[k]) { + return; + } + // ignore RSA private parts + if (-1 !== ["d", "p", "q", "dp", "dq", "qi"].indexOf(k)) { + return; + } + jwk[k] = JSON.parse(JSON.stringify(opts.jwk[k])); + }); + return jwk; + }; + + // https://stackoverflow.com/questions/42588786/how-to-fingerprint-a-jwk + RSA.__thumbprint = function(jwk) { + // Use the same entropy for SHA as for key + var len = Math.floor(jwk.n.length * 0.75); + var alg = "SHA-256"; + // TODO this may be a bug + // need to confirm that the padding is no more or less than 1 byte + if (len >= 511) { + alg = "SHA-512"; + } else if (len >= 383) { + alg = "SHA-384"; + } + return window.crypto.subtle + .digest( + { name: alg }, + textEncoder.encode( + '{"e":"' + jwk.e + '","kty":"RSA","n":"' + jwk.n + '"}' + ) + ) + .then(function(hash) { + return Enc.bufToUrlBase64(new Uint8Array(hash)); + }); + }; + + RSA.thumbprint = function(opts) { + return Promise.resolve().then(function() { + var jwk; + if ("EC" === opts.kty) { + jwk = opts; + } else if (opts.jwk) { + jwk = opts.jwk; + } else { + return RSA.import(opts).then(function(jwk) { + return RSA.__thumbprint(jwk); + }); + } + return RSA.__thumbprint(jwk); + }); + }; + + RSA.export = function(opts) { + return Promise.resolve().then(function() { + if (!opts || !opts.jwk || "object" !== typeof opts.jwk) { + throw new Error("must pass { jwk: jwk }"); + } + var jwk = JSON.parse(JSON.stringify(opts.jwk)); + var format = opts.format; + var pub = opts.public; + if (pub || -1 !== ["spki", "pkix", "ssh", "rfc4716"].indexOf(format)) { + jwk = RSA.neuter({ jwk: jwk }); + } + if ("RSA" !== jwk.kty) { + throw new Error("options.jwk.kty must be 'RSA' for RSA keys"); + } + if (!jwk.p) { + // TODO test for n and e + pub = true; + if (!format || "pkcs1" === format) { + format = "pkcs1"; + } else if (-1 !== ["spki", "pkix"].indexOf(format)) { + format = "spki"; + } else if (-1 !== ["ssh", "rfc4716"].indexOf(format)) { + format = "ssh"; + } else { + throw new Error( + "options.format must be 'spki', 'pkcs1', or 'ssh' for public RSA keys, not (" + + typeof format + + ") " + + format + ); + } + } else { + // TODO test for all necessary keys (d, p, q ...) + if (!format || "pkcs1" === format) { + format = "pkcs1"; + } else if ("pkcs8" !== format) { + throw new Error( + "options.format must be 'pkcs1' or 'pkcs8' for private RSA keys" + ); + } + } + + if ("pkcs1" === format) { + if (jwk.d) { + return PEM.packBlock({ + type: "RSA PRIVATE KEY", + bytes: x509.packPkcs1(jwk) + }); + } else { + return PEM.packBlock({ + type: "RSA PUBLIC KEY", + bytes: x509.packPkcs1(jwk) + }); + } + } else if ("pkcs8" === format) { + return PEM.packBlock({ + type: "PRIVATE KEY", + bytes: x509.packPkcs8(jwk) + }); + } else if (-1 !== ["spki", "pkix"].indexOf(format)) { + return PEM.packBlock({ type: "PUBLIC KEY", bytes: x509.packSpki(jwk) }); + } else if (-1 !== ["ssh", "rfc4716"].indexOf(format)) { + return SSH.pack({ jwk: jwk, comment: opts.comment }); + } else { + throw new Error( + "Sanity Error: reached unreachable code block with format: " + format + ); + } + }); + }; + RSA.pack = function(opts) { + // wrapped in a promise for API compatibility + // with the forthcoming browser version + // (and potential future native node capability) + return Promise.resolve().then(function() { + return RSA.export(opts); + }); + }; + + Enc.bufToUrlBase64 = function(u8) { + return Enc.bufToBase64(u8) + .replace(/\+/g, "-") + .replace(/\//g, "_") + .replace(/=/g, ""); + }; + + Enc.bufToBase64 = function(u8) { + var bin = ""; + u8.forEach(function(i) { + bin += String.fromCharCode(i); + }); + return btoa(bin); + }; +})("undefined" !== typeof module ? module.exports : window); /*global Promise*/ -(function (exports) { -'use strict'; - -var Keypairs = exports.Keypairs = {}; -var Rasha = exports.Rasha; -var Eckles = exports.Eckles; -var Enc = exports.Enc || {}; - -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) { - opts = opts || {}; - var p; - if (!opts.kty) { opts.kty = opts.type; } - if (!opts.kty) { opts.kty = 'EC'; } - if (/^EC/i.test(opts.kty)) { - p = Eckles.generate(opts); - } else if (/^RSA$/i.test(opts.kty)) { - p = Rasha.generate(opts); - } else { - return Promise.Reject(new Error("'" + opts.kty + "' is not a well-supported key type." - + Keypairs._universal - + " Please choose 'EC', or 'RSA' if you have good reason to.")); - } - return p.then(function (pair) { - return Keypairs.thumbprint({ jwk: pair.public }).then(function (thumb) { - pair.private.kid = thumb; // maybe not the same id on the private key? - pair.public.kid = thumb; - return pair; - }); - }); -}; - -Keypairs.export = function (opts) { - return Eckles.export(opts).catch(function (err) { - return Rasha.export(opts).catch(function () { - return Promise.reject(err); - }); - }); -}; - - -/** - * Chopping off the private parts is now part of the public API. - * I thought it sounded a little too crude at first, but it really is the best name in every possible way. - */ -Keypairs.neuter = function (opts) { - /** trying to find the best balance of an immutable copy with custom attributes */ - var jwk = {}; - Object.keys(opts.jwk).forEach(function (k) { - if ('undefined' === typeof opts.jwk[k]) { return; } - // ignore RSA and EC private parts - if (-1 !== ['d', 'p', 'q', 'dp', 'dq', 'qi'].indexOf(k)) { return; } - jwk[k] = JSON.parse(JSON.stringify(opts.jwk[k])); - }); - return jwk; -}; - -Keypairs.thumbprint = function (opts) { - return Promise.resolve().then(function () { - if (/EC/i.test(opts.jwk.kty)) { - return Eckles.thumbprint(opts); - } else { - return Rasha.thumbprint(opts); - } - }); -}; - -Keypairs.publish = function (opts) { - if ('object' !== typeof opts.jwk || !opts.jwk.kty) { throw new Error("invalid jwk: " + JSON.stringify(opts.jwk)); } - - /** returns a copy */ - var jwk = Keypairs.neuter(opts); - - if (jwk.exp) { - jwk.exp = setTime(jwk.exp); - } else { - if (opts.exp) { jwk.exp = setTime(opts.exp); } - else if (opts.expiresIn) { jwk.exp = Math.round(Date.now()/1000) + opts.expiresIn; } - else if (opts.expiresAt) { jwk.exp = opts.expiresAt; } - } - if (!jwk.use && false !== jwk.use) { jwk.use = "sig"; } - - if (jwk.kid) { return Promise.resolve(jwk); } - return Keypairs.thumbprint({ jwk: jwk }).then(function (thumb) { jwk.kid = thumb; return jwk; }); -}; - -// JWT a.k.a. JWS with Claims using Compact Serialization -Keypairs.signJwt = function (opts) { - return Keypairs.thumbprint({ jwk: opts.jwk }).then(function (thumb) { - var header = opts.header || {}; - var claims = JSON.parse(JSON.stringify(opts.claims || {})); - header.typ = 'JWT'; - - if (!header.kid) { header.kid = thumb; } - if (!header.alg && opts.alg) { header.alg = opts.alg; } - if (!claims.iat && (false === claims.iat || false === opts.iat)) { - claims.iat = undefined; - } else if (!claims.iat) { - claims.iat = Math.round(Date.now()/1000); - } - - if (opts.exp) { - claims.exp = setTime(opts.exp); - } else if (!claims.exp && (false === claims.exp || false === opts.exp)) { - claims.exp = undefined; - } else if (!claims.exp) { - throw new Error("opts.claims.exp should be the expiration date as seconds, human form (i.e. '1h' or '15m') or false"); - } - - if (opts.iss) { claims.iss = opts.iss; } - if (!claims.iss && (false === claims.iss || false === opts.iss)) { - claims.iss = undefined; - } else if (!claims.iss) { - throw new Error("opts.claims.iss should be in the form of https://example.com/, a secure OIDC base url"); - } - - return Keypairs.signJws({ - jwk: opts.jwk - , pem: opts.pem - , protected: header - , header: undefined - , payload: claims - }).then(function (jws) { - return [ jws.protected, jws.payload, jws.signature ].join('.'); - }); - }); -}; - -Keypairs.signJws = function (opts) { - return Keypairs.thumbprint(opts).then(function (thumb) { - - function alg() { - if (!opts.jwk) { - throw new Error("opts.jwk must exist and must declare 'typ'"); - } - if (opts.jwk.alg) { return opts.jwk.alg; } - var typ = ('RSA' === opts.jwk.kty) ? "RS" : "ES"; - return typ + Keypairs._getBits(opts); - } - - function sign() { - var protect = opts.protected; - var payload = opts.payload; - - // Compute JWS signature - var protectedHeader = ""; - // Because unprotected headers are allowed, regrettably... - // https://stackoverflow.com/a/46288694 - if (false !== protect) { - if (!protect) { protect = {}; } - if (!protect.alg) { protect.alg = alg(); } - // There's a particular request where ACME / Let's Encrypt explicitly doesn't use a kid - if (false === protect.kid) { protect.kid = undefined; } - else if (!protect.kid) { protect.kid = thumb; } - protectedHeader = JSON.stringify(protect); - } - - // Trying to detect if it's a plain object (not Buffer, ArrayBuffer, Array, Uint8Array, etc) - if (payload && ('string' !== typeof payload) - && ('undefined' === typeof payload.byteLength) - && ('undefined' === typeof payload.buffer) - ) { - payload = JSON.stringify(payload); - } - // Converting to a buffer, even if it was just converted to a string - if ('string' === typeof payload) { - payload = Enc.binToBuf(payload); - } - - // node specifies RSA-SHAxxx even when it's actually ecdsa (it's all encoded x509 shasums anyway) - var protected64 = Enc.strToUrlBase64(protectedHeader); - var payload64 = Enc.bufToUrlBase64(payload); - var msg = protected64 + '.' + payload64; - - return Keypairs._sign(opts, msg).then(function (buf) { - var signedMsg = { - protected: protected64 - , payload: payload64 - , signature: Enc.bufToUrlBase64(buf) - }; - - return signedMsg; - }); - } - - if (opts.jwk) { - return sign(); - } else { - return Keypairs.import({ pem: opts.pem }).then(function (pair) { - opts.jwk = pair.private; - return sign(); - }); - } - }); -}; - -Keypairs._sign = function (opts, payload) { - return Keypairs._import(opts).then(function (privkey) { - if ('string' === typeof payload) { - payload = (new TextEncoder()).encode(payload); - } - return window.crypto.subtle.sign( - { name: Keypairs._getName(opts) - , hash: { name: 'SHA-' + Keypairs._getBits(opts) } - } - , privkey - , payload - ).then(function (signature) { - signature = new Uint8Array(signature); // ArrayBuffer -> u8 - // This will come back into play for CSRs, but not for JOSE - if ('EC' === opts.jwk.kty && /x509|asn1/i.test(opts.format)) { - return Keypairs._ecdsaJoseSigToAsn1Sig(signature); - } else { - // jose/jws/jwt - return signature; - } - }); - }); -}; -Keypairs._getBits = function (opts) { - if (opts.alg) { return opts.alg.replace(/[a-z\-]/ig, ''); } - // base64 len to byte len - var len = Math.floor((opts.jwk.n||'').length * 0.75); - - // TODO this may be a bug - // need to confirm that the padding is no more or less than 1 byte - if (/521/.test(opts.jwk.crv) || len >= 511) { - return '512'; - } else if (/384/.test(opts.jwk.crv) || len >= 383) { - return '384'; - } - - return '256'; -}; -Keypairs._getName = function (opts) { - if (/EC/i.test(opts.jwk.kty)) { - return 'ECDSA'; - } else { - return 'RSASSA-PKCS1-v1_5'; - } -}; -Keypairs._import = function (opts) { - return Promise.resolve().then(function () { - var ops; - // all private keys just happen to have a 'd' - if (opts.jwk.d) { - ops = [ 'sign' ]; - } else { - ops = [ 'verify' ]; - } - // gotta mark it as extractable, as if it matters - opts.jwk.ext = true; - opts.jwk.key_ops = ops; - - return window.crypto.subtle.importKey( - "jwk" - , opts.jwk - , { name: Keypairs._getName(opts) - , namedCurve: opts.jwk.crv - , hash: { name: 'SHA-' + Keypairs._getBits(opts) } } - , true - , ops - ).then(function (privkey) { - delete opts.jwk.ext; - return privkey; - }); - }); -}; -// ECDSA JOSE / JWS / JWT signatures differ from "normal" ASN1/X509 ECDSA signatures -// https://tools.ietf.org/html/rfc7518#section-3.4 -Keypairs._ecdsaJoseSigToAsn1Sig = function (bufsig) { - // it's easier to do the manipulation in the browser with an array - bufsig = Array.from(bufsig); - var hlen = bufsig.length / 2; // should be even - var r = bufsig.slice(0, hlen); - var s = bufsig.slice(hlen); - // unpad positive ints less than 32 bytes wide - while (!r[0]) { r = r.slice(1); } - while (!s[0]) { s = s.slice(1); } - // pad (or re-pad) ambiguously non-negative BigInts, up to 33 bytes wide - if (0x80 & r[0]) { r.unshift(0); } - if (0x80 & s[0]) { s.unshift(0); } - - var len = 2 + r.length + 2 + s.length; - var head = [0x30]; - // hard code 0x80 + 1 because it won't be longer than - // two SHA512 plus two pad bytes (130 bytes <= 256) - if (len >= 0x80) { head.push(0x81); } - head.push(len); - - return Uint8Array.from(head.concat([0x02, r.length], r, [0x02, s.length], s)); -}; - -function setTime(time) { - if ('number' === typeof time) { return time; } - - var t = time.match(/^(\-?\d+)([dhms])$/i); - if (!t || !t[0]) { - throw new Error("'" + time + "' should be datetime in seconds or human-readable format (i.e. 3d, 1h, 15m, 30s"); - } - - var now = Math.round(Date.now()/1000); - var num = parseInt(t[1], 10); - var unit = t[2]; - var mult = 1; - switch(unit) { - // fancy fallthrough, what fun! - case 'd': - mult *= 24; - /*falls through*/ - case 'h': - mult *= 60; - /*falls through*/ - case 'm': - mult *= 60; - /*falls through*/ - case 's': - mult *= 1; - } - - return now + (mult * num); -} - -Enc.hexToBuf = function (hex) { - var arr = []; - hex.match(/.{2}/g).forEach(function (h) { - arr.push(parseInt(h, 16)); - }); - return 'undefined' !== typeof Uint8Array ? new Uint8Array(arr) : arr; -}; -Enc.strToUrlBase64 = function (str) { - return Enc.bufToUrlBase64(Enc.binToBuf(str)); -}; -Enc.binToBuf = function (bin) { - var arr = bin.split('').map(function (ch) { - return ch.charCodeAt(0); - }); - return 'undefined' !== typeof Uint8Array ? new Uint8Array(arr) : arr; -}; - -}('undefined' !== typeof module ? module.exports : window)); +(function(exports) { + "use strict"; + + var Keypairs = (exports.Keypairs = {}); + var Rasha = exports.Rasha; + var Eckles = exports.Eckles; + var Enc = exports.Enc || {}; + + 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) { + opts = opts || {}; + var p; + if (!opts.kty) { + opts.kty = opts.type; + } + if (!opts.kty) { + opts.kty = "EC"; + } + if (/^EC/i.test(opts.kty)) { + p = Eckles.generate(opts); + } else if (/^RSA$/i.test(opts.kty)) { + p = Rasha.generate(opts); + } else { + return Promise.Reject( + new Error( + "'" + + opts.kty + + "' is not a well-supported key type." + + Keypairs._universal + + " Please choose 'EC', or 'RSA' if you have good reason to." + ) + ); + } + return p.then(function(pair) { + return Keypairs.thumbprint({ jwk: pair.public }).then(function(thumb) { + pair.private.kid = thumb; // maybe not the same id on the private key? + pair.public.kid = thumb; + return pair; + }); + }); + }; + + Keypairs.export = function(opts) { + return Eckles.export(opts).catch(function(err) { + return Rasha.export(opts).catch(function() { + return Promise.reject(err); + }); + }); + }; + + /** + * Chopping off the private parts is now part of the public API. + * I thought it sounded a little too crude at first, but it really is the best name in every possible way. + */ + Keypairs.neuter = function(opts) { + /** trying to find the best balance of an immutable copy with custom attributes */ + var jwk = {}; + Object.keys(opts.jwk).forEach(function(k) { + if ("undefined" === typeof opts.jwk[k]) { + return; + } + // ignore RSA and EC private parts + if (-1 !== ["d", "p", "q", "dp", "dq", "qi"].indexOf(k)) { + return; + } + jwk[k] = JSON.parse(JSON.stringify(opts.jwk[k])); + }); + return jwk; + }; + + Keypairs.thumbprint = function(opts) { + return Promise.resolve().then(function() { + if (/EC/i.test(opts.jwk.kty)) { + return Eckles.thumbprint(opts); + } else { + return Rasha.thumbprint(opts); + } + }); + }; + + Keypairs.publish = function(opts) { + if ("object" !== typeof opts.jwk || !opts.jwk.kty) { + throw new Error("invalid jwk: " + JSON.stringify(opts.jwk)); + } + + /** returns a copy */ + var jwk = Keypairs.neuter(opts); + + if (jwk.exp) { + jwk.exp = setTime(jwk.exp); + } else { + if (opts.exp) { + jwk.exp = setTime(opts.exp); + } else if (opts.expiresIn) { + jwk.exp = Math.round(Date.now() / 1000) + opts.expiresIn; + } else if (opts.expiresAt) { + jwk.exp = opts.expiresAt; + } + } + if (!jwk.use && false !== jwk.use) { + jwk.use = "sig"; + } + + if (jwk.kid) { + return Promise.resolve(jwk); + } + return Keypairs.thumbprint({ jwk: jwk }).then(function(thumb) { + jwk.kid = thumb; + return jwk; + }); + }; + + // JWT a.k.a. JWS with Claims using Compact Serialization + Keypairs.signJwt = function(opts) { + return Keypairs.thumbprint({ jwk: opts.jwk }).then(function(thumb) { + var header = opts.header || {}; + var claims = JSON.parse(JSON.stringify(opts.claims || {})); + header.typ = "JWT"; + + if (!header.kid) { + header.kid = thumb; + } + if (!header.alg && opts.alg) { + header.alg = opts.alg; + } + if (!claims.iat && (false === claims.iat || false === opts.iat)) { + claims.iat = undefined; + } else if (!claims.iat) { + claims.iat = Math.round(Date.now() / 1000); + } + + if (opts.exp) { + claims.exp = setTime(opts.exp); + } else if (!claims.exp && (false === claims.exp || false === opts.exp)) { + claims.exp = undefined; + } else if (!claims.exp) { + throw new Error( + "opts.claims.exp should be the expiration date as seconds, human form (i.e. '1h' or '15m') or false" + ); + } + + if (opts.iss) { + claims.iss = opts.iss; + } + if (!claims.iss && (false === claims.iss || false === opts.iss)) { + claims.iss = undefined; + } else if (!claims.iss) { + throw new Error( + "opts.claims.iss should be in the form of https://example.com/, a secure OIDC base url" + ); + } + + return Keypairs.signJws({ + jwk: opts.jwk, + pem: opts.pem, + protected: header, + header: undefined, + payload: claims + }).then(function(jws) { + return [jws.protected, jws.payload, jws.signature].join("."); + }); + }); + }; + + Keypairs.signJws = function(opts) { + return Keypairs.thumbprint(opts).then(function(thumb) { + function alg() { + if (!opts.jwk) { + throw new Error("opts.jwk must exist and must declare 'typ'"); + } + if (opts.jwk.alg) { + return opts.jwk.alg; + } + var typ = "RSA" === opts.jwk.kty ? "RS" : "ES"; + return typ + Keypairs._getBits(opts); + } + + function sign() { + var protect = opts.protected; + var payload = opts.payload; + + // Compute JWS signature + var protectedHeader = ""; + // Because unprotected headers are allowed, regrettably... + // https://stackoverflow.com/a/46288694 + if (false !== protect) { + if (!protect) { + protect = {}; + } + if (!protect.alg) { + protect.alg = alg(); + } + // There's a particular request where ACME / Let's Encrypt explicitly doesn't use a kid + if (false === protect.kid) { + protect.kid = undefined; + } else if (!protect.kid) { + protect.kid = thumb; + } + protectedHeader = JSON.stringify(protect); + } + + // Trying to detect if it's a plain object (not Buffer, ArrayBuffer, Array, Uint8Array, etc) + if ( + payload && + "string" !== typeof payload && + "undefined" === typeof payload.byteLength && + "undefined" === typeof payload.buffer + ) { + payload = JSON.stringify(payload); + } + // Converting to a buffer, even if it was just converted to a string + if ("string" === typeof payload) { + payload = Enc.binToBuf(payload); + } + + // node specifies RSA-SHAxxx even when it's actually ecdsa (it's all encoded x509 shasums anyway) + var protected64 = Enc.strToUrlBase64(protectedHeader); + var payload64 = Enc.bufToUrlBase64(payload); + var msg = protected64 + "." + payload64; + + return Keypairs._sign(opts, msg).then(function(buf) { + var signedMsg = { + protected: protected64, + payload: payload64, + signature: Enc.bufToUrlBase64(buf) + }; + + return signedMsg; + }); + } + + if (opts.jwk) { + return sign(); + } else { + return Keypairs.import({ pem: opts.pem }).then(function(pair) { + opts.jwk = pair.private; + return sign(); + }); + } + }); + }; + + Keypairs._sign = function(opts, payload) { + return Keypairs._import(opts).then(function(privkey) { + if ("string" === typeof payload) { + payload = new TextEncoder().encode(payload); + } + return window.crypto.subtle + .sign( + { + name: Keypairs._getName(opts), + hash: { name: "SHA-" + Keypairs._getBits(opts) } + }, + privkey, + payload + ) + .then(function(signature) { + signature = new Uint8Array(signature); // ArrayBuffer -> u8 + // This will come back into play for CSRs, but not for JOSE + if ("EC" === opts.jwk.kty && /x509|asn1/i.test(opts.format)) { + return Keypairs._ecdsaJoseSigToAsn1Sig(signature); + } else { + // jose/jws/jwt + return signature; + } + }); + }); + }; + Keypairs._getBits = function(opts) { + if (opts.alg) { + return opts.alg.replace(/[a-z\-]/gi, ""); + } + // base64 len to byte len + var len = Math.floor((opts.jwk.n || "").length * 0.75); + + // TODO this may be a bug + // need to confirm that the padding is no more or less than 1 byte + if (/521/.test(opts.jwk.crv) || len >= 511) { + return "512"; + } else if (/384/.test(opts.jwk.crv) || len >= 383) { + return "384"; + } + + return "256"; + }; + Keypairs._getName = function(opts) { + if (/EC/i.test(opts.jwk.kty)) { + return "ECDSA"; + } else { + return "RSASSA-PKCS1-v1_5"; + } + }; + Keypairs._import = function(opts) { + return Promise.resolve().then(function() { + var ops; + // all private keys just happen to have a 'd' + if (opts.jwk.d) { + ops = ["sign"]; + } else { + ops = ["verify"]; + } + // gotta mark it as extractable, as if it matters + opts.jwk.ext = true; + opts.jwk.key_ops = ops; + + return window.crypto.subtle + .importKey( + "jwk", + opts.jwk, + { + name: Keypairs._getName(opts), + namedCurve: opts.jwk.crv, + hash: { name: "SHA-" + Keypairs._getBits(opts) } + }, + true, + ops + ) + .then(function(privkey) { + delete opts.jwk.ext; + return privkey; + }); + }); + }; + // ECDSA JOSE / JWS / JWT signatures differ from "normal" ASN1/X509 ECDSA signatures + // https://tools.ietf.org/html/rfc7518#section-3.4 + Keypairs._ecdsaJoseSigToAsn1Sig = function(bufsig) { + // it's easier to do the manipulation in the browser with an array + bufsig = Array.from(bufsig); + var hlen = bufsig.length / 2; // should be even + var r = bufsig.slice(0, hlen); + var s = bufsig.slice(hlen); + // unpad positive ints less than 32 bytes wide + while (!r[0]) { + r = r.slice(1); + } + while (!s[0]) { + s = s.slice(1); + } + // pad (or re-pad) ambiguously non-negative BigInts, up to 33 bytes wide + if (0x80 & r[0]) { + r.unshift(0); + } + if (0x80 & s[0]) { + s.unshift(0); + } + + var len = 2 + r.length + 2 + s.length; + var head = [0x30]; + // hard code 0x80 + 1 because it won't be longer than + // two SHA512 plus two pad bytes (130 bytes <= 256) + if (len >= 0x80) { + head.push(0x81); + } + head.push(len); + + return Uint8Array.from( + head.concat([0x02, r.length], r, [0x02, s.length], s) + ); + }; + + function setTime(time) { + if ("number" === typeof time) { + return time; + } + + var t = time.match(/^(\-?\d+)([dhms])$/i); + if (!t || !t[0]) { + throw new Error( + "'" + + time + + "' should be datetime in seconds or human-readable format (i.e. 3d, 1h, 15m, 30s" + ); + } + + var now = Math.round(Date.now() / 1000); + var num = parseInt(t[1], 10); + var unit = t[2]; + var mult = 1; + switch (unit) { + // fancy fallthrough, what fun! + case "d": + mult *= 24; + /*falls through*/ + case "h": + mult *= 60; + /*falls through*/ + case "m": + mult *= 60; + /*falls through*/ + case "s": + mult *= 1; + } + + return now + mult * num; + } + + Enc.hexToBuf = function(hex) { + var arr = []; + hex.match(/.{2}/g).forEach(function(h) { + arr.push(parseInt(h, 16)); + }); + return "undefined" !== typeof Uint8Array ? new Uint8Array(arr) : arr; + }; + Enc.strToUrlBase64 = function(str) { + return Enc.bufToUrlBase64(Enc.binToBuf(str)); + }; + Enc.binToBuf = function(bin) { + var arr = bin.split("").map(function(ch) { + return ch.charCodeAt(0); + }); + return "undefined" !== typeof Uint8Array ? new Uint8Array(arr) : arr; + }; +})("undefined" !== typeof module ? module.exports : window); // Copyright 2018 AJ ONeal. All rights reserved /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -;(function (exports) { -'use strict'; - -if (!exports.ASN1) { exports.ASN1 = {}; } -if (!exports.Enc) { exports.Enc = {}; } -if (!exports.PEM) { exports.PEM = {}; } - -var ASN1 = exports.ASN1; -var Enc = exports.Enc; -var PEM = exports.PEM; - -// -// Parser -// - -// Although I've only seen 9 max in https certificates themselves, -// but each domain list could have up to 100 -ASN1.ELOOPN = 102; -ASN1.ELOOP = "uASN1.js Error: iterated over " + ASN1.ELOOPN + "+ elements (probably a malformed file)"; -// I've seen https certificates go 29 deep -ASN1.EDEEPN = 60; -ASN1.EDEEP = "uASN1.js Error: element nested " + ASN1.EDEEPN + "+ layers deep (probably a malformed file)"; -// Container Types are Sequence 0x30, Container Array? (0xA0, 0xA1) -// Value Types are Boolean 0x01, Integer 0x02, Null 0x05, Object ID 0x06, String 0x0C, 0x16, 0x13, 0x1e Value Array? (0x82) -// Bit String (0x03) and Octet String (0x04) may be values or containers -// Sometimes Bit String is used as a container (RSA Pub Spki) -ASN1.CTYPES = [ 0x30, 0x31, 0xa0, 0xa1 ]; -ASN1.VTYPES = [ 0x01, 0x02, 0x05, 0x06, 0x0c, 0x82 ]; -ASN1.parse = function parseAsn1Helper(buf) { - //var ws = ' '; - function parseAsn1(buf, depth, eager) { - if (depth.length >= ASN1.EDEEPN) { throw new Error(ASN1.EDEEP); } - - var index = 2; // we know, at minimum, data starts after type (0) and lengthSize (1) - var asn1 = { type: buf[0], lengthSize: 0, length: buf[1] }; - var child; - var iters = 0; - var adjust = 0; - var adjustedLen; - - // Determine how many bytes the length uses, and what it is - if (0x80 & asn1.length) { - asn1.lengthSize = 0x7f & asn1.length; - // I think that buf->hex->int solves the problem of Endianness... not sure - asn1.length = parseInt(Enc.bufToHex(buf.slice(index, index + asn1.lengthSize)), 16); - index += asn1.lengthSize; - } - - // High-order bit Integers have a leading 0x00 to signify that they are positive. - // Bit Streams use the first byte to signify padding, which x.509 doesn't use. - if (0x00 === buf[index] && (0x02 === asn1.type || 0x03 === asn1.type)) { - // However, 0x00 on its own is a valid number - if (asn1.length > 1) { - index += 1; - adjust = -1; - } - } - adjustedLen = asn1.length + adjust; - - //console.warn(depth.join(ws) + '0x' + Enc.numToHex(asn1.type), index, 'len:', asn1.length, asn1); - - function parseChildren(eager) { - asn1.children = []; - //console.warn('1 len:', (2 + asn1.lengthSize + asn1.length), 'idx:', index, 'clen:', 0); - while (iters < ASN1.ELOOPN && index < (2 + asn1.length + asn1.lengthSize)) { - iters += 1; - depth.length += 1; - child = parseAsn1(buf.slice(index, index + adjustedLen), depth, eager); - depth.length -= 1; - // The numbers don't match up exactly and I don't remember why... - // probably something with adjustedLen or some such, but the tests pass - index += (2 + child.lengthSize + child.length); - //console.warn('2 len:', (2 + asn1.lengthSize + asn1.length), 'idx:', index, 'clen:', (2 + child.lengthSize + child.length)); - if (index > (2 + asn1.lengthSize + asn1.length)) { - if (!eager) { console.error(JSON.stringify(asn1, ASN1._replacer, 2)); } - throw new Error("Parse error: child value length (" + child.length - + ") is greater than remaining parent length (" + (asn1.length - index) - + " = " + asn1.length + " - " + index + ")"); - } - asn1.children.push(child); - //console.warn(depth.join(ws) + '0x' + Enc.numToHex(asn1.type), index, 'len:', asn1.length, asn1); - } - if (index !== (2 + asn1.lengthSize + asn1.length)) { - //console.warn('index:', index, 'length:', (2 + asn1.lengthSize + asn1.length)); - throw new Error("premature end-of-file"); - } - if (iters >= ASN1.ELOOPN) { throw new Error(ASN1.ELOOP); } - - delete asn1.value; - return asn1; - } - - // Recurse into types that are _always_ containers - if (-1 !== ASN1.CTYPES.indexOf(asn1.type)) { return parseChildren(eager); } - - // Return types that are _always_ values - asn1.value = buf.slice(index, index + adjustedLen); - if (-1 !== ASN1.VTYPES.indexOf(asn1.type)) { return asn1; } - - // For ambigious / unknown types, recurse and return on failure - // (and return child array size to zero) - try { return parseChildren(true); } - catch(e) { asn1.children.length = 0; return asn1; } - } - - var asn1 = parseAsn1(buf, []); - var len = buf.byteLength || buf.length; - if (len !== 2 + asn1.lengthSize + asn1.length) { - throw new Error("Length of buffer does not match length of ASN.1 sequence."); - } - return asn1; -}; -ASN1._replacer = function (k, v) { - if ('type' === k) { return '0x' + Enc.numToHex(v); } - if (v && 'value' === k) { return '0x' + Enc.bufToHex(v.data || v); } - return v; -}; - -// 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) }; -}; - -Enc.base64ToBuf = function (b64) { - return Enc.binToBuf(atob(b64)); -}; -Enc.binToBuf = function (bin) { - var arr = bin.split('').map(function (ch) { - return ch.charCodeAt(0); - }); - return 'undefined' !== typeof Uint8Array ? new Uint8Array(arr) : arr; -}; -Enc.bufToHex = function (u8) { - var hex = []; - var i, h; - var len = (u8.byteLength || u8.length); - - for (i = 0; i < len; i += 1) { - h = u8[i].toString(16); - if (h.length % 2) { h = '0' + h; } - hex.push(h); - } - - return hex.join('').toLowerCase(); -}; -Enc.numToHex = function (d) { - d = d.toString(16); - if (d.length % 2) { - return '0' + d; - } - return d; -}; - -}('undefined' !== typeof window ? window : module.exports)); +(function(exports) { + "use strict"; + + if (!exports.ASN1) { + exports.ASN1 = {}; + } + if (!exports.Enc) { + exports.Enc = {}; + } + if (!exports.PEM) { + exports.PEM = {}; + } + + var ASN1 = exports.ASN1; + var Enc = exports.Enc; + var PEM = exports.PEM; + + // + // Parser + // + + // Although I've only seen 9 max in https certificates themselves, + // but each domain list could have up to 100 + ASN1.ELOOPN = 102; + ASN1.ELOOP = + "uASN1.js Error: iterated over " + + ASN1.ELOOPN + + "+ elements (probably a malformed file)"; + // I've seen https certificates go 29 deep + ASN1.EDEEPN = 60; + ASN1.EDEEP = + "uASN1.js Error: element nested " + + ASN1.EDEEPN + + "+ layers deep (probably a malformed file)"; + // Container Types are Sequence 0x30, Container Array? (0xA0, 0xA1) + // Value Types are Boolean 0x01, Integer 0x02, Null 0x05, Object ID 0x06, String 0x0C, 0x16, 0x13, 0x1e Value Array? (0x82) + // Bit String (0x03) and Octet String (0x04) may be values or containers + // Sometimes Bit String is used as a container (RSA Pub Spki) + ASN1.CTYPES = [0x30, 0x31, 0xa0, 0xa1]; + ASN1.VTYPES = [0x01, 0x02, 0x05, 0x06, 0x0c, 0x82]; + ASN1.parse = function parseAsn1Helper(buf) { + //var ws = ' '; + function parseAsn1(buf, depth, eager) { + if (depth.length >= ASN1.EDEEPN) { + throw new Error(ASN1.EDEEP); + } + + var index = 2; // we know, at minimum, data starts after type (0) and lengthSize (1) + var asn1 = { type: buf[0], lengthSize: 0, length: buf[1] }; + var child; + var iters = 0; + var adjust = 0; + var adjustedLen; + + // Determine how many bytes the length uses, and what it is + if (0x80 & asn1.length) { + asn1.lengthSize = 0x7f & asn1.length; + // I think that buf->hex->int solves the problem of Endianness... not sure + asn1.length = parseInt( + Enc.bufToHex(buf.slice(index, index + asn1.lengthSize)), + 16 + ); + index += asn1.lengthSize; + } + + // High-order bit Integers have a leading 0x00 to signify that they are positive. + // Bit Streams use the first byte to signify padding, which x.509 doesn't use. + if (0x00 === buf[index] && (0x02 === asn1.type || 0x03 === asn1.type)) { + // However, 0x00 on its own is a valid number + if (asn1.length > 1) { + index += 1; + adjust = -1; + } + } + adjustedLen = asn1.length + adjust; + + //console.warn(depth.join(ws) + '0x' + Enc.numToHex(asn1.type), index, 'len:', asn1.length, asn1); + + function parseChildren(eager) { + asn1.children = []; + //console.warn('1 len:', (2 + asn1.lengthSize + asn1.length), 'idx:', index, 'clen:', 0); + while ( + iters < ASN1.ELOOPN && + index < 2 + asn1.length + asn1.lengthSize + ) { + iters += 1; + depth.length += 1; + child = parseAsn1( + buf.slice(index, index + adjustedLen), + depth, + eager + ); + depth.length -= 1; + // The numbers don't match up exactly and I don't remember why... + // probably something with adjustedLen or some such, but the tests pass + index += 2 + child.lengthSize + child.length; + //console.warn('2 len:', (2 + asn1.lengthSize + asn1.length), 'idx:', index, 'clen:', (2 + child.lengthSize + child.length)); + if (index > 2 + asn1.lengthSize + asn1.length) { + if (!eager) { + console.error(JSON.stringify(asn1, ASN1._replacer, 2)); + } + throw new Error( + "Parse error: child value length (" + + child.length + + ") is greater than remaining parent length (" + + (asn1.length - index) + + " = " + + asn1.length + + " - " + + index + + ")" + ); + } + asn1.children.push(child); + //console.warn(depth.join(ws) + '0x' + Enc.numToHex(asn1.type), index, 'len:', asn1.length, asn1); + } + if (index !== 2 + asn1.lengthSize + asn1.length) { + //console.warn('index:', index, 'length:', (2 + asn1.lengthSize + asn1.length)); + throw new Error("premature end-of-file"); + } + if (iters >= ASN1.ELOOPN) { + throw new Error(ASN1.ELOOP); + } + + delete asn1.value; + return asn1; + } + + // Recurse into types that are _always_ containers + if (-1 !== ASN1.CTYPES.indexOf(asn1.type)) { + return parseChildren(eager); + } + + // Return types that are _always_ values + asn1.value = buf.slice(index, index + adjustedLen); + if (-1 !== ASN1.VTYPES.indexOf(asn1.type)) { + return asn1; + } + + // For ambigious / unknown types, recurse and return on failure + // (and return child array size to zero) + try { + return parseChildren(true); + } catch (e) { + asn1.children.length = 0; + return asn1; + } + } + + var asn1 = parseAsn1(buf, []); + var len = buf.byteLength || buf.length; + if (len !== 2 + asn1.lengthSize + asn1.length) { + throw new Error( + "Length of buffer does not match length of ASN.1 sequence." + ); + } + return asn1; + }; + ASN1._replacer = function(k, v) { + if ("type" === k) { + return "0x" + Enc.numToHex(v); + } + if (v && "value" === k) { + return "0x" + Enc.bufToHex(v.data || v); + } + return v; + }; + + // 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) }; + }; + + Enc.base64ToBuf = function(b64) { + return Enc.binToBuf(atob(b64)); + }; + Enc.binToBuf = function(bin) { + var arr = bin.split("").map(function(ch) { + return ch.charCodeAt(0); + }); + return "undefined" !== typeof Uint8Array ? new Uint8Array(arr) : arr; + }; + Enc.bufToHex = function(u8) { + var hex = []; + var i, h; + var len = u8.byteLength || u8.length; + + for (i = 0; i < len; i += 1) { + h = u8[i].toString(16); + if (h.length % 2) { + h = "0" + h; + } + hex.push(h); + } + + return hex.join("").toLowerCase(); + }; + Enc.numToHex = function(d) { + d = d.toString(16); + if (d.length % 2) { + return "0" + d; + } + return d; + }; +})("undefined" !== typeof window ? window : module.exports); // 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)); +(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); // Copyright 2018-present AJ ONeal. All rights reserved /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -(function (exports) { -'use strict'; -/* globals Promise */ - -var ACME = exports.ACME = {}; -//var Keypairs = exports.Keypairs || {}; -//var CSR = exports.CSR; -var Enc = exports.Enc || {}; -var Crypto = exports.Crypto || {}; - -ACME.formatPemChain = function formatPemChain(str) { - return str.trim().replace(/[\r\n]+/g, '\n').replace(/\-\n\-/g, '-\n\n-') + '\n'; -}; -ACME.splitPemChain = function splitPemChain(str) { - return str.trim().split(/[\r\n]{2,}/g).map(function (str) { - return str + '\n'; - }); -}; - - -// http-01: GET https://example.org/.well-known/acme-challenge/{{token}} => {{keyAuth}} -// dns-01: TXT _acme-challenge.example.org. => "{{urlSafeBase64(sha256(keyAuth))}}" -ACME.challengePrefixes = { - 'http-01': '/.well-known/acme-challenge' -, 'dns-01': '_acme-challenge' -}; -ACME.challengeTests = { - 'http-01': function (me, auth) { - return me.http01(auth).then(function (keyAuth) { - var err; - - // TODO limit the number of bytes that are allowed to be downloaded - if (auth.keyAuthorization === (keyAuth||'').trim()) { - return true; - } - - err = new Error( - "Error: Failed HTTP-01 Pre-Flight / Dry Run.\n" - + "curl '" + auth.challengeUrl + "'\n" - + "Expected: '" + auth.keyAuthorization + "'\n" - + "Got: '" + keyAuth + "'\n" - + "See https://git.coolaj86.com/coolaj86/acme-v2.js/issues/4" - ); - err.code = 'E_FAIL_DRY_CHALLENGE'; - return Promise.reject(err); - }); - } -, 'dns-01': function (me, auth) { - // remove leading *. on wildcard domains - return me.dns01(auth).then(function (ans) { - var err; - - if (ans.answer.some(function (txt) { - return auth.dnsAuthorization === txt.data[0]; - })) { - return true; - } - - err = new Error( - "Error: Failed DNS-01 Pre-Flight Dry Run.\n" - + "dig TXT '" + auth.dnsHost + "' does not return '" + auth.dnsAuthorization + "'\n" - + "See https://git.coolaj86.com/coolaj86/acme-v2.js/issues/4" - ); - err.code = 'E_FAIL_DRY_CHALLENGE'; - return Promise.reject(err); - }); - } -}; - -ACME._directory = function (me) { - // GET-as-GET ok - return ACME._request(me, { method: 'GET', url: me.directoryUrl }); -}; -ACME._getNonce = function (me) { - // GET-as-GET, HEAD-as-HEAD ok - var nonce; - while (true) { - nonce = me._nonces.shift(); - if (!nonce) { break; } - if (Date.now() - nonce.createdAt > (15 * 60 * 1000)) { - nonce = null; - } else { - break; - } - } - if (nonce) { return Promise.resolve(nonce.nonce); } - // GET-as-GET ok - return ACME._request(me, { method: 'HEAD', url: me._directoryUrls.newNonce }).then(function (resp) { - return resp.headers['replay-nonce']; - }); -}; -ACME._setNonce = function (me, nonce) { - me._nonces.unshift({ nonce: nonce, createdAt: Date.now() }); -}; -// ACME RFC Section 7.3 Account Creation -/* +(function(exports) { + "use strict"; + /* globals Promise */ + + var ACME = (exports.ACME = {}); + //var Keypairs = exports.Keypairs || {}; + //var CSR = exports.CSR; + var Enc = exports.Enc || {}; + var Crypto = exports.Crypto || {}; + + ACME.formatPemChain = function formatPemChain(str) { + return ( + str + .trim() + .replace(/[\r\n]+/g, "\n") + .replace(/\-\n\-/g, "-\n\n-") + "\n" + ); + }; + ACME.splitPemChain = function splitPemChain(str) { + return str + .trim() + .split(/[\r\n]{2,}/g) + .map(function(str) { + return str + "\n"; + }); + }; + + // http-01: GET https://example.org/.well-known/acme-challenge/{{token}} => {{keyAuth}} + // dns-01: TXT _acme-challenge.example.org. => "{{urlSafeBase64(sha256(keyAuth))}}" + ACME.challengePrefixes = { + "http-01": "/.well-known/acme-challenge", + "dns-01": "_acme-challenge" + }; + ACME.challengeTests = { + "http-01": function(me, auth) { + return me.http01(auth).then(function(keyAuth) { + var err; + + // TODO limit the number of bytes that are allowed to be downloaded + if (auth.keyAuthorization === (keyAuth || "").trim()) { + return true; + } + + err = new Error( + "Error: Failed HTTP-01 Pre-Flight / Dry Run.\n" + + "curl '" + + auth.challengeUrl + + "'\n" + + "Expected: '" + + auth.keyAuthorization + + "'\n" + + "Got: '" + + keyAuth + + "'\n" + + "See https://git.coolaj86.com/coolaj86/acme-v2.js/issues/4" + ); + err.code = "E_FAIL_DRY_CHALLENGE"; + return Promise.reject(err); + }); + }, + "dns-01": function(me, auth) { + // remove leading *. on wildcard domains + return me.dns01(auth).then(function(ans) { + var err; + + if ( + ans.answer.some(function(txt) { + return auth.dnsAuthorization === txt.data[0]; + }) + ) { + return true; + } + + err = new Error( + "Error: Failed DNS-01 Pre-Flight Dry Run.\n" + + "dig TXT '" + + auth.dnsHost + + "' does not return '" + + auth.dnsAuthorization + + "'\n" + + "See https://git.coolaj86.com/coolaj86/acme-v2.js/issues/4" + ); + err.code = "E_FAIL_DRY_CHALLENGE"; + return Promise.reject(err); + }); + } + }; + + ACME._directory = function(me) { + // GET-as-GET ok + return ACME._request(me, { method: "GET", url: me.directoryUrl }); + }; + ACME._getNonce = function(me) { + // GET-as-GET, HEAD-as-HEAD ok + var nonce; + while (true) { + nonce = me._nonces.shift(); + if (!nonce) { + break; + } + if (Date.now() - nonce.createdAt > 15 * 60 * 1000) { + nonce = null; + } else { + break; + } + } + if (nonce) { + return Promise.resolve(nonce.nonce); + } + // GET-as-GET ok + return ACME._request(me, { + method: "HEAD", + url: me._directoryUrls.newNonce + }).then(function(resp) { + return resp.headers["replay-nonce"]; + }); + }; + ACME._setNonce = function(me, nonce) { + me._nonces.unshift({ nonce: nonce, createdAt: Date.now() }); + }; + // ACME RFC Section 7.3 Account Creation + /* { "protected": base64url({ "alg": "ES256", @@ -1828,93 +2200,104 @@ ACME._setNonce = function (me, nonce) { "signature": "RZPOnYoPs1PhjszF...-nh6X1qtOFPB519I" } */ -ACME._registerAccount = function (me, options) { - //#console.debug('[acme-v2] accounts.create'); - - function agree(tosUrl) { - var err; - if (me._tos !== tosUrl) { - err = new Error("You must agree to the ToS at '" + me._tos + "'"); - err.code = "E_AGREE_TOS"; - throw err; - } - - return ACME._importKeypair(me, options.accountKey || options.accountKeypair).then(function (pair) { - var contact; - if (options.contact) { - contact = options.contact.slice(0); - } else if (options.email) { - contact = [ 'mailto:' + options.email ]; - } - var body = { - termsOfServiceAgreed: tosUrl === me._tos - , onlyReturnExisting: false - , contact: contact - }; - var pExt; - if (options.externalAccount) { - pExt = me.Keypairs.signJws({ - // TODO is HMAC the standard, or is this arbitrary? - secret: options.externalAccount.secret - , protected: { - alg: options.externalAccount.alg || "HS256" - , kid: options.externalAccount.id - , url: me._directoryUrls.newAccount - } - , payload: Enc.binToBuf(JSON.stringify(pair.public)) - }).then(function (jws) { - body.externalAccountBinding = jws; - return body; - }); - } else { - pExt = Promise.resolve(body); - } - return pExt.then(function (body) { - var payload = JSON.stringify(body); - return ACME._jwsRequest(me, { - options: options - , url: me._directoryUrls.newAccount - , protected: { kid: false, jwk: pair.public } - , payload: Enc.binToBuf(payload) - }).then(function (resp) { - var account = resp.body; - - if (2 !== Math.floor(resp.statusCode / 100)) { - throw new Error('account error: ' + JSON.stringify(resp.body)); - } - - var location = resp.headers.location; - // the account id url - options._kid = location; - //#console.debug('[DEBUG] new account location:'); - //#console.debug(location); - //#console.debug(resp); - - /* + ACME._registerAccount = function(me, options) { + //#console.debug('[acme-v2] accounts.create'); + + function agree(tosUrl) { + var err; + if (me._tos !== tosUrl) { + err = new Error("You must agree to the ToS at '" + me._tos + "'"); + err.code = "E_AGREE_TOS"; + throw err; + } + + return ACME._importKeypair( + me, + options.accountKey || options.accountKeypair + ).then(function(pair) { + var contact; + if (options.contact) { + contact = options.contact.slice(0); + } else if (options.email) { + contact = ["mailto:" + options.email]; + } + var body = { + termsOfServiceAgreed: tosUrl === me._tos, + onlyReturnExisting: false, + contact: contact + }; + var pExt; + if (options.externalAccount) { + pExt = me.Keypairs.signJws({ + // TODO is HMAC the standard, or is this arbitrary? + secret: options.externalAccount.secret, + protected: { + alg: options.externalAccount.alg || "HS256", + kid: options.externalAccount.id, + url: me._directoryUrls.newAccount + }, + payload: Enc.binToBuf(JSON.stringify(pair.public)) + }).then(function(jws) { + body.externalAccountBinding = jws; + return body; + }); + } else { + pExt = Promise.resolve(body); + } + return pExt.then(function(body) { + var payload = JSON.stringify(body); + return ACME._jwsRequest(me, { + options: options, + url: me._directoryUrls.newAccount, + protected: { kid: false, jwk: pair.public }, + payload: Enc.binToBuf(payload) + }).then(function(resp) { + var account = resp.body; + + if (2 !== Math.floor(resp.statusCode / 100)) { + throw new Error("account error: " + JSON.stringify(resp.body)); + } + + var location = resp.headers.location; + // the account id url + options._kid = location; + //#console.debug('[DEBUG] new account location:'); + //#console.debug(location); + //#console.debug(resp); + + /* { contact: ["mailto:jon@example.com"], orders: "https://some-url", status: 'valid' } */ - if (!account) { account = { _emptyResponse: true }; } - // https://git.coolaj86.com/coolaj86/acme-v2.js/issues/8 - if (!account.key) { account.key = {}; } - account.key.kid = options._kid; - return account; - }); - }); - }); - } - - return Promise.resolve().then(function () { - if (true === options.agreeToTerms) { - options.agreeToTerms = function (tos) { return tos; }; - } - return options.agreeToTerms(me._tos); - }).then(agree); -}; -/* + if (!account) { + account = { _emptyResponse: true }; + } + // https://git.coolaj86.com/coolaj86/acme-v2.js/issues/8 + if (!account.key) { + account.key = {}; + } + account.key.kid = options._kid; + return account; + }); + }); + }); + } + + return Promise.resolve() + .then(function() { + if (true === options.agreeToTerms) { + options.agreeToTerms = function(tos) { + return tos; + }; + } + return options.agreeToTerms(me._tos); + }) + .then(agree); + }; + /* POST /acme/new-order HTTP/1.1 Host: example.com Content-Type: application/jose+json @@ -1934,200 +2317,229 @@ ACME._registerAccount = function (me, options) { "signature": "H6ZXtGjTZyUnPeKn...wEA4TklBdh3e454g" } */ -ACME._getChallenges = function (me, options, authUrl) { - //#console.debug('\n[DEBUG] getChallenges\n'); - return ACME._jwsRequest(me, { - options: options - , protected: { kid: options._kid } - , payload: '' - , url: authUrl - }).then(function (resp) { - // Pre-emptive rather than lazy for interfaces that need to show the challenges to the user first - return ACME._computeAuths(me, options, resp.body, false).then(function (auths) { - resp.body._rawChallenges = resp.body.challenges; - resp.body.challenges = auths; - return resp.body; - }); - }); -}; -ACME._wait = function wait(ms) { - return new Promise(function (resolve) { - setTimeout(resolve, (ms || 1100)); - }); -}; - -ACME._testChallengeOptions = function () { - var chToken = ACME._prnd(16); - return [ - { - "type": "http-01", - "status": "pending", - "url": "https://acme-staging-v02.example.com/0", - "token": "test-" + chToken + "-0" - } - , { - "type": "dns-01", - "status": "pending", - "url": "https://acme-staging-v02.example.com/1", - "token": "test-" + chToken + "-1", - "_wildcard": true - } - , { - "type": "tls-alpn-01", - "status": "pending", - "url": "https://acme-staging-v02.example.com/3", - "token": "test-" + chToken + "-3" - } - ]; -}; -ACME._testChallenges = function (me, reals) { - //#console.log('[DEBUG] testChallenges'); - if (me.skipDryRun || me.skipChallengeTest) { - return Promise.resolve(); - } - - var nopts = {}; - Object.keys(reals).forEach(function (key) { - nopts[key] = reals[key]; - }); - nopts.order = {}; - - return Promise.all(nopts.domains.map(function (name) { - var challenges = ACME._testChallengeOptions(); - var wild = '*.' === name.slice(0, 2); - if (wild) { - challenges = challenges.filter(function (ch) { return ch._wildcard; }); - } - var resp = { - body: { - identifier: { type: 'dns' , value: name.replace('*.', '') } - , challenges: challenges - , expires: new Date(Date.now() + (60 * 1000)).toISOString() - , wildcard: name.includes('*.') || undefined - } - }; - // The dry-run comes first in the spirit of "fail fast" - // (and protecting against challenge failure rate limits) - var dryrun = true; - return ACME._computeAuths(me, nopts, resp.body, dryrun).then(function (auths) { - resp.body.challenges = auths; - }); - })).then(function (claims) { - nopts.order.claims = claims; - nopts.setChallengeWait = 0; - - return ACME._setChallengesAll(me, nopts).then(function (valids) { - return Promise.all(valids.map(function (auth) { - ACME._removeChallenge(me, nopts, auth); - })); - }); - }); -}; -ACME._chooseType = function(options, auths) { - // For each of the challenge types that we support - var auth; - var challengeTypes = Object.keys(options.challenges || ACME._challengesMap); - // ordered from most to least preferred - challengeTypes = (options.challengePriority||[ 'tls-alpn-01', 'http-01', 'dns-01' ]).filter(function (chType) { - return challengeTypes.includes(chType); - }); - - challengeTypes.some(function (chType) { - // And for each of the challenge types that are allowed - return auths.some(function (ch) { - // Check to see if there are any matches - if (ch.type === chType) { - auth = ch; - return true; - } - }); - }); - - return auth; -}; -ACME._challengesMap = {'http-01':0,'dns-01':0,'tls-alpn-01':0}; -ACME._computeAuths = function (me, options, request, dryrun) { - //#console.log('[DEBUG] computeAuths'); - // we don't poison the dns cache with our dummy request - var dnsPrefix = ACME.challengePrefixes['dns-01']; - if (dryrun) { - dnsPrefix = dnsPrefix.replace('acme-challenge', 'greenlock-dryrun-' + ACME._prnd(4)); - } - var challengeTypes = Object.keys(options.challenges || ACME._challengesMap); - - return ACME._importKeypair(me, options.accountKey || options.accountKeypair).then(function (pair) { - return me.Keypairs.thumbprint({ jwk: pair.public }).then(function (thumb) { - return Promise.all(request.challenges.map(function (challenge) { - // Don't do extra work for challenges that we can't satisfy - if (!challengeTypes.includes(challenge.type)) { - return null; - } - - var auth = {}; - - // straight copy from the new order response - // { identifier, status, expires, challenges, wildcard } - Object.keys(request).forEach(function (key) { - auth[key] = request[key]; - }); - - // copy from the challenge we've chosen - // { type, status, url, token } - // (note the duplicate status overwrites the one above, but they should be the same) - Object.keys(challenge).forEach(function (key) { - // don't confused devs with the id url - auth[key] = challenge[key]; - }); - - // batteries-included helpers - auth.hostname = auth.identifier.value; - // because I'm not 100% clear if the wildcard identifier does or doesn't have the leading *. in all cases - auth.altname = ACME._untame(auth.identifier.value, auth.wildcard); - - auth.thumbprint = thumb; - // keyAuthorization = token || '.' || base64url(JWK_Thumbprint(accountKey)) - auth.keyAuthorization = challenge.token + '.' + auth.thumbprint; - - if ('http-01' === auth.type) { - // 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; - return auth; - } - - if ('dns-01' !== auth.type) { - return auth; - } - - return Crypto._sha('sha256', auth.keyAuthorization).then(function (hash) { - auth.dnsHost = dnsPrefix + '.' + auth.hostname.replace('*.', ''); - auth.dnsAuthorization = hash; - auth.keyAuthorizationDigest = hash; - return auth; - }); - })).then(function (auths) { - return auths.filter(Boolean); - }); - }); - }); -}; - -ACME._untame = function (name, wild) { - if (wild) { name = '*.' + name.replace('*.', ''); } - return name; -}; - -// https://tools.ietf.org/html/draft-ietf-acme-acme-10#section-7.5.1 -ACME._postChallenge = function (me, options, auth) { - var RETRY_INTERVAL = me.retryInterval || 5000; - var DEAUTH_INTERVAL = me.deauthWait || 10 * 1000; - var MAX_POLL = me.retryPoll || 8; - var MAX_PEND = me.retryPending || 4; - var count = 0; - - var altname = ACME._untame(auth.identifier.value, auth.wildcard); - - /* + ACME._getChallenges = function(me, options, authUrl) { + //#console.debug('\n[DEBUG] getChallenges\n'); + return ACME._jwsRequest(me, { + options: options, + protected: { kid: options._kid }, + payload: "", + url: authUrl + }).then(function(resp) { + // Pre-emptive rather than lazy for interfaces that need to show the challenges to the user first + return ACME._computeAuths(me, options, resp.body, false).then(function( + auths + ) { + resp.body._rawChallenges = resp.body.challenges; + resp.body.challenges = auths; + return resp.body; + }); + }); + }; + ACME._wait = function wait(ms) { + return new Promise(function(resolve) { + setTimeout(resolve, ms || 1100); + }); + }; + + ACME._testChallengeOptions = function() { + var chToken = ACME._prnd(16); + return [ + { + type: "http-01", + status: "pending", + url: "https://acme-staging-v02.example.com/0", + token: "test-" + chToken + "-0" + }, + { + type: "dns-01", + status: "pending", + url: "https://acme-staging-v02.example.com/1", + token: "test-" + chToken + "-1", + _wildcard: true + }, + { + type: "tls-alpn-01", + status: "pending", + url: "https://acme-staging-v02.example.com/3", + token: "test-" + chToken + "-3" + } + ]; + }; + ACME._testChallenges = function(me, reals) { + //#console.log('[DEBUG] testChallenges'); + if (me.skipDryRun || me.skipChallengeTest) { + return Promise.resolve(); + } + + var nopts = {}; + Object.keys(reals).forEach(function(key) { + nopts[key] = reals[key]; + }); + nopts.order = {}; + + return Promise.all( + nopts.domains.map(function(name) { + var challenges = ACME._testChallengeOptions(); + var wild = "*." === name.slice(0, 2); + if (wild) { + challenges = challenges.filter(function(ch) { + return ch._wildcard; + }); + } + var resp = { + body: { + identifier: { type: "dns", value: name.replace("*.", "") }, + challenges: challenges, + expires: new Date(Date.now() + 60 * 1000).toISOString(), + wildcard: name.includes("*.") || undefined + } + }; + // The dry-run comes first in the spirit of "fail fast" + // (and protecting against challenge failure rate limits) + var dryrun = true; + return ACME._computeAuths(me, nopts, resp.body, dryrun).then(function( + auths + ) { + resp.body.challenges = auths; + }); + }) + ).then(function(claims) { + nopts.order.claims = claims; + nopts.setChallengeWait = 0; + + return ACME._setChallengesAll(me, nopts).then(function(valids) { + return Promise.all( + valids.map(function(auth) { + ACME._removeChallenge(me, nopts, auth); + }) + ); + }); + }); + }; + ACME._chooseType = function(options, auths) { + // For each of the challenge types that we support + var auth; + var challengeTypes = Object.keys(options.challenges || ACME._challengesMap); + // ordered from most to least preferred + challengeTypes = ( + options.challengePriority || ["tls-alpn-01", "http-01", "dns-01"] + ).filter(function(chType) { + return challengeTypes.includes(chType); + }); + + challengeTypes.some(function(chType) { + // And for each of the challenge types that are allowed + return auths.some(function(ch) { + // Check to see if there are any matches + if (ch.type === chType) { + auth = ch; + return true; + } + }); + }); + + return auth; + }; + ACME._challengesMap = { "http-01": 0, "dns-01": 0, "tls-alpn-01": 0 }; + ACME._computeAuths = function(me, options, request, dryrun) { + //#console.log('[DEBUG] computeAuths'); + // we don't poison the dns cache with our dummy request + var dnsPrefix = ACME.challengePrefixes["dns-01"]; + if (dryrun) { + dnsPrefix = dnsPrefix.replace( + "acme-challenge", + "greenlock-dryrun-" + ACME._prnd(4) + ); + } + var challengeTypes = Object.keys(options.challenges || ACME._challengesMap); + + return ACME._importKeypair( + me, + options.accountKey || options.accountKeypair + ).then(function(pair) { + return me.Keypairs.thumbprint({ jwk: pair.public }).then(function(thumb) { + return Promise.all( + request.challenges.map(function(challenge) { + // Don't do extra work for challenges that we can't satisfy + if (!challengeTypes.includes(challenge.type)) { + return null; + } + + var auth = {}; + + // straight copy from the new order response + // { identifier, status, expires, challenges, wildcard } + Object.keys(request).forEach(function(key) { + auth[key] = request[key]; + }); + + // copy from the challenge we've chosen + // { type, status, url, token } + // (note the duplicate status overwrites the one above, but they should be the same) + Object.keys(challenge).forEach(function(key) { + // don't confused devs with the id url + auth[key] = challenge[key]; + }); + + // batteries-included helpers + auth.hostname = auth.identifier.value; + // because I'm not 100% clear if the wildcard identifier does or doesn't have the leading *. in all cases + auth.altname = ACME._untame(auth.identifier.value, auth.wildcard); + + auth.thumbprint = thumb; + // keyAuthorization = token || '.' || base64url(JWK_Thumbprint(accountKey)) + auth.keyAuthorization = challenge.token + "." + auth.thumbprint; + + if ("http-01" === auth.type) { + // 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; + return auth; + } + + if ("dns-01" !== auth.type) { + return auth; + } + + return Crypto._sha("sha256", auth.keyAuthorization).then(function( + hash + ) { + auth.dnsHost = dnsPrefix + "." + auth.hostname.replace("*.", ""); + auth.dnsAuthorization = hash; + auth.keyAuthorizationDigest = hash; + return auth; + }); + }) + ).then(function(auths) { + return auths.filter(Boolean); + }); + }); + }); + }; + + ACME._untame = function(name, wild) { + if (wild) { + name = "*." + name.replace("*.", ""); + } + return name; + }; + + // https://tools.ietf.org/html/draft-ietf-acme-acme-10#section-7.5.1 + ACME._postChallenge = function(me, options, auth) { + var RETRY_INTERVAL = me.retryInterval || 5000; + var DEAUTH_INTERVAL = me.deauthWait || 10 * 1000; + var MAX_POLL = me.retryPoll || 8; + var MAX_PEND = me.retryPending || 4; + var count = 0; + + var altname = ACME._untame(auth.identifier.value, auth.wildcard); + + /* POST /acme/authz/1234 HTTP/1.1 Host: example.com Content-Type: application/jose+json @@ -2145,753 +2557,932 @@ ACME._postChallenge = function (me, options, auth) { "signature": "srX9Ji7Le9bjszhu...WTFdtujObzMtZcx4" } */ - function deactivate() { - //#console.debug('[acme-v2.js] deactivate:'); - return ACME._jwsRequest(me, { - options: options - , url: auth.url - , protected: { kid: options._kid } - , payload: Enc.binToBuf(JSON.stringify({ "status": "deactivated" })) - }).then(function (/*#resp*/) { - //#console.debug('deactivate challenge: resp.body:'); - //#console.debug(resp.body); - return ACME._wait(DEAUTH_INTERVAL); - }); - } - - function pollStatus() { - if (count >= MAX_POLL) { - var err = new Error( - "[acme-v2] stuck in bad pending/processing state for '" + altname + "'" - ); - err.detail = "Too many attempts to validate challenge."; - return Promise.reject(); - } - - count += 1; - - //#console.debug('\n[DEBUG] statusChallenge\n'); - // POST-as-GET - return ACME._jwsRequest(me, { - options: options - , url: auth.url - , protected: { kid: options._kid } - , payload: Enc.binToBuf('') - }).then(checkResult).catch(transformError); - } - - function checkResult(resp) { - if (options.onChallengeStatus) { - try { - options.onChallengeStatus({ - altname: altname, type: auth.type, status: resp.body.status, wildcard: auth.wildcard - }); - } catch(e) { - console.warn('options.onChallengeStatus Error:'); - console.warn(e); - } - } - - if ('processing' === resp.body.status) { - //#console.debug('poll: again'); - return ACME._wait(RETRY_INTERVAL).then(pollStatus); - } - - // This state should never occur - if ('pending' === resp.body.status) { - if (count >= MAX_PEND) { - return ACME._wait(RETRY_INTERVAL).then(deactivate).then(respondToChallenge); - } - //#console.debug('poll: again'); - return ACME._wait(RETRY_INTERVAL).then(pollStatus); - } - - if ('valid' === resp.body.status) { - //#console.debug('poll: valid'); - - try { - ACME._removeChallenge(me, options, auth); - } catch(e) {} - return resp.body; - } - - var code = 'E_ACME_UNKNOWN'; - var err = new Error("[acme-v2] " + auth.altname + " (" + code + "): " + JSON.stringify(resp.body, null, 2)); - err.code = code; - - return Promise.reject(err); - } - - function transformError(e) { - var err = e; - if (err.urn) { - err = new Error("[acme-v2] " + auth.altname + " status:" + e.status + " " + e.detail); - err.auth = auth; - err.altname = auth.altname; - err.type = auth.type; - err.code = ('invalid' === e.status) ? 'E_ACME_CHALLENGE' : 'E_ACME_UNKNOWN'; - } - - throw err; - } - - function respondToChallenge() { - //#console.debug('[acme-v2.js] responding to accept challenge:'); - // POST-as-POST (empty JSON object) - return ACME._jwsRequest(me, { - options: options - , url: auth.url - , protected: { kid: options._kid } - , payload: Enc.binToBuf(JSON.stringify({})) - }).then(checkResult).catch(transformError); - } - - return respondToChallenge(); -}; - -// options = { domains, claims, challenges, challengePriority } -ACME._setChallengesAll = function (me, options) { - //#console.log("[DEBUG] setChallengesAll"); - var claims = options.order.claims.slice(0); - var valids = []; - var auths = []; - // TODO: Do we still need this delay? Or shall we leave it to plugins to account for themselves? - var DELAY = options.setChallengeWait || me.setChallengeWait || 500; - - // Set any challenges, excpting ones that have already been validated - function setNext() { - var claim = claims.shift(); - if (!claim) { return Promise.resolve(); } - - return Promise.resolve().then(function () { - // For any challenges that are already valid, - // add to the list and skip any checks. - if (claim.challenges.some(function (ch) { - if ('valid' === ch.status) { - valids.push(ch); - return true; - } - })) { - return; - } - - // Get the list of challenge types we can validate. - // Then order that list by preference - // Select the first matching offered challenge type - var usable = Object.keys(options.challenges || ACME._challengesMap); - var selected = (options.challengePriority||[ 'tls-alpn-01', 'http-01', 'dns-01' ]).map(function (chType) { - if (!usable.includes(chType)) { return; } - return claim.challenges.filter(function (ch) { - return ch.type === chType; - })[0]; - }).filter(Boolean)[0]; - var ch; - - // Bail with a descriptive message if no usable challenge could be selected - if (!selected) { - var enabled = usable.join(', ') || 'none'; - var suitable = claim.challenges.map(function (r) { return r.type; }).join(', ') || 'none'; - throw new Error( - "None of the challenge types that you've enabled ( " + enabled + " )" - + " are suitable for validating the domain you've selected (" + claim.altname + ")." - + " You must enable one of ( " + suitable + " )." - ); - } - auths.push(selected); - - // Give the nameservers a moment to propagate - if ('dns-01' === selected.type) { - DELAY = 1.5 * 1000; - } - - if (false === options.challenges) { return; } - ch = options.challenges[selected.type] || {}; - if (!ch.set) { - throw new Error("no handler for setting challenge"); - } - return ch.set(selected); - }).then(setNext); - } - - function checkNext() { - var auth = auths.shift(); - if (!auth) { return Promise.resolve(valids); } - - // These are not as much "valids" as they are "not invalids" - if (!me._canUse[auth.type] || me.skipChallengeTest) { - valids.push(auth); - return checkNext(); - } - - return ACME.challengeTests[auth.type](me, auth).then(function () { - valids.push(auth); - }).then(checkNext); - } - - // The reason we set every challenge in a batch first before checking any - // is so that we don't poison our own DNS cache with misses. - return setNext().then(function () { - //#console.debug('\n[DEBUG] waitChallengeDelay %s\n', DELAY); - return ACME._wait(DELAY); - }).then(checkNext); -}; -ACME._finalizeOrder = function (me, options) { - return ACME._getAccountKid(me, options).then(function () { - if (!options.challenges && !options.challengePriority) { - throw new Error("You must set either challenges or challengePrority"); - } - return ACME._setChallengesAll(me, options).then(function (valids) { - // options._kid added - //#console.debug('finalizeOrder:'); - var order = options.order; - var validatedDomains = options.order.identifiers.map(function (ident) { - return ident.value; - }); - - // Actually sets the challenge via ACME - function challengeNext() { - var auth = valids.shift(); - if (!auth) { return Promise.resolve(); } - return ACME._postChallenge(me, options, auth).then(challengeNext); - } - return challengeNext().then(function () { - //#console.debug("[getCertificate] next.then"); - //#console.log('DEBUG order:'); - //#console.log(options.order); - return options.order.identifiers.map(function (ident) { - return ident.value; - }); - }).then(function () { - return ACME._getCsrWeb64(me, options, validatedDomains).then(function (csr) { - var body = { csr: csr }; - var payload = JSON.stringify(body); - - function pollCert() { - //#console.debug('[acme-v2.js] pollCert:'); - return ACME._jwsRequest(me, { - options: options - , url: options.order.finalizeUrl - , protected: { kid: options._kid } - , payload: Enc.binToBuf(payload) - }).then(function (resp) { - //#console.debug('order finalized: resp.body:'); - //#console.debug(resp.body); - - // https://tools.ietf.org/html/draft-ietf-acme-acme-12#section-7.1.3 - // Possible values are: "pending" => ("invalid" || "ready") => "processing" => "valid" - if ('valid' === resp.body.status) { - options._expires = resp.body.expires; - options._certificate = resp.body.certificate; - - return resp.body; // return order - } - - if ('processing' === resp.body.status) { - return ACME._wait().then(pollCert); - } - - //#console.debug("Error: bad status:\n" + JSON.stringify(resp.body, null, 2)); - - if ('pending' === resp.body.status) { - return Promise.reject(new Error( - "Did not finalize order: status 'pending'." - + " Best guess: You have not accepted at least one challenge for each domain:\n" - + "Requested: '" + options.domains.join(', ') + "'\n" - + "Validated: '" + validatedDomains.join(', ') + "'\n" - + JSON.stringify(resp.body, null, 2) - )); - } - - if ('invalid' === resp.body.status) { - return Promise.reject(new Error( - "Did not finalize order: status 'invalid'." - + " Best guess: One or more of the domain challenges could not be verified" - + " (or the order was canceled).\n" - + "Requested: '" + options.domains.join(', ') + "'\n" - + "Validated: '" + validatedDomains.join(', ') + "'\n" - + JSON.stringify(resp.body, null, 2) - )); - } - - if ('ready' === resp.body.status) { - return Promise.reject(new Error( - "Did not finalize order: status 'ready'." - + " Hmmm... this state shouldn't be possible here. That was the last state." - + " This one should at least be 'processing'.\n" - + "Requested: '" + options.domains.join(', ') + "'\n" - + "Validated: '" + validatedDomains.join(', ') + "'\n" - + JSON.stringify(resp.body, null, 2) + "\n\n" - + "Please open an issue at https://git.coolaj86.com/coolaj86/acme-v2.js" - )); - } - - return Promise.reject(new Error( - "Didn't finalize order: Unhandled status '" + resp.body.status + "'." - + " This is not one of the known statuses...\n" - + "Requested: '" + options.domains.join(', ') + "'\n" - + "Validated: '" + validatedDomains.join(', ') + "'\n" - + JSON.stringify(resp.body, null, 2) + "\n\n" - + "Please open an issue at https://git.coolaj86.com/coolaj86/acme-v2.js" - )); - }); - } - - return pollCert(); - }).then(function () { - //#console.debug('acme-v2: order was finalized'); - // POST-as-GET - return ACME._jwsRequest(me, { - options: options - , url: options._certificate - , protected: { kid: options._kid } - , payload: Enc.binToBuf('') - }).then(function (resp) { - //#console.debug('acme-v2: csr submitted and cert received:'); - // https://github.com/certbot/certbot/issues/5721 - var certsarr = ACME.splitPemChain(ACME.formatPemChain((resp.body||''))); - // cert, chain, fullchain, privkey, /*TODO, subject, altnames, issuedAt, expiresAt */ - // TODO CSR.info - var certs = { - expires: order.expires - , identifiers: order.identifiers - , cert: certsarr.shift() - , chain: certsarr.join('\n') - }; - //#console.debug(certs); - return certs; - }); - }); - }); - }); - }); -}; -ACME._createOrder = function (me, options) { - return ACME._getAccountKid(me, options).then(function () { - // options._kid added - var body = { - // raw wildcard syntax MUST be used here - identifiers: options.domains.sort(function (a, b) { - // the first in the list will be the subject of the certificate, I believe (and hope) - if (!options.subject) { return 0; } - if (options.subject === a) { return -1; } - if (options.subject === b) { return 1; } - return 0; - }).map(function (hostname) { - return { type: "dns", value: hostname }; - }) - //, "notBefore": "2016-01-01T00:00:00Z" - //, "notAfter": "2016-01-08T00:00:00Z" - }; - - var payload = JSON.stringify(body); - //#console.debug('\n[DEBUG] newOrder\n'); - return ACME._jwsRequest(me, { - options: options - , url: me._directoryUrls.newOrder - , protected: { kid: options._kid } - , payload: Enc.binToBuf(payload) - }).then(function (resp) { - var order = { - orderUrl: resp.headers.location - , finalizeUrl: resp.body.finalize - , authorizations: resp.body.authorizations - , identifiers: body.identifiers - , _response: resp.body - }; - //#console.debug('[ordered]', location); // the account id url - //#console.debug(resp); - - if (!order.authorizations) { - return Promise.reject(new Error( - "[acme-v2.js] authorizations were not fetched for '" + options.domains.join() + "':\n" - + JSON.stringify(resp.body) - )); - } - - return order; - }).then(function (order) { - var claims = []; - //#console.debug("[acme-v2] POST newOrder has authorizations"); - var challengeAuths = order.authorizations.slice(0); - - function getNext() { - var authUrl = challengeAuths.shift(); - if (!authUrl) { return claims; } - - return ACME._getChallenges(me, options, authUrl).then(function (claim) { - // var domain = options.domains[i]; // claim.identifier.value - claims.push(claim); - return getNext(); - }); - } - - return getNext().then(function () { - order.claims = claims; - options.order = order; - return order; - }); - }); - }); -}; -ACME._getAccountKid = function (me, options) { - // It's just fine if there's no account, we'll go get the key id we need via the existing key - options._kid = options._kid || options.accountKid - || (options.account && (options.account.kid - || (options.account.key && options.account.key.kid))); - if (options._kid) { - return Promise.resolve(options._kid); - } - - //return Promise.reject(new Error("must include KeyID")); - // This is an idempotent request. It'll return the same account for the same public key. - return ACME._registerAccount(me, options).then(function (account) { - options._kid = account.key.kid; - // start back from the top - return options._kid; - }); -}; - -// -// Helper Methods -// -ACME._getCertificate = function (me, options) { - //#console.debug('[acme-v2] DEBUG get cert 1'); - - 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)) { - 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).")); - } - if (!options.challenges) { - return Promise.reject(new Error("You must specify challenge handlers.")); - } - - // Do a little dry-run / self-test - return ACME._testChallenges(me, options).then(function () { - //#console.debug('[acme-v2] certificates.create'); - return ACME._createOrder(me, options).then(function (/*order*/) { - // options.order = order; - return ACME._finalizeOrder(me, options); - }); - }); -}; -ACME._getCsrWeb64 = function (me, options, validatedDomains) { - var csr; - 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.serverKey || options.serverKeypair || options.domainKeypair).then(function (pair) { - return me.CSR({ jwk: pair.private, domains: validatedDomains, encoding: 'der' }).then(function (der) { - return Enc.bufToUrlBase64(der); - }); - }); -}; - -ACME.create = function create(me) { - if (!me) { me = {}; } - // me.debug = true; - me.challengePrefixes = ACME.challengePrefixes; - me.Keypairs = me.Keypairs || exports.Keypairs || require('keypairs').Keypairs; - me.CSR = me.CSR || exports.CSR || require('CSR').CSR; - me._nonces = []; - me._canUse = {}; - if (!me._baseUrl) { - me._baseUrl = ""; - } - //me.Keypairs = me.Keypairs || require('keypairs'); - //me.request = me.request || require('@root/request'); - if (!me.dns01) { - me.dns01 = function (auth) { - return ACME._dns01(me, auth); - }; - } - // backwards compat - if (!me.dig) { me.dig = me.dns01; } - if (!me.http01) { - me.http01 = function (auth) { - return ACME._http01(me, auth); - }; - } - - if ('function' !== typeof me.request) { - me.request = ACME._defaultRequest; - } - - me.init = function (opts) { - function fin(dir) { - me._directoryUrls = dir; - me._tos = dir.meta.termsOfService; - return dir; - } - if (opts && opts.meta && opts.termsOfService) { - return Promise.resolve(fin(opts)); - } - if (!me.directoryUrl) { me.directoryUrl = opts; } - 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"); - } - var p = Promise.resolve(); - if (!me.skipChallengeTest) { - // Not ACME - 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 fin(resp.body); - }); - }); - }; - me.accounts = { - create: function (options) { - return ACME._registerAccount(me, options); - } - }; - me.orders = { - // create + get challlenges - request: function (options) { - return ACME._createOrder(me, options); - } - // set challenges, check challenges, finalize order, return order - , complete: function (options) { - return ACME._finalizeOrder(me, options); - } - }; - me.certificates = { - create: function (options) { - return ACME._getCertificate(me, options); - } - }; - return me; -}; - -// Handle nonce, signing, and request altogether -ACME._jwsRequest = function (me, bigopts) { - return ACME._getNonce(me).then(function (nonce) { - bigopts.protected.nonce = nonce; - bigopts.protected.url = bigopts.url; - // 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( - { jwk: bigopts.accountKey || bigopts.options.accountKeypair.privateKeyJwk - , protected: bigopts.protected - , payload: bigopts.payload - } - ).then(function (jws) { - //#console.debug('[acme-v2] ' + bigopts.url + ':'); - //#console.debug(jws); - return ACME._request(me, { url: bigopts.url, json: jws }); - }).catch(function (e) { - if (/badNonce$/.test(e.urn)) { - // retry badNonces - var retryable = (bigopts._retries >= 2); - if (!retryable) { - bigopts._retries = (bigopts._retries || 0) + 1; - return ACME._jwsRequest(me, bigopts); - } - } - throw e; - }); - }); -}; -// Handle some ACME-specific defaults -ACME._request = function (me, opts) { - if (!opts.headers) { opts.headers = {}; } - if (opts.json && true !== opts.json) { - opts.headers['Content-Type'] = 'application/jose+json'; - opts.body = JSON.stringify(opts.json); - if (!opts.method) { opts.method = 'POST'; } - } - return me.request(opts).then(function (resp) { - if (resp.toJSON) { resp = resp.toJSON(); } - if (resp.headers['replay-nonce']) { - ACME._setNonce(me, resp.headers['replay-nonce']); - } - - var e; - var err; - if (resp.body) { - err = resp.body.error; - e = new Error(""); - if (400 === resp.body.status) { - err = { type: resp.body.type, detail: resp.body.detail }; - } - if (err) { - e.status = resp.body.status; - e.code = 'E_ACME'; - if (e.status) { - e.message = "[" + e.status + "] "; - } - e.detail = err.detail; - e.message += (err.detail || JSON.stringify(err)); - e.urn = err.type; - e.uri = resp.body.url; - e._rawError = err; - e._rawBody = resp.body; - throw e; - } - } - - return resp; - }); -}; -// A very generic, swappable request lib -ACME._defaultRequest = function (opts) { - // Note: normally we'd have to supply a User-Agent string, but not here in a browser - if (!opts.headers) { opts.headers = {}; } - if (opts.json) { - opts.headers.Accept = 'application/json'; - if (true !== opts.json) { opts.body = JSON.stringify(opts.json); } - } - if (!opts.method) { - opts.method = 'GET'; - if (opts.body) { opts.method = 'POST'; } - } - opts.cors = true; - return window.fetch(opts.url, opts).then(function (resp) { - var headers = {}; - var result = { statusCode: resp.status, headers: headers, toJSON: function () { return this; } }; - Array.from(resp.headers.entries()).forEach(function (h) { headers[h[0]] = h[1]; }); - if (!headers['content-type']) { - return result; - } - if (/json/.test(headers['content-type'])) { - return resp.json().then(function (json) { - result.body = json; - return result; - }); - } - return resp.text().then(function (txt) { - result.body = txt; - return result; - }); - }); -}; - -ACME._importKeypair = function (me, kp) { - var jwk = kp.privateKeyJwk || kp.kty && kp; - var p; - if (jwk) { - // nix the browser jwk extras - jwk.key_ops = undefined; - jwk.ext = undefined; - p = Promise.resolve({ private: jwk, public: me.Keypairs.neuter({ jwk: jwk }) }); - } else { - p = me.Keypairs.import({ pem: kp.privateKeyPem }); - } - return p.then(function (pair) { - kp.privateKeyJwk = pair.private; - kp.publicKeyJwk = pair.public; - if (pair.public.kid) { - pair = JSON.parse(JSON.stringify(pair)); - delete pair.public.kid; - delete pair.private.kid; - } - return pair; - }); -}; - -ACME._toWebsafeBase64 = function (b64) { - return b64.replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g,""); -}; - -// In v8 this is crypto random, but we're just using it for pseudorandom -ACME._prnd = function (n) { - var rnd = ''; - while (rnd.length / 2 < n) { - var num = Math.random().toString().substr(2); - if (num.length % 2) { - num = '0' + num; - } - var pairs = num.match(/(..?)/g); - rnd += pairs.map(ACME._toHex).join(''); - } - return rnd.substr(0, n*2); -}; -ACME._toHex = function (pair) { - return parseInt(pair, 10).toString(16); -}; -ACME._dns01 = function (me, auth) { - // Not ACME - 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); - // Not ACME - return new me.request({ url: me._baseUrl + "/api/http?url=" + url }).then(function (resp) { - return resp.body; - }); -}; -ACME._removeChallenge = function (me, options, auth) { - return Promise.resolve().then(function () { - if (!options.challenges) { return; } - var ch = options.challenges[auth.type]; - ch.remove(auth).catch(function (e) { - console.warn("challenge.remove error:"); - console.warn(e); - }); - }); -}; - -Enc.bufToUrlBase64 = function (u8) { - return Enc.bufToBase64(u8) - .replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, ''); -}; -Enc.bufToBase64 = function (u8) { - var bin = ''; - u8.forEach(function (i) { - bin += String.fromCharCode(i); - }); - return btoa(bin); -}; - -Crypto._sha = function (sha, str) { - var encoder = new TextEncoder(); - var data = encoder.encode(str); - sha = 'SHA-' + sha.replace(/^sha-?/i, ''); - return window.crypto.subtle.digest(sha, data).then(function (hash) { - return Enc.bufToUrlBase64(new Uint8Array(hash)); - }); -}; - -}('undefined' === typeof window ? module.exports : window)); + function deactivate() { + //#console.debug('[acme-v2.js] deactivate:'); + return ACME._jwsRequest(me, { + options: options, + url: auth.url, + protected: { kid: options._kid }, + payload: Enc.binToBuf(JSON.stringify({ status: "deactivated" })) + }).then(function(/*#resp*/) { + //#console.debug('deactivate challenge: resp.body:'); + //#console.debug(resp.body); + return ACME._wait(DEAUTH_INTERVAL); + }); + } + + function pollStatus() { + if (count >= MAX_POLL) { + var err = new Error( + "[acme-v2] stuck in bad pending/processing state for '" + + altname + + "'" + ); + err.detail = "Too many attempts to validate challenge."; + return Promise.reject(); + } + + count += 1; + + //#console.debug('\n[DEBUG] statusChallenge\n'); + // POST-as-GET + return ACME._jwsRequest(me, { + options: options, + url: auth.url, + protected: { kid: options._kid }, + payload: Enc.binToBuf("") + }) + .then(checkResult) + .catch(transformError); + } + + function checkResult(resp) { + if (options.onChallengeStatus) { + try { + options.onChallengeStatus({ + altname: altname, + type: auth.type, + status: resp.body.status, + wildcard: auth.wildcard + }); + } catch (e) { + console.warn("options.onChallengeStatus Error:"); + console.warn(e); + } + } + + if ("processing" === resp.body.status) { + //#console.debug('poll: again'); + return ACME._wait(RETRY_INTERVAL).then(pollStatus); + } + + // This state should never occur + if ("pending" === resp.body.status) { + if (count >= MAX_PEND) { + return ACME._wait(RETRY_INTERVAL) + .then(deactivate) + .then(respondToChallenge); + } + //#console.debug('poll: again'); + return ACME._wait(RETRY_INTERVAL).then(pollStatus); + } + + if ("valid" === resp.body.status) { + //#console.debug('poll: valid'); + + try { + ACME._removeChallenge(me, options, auth); + } catch (e) {} + return resp.body; + } + + var code = "E_ACME_UNKNOWN"; + var err = new Error( + "[acme-v2] " + + auth.altname + + " (" + + code + + "): " + + JSON.stringify(resp.body, null, 2) + ); + err.code = code; + + return Promise.reject(err); + } + + function transformError(e) { + var err = e; + if (err.urn) { + err = new Error( + "[acme-v2] " + auth.altname + " status:" + e.status + " " + e.detail + ); + err.auth = auth; + err.altname = auth.altname; + err.type = auth.type; + err.code = + "invalid" === e.status ? "E_ACME_CHALLENGE" : "E_ACME_UNKNOWN"; + } + + throw err; + } + + function respondToChallenge() { + //#console.debug('[acme-v2.js] responding to accept challenge:'); + // POST-as-POST (empty JSON object) + return ACME._jwsRequest(me, { + options: options, + url: auth.url, + protected: { kid: options._kid }, + payload: Enc.binToBuf(JSON.stringify({})) + }) + .then(checkResult) + .catch(transformError); + } + + return respondToChallenge(); + }; + + // options = { domains, claims, challenges, challengePriority } + ACME._setChallengesAll = function(me, options) { + //#console.log("[DEBUG] setChallengesAll"); + var claims = options.order.claims.slice(0); + var valids = []; + var auths = []; + // TODO: Do we still need this delay? Or shall we leave it to plugins to account for themselves? + var DELAY = options.setChallengeWait || me.setChallengeWait || 500; + + // Set any challenges, excpting ones that have already been validated + function setNext() { + var claim = claims.shift(); + if (!claim) { + return Promise.resolve(); + } + + return Promise.resolve() + .then(function() { + // For any challenges that are already valid, + // add to the list and skip any checks. + if ( + claim.challenges.some(function(ch) { + if ("valid" === ch.status) { + valids.push(ch); + return true; + } + }) + ) { + return; + } + + // Get the list of challenge types we can validate. + // Then order that list by preference + // Select the first matching offered challenge type + var usable = Object.keys(options.challenges || ACME._challengesMap); + var selected = ( + options.challengePriority || ["tls-alpn-01", "http-01", "dns-01"] + ) + .map(function(chType) { + if (!usable.includes(chType)) { + return; + } + return claim.challenges.filter(function(ch) { + return ch.type === chType; + })[0]; + }) + .filter(Boolean)[0]; + var ch; + + // Bail with a descriptive message if no usable challenge could be selected + if (!selected) { + var enabled = usable.join(", ") || "none"; + var suitable = + claim.challenges + .map(function(r) { + return r.type; + }) + .join(", ") || "none"; + throw new Error( + "None of the challenge types that you've enabled ( " + + enabled + + " )" + + " are suitable for validating the domain you've selected (" + + claim.altname + + ")." + + " You must enable one of ( " + + suitable + + " )." + ); + } + auths.push(selected); + + // Give the nameservers a moment to propagate + if ("dns-01" === selected.type) { + DELAY = 1.5 * 1000; + } + + if (false === options.challenges) { + return; + } + ch = options.challenges[selected.type] || {}; + if (!ch.set) { + throw new Error("no handler for setting challenge"); + } + return ch.set(selected); + }) + .then(setNext); + } + + function checkNext() { + var auth = auths.shift(); + if (!auth) { + return Promise.resolve(valids); + } + + // These are not as much "valids" as they are "not invalids" + if (!me._canUse[auth.type] || me.skipChallengeTest) { + valids.push(auth); + return checkNext(); + } + + return ACME.challengeTests[auth.type](me, auth) + .then(function() { + valids.push(auth); + }) + .then(checkNext); + } + + // The reason we set every challenge in a batch first before checking any + // is so that we don't poison our own DNS cache with misses. + return setNext() + .then(function() { + //#console.debug('\n[DEBUG] waitChallengeDelay %s\n', DELAY); + return ACME._wait(DELAY); + }) + .then(checkNext); + }; + ACME._finalizeOrder = function(me, options) { + return ACME._getAccountKid(me, options).then(function() { + if (!options.challenges && !options.challengePriority) { + throw new Error("You must set either challenges or challengePrority"); + } + return ACME._setChallengesAll(me, options).then(function(valids) { + // options._kid added + //#console.debug('finalizeOrder:'); + var order = options.order; + var validatedDomains = options.order.identifiers.map(function(ident) { + return ident.value; + }); + + // Actually sets the challenge via ACME + function challengeNext() { + var auth = valids.shift(); + if (!auth) { + return Promise.resolve(); + } + return ACME._postChallenge(me, options, auth).then(challengeNext); + } + return challengeNext() + .then(function() { + //#console.debug("[getCertificate] next.then"); + //#console.log('DEBUG order:'); + //#console.log(options.order); + return options.order.identifiers.map(function(ident) { + return ident.value; + }); + }) + .then(function() { + return ACME._getCsrWeb64(me, options, validatedDomains) + .then(function(csr) { + var body = { csr: csr }; + var payload = JSON.stringify(body); + + function pollCert() { + //#console.debug('[acme-v2.js] pollCert:'); + return ACME._jwsRequest(me, { + options: options, + url: options.order.finalizeUrl, + protected: { kid: options._kid }, + payload: Enc.binToBuf(payload) + }).then(function(resp) { + //#console.debug('order finalized: resp.body:'); + //#console.debug(resp.body); + + // https://tools.ietf.org/html/draft-ietf-acme-acme-12#section-7.1.3 + // Possible values are: "pending" => ("invalid" || "ready") => "processing" => "valid" + if ("valid" === resp.body.status) { + options._expires = resp.body.expires; + options._certificate = resp.body.certificate; + + return resp.body; // return order + } + + if ("processing" === resp.body.status) { + return ACME._wait().then(pollCert); + } + + //#console.debug("Error: bad status:\n" + JSON.stringify(resp.body, null, 2)); + + if ("pending" === resp.body.status) { + return Promise.reject( + new Error( + "Did not finalize order: status 'pending'." + + " Best guess: You have not accepted at least one challenge for each domain:\n" + + "Requested: '" + + options.domains.join(", ") + + "'\n" + + "Validated: '" + + validatedDomains.join(", ") + + "'\n" + + JSON.stringify(resp.body, null, 2) + ) + ); + } + + if ("invalid" === resp.body.status) { + return Promise.reject( + new Error( + "Did not finalize order: status 'invalid'." + + " Best guess: One or more of the domain challenges could not be verified" + + " (or the order was canceled).\n" + + "Requested: '" + + options.domains.join(", ") + + "'\n" + + "Validated: '" + + validatedDomains.join(", ") + + "'\n" + + JSON.stringify(resp.body, null, 2) + ) + ); + } + + if ("ready" === resp.body.status) { + return Promise.reject( + new Error( + "Did not finalize order: status 'ready'." + + " Hmmm... this state shouldn't be possible here. That was the last state." + + " This one should at least be 'processing'.\n" + + "Requested: '" + + options.domains.join(", ") + + "'\n" + + "Validated: '" + + validatedDomains.join(", ") + + "'\n" + + JSON.stringify(resp.body, null, 2) + + "\n\n" + + "Please open an issue at https://git.coolaj86.com/coolaj86/acme-v2.js" + ) + ); + } + + return Promise.reject( + new Error( + "Didn't finalize order: Unhandled status '" + + resp.body.status + + "'." + + " This is not one of the known statuses...\n" + + "Requested: '" + + options.domains.join(", ") + + "'\n" + + "Validated: '" + + validatedDomains.join(", ") + + "'\n" + + JSON.stringify(resp.body, null, 2) + + "\n\n" + + "Please open an issue at https://git.coolaj86.com/coolaj86/acme-v2.js" + ) + ); + }); + } + + return pollCert(); + }) + .then(function() { + //#console.debug('acme-v2: order was finalized'); + // POST-as-GET + return ACME._jwsRequest(me, { + options: options, + url: options._certificate, + protected: { kid: options._kid }, + payload: Enc.binToBuf("") + }).then(function(resp) { + //#console.debug('acme-v2: csr submitted and cert received:'); + // https://github.com/certbot/certbot/issues/5721 + var certsarr = ACME.splitPemChain( + ACME.formatPemChain(resp.body || "") + ); + // cert, chain, fullchain, privkey, /*TODO, subject, altnames, issuedAt, expiresAt */ + // TODO CSR.info + var certs = { + expires: order.expires, + identifiers: order.identifiers, + cert: certsarr.shift(), + chain: certsarr.join("\n") + }; + //#console.debug(certs); + return certs; + }); + }); + }); + }); + }); + }; + ACME._createOrder = function(me, options) { + return ACME._getAccountKid(me, options).then(function() { + // options._kid added + var body = { + // raw wildcard syntax MUST be used here + identifiers: options.domains + .sort(function(a, b) { + // the first in the list will be the subject of the certificate, I believe (and hope) + if (!options.subject) { + return 0; + } + if (options.subject === a) { + return -1; + } + if (options.subject === b) { + return 1; + } + return 0; + }) + .map(function(hostname) { + return { type: "dns", value: hostname }; + }) + //, "notBefore": "2016-01-01T00:00:00Z" + //, "notAfter": "2016-01-08T00:00:00Z" + }; + + var payload = JSON.stringify(body); + //#console.debug('\n[DEBUG] newOrder\n'); + return ACME._jwsRequest(me, { + options: options, + url: me._directoryUrls.newOrder, + protected: { kid: options._kid }, + payload: Enc.binToBuf(payload) + }) + .then(function(resp) { + var order = { + orderUrl: resp.headers.location, + finalizeUrl: resp.body.finalize, + authorizations: resp.body.authorizations, + identifiers: body.identifiers, + _response: resp.body + }; + //#console.debug('[ordered]', location); // the account id url + //#console.debug(resp); + + if (!order.authorizations) { + return Promise.reject( + new Error( + "[acme-v2.js] authorizations were not fetched for '" + + options.domains.join() + + "':\n" + + JSON.stringify(resp.body) + ) + ); + } + + return order; + }) + .then(function(order) { + var claims = []; + //#console.debug("[acme-v2] POST newOrder has authorizations"); + var challengeAuths = order.authorizations.slice(0); + + function getNext() { + var authUrl = challengeAuths.shift(); + if (!authUrl) { + return claims; + } + + return ACME._getChallenges(me, options, authUrl).then(function( + claim + ) { + // var domain = options.domains[i]; // claim.identifier.value + claims.push(claim); + return getNext(); + }); + } + + return getNext().then(function() { + order.claims = claims; + options.order = order; + return order; + }); + }); + }); + }; + ACME._getAccountKid = function(me, options) { + // It's just fine if there's no account, we'll go get the key id we need via the existing key + options._kid = + options._kid || + options.accountKid || + (options.account && + (options.account.kid || + (options.account.key && options.account.key.kid))); + if (options._kid) { + return Promise.resolve(options._kid); + } + + //return Promise.reject(new Error("must include KeyID")); + // This is an idempotent request. It'll return the same account for the same public key. + return ACME._registerAccount(me, options).then(function(account) { + options._kid = account.key.kid; + // start back from the top + return options._kid; + }); + }; + + // + // Helper Methods + // + ACME._getCertificate = function(me, options) { + //#console.debug('[acme-v2] DEBUG get cert 1'); + + 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)) { + 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)." + ) + ); + } + if (!options.challenges) { + return Promise.reject(new Error("You must specify challenge handlers.")); + } + + // Do a little dry-run / self-test + return ACME._testChallenges(me, options).then(function() { + //#console.debug('[acme-v2] certificates.create'); + return ACME._createOrder(me, options).then(function(/*order*/) { + // options.order = order; + return ACME._finalizeOrder(me, options); + }); + }); + }; + ACME._getCsrWeb64 = function(me, options, validatedDomains) { + var csr; + 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.serverKey || options.serverKeypair || options.domainKeypair + ).then(function(pair) { + return me + .CSR({ jwk: pair.private, domains: validatedDomains, encoding: "der" }) + .then(function(der) { + return Enc.bufToUrlBase64(der); + }); + }); + }; + + ACME.create = function create(me) { + if (!me) { + me = {}; + } + // me.debug = true; + me.challengePrefixes = ACME.challengePrefixes; + me.Keypairs = + me.Keypairs || exports.Keypairs || require("keypairs").Keypairs; + me.CSR = me.CSR || exports.CSR || require("CSR").CSR; + me._nonces = []; + me._canUse = {}; + if (!me._baseUrl) { + me._baseUrl = ""; + } + //me.Keypairs = me.Keypairs || require('keypairs'); + //me.request = me.request || require('@root/request'); + if (!me.dns01) { + me.dns01 = function(auth) { + return ACME._dns01(me, auth); + }; + } + // backwards compat + if (!me.dig) { + me.dig = me.dns01; + } + if (!me.http01) { + me.http01 = function(auth) { + return ACME._http01(me, auth); + }; + } + + if ("function" !== typeof me.request) { + me.request = ACME._defaultRequest; + } + + me.init = function(opts) { + function fin(dir) { + me._directoryUrls = dir; + me._tos = dir.meta.termsOfService; + return dir; + } + if (opts && opts.meta && opts.termsOfService) { + return Promise.resolve(fin(opts)); + } + if (!me.directoryUrl) { + me.directoryUrl = opts; + } + 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" + ); + } + var p = Promise.resolve(); + if (!me.skipChallengeTest) { + // Not ACME + 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 fin(resp.body); + }); + }); + }; + me.accounts = { + create: function(options) { + return ACME._registerAccount(me, options); + } + }; + me.orders = { + // create + get challlenges + request: function(options) { + return ACME._createOrder(me, options); + }, + // set challenges, check challenges, finalize order, return order + complete: function(options) { + return ACME._finalizeOrder(me, options); + } + }; + me.certificates = { + create: function(options) { + return ACME._getCertificate(me, options); + } + }; + return me; + }; + + // Handle nonce, signing, and request altogether + ACME._jwsRequest = function(me, bigopts) { + return ACME._getNonce(me).then(function(nonce) { + bigopts.protected.nonce = nonce; + bigopts.protected.url = bigopts.url; + // 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({ + jwk: bigopts.accountKey || bigopts.options.accountKeypair.privateKeyJwk, + protected: bigopts.protected, + payload: bigopts.payload + }) + .then(function(jws) { + //#console.debug('[acme-v2] ' + bigopts.url + ':'); + //#console.debug(jws); + return ACME._request(me, { url: bigopts.url, json: jws }); + }) + .catch(function(e) { + if (/badNonce$/.test(e.urn)) { + // retry badNonces + var retryable = bigopts._retries >= 2; + if (!retryable) { + bigopts._retries = (bigopts._retries || 0) + 1; + return ACME._jwsRequest(me, bigopts); + } + } + throw e; + }); + }); + }; + // Handle some ACME-specific defaults + ACME._request = function(me, opts) { + if (!opts.headers) { + opts.headers = {}; + } + if (opts.json && true !== opts.json) { + opts.headers["Content-Type"] = "application/jose+json"; + opts.body = JSON.stringify(opts.json); + if (!opts.method) { + opts.method = "POST"; + } + } + return me.request(opts).then(function(resp) { + if (resp.toJSON) { + resp = resp.toJSON(); + } + if (resp.headers["replay-nonce"]) { + ACME._setNonce(me, resp.headers["replay-nonce"]); + } + + var e; + var err; + if (resp.body) { + err = resp.body.error; + e = new Error(""); + if (400 === resp.body.status) { + err = { type: resp.body.type, detail: resp.body.detail }; + } + if (err) { + e.status = resp.body.status; + e.code = "E_ACME"; + if (e.status) { + e.message = "[" + e.status + "] "; + } + e.detail = err.detail; + e.message += err.detail || JSON.stringify(err); + e.urn = err.type; + e.uri = resp.body.url; + e._rawError = err; + e._rawBody = resp.body; + throw e; + } + } + + return resp; + }); + }; + // A very generic, swappable request lib + ACME._defaultRequest = function(opts) { + // Note: normally we'd have to supply a User-Agent string, but not here in a browser + if (!opts.headers) { + opts.headers = {}; + } + if (opts.json) { + opts.headers.Accept = "application/json"; + if (true !== opts.json) { + opts.body = JSON.stringify(opts.json); + } + } + if (!opts.method) { + opts.method = "GET"; + if (opts.body) { + opts.method = "POST"; + } + } + opts.cors = true; + return window.fetch(opts.url, opts).then(function(resp) { + var headers = {}; + var result = { + statusCode: resp.status, + headers: headers, + toJSON: function() { + return this; + } + }; + Array.from(resp.headers.entries()).forEach(function(h) { + headers[h[0]] = h[1]; + }); + if (!headers["content-type"]) { + return result; + } + if (/json/.test(headers["content-type"])) { + return resp.json().then(function(json) { + result.body = json; + return result; + }); + } + return resp.text().then(function(txt) { + result.body = txt; + return result; + }); + }); + }; + + ACME._importKeypair = function(me, kp) { + var jwk = kp.privateKeyJwk || (kp.kty && kp); + var p; + if (jwk) { + // nix the browser jwk extras + jwk.key_ops = undefined; + jwk.ext = undefined; + p = Promise.resolve({ + private: jwk, + public: me.Keypairs.neuter({ jwk: jwk }) + }); + } else { + p = me.Keypairs.import({ pem: kp.privateKeyPem }); + } + return p.then(function(pair) { + kp.privateKeyJwk = pair.private; + kp.publicKeyJwk = pair.public; + if (pair.public.kid) { + pair = JSON.parse(JSON.stringify(pair)); + delete pair.public.kid; + delete pair.private.kid; + } + return pair; + }); + }; + + ACME._toWebsafeBase64 = function(b64) { + return b64 + .replace(/\+/g, "-") + .replace(/\//g, "_") + .replace(/=/g, ""); + }; + + // In v8 this is crypto random, but we're just using it for pseudorandom + ACME._prnd = function(n) { + var rnd = ""; + while (rnd.length / 2 < n) { + var num = Math.random() + .toString() + .substr(2); + if (num.length % 2) { + num = "0" + num; + } + var pairs = num.match(/(..?)/g); + rnd += pairs.map(ACME._toHex).join(""); + } + return rnd.substr(0, n * 2); + }; + ACME._toHex = function(pair) { + return parseInt(pair, 10).toString(16); + }; + ACME._dns01 = function(me, auth) { + // Not ACME + 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); + // Not ACME + return new me.request({ url: me._baseUrl + "/api/http?url=" + url }).then( + function(resp) { + return resp.body; + } + ); + }; + ACME._removeChallenge = function(me, options, auth) { + return Promise.resolve().then(function() { + if (!options.challenges) { + return; + } + var ch = options.challenges[auth.type]; + ch.remove(auth).catch(function(e) { + console.warn("challenge.remove error:"); + console.warn(e); + }); + }); + }; + + Enc.bufToUrlBase64 = function(u8) { + return Enc.bufToBase64(u8) + .replace(/\+/g, "-") + .replace(/\//g, "_") + .replace(/=/g, ""); + }; + Enc.bufToBase64 = function(u8) { + var bin = ""; + u8.forEach(function(i) { + bin += String.fromCharCode(i); + }); + return btoa(bin); + }; + + Crypto._sha = function(sha, str) { + var encoder = new TextEncoder(); + var data = encoder.encode(str); + sha = "SHA-" + sha.replace(/^sha-?/i, ""); + return window.crypto.subtle.digest(sha, data).then(function(hash) { + return Enc.bufToUrlBase64(new Uint8Array(hash)); + }); + }; +})("undefined" === typeof window ? module.exports : window); diff --git a/app/js/greenlock.js b/app/js/greenlock.js index 6466c13..c1fd567 100644 --- a/app/js/greenlock.js +++ b/app/js/greenlock.js @@ -1,504 +1,607 @@ -(function () { -'use strict'; - - /*global URLSearchParams,Headers*/ - var PromiseA = window.Promise; - var VERSION = '2'; - // ACME recommends ECDSA P-256, but RSA 2048 is still required by some old servers (like what replicated.io uses ) - // ECDSA P-384, P-521, and RSA 3072, 4096 are NOT recommend standards (and not properly supported) - var BROWSER_SUPPORTS_RSA = false; - var ECDSA_OPTS = { kty: 'EC', namedCurve: 'P-256' }; - var RSA_OPTS = { kty: 'RSA', modulusLength: 2048 }; - var Promise = window.Promise; - var Keypairs = window.Keypairs; - var ACME = window.ACME; - var CSR = window.CSR; - var $qs = function (s) { return window.document.querySelector(s); }; - var $qsa = function (s) { return window.document.querySelectorAll(s); }; - var acme; - var info = {}; - var steps = {}; - var i = 1; - var apiUrl = 'https://acme-{{env}}.api.letsencrypt.org/directory'; - - // fix previous browsers - var isCurrent = (localStorage.getItem('version') === VERSION); - if (!isCurrent) { - localStorage.clear(); - localStorage.setItem('version', VERSION); - } - localStorage.setItem('version', VERSION); - - function updateApiType() { - /*jshint validthis: true */ - var input = this || Array.prototype.filter.call( - $qsa('.js-acme-api-type'), function ($el) { return $el.checked; } - )[0]; - //#console.log('ACME api type radio:', input.value); - $qs('.js-acme-directory-url').value = apiUrl.replace(/{{env}}/g, input.value); - } - - function hideForms() { - $qsa('.js-acme-form').forEach(function (el) { - el.hidden = true; - }); - } - - function updateProgress(currentStep) { - var progressSteps = $qs("#js-progress-bar").children; - var j; - for (j = 0; j < progressSteps.length; j += 1) { - if (j < currentStep) { - progressSteps[j].classList.add("js-progress-step-complete"); - progressSteps[j].classList.remove("js-progress-step-started"); - } else if (j === currentStep) { - progressSteps[j].classList.remove("js-progress-step-complete"); - progressSteps[j].classList.add("js-progress-step-started"); - } else { - progressSteps[j].classList.remove("js-progress-step-complete"); - progressSteps[j].classList.remove("js-progress-step-started"); - } - } - } - - function newAlert(str) { - return new Promise(function () { - setTimeout(function () { - window.alert(str); - if (window.confirm("Start over?")) { - document.location.href = document.location.href.replace(/\/app.*/, '/'); - } - }, 10); - }); - } - - function submitForm(ev) { - var j = i; - i += 1; - - return PromiseA.resolve().then(function () { - return steps[j].submit(ev); - }).catch(function (err) { - var ourfault = true; - console.error(err); - if (/failed to fetch/i.test(err.message)) { - return newAlert("Network connection failure."); - } - - if ('E_ACME_CHALLENGE' === err.code) { - if ('dns-01' === err.type) { - ourfault = false; - return newAlert("It looks like the DNS record you set for " - + err.altname + " was incorrect or did not propagate. " - + "The error message was '" + err.message + "'"); - } else if ('http-01' === err.type) { - ourfault = false; - return newAlert("It looks like the file you uploaded for " - + err.altname + " was incorrect or could not be downloaded. " - + "The error message was '" + err.message + "'"); - } - } - - if (ourfault) { - err.auth = undefined; - window.alert("Something went wrong. It's probably our fault, not yours." - + " Please email aj@rootprojects.org to let him know. The error message is: \n" - + JSON.stringify(err, null, 2)); - return new Promise(function () {}); - } - }); - } - - function testKeypairSupport() { - return Keypairs.generate(RSA_OPTS).then(function () { - console.info("[crypto] RSA is supported"); - BROWSER_SUPPORTS_RSA = true; - }).catch(function () { - console.warn("[crypto] RSA is NOT supported"); - return Keypairs.generate(ECDSA_OPTS).then(function () { - console.info('[crypto] ECDSA is supported'); - }).catch(function (e) { - console.warn("[crypto] EC is NOT supported"); - throw e; - }); - }); - } - - function getServerKeypair() { - var sortedAltnames = info.identifiers.map(function (ident) { return ident.value; }).sort().join(','); - var serverJwk = JSON.parse(localStorage.getItem('server:' + sortedAltnames) || 'null'); - if (serverJwk) { - return PromiseA.resolve(serverJwk); - } - - var keypairOpts; - // TODO allow for user preference - if (BROWSER_SUPPORTS_RSA) { - keypairOpts = RSA_OPTS; - } else { - keypairOpts = ECDSA_OPTS; - } - - return Keypairs.generate(RSA_OPTS).catch(function (err) { - console.error("[Error] Keypairs.generate(" + JSON.stringify(RSA_OPTS) + "):"); - throw err; - }).then(function (pair) { - localStorage.setItem('server:'+sortedAltnames, JSON.stringify(pair.private)); - return pair.private; - }); - } - - function getAccountKeypair(email) { - var json = localStorage.getItem('account:'+email); - if (json) { - return Promise.resolve(JSON.parse(json)); - } - - return Keypairs.generate(ECDSA_OPTS).catch(function (err) { - console.warn("[Error] Keypairs.generate(" + JSON.stringify(ECDSA_OPTS) + "):\n", err); - return Keypairs.generate(RSA_OPTS).catch(function (err) { - console.error("[Error] Keypairs.generate(" + JSON.stringify(RSA_OPTS) + "):"); - throw err; - }); - }).then(function (pair) { - localStorage.setItem('account:'+email, JSON.stringify(pair.private)); - return pair.private; - }); - } - - function updateChallengeType() { - /*jshint validthis: true*/ - var input = this || Array.prototype.filter.call( - $qsa('.js-acme-challenge-type'), function ($el) { return $el.checked; } - )[0]; - $qs('.js-acme-verification-wildcard').hidden = true; - $qs('.js-acme-verification-http-01').hidden = true; - $qs('.js-acme-verification-dns-01').hidden = true; - if (info.challenges.wildcard) { - $qs('.js-acme-verification-wildcard').hidden = false; - } - if (info.challenges[input.value]) { - $qs('.js-acme-verification-' + input.value).hidden = false; - } - } - - function saveContact(email, domains) { - // to be used for good, not evil - return window.fetch('https://api.rootprojects.org/api/rootprojects.org/public/community', { - method: 'POST' - , cors: true - , headers: new Headers({ 'Content-Type': 'application/json' }) - , body: JSON.stringify({ - address: email - , project: 'greenlock-domains@rootprojects.org' - , timezone: new Intl.DateTimeFormat().resolvedOptions().timeZone - , domain: domains.join(',') - }) - }).catch(function (err) { - console.error(err); - }); - } - - steps[1] = function () { - console.info("\n1. Show domains form"); - updateProgress(0); - hideForms(); - $qs('.js-acme-form-domains').hidden = false; - }; - steps[1].submit = function () { - console.info("[submit] 1. Process domains, create ACME client", info.domains); - info.domains = $qs('.js-acme-domains').value - .replace(/https?:\/\//g, ' ').replace(/[,+]/g, ' ').trim().split(/\s+/g); - console.info("[domains]", info.domains.join(' ')); - - info.identifiers = info.domains.map(function (hostname) { - return { type: 'dns', value: hostname.toLowerCase().trim() }; - }); - info.identifiers.sort(function (a, b) { - if (a === b) { return 0; } - if (a < b) { return 1; } - if (a > b) { return -1; } - }); - - var acmeDirectoryUrl = $qs('.js-acme-directory-url').value; - acme = ACME.create({ Keypairs: Keypairs, CSR: CSR }); - return acme.init(acmeDirectoryUrl).then(function (directory) { - $qs('.js-acme-tos-url').href = directory.meta.termsOfService; - return steps[i](); - }); - }; - - steps[2] = function () { - console.info("\n2. Show account (email, ToS) form"); - - updateProgress(0); - hideForms(); - $qs('.js-acme-form-account').hidden = false; - }; - steps[2].submit = function () { - console.info("[submit] 2. Create ACME account (get Key ID)"); - - var email = $qs('.js-acme-account-email').value.toLowerCase().trim(); - info.email = email; - info.contact = [ 'mailto:' + email ]; - info.agree = $qs('.js-acme-account-tos').checked; - //info.greenlockAgree = $qs('.js-gl-tos').checked; - - // TODO ping with version and account creation - setTimeout(saveContact, 100, email, info.domains); - - $qs('.js-account-next').disabled = true; - - return info.cryptoCheck.then(function () { - return getAccountKeypair(email).then(function (jwk) { - // TODO save account id rather than always retrieving it? - console.info("[accounts] upsert for", email); - return acme.accounts.create({ - email: email - , agreeToTerms: info.agree && true - , accountKeypair: { privateKeyJwk: jwk } - }).then(function (account) { - console.info("[accounts] result:", account); - info.account = account; - info.privateJwk = jwk; - info.email = email; - }).catch(function (err) { - console.error("[accounts] failed to upsert account:"); - console.error(err); - return newAlert(err.message || JSON.stringify(err, null, 2)); - }); - }); - }).then(function () { - var jwk = info.privateJwk; - var account = info.account; - - console.info("[orders] requesting"); - return acme.orders.request({ - account: account - , accountKeypair: { privateKeyJwk: jwk } - , domains: info.domains - }).then(function (order) { - info.order = order; - console.info("[orders] created ", order); - - var claims = order.claims; - - var obj = { 'dns-01': [], 'http-01': [], 'wildcard': [] }; - info.challenges = obj; - - var $httpList = $qs('.js-acme-http'); - var $dnsList = $qs('.js-acme-dns'); - var $wildList = $qs('.js-acme-wildcard'); - var httpTpl = $httpList.innerHTML; - var dnsTpl = $dnsList.innerHTML; - var wildTpl = $wildList.innerHTML; - $httpList.innerHTML = ''; - $dnsList.innerHTML = ''; - $wildList.innerHTML = ''; - - claims.forEach(function (claim) { - //#console.log("claims[i]", claim); - var hostname = claim.identifier.value; - claim.challenges.forEach(function (c) { - var auth = c; - var data = { - type: c.type - , hostname: hostname - , url: c.url - , token: c.token - , httpPath: auth.challengeUrl - , httpAuth: auth.keyAuthorization - , dnsType: 'TXT' - , dnsHost: auth.dnsHost - , dnsAnswer: auth.keyAuthorizationDigest - }; - //#console.log("claims[i].challenge", data); - - var $tpl = document.createElement("div"); - if (claim.wildcard) { - obj.wildcard.push(data); - $tpl.innerHTML = wildTpl; - $tpl.querySelector(".js-acme-ver-txt-host").innerHTML = data.dnsHost; - $tpl.querySelector(".js-acme-ver-txt-value").innerHTML = data.dnsAnswer; - $wildList.appendChild($tpl); - } else if(obj[data.type]) { - - obj[data.type].push(data); - - if ('dns-01' === data.type) { - $tpl.innerHTML = dnsTpl; - $tpl.querySelector(".js-acme-ver-txt-host").innerHTML = data.dnsHost; - $tpl.querySelector(".js-acme-ver-txt-value").innerHTML = data.dnsAnswer; - $dnsList.appendChild($tpl); - } else if ('http-01' === data.type) { - $tpl.innerHTML = httpTpl; - $tpl.querySelector(".js-acme-ver-file-location").innerHTML = data.httpPath.split("/").slice(-1); - $tpl.querySelector(".js-acme-ver-content").innerHTML = data.httpAuth; - $tpl.querySelector(".js-acme-ver-uri").innerHTML = data.httpPath; - $tpl.querySelector(".js-download-verify-link").href = - "data:text/octet-stream;base64," + window.btoa(data.httpAuth); - $tpl.querySelector(".js-download-verify-link").download = data.httpPath.split("/").slice(-1); - $httpList.appendChild($tpl); - } - } - }); - }); - - // hide wildcard if no wildcard - // hide http-01 and dns-01 if only wildcard - if (!obj.wildcard.length) { - $qs('.js-acme-wildcard-challenges').hidden = true; - } - if (!obj['http-01'].length) { - $qs('.js-acme-challenges').hidden = true; - } - - console.info("[housekeeping] challenges", info.challenges); - - updateChallengeType(); - return steps[i](); - }).catch(function (err) { - if (err.detail || err.urn) { - console.error("(Probably) User Error:"); - console.error(err); - return newAlert("There was an error, probably with your email or domain:\n" + err.message); - } - throw err; - }); - }).catch(function (err) { - console.error('Step \'\' Error:'); - console.error(err, err.stack); - return newAlert("An error happened (but it's not your fault)." - + " Email aj@rootprojects.org to let him know that 'order and get challenges' failed."); - }); - }; - - steps[3] = function () { - console.info("\n3. Present challenge options"); - updateProgress(1); - hideForms(); - $qs('.js-acme-form-challenges').hidden = false; - }; - steps[3].submit = function () { - console.info("[submit] 3. Fulfill challenges, fetch certificate"); - - var challengePriority = [ 'dns-01' ]; - if ('http-01' === $qs('.js-acme-challenge-type:checked').value) { - challengePriority.unshift('http-01'); - } - console.info("[challenge] selected ", challengePriority[0]); - - // for now just show the next page immediately (its a spinner) - steps[i](); - - return getAccountKeypair(info.email).then(function (jwk) { - // TODO put a test challenge in the list - // info.order.claims.push(...) - // TODO warn about wait-time if DNS - return getServerKeypair().then(function (serverJwk) { - return acme.orders.complete({ - account: info.account - , accountKeypair: { privateKeyJwk: jwk } - , order: info.order - , domains: info.domains - , domainKeypair: { privateKeyJwk: serverJwk } - , challengePriority: challengePriority - , challenges: false - , onChallengeStatus: function (details) { - $qs('.js-challenge-responses').hidden = false; - $qs('.js-challenge-response-type').innerText = details.type; - $qs('.js-challenge-response-status').innerText = details.status; - $qs('.js-challenge-response-altname').innerText = details.altname; - } - }).then(function (certs) { - return Keypairs.export({ jwk: serverJwk }).then(function (keyPem) { - console.info('WINNING!'); - console.info(certs); - $qs('#js-fullchain').innerHTML = [ - certs.cert.trim() + "\n" - , certs.chain + "\n" - ].join("\n"); - $qs("#js-download-fullchain-link").href = - "data:text/octet-stream;base64," + window.btoa(certs); - - $qs('#js-privkey').innerHTML = keyPem; - $qs("#js-download-privkey-link").href = - "data:text/octet-stream;base64," + window.btoa(keyPem); - return submitForm(); - }); - }); - }); - }); - }; - - // spinner - steps[4] = function () { - console.info('\n4. Show loading spinner'); - updateProgress(1); - hideForms(); - $qs('.js-acme-form-poll').hidden = false; - }; - steps[4].submit = function () { - console.info('[submit] 4. Order complete'); - - return steps[i](); - }; - - steps[5] = function () { - console.info('\n5. Present certificates (yay!)'); - updateProgress(2); - hideForms(); - $qs('.js-acme-form-download').hidden = false; - }; - - function init() { - $qsa('.js-acme-api-type').forEach(function ($el) { - $el.addEventListener('change', updateApiType); - }); - updateApiType(); - - $qsa('.js-acme-form').forEach(function ($el) { - $el.addEventListener('submit', function (ev) { - ev.preventDefault(); - return submitForm(ev); - }); - }); - - $qsa('.js-acme-challenge-type').forEach(function ($el) { - $el.addEventListener('change', updateChallengeType); - }); - - - var params = new URLSearchParams(window.location.search); - var apiType = params.get('acme-api-type') || "staging-v02"; - if (params.has('acme-domains')) { - $qs('.js-acme-domains').value = params.get('acme-domains'); - - $qsa('.js-acme-api-type').forEach(function(ele) { - if(ele.value === apiType) { - ele.checked = true; - } - }); - - updateApiType(); - steps[2](); - return submitForm(); - } else { - steps[1](); - } - } - - init(); - $qs('body').hidden = false; - - // in the background - info.cryptoCheck = testKeypairSupport().then(function () { - console.info("[crypto] self-check: passed"); - }).catch(function (err) { - console.error('[crypto] could not use either RSA nor EC.'); - console.error(err); - window.alert("Generating secure certificates requires a browser with cryptography support." - + "Please consider a recent version of Chrome, Firefox, or Safari."); - throw err; - }); -}()); +(function() { + "use strict"; + + /*global URLSearchParams,Headers*/ + var PromiseA = window.Promise; + var VERSION = "2"; + // ACME recommends ECDSA P-256, but RSA 2048 is still required by some old servers (like what replicated.io uses ) + // ECDSA P-384, P-521, and RSA 3072, 4096 are NOT recommend standards (and not properly supported) + var BROWSER_SUPPORTS_RSA = false; + var ECDSA_OPTS = { kty: "EC", namedCurve: "P-256" }; + var RSA_OPTS = { kty: "RSA", modulusLength: 2048 }; + var Promise = window.Promise; + var Keypairs = window.Keypairs; + var ACME = window.ACME; + var CSR = window.CSR; + var $qs = function(s) { + return window.document.querySelector(s); + }; + var $qsa = function(s) { + return window.document.querySelectorAll(s); + }; + var acme; + var info = {}; + var steps = {}; + var i = 1; + var apiUrl = "https://acme-{{env}}.api.letsencrypt.org/directory"; + + // fix previous browsers + var isCurrent = localStorage.getItem("version") === VERSION; + if (!isCurrent) { + localStorage.clear(); + localStorage.setItem("version", VERSION); + } + localStorage.setItem("version", VERSION); + + function updateApiType() { + /*jshint validthis: true */ + var input = + this || + Array.prototype.filter.call($qsa(".js-acme-api-type"), function($el) { + return $el.checked; + })[0]; + //#console.log('ACME api type radio:', input.value); + $qs(".js-acme-directory-url").value = apiUrl.replace( + /{{env}}/g, + input.value + ); + } + + function hideForms() { + $qsa(".js-acme-form").forEach(function(el) { + el.hidden = true; + }); + } + + function updateProgress(currentStep) { + var progressSteps = $qs("#js-progress-bar").children; + var j; + for (j = 0; j < progressSteps.length; j += 1) { + if (j < currentStep) { + progressSteps[j].classList.add("js-progress-step-complete"); + progressSteps[j].classList.remove("js-progress-step-started"); + } else if (j === currentStep) { + progressSteps[j].classList.remove("js-progress-step-complete"); + progressSteps[j].classList.add("js-progress-step-started"); + } else { + progressSteps[j].classList.remove("js-progress-step-complete"); + progressSteps[j].classList.remove("js-progress-step-started"); + } + } + } + + function newAlert(str) { + return new Promise(function() { + setTimeout(function() { + window.alert(str); + if (window.confirm("Start over?")) { + document.location.href = document.location.href.replace( + /\/app.*/, + "/" + ); + } + }, 10); + }); + } + + function submitForm(ev) { + var j = i; + i += 1; + + return PromiseA.resolve() + .then(function() { + return steps[j].submit(ev); + }) + .catch(function(err) { + var ourfault = true; + console.error(err); + if (/failed to fetch/i.test(err.message)) { + return newAlert("Network connection failure."); + } + + if ("E_ACME_CHALLENGE" === err.code) { + if ("dns-01" === err.type) { + ourfault = false; + return newAlert( + "It looks like the DNS record you set for " + + err.altname + + " was incorrect or did not propagate. " + + "The error message was '" + + err.message + + "'" + ); + } else if ("http-01" === err.type) { + ourfault = false; + return newAlert( + "It looks like the file you uploaded for " + + err.altname + + " was incorrect or could not be downloaded. " + + "The error message was '" + + err.message + + "'" + ); + } + } + + if (ourfault) { + err.auth = undefined; + window.alert( + "Something went wrong. It's probably our fault, not yours." + + " Please email aj@rootprojects.org to let him know. The error message is: \n" + + JSON.stringify(err, null, 2) + ); + return new Promise(function() {}); + } + }); + } + + function testKeypairSupport() { + return Keypairs.generate(RSA_OPTS) + .then(function() { + console.info("[crypto] RSA is supported"); + BROWSER_SUPPORTS_RSA = true; + }) + .catch(function() { + console.warn("[crypto] RSA is NOT supported"); + return Keypairs.generate(ECDSA_OPTS) + .then(function() { + console.info("[crypto] ECDSA is supported"); + }) + .catch(function(e) { + console.warn("[crypto] EC is NOT supported"); + throw e; + }); + }); + } + + function getServerKeypair() { + var sortedAltnames = info.identifiers + .map(function(ident) { + return ident.value; + }) + .sort() + .join(","); + var serverJwk = JSON.parse( + localStorage.getItem("server:" + sortedAltnames) || "null" + ); + if (serverJwk) { + return PromiseA.resolve(serverJwk); + } + + var keypairOpts; + // TODO allow for user preference + if (BROWSER_SUPPORTS_RSA) { + keypairOpts = RSA_OPTS; + } else { + keypairOpts = ECDSA_OPTS; + } + + return Keypairs.generate(RSA_OPTS) + .catch(function(err) { + console.error( + "[Error] Keypairs.generate(" + JSON.stringify(RSA_OPTS) + "):" + ); + throw err; + }) + .then(function(pair) { + localStorage.setItem( + "server:" + sortedAltnames, + JSON.stringify(pair.private) + ); + return pair.private; + }); + } + + function getAccountKeypair(email) { + var json = localStorage.getItem("account:" + email); + if (json) { + return Promise.resolve(JSON.parse(json)); + } + + return Keypairs.generate(ECDSA_OPTS) + .catch(function(err) { + console.warn( + "[Error] Keypairs.generate(" + JSON.stringify(ECDSA_OPTS) + "):\n", + err + ); + return Keypairs.generate(RSA_OPTS).catch(function(err) { + console.error( + "[Error] Keypairs.generate(" + JSON.stringify(RSA_OPTS) + "):" + ); + throw err; + }); + }) + .then(function(pair) { + localStorage.setItem("account:" + email, JSON.stringify(pair.private)); + return pair.private; + }); + } + + function updateChallengeType() { + /*jshint validthis: true*/ + var input = + this || + Array.prototype.filter.call($qsa(".js-acme-challenge-type"), function( + $el + ) { + return $el.checked; + })[0]; + $qs(".js-acme-verification-wildcard").hidden = true; + $qs(".js-acme-verification-http-01").hidden = true; + $qs(".js-acme-verification-dns-01").hidden = true; + if (info.challenges.wildcard) { + $qs(".js-acme-verification-wildcard").hidden = false; + } + if (info.challenges[input.value]) { + $qs(".js-acme-verification-" + input.value).hidden = false; + } + } + + function saveContact(email, domains) { + // to be used for good, not evil + return window + .fetch( + "https://api.rootprojects.org/api/rootprojects.org/public/community", + { + method: "POST", + cors: true, + headers: new Headers({ "Content-Type": "application/json" }), + body: JSON.stringify({ + address: email, + project: "greenlock-domains@rootprojects.org", + timezone: new Intl.DateTimeFormat().resolvedOptions().timeZone, + domain: domains.join(",") + }) + } + ) + .catch(function(err) { + console.error(err); + }); + } + + steps[1] = function() { + console.info("\n1. Show domains form"); + updateProgress(0); + hideForms(); + $qs(".js-acme-form-domains").hidden = false; + }; + steps[1].submit = function() { + console.info( + "[submit] 1. Process domains, create ACME client", + info.domains + ); + info.domains = $qs(".js-acme-domains") + .value.replace(/https?:\/\//g, " ") + .replace(/[,+]/g, " ") + .trim() + .split(/\s+/g); + console.info("[domains]", info.domains.join(" ")); + + info.identifiers = info.domains.map(function(hostname) { + return { type: "dns", value: hostname.toLowerCase().trim() }; + }); + info.identifiers.sort(function(a, b) { + if (a === b) { + return 0; + } + if (a < b) { + return 1; + } + if (a > b) { + return -1; + } + }); + + var acmeDirectoryUrl = $qs(".js-acme-directory-url").value; + acme = ACME.create({ Keypairs: Keypairs, CSR: CSR }); + return acme.init(acmeDirectoryUrl).then(function(directory) { + $qs(".js-acme-tos-url").href = directory.meta.termsOfService; + return steps[i](); + }); + }; + + steps[2] = function() { + console.info("\n2. Show account (email, ToS) form"); + + updateProgress(0); + hideForms(); + $qs(".js-acme-form-account").hidden = false; + }; + steps[2].submit = function() { + console.info("[submit] 2. Create ACME account (get Key ID)"); + + var email = $qs(".js-acme-account-email") + .value.toLowerCase() + .trim(); + info.email = email; + info.contact = ["mailto:" + email]; + info.agree = $qs(".js-acme-account-tos").checked; + //info.greenlockAgree = $qs('.js-gl-tos').checked; + + // TODO ping with version and account creation + setTimeout(saveContact, 100, email, info.domains); + + $qs(".js-account-next").disabled = true; + + return info.cryptoCheck + .then(function() { + return getAccountKeypair(email).then(function(jwk) { + // TODO save account id rather than always retrieving it? + console.info("[accounts] upsert for", email); + return acme.accounts + .create({ + email: email, + agreeToTerms: info.agree && true, + accountKeypair: { privateKeyJwk: jwk } + }) + .then(function(account) { + console.info("[accounts] result:", account); + info.account = account; + info.privateJwk = jwk; + info.email = email; + }) + .catch(function(err) { + console.error("[accounts] failed to upsert account:"); + console.error(err); + return newAlert(err.message || JSON.stringify(err, null, 2)); + }); + }); + }) + .then(function() { + var jwk = info.privateJwk; + var account = info.account; + + console.info("[orders] requesting"); + return acme.orders + .request({ + account: account, + accountKeypair: { privateKeyJwk: jwk }, + domains: info.domains + }) + .then(function(order) { + info.order = order; + console.info("[orders] created ", order); + + var claims = order.claims; + + var obj = { "dns-01": [], "http-01": [], wildcard: [] }; + info.challenges = obj; + + var $httpList = $qs(".js-acme-http"); + var $dnsList = $qs(".js-acme-dns"); + var $wildList = $qs(".js-acme-wildcard"); + var httpTpl = $httpList.innerHTML; + var dnsTpl = $dnsList.innerHTML; + var wildTpl = $wildList.innerHTML; + $httpList.innerHTML = ""; + $dnsList.innerHTML = ""; + $wildList.innerHTML = ""; + + claims.forEach(function(claim) { + //#console.log("claims[i]", claim); + var hostname = claim.identifier.value; + claim.challenges.forEach(function(c) { + var auth = c; + var data = { + type: c.type, + hostname: hostname, + url: c.url, + token: c.token, + httpPath: auth.challengeUrl, + httpAuth: auth.keyAuthorization, + dnsType: "TXT", + dnsHost: auth.dnsHost, + dnsAnswer: auth.keyAuthorizationDigest + }; + //#console.log("claims[i].challenge", data); + + var $tpl = document.createElement("div"); + if (claim.wildcard) { + obj.wildcard.push(data); + $tpl.innerHTML = wildTpl; + $tpl.querySelector(".js-acme-ver-txt-host").innerHTML = + data.dnsHost; + $tpl.querySelector(".js-acme-ver-txt-value").innerHTML = + data.dnsAnswer; + $wildList.appendChild($tpl); + } else if (obj[data.type]) { + obj[data.type].push(data); + + if ("dns-01" === data.type) { + $tpl.innerHTML = dnsTpl; + $tpl.querySelector(".js-acme-ver-txt-host").innerHTML = + data.dnsHost; + $tpl.querySelector(".js-acme-ver-txt-value").innerHTML = + data.dnsAnswer; + $dnsList.appendChild($tpl); + } else if ("http-01" === data.type) { + $tpl.innerHTML = httpTpl; + $tpl.querySelector( + ".js-acme-ver-file-location" + ).innerHTML = data.httpPath.split("/").slice(-1); + $tpl.querySelector(".js-acme-ver-content").innerHTML = + data.httpAuth; + $tpl.querySelector(".js-acme-ver-uri").innerHTML = + data.httpPath; + $tpl.querySelector(".js-download-verify-link").href = + "data:text/octet-stream;base64," + + window.btoa(data.httpAuth); + $tpl.querySelector( + ".js-download-verify-link" + ).download = data.httpPath.split("/").slice(-1); + $httpList.appendChild($tpl); + } + } + }); + }); + + // hide wildcard if no wildcard + // hide http-01 and dns-01 if only wildcard + if (!obj.wildcard.length) { + $qs(".js-acme-wildcard-challenges").hidden = true; + } + if (!obj["http-01"].length) { + $qs(".js-acme-challenges").hidden = true; + } + + console.info("[housekeeping] challenges", info.challenges); + + updateChallengeType(); + return steps[i](); + }) + .catch(function(err) { + if (err.detail || err.urn) { + console.error("(Probably) User Error:"); + console.error(err); + return newAlert( + "There was an error, probably with your email or domain:\n" + + err.message + ); + } + throw err; + }); + }) + .catch(function(err) { + console.error("Step '' Error:"); + console.error(err, err.stack); + return newAlert( + "An error happened (but it's not your fault)." + + " Email aj@rootprojects.org to let him know that 'order and get challenges' failed." + ); + }); + }; + + steps[3] = function() { + console.info("\n3. Present challenge options"); + updateProgress(1); + hideForms(); + $qs(".js-acme-form-challenges").hidden = false; + }; + steps[3].submit = function() { + console.info("[submit] 3. Fulfill challenges, fetch certificate"); + + var challengePriority = ["dns-01"]; + if ("http-01" === $qs(".js-acme-challenge-type:checked").value) { + challengePriority.unshift("http-01"); + } + console.info("[challenge] selected ", challengePriority[0]); + + // for now just show the next page immediately (its a spinner) + steps[i](); + + return getAccountKeypair(info.email).then(function(jwk) { + // TODO put a test challenge in the list + // info.order.claims.push(...) + // TODO warn about wait-time if DNS + return getServerKeypair().then(function(serverJwk) { + return acme.orders + .complete({ + account: info.account, + accountKeypair: { privateKeyJwk: jwk }, + order: info.order, + domains: info.domains, + domainKeypair: { privateKeyJwk: serverJwk }, + challengePriority: challengePriority, + challenges: false, + onChallengeStatus: function(details) { + $qs(".js-challenge-responses").hidden = false; + $qs(".js-challenge-response-type").innerText = details.type; + $qs(".js-challenge-response-status").innerText = details.status; + $qs(".js-challenge-response-altname").innerText = details.altname; + } + }) + .then(function(certs) { + return Keypairs.export({ jwk: serverJwk }).then(function(keyPem) { + console.info("WINNING!"); + console.info(certs); + $qs("#js-fullchain").innerHTML = [ + certs.cert.trim() + "\n", + certs.chain + "\n" + ].join("\n"); + $qs("#js-download-fullchain-link").href = + "data:text/octet-stream;base64," + window.btoa(certs); + + $qs("#js-privkey").innerHTML = keyPem; + $qs("#js-download-privkey-link").href = + "data:text/octet-stream;base64," + window.btoa(keyPem); + return submitForm(); + }); + }); + }); + }); + }; + + // spinner + steps[4] = function() { + console.info("\n4. Show loading spinner"); + updateProgress(1); + hideForms(); + $qs(".js-acme-form-poll").hidden = false; + }; + steps[4].submit = function() { + console.info("[submit] 4. Order complete"); + + return steps[i](); + }; + + steps[5] = function() { + console.info("\n5. Present certificates (yay!)"); + updateProgress(2); + hideForms(); + $qs(".js-acme-form-download").hidden = false; + }; + + function init() { + $qsa(".js-acme-api-type").forEach(function($el) { + $el.addEventListener("change", updateApiType); + }); + updateApiType(); + + $qsa(".js-acme-form").forEach(function($el) { + $el.addEventListener("submit", function(ev) { + ev.preventDefault(); + return submitForm(ev); + }); + }); + + $qsa(".js-acme-challenge-type").forEach(function($el) { + $el.addEventListener("change", updateChallengeType); + }); + + var params = new URLSearchParams(window.location.search); + var apiType = params.get("acme-api-type") || "staging-v02"; + if (params.has("acme-domains")) { + $qs(".js-acme-domains").value = params.get("acme-domains"); + + $qsa(".js-acme-api-type").forEach(function(ele) { + if (ele.value === apiType) { + ele.checked = true; + } + }); + + updateApiType(); + steps[2](); + return submitForm(); + } else { + steps[1](); + } + } + + init(); + $qs("body").hidden = false; + + // in the background + info.cryptoCheck = testKeypairSupport() + .then(function() { + console.info("[crypto] self-check: passed"); + }) + .catch(function(err) { + console.error("[crypto] could not use either RSA nor EC."); + console.error(err); + window.alert( + "Generating secure certificates requires a browser with cryptography support." + + "Please consider a recent version of Chrome, Firefox, or Safari." + ); + throw err; + }); +})(); diff --git a/app/styles/main.css b/app/styles/main.css index d735684..4b0fe2c 100644 --- a/app/styles/main.css +++ b/app/styles/main.css @@ -1,263 +1,270 @@ body { - font-size: 18px; - font-family: Source Sans Pro, sans-serif; - margin: 0; - line-height: 1.33; - color: #1a1a1a; + font-size: 18px; + font-family: Source Sans Pro, sans-serif; + margin: 0; + line-height: 1.33; + color: #1a1a1a; } h1 { - text-align: center; - font-size: 1.77777778em; + text-align: center; + font-size: 1.77777778em; } a { - color: #1a1a1a; + color: #1a1a1a; } -input[type=email], input[type=text] { - font-size: 1em; - padding: 0.444444444em 0.888889em; - width: 100%; - border: solid 1px #d9d9d9; - border-radius: 2px; +input[type="email"], +input[type="text"] { + font-size: 1em; + padding: 0.444444444em 0.888889em; + width: 100%; + border: solid 1px #d9d9d9; + border-radius: 2px; } pre { - margin: 0; - font-family: Source Code Pro, monospace; + margin: 0; + font-family: Source Code Pro, monospace; } .column-row { - width: 22.222222em; + width: 22.222222em; } .column-container { - display: flex; - flex-direction: column; - align-items: center; + display: flex; + flex-direction: column; + align-items: center; } .progress-bar { - height: 0; - border: solid 1px #5bc17f; - background-color: #5bc17f; - display: flex; - justify-content: space-between; - align-items: center; - width: 22em; - margin: 1.388888889em auto; + height: 0; + border: solid 1px #5bc17f; + background-color: #5bc17f; + display: flex; + justify-content: space-between; + align-items: center; + width: 22em; + margin: 1.388888889em auto; } .greenlock-logo-badge > img { - width: 100%; + width: 100%; } .greenlock-logo-badge { - display: inline-block; - border: solid 1px #d9d9d9; - border-radius: 500px; - width: 5.333333333em; - height: 5.333333333em; - margin-top: 4.277777778em; + display: inline-block; + border: solid 1px #d9d9d9; + border-radius: 500px; + width: 5.333333333em; + height: 5.333333333em; + margin-top: 4.277777778em; } .header-row { - text-align: center; + text-align: center; } .progress-bar-step { - position: relative; - margin: -0.722222222em -0.166666667em; - display: inline-block; - background-color: white; - /* border-radius: 100%; */ - padding: 0 0.111111em; + position: relative; + margin: -0.722222222em -0.166666667em; + display: inline-block; + background-color: white; + /* border-radius: 100%; */ + padding: 0 0.111111em; } .progress-bar-step > .circle { - content: ""; - display: inline-block; - border: solid 0.111111111em #5bc17f; - width: 0.888888889em; - height: 0.888888889em; - border-radius: 100%; - background: white; + content: ""; + display: inline-block; + border: solid 0.111111111em #5bc17f; + width: 0.888888889em; + height: 0.888888889em; + border-radius: 100%; + background: white; } .progress-step-label { - text-align: center; - position: absolute; - left: 50%; - =: block font-size: ; - top: 139%; - font-size: 0.722222222em; - white-space: nowrap; + text-align: center; + position: absolute; + left: 50%; + top: 139%; + font-size: 0.722222222em; + white-space: nowrap; } .progress-step-label > div { - position: relative; - right: 50%; + position: relative; + right: 50%; } .greenlock-name { - color: #808080; + color: #808080; } .file-preview { - background: #f7f7f7; - position: relative; - font-size: 0.833333333em; - padding: 1.6em 2.9333em 1.6em 1.6em; + background: #f7f7f7; + position: relative; + font-size: 0.833333333em; + padding: 1.6em 2.9333em 1.6em 1.6em; } -.js-progress-step-complete > .circle, .js-progress-step-started > .circle { - background-color: #5bc17f; +.js-progress-step-complete > .circle, +.js-progress-step-started > .circle { + background-color: #5bc17f; } .progress-bar-step.js-progress-step-complete svg { - fill: white; - /* stroke: none; */ - display: initial; + fill: white; + /* stroke: none; */ + display: initial; } .checkbox-array { - display: flex; - flex-direction: column; - padding: 1em 0; + display: flex; + flex-direction: column; + padding: 1em 0; } -.checkbox-array input[type=checkbox] { - opacity: 0; - position: absolute; +.checkbox-array input[type="checkbox"] { + opacity: 0; + position: absolute; } -.checkbox-array input[type=checkbox] ~ .icon-checked-box { - display: none; +.checkbox-array input[type="checkbox"] ~ .icon-checked-box { + display: none; } -.checkbox-array input[type=checkbox] ~ .icon-unchecked-box { - display: initial; +.checkbox-array input[type="checkbox"] ~ .icon-unchecked-box { + display: initial; } -.checkbox-array input[type=checkbox]:checked ~ .icon-checked-box { - display: initial; +.checkbox-array input[type="checkbox"]:checked ~ .icon-checked-box { + display: initial; } -.checkbox-array input[type=checkbox]:checked ~ .icon-unchecked-box { - display: none; +.checkbox-array input[type="checkbox"]:checked ~ .icon-unchecked-box { + display: none; } -.checkbox-array .icon-checked-box, .checkbox-array .icon-unchecked-box { - width: 1.333333333em; - fill: #5bc17f; - margin-right: 0.666666667em; +.checkbox-array .icon-checked-box, +.checkbox-array .icon-unchecked-box { + width: 1.333333333em; + fill: #5bc17f; + margin-right: 0.666666667em; } .checkbox-array label { - display: flex; - height: 1.333333333em; - font-size: 0.833333333em; - margin: 0.4em 0; + display: flex; + height: 1.333333333em; + font-size: 0.833333333em; + margin: 0.4em 0; } -.checkbox-array input[type=checkbox]:focus ~ .icon-checked-box, .checkbox-array input[type=checkbox]:focus ~ .icon-unchecked-box { - background: #5bc17f52; +.checkbox-array input[type="checkbox"]:focus ~ .icon-checked-box, +.checkbox-array input[type="checkbox"]:focus ~ .icon-unchecked-box { + background: #5bc17f52; } .email-usage { - color: #666666; - font-size: 0.833333333em; - margin: 2em 0; + color: #666666; + font-size: 0.833333333em; + margin: 2em 0; } .button-next { - width: 100%; - background-color: #5bc17f; - border: none; - font-size: 1em; - color: white; - padding: 0.44444em; - margin: 1em 0; + width: 100%; + background-color: #5bc17f; + border: none; + font-size: 1em; + color: white; + padding: 0.44444em; + margin: 1em 0; } .tabbed-selector label { - width: 50%; - padding: 0.5em 0; + width: 50%; + padding: 0.5em 0; } .tabbed-selector { - display: flex; - font-weight: bold; - text-align: center; + display: flex; + font-weight: bold; + text-align: center; } -.tabbed-selector input[type=radio] { - display: none; +.tabbed-selector input[type="radio"] { + display: none; } .download-file svg { - fill: #5bc17f; - width: 1.333333333em; + fill: #5bc17f; + width: 1.333333333em; } .download-file a { - color: #5bc17f; + color: #5bc17f; } .mdicon { - position: relative; - top: 0.4em; + position: relative; + top: 0.4em; } .http-verification-info { - padding-right: 6.933333333em; + padding-right: 6.933333333em; } .paper-fold { - position: absolute; - width: 2em; - height: 2em; - border-left: solid #d9d9d9 1px; - border-bottom: solid #d9d9d9 1px; - right: 0; - top: 0; - background: linear-gradient(45deg, #f7f7f7 0%,#f7f7f7 50%,#ffffff 50%,#ffffff 100%); + position: absolute; + width: 2em; + height: 2em; + border-left: solid #d9d9d9 1px; + border-bottom: solid #d9d9d9 1px; + right: 0; + top: 0; + background: linear-gradient( + 45deg, + #f7f7f7 0%, + #f7f7f7 50%, + #ffffff 50%, + #ffffff 100% + ); } .file-ver-info-header { - color: #808080; + color: #808080; } .http-verification-info hr { - border: none; - border-bottom: solid 1px #d9d9d9; + border: none; + border-bottom: solid 1px #d9d9d9; } .acme-ver-uri { - word-break: break-all; - margin: auto; + word-break: break-all; + margin: auto; } .acme-ver-dns-label { - margin: 1.777777778em 0 0.444444444em 0; - border-bottom: solid 1px #d9d9d9; - font-weight: bold; - padding-bottom: 0.166666667em; + margin: 1.777777778em 0 0.444444444em 0; + border-bottom: solid 1px #d9d9d9; + font-weight: bold; + padding-bottom: 0.166666667em; } .tabbed-selector input[type="radio"]:checked ~ div { - border: solid 1px #5bc17f; - background-color: #5bc17f; + border: solid 1px #5bc17f; + background-color: #5bc17f; } .file-preview pre { - white-space: pre-line; - word-break: break-all; + white-space: pre-line; + word-break: break-all; } - .cert-download-container { - margin: 0 -31%; + margin: 0 -31%; } - diff --git a/index.html b/index.html index 940ba2c..034ee05 100644 --- a/index.html +++ b/index.html @@ -1,99 +1,171 @@ - - Greenlock™ - - - - - - - - - - - - -
+ + Greenlock™ + + + + + + + + + + + + +
+
+ Greenlock logo +
+
+

Get the green lock for your website

+
+
+
+ Greenlock will process the CSR in the browser and request the + certificates directly from letsencrypt.org. Please enable Javascript + before continuing. +
+
+
+ Secure | + https:// +
+ +
+ Domain, subdomain, or wildcard domain +
-
- -
-
-

Get the green lock for your website

-
-
-
- Greenlock will process the CSR in the browser and request the certificates directly from letsencrypt.org. - Please enable Javascript before continuing. -
- -
- Secure | https:// -
- -
Domain, subdomain, or wildcard domain
- -
- - - -
- API Compatibility: Let's Encrypt v2 / ACME draft 15 -
-
- A Root Project - | View Source (git) - | Terms of Service - | Privacy Policy -
-
- -
-
-
-

Why you need HTTPS

- SSL Certificates are required for secure login, accepting payments, and for browsers like Google Chrome to stop showing security warnings to your users. -
-
- - + - - - + -
- + gtag("config", "UA-118745161-2"); + +
+ diff --git a/js/app.js b/js/app.js index c4e2cb4..d792b3b 100644 --- a/js/app.js +++ b/js/app.js @@ -1,32 +1,39 @@ -(function () { -'use strict'; - - var $qs = function (s) { return window.document.querySelector(s); }; - - $qs('.js-javascript-warning').hidden = true; - - var apiUrl = 'https://acme-{{env}}.api.letsencrypt.org/directory'; - - function updateApiType() { - var formData = new FormData($qs("#js-acme-form")); - - console.log('ACME api type radio:'); - - var value = formData.get("acme-api-type"); - $qs('#js-acme-api-url').value = apiUrl.replace(/{{env}}/g, value); - } - - $qs('#js-acme-form').addEventListener('change', updateApiType); - //$qs('#js-acme-form').addEventListener('submit', prettyRedirect); - - updateApiType(); - try { - document.fonts.load().then(function() { - $qs('body').classList.add("js-app-ready"); - }).catch(function(e) { - $qs('body').classList.add("js-app-ready"); - }); - } catch(e) { - setTimeout(function() {$qs('body').classList.add("js-app-ready");}, 200); - } -}()); +(function() { + "use strict"; + + var $qs = function(s) { + return window.document.querySelector(s); + }; + + $qs(".js-javascript-warning").hidden = true; + + var apiUrl = "https://acme-{{env}}.api.letsencrypt.org/directory"; + + function updateApiType() { + var formData = new FormData($qs("#js-acme-form")); + + console.log("ACME api type radio:"); + + var value = formData.get("acme-api-type"); + $qs("#js-acme-api-url").value = apiUrl.replace(/{{env}}/g, value); + } + + $qs("#js-acme-form").addEventListener("change", updateApiType); + //$qs('#js-acme-form').addEventListener('submit', prettyRedirect); + + updateApiType(); + try { + document.fonts + .load() + .then(function() { + $qs("body").classList.add("js-app-ready"); + }) + .catch(function(e) { + $qs("body").classList.add("js-app-ready"); + }); + } catch (e) { + setTimeout(function() { + $qs("body").classList.add("js-app-ready"); + }, 200); + } +})(); diff --git a/legal.html b/legal.html index 1998036..06a321c 100644 --- a/legal.html +++ b/legal.html @@ -1,201 +1,315 @@ -

Greetings!

- -

I, AJ ONeal, am not a big fan of legalize, but I am a fan of communicating -clearly. I hope that this accomplish both defining some legal boundaries as well -as communicating in a friendly and clear way, at least to the degree that suits -our needs for the current stage of our products and services. - -

This is important because it is our intent to create sustainable open source -projects, which means that we do want to create brand value, grow community, -and, eventually, be able to work full time on creating more great software and services. - -

If you'd like to contact me, especially if you feel that I (or we) have made -a mistake in how we operate, please do so: - -

- -

Contents

-

Here's what I've worked through so far: - -

- -

Greenlock Domains™

- -

Greenlock Domains is a service provided by -AJ, Brian, - John, & Josh -(collectively Root) -for automated TLS, SSL, and HTTPS. - -

- -

Greenlock Domains is an important product / service combo to us -because it's a huge milestone on the path to a more decentralized web. -We believe in ownership and control and we're -building a Home Server -because we envision a world in which everyone is empowered to make -the choice of whether to rent or own their stuff. - -

If we don't do this, well, with the way the cloud is headed, -renting may be the only option in the future. - -

We need Root because we want ownership. - -

If at any time you feel that any of our messaging or practices -are in conflict with our mission or these values, please let us know. - -

Licensing

- -

Each of our products comes with its own LICENSE file and the license(s) -may alse be in some sort of manifest file (such as package.json). - -

We typically use the MIT and Apache-2.0 licenses for libraries that we -actively want others to copy, modify, use and redistribute. - -

We typically use ISC and MPL-2.0 with products for which we're a little more -concerned about branding or about which we have particularly strong opinions. - -

Although we do keep some of our software proprietary and we do use trademarks, -because we believe in empowerment and choice we do our best to provide usable -self-service forms of our products and services for personal use. - -

If at any time you feel that our Licensing is in conflict with our mission or values, -please let us know. - -

Terms of Service

- -

We want to make the world a better place. -Everyone has a different definition of what "a better place" means, -so the purpose of our terms is to rule out some things that -we think makes the world (and particularly our world) a worse place: - -

You agree that you will use the Greenlock™ service, code, libraries, -documentation, etc (provided by us) -primarily for securing network connections for yourself, your customers, -on your and your customer's devices on internets, intranets, and... other nets. - -

You agree that you will take reasonable measures to keep up-to-date with security -releases. - -

You agree to not use our products or services in a way that would cause unusual -or undue burden on our servers or services, our partners servers or services, or our -customers servers or services, or in a way that harms or misrepresents the reputation -or brand value (including causing brand confusion) of the aforementioned parties -(or really anybody). - -

This is not to say that you can't publicly have a negative opinion, but don't -bite the hand that feeds and don't be vicious or misrepresentative. - -

If you have a use case that may be in violation of these terms (particularly -the part about undue burden), but you feel contributes to making the world a better -place, we're here to help (assuming it also aligns with our values). -Although it may not be appropriate to use our services, but perhaps we can help -you with a solution based on our no-cost, low-cost or open source products. - -

If at any time you feel that our Terms of Service are in conflict with our -mission or values, please let us know. - -

Trademark

- -

"Greenlock" and the "green G lock" mark are Trademarks of -AJ ONeal. - -

We'll be coming out with a brand guide as to how you should use -the marks. In the meantime: don't change the proportions, colors -(excepting the case of greyscale and black and white). - -

It is appropriate to use the trademark in a way that promotes the -brand with proper attribution, linking to the official project repositories, etc. - -

It is appropriate use the name greenlock in a plugin for Greenlock™, -as long as it is clear that it is a community contribution. - -

If you create a "hard" fork of our code or any products or services, -you should give your fork its own name, and not use ours. -That sound, we gladly welcome your suggestiosn and pull requests. - -

If you mirror our code you should make it clear that it is a mirror -and link to the official repository. -in association with usand the disclose that you use Greenlock - -

If at any time you feel that our Trademark policies are in conflict with our -values, please let us know. - -

Privacy Policy

- -

What we collect and (more importantly) Why: - -

Name: -

In the cases that we collect your name, it's because we want to know how to address you. -All four of us want to be personable if and when we reach out. - -

Email: -

There are three main purposes for which we may use your email address: - -

1. A one-time outreach to ask if you were able to do what you intended to do. -We want to make a great product. Although open source projects traditionally have -a reactive approach to communication (i.e. you file a bug and wait for a response), -we believe that creating sustainable open source requires a proactive approach. - -

2. Security and legal notifications. It's important that we have a way to contact you -if we've made a mistake or discover a mistake that needs to be addressed. This -may include vulnerabilities as well as mandatory upgrades (such as when a -significant change to the Let's Encrypt API is made). Making sure that our products -work and are secure aligns with our values and contributes to our brand identity. - -

3. Opt-in updates. Many of you want to know when we have significant feature updates -or when we have something that we believe is really valuable to share. We've created an -opt-in avenue for that. And you can always opt-out as well. - -

Telemetry: -

We believe that the current open source model needs improvement - it often -relies heavily on large centralized platforms which aggregate a lot of user -information for the platform without appropriately targeting the relationship -between authors and users of projcts (i.e. npm, github, etc). We believe that -making open source sustainable means a greater focus on empowering authors -and users. We've learned from other projects (Caddy, Heroku, and others) which -use telemetry as part of a proactive approach to open source and we believe that -it can be a great avenue for us to be proactive as well. - -

We may use telemetry about operating system, browser, node version, code version, -and other system-level information to better understand how we can serve our users (you) -and proactively solve problems that we might not otherwise hear about. For example, if -we see many page visits in a certain browser (or installs with a new version of node), -but few successful registrations, we know that something is wrong. - -

Other: -

We also use Google Analytics on our web sites for basic functionality. -Other than that, nothing else comes to mind right now. -As we consider what we will do in the future, it will be measured against our mission and values. -We never want to come across as spammy or forceful. We want to do things that help us build -our brand, acknowledge our customers; things that are proactive, and that -promote sustainable source. - -

If at any time you feel that our Privacy policy is in conflict with our mission or values, -please let us know. - -
-
-

Copyright 2018 AJ ONeal + + + + Root Legal + + +

Greetings!

+ +

+ I, AJ ONeal, am not a big fan of legalize, but I am a fan of communicating + clearly. I hope that this accomplish both defining some legal boundaries + as well as communicating in a friendly and clear way, at least to the + degree that suits our needs for the current stage of our products and + services. +

+ +

+ This is important because it is our intent to create sustainable open + source projects, which means that we do want to create brand value, grow + community, and, eventually, be able to work full time on creating more + great software and services. +

+ +

+ If you'd like to contact me, especially if you feel that I (or we) have + made a mistake in how we operate, please do so: +

+ + + +

Contents

+

Here's what I've worked through so far:

+ + + +

Greenlock Domains™

+ +

+ Greenlock Domains is a service provided by + AJ, Brian, + John, & Josh + (collectively Root) for automated + TLS, SSL, and HTTPS. +

+ + + +

+ Greenlock Domains is an important product / service combo to us because + it's a huge milestone on the path to a more decentralized web. We believe + in + ownership and control and we're building a + Home Server because we envision a + world in which everyone is empowered to make the choice of whether to rent + or own their stuff. +

+ +

+ If we don't do this, well, with the way the cloud is headed, renting may + be the only option in the future. +

+ +

We need Root because we want ownership.

+ +

+ If at any time you feel that any of our messaging or practices are in + conflict with our mission or these values, please let us know. +

+ +

Licensing

+ +

+ Each of our products comes with its own LICENSE file and the license(s) + may alse be in some sort of manifest file (such as package.json). +

+ +

+ We typically use the MIT and Apache-2.0 licenses for libraries that we + actively want others to copy, modify, use and redistribute. +

+ +

+ We typically use ISC and MPL-2.0 with products for which we're a little + more concerned about branding or about which we have particularly strong + opinions. +

+ +

+ Although we do keep some of our software proprietary and we do use + trademarks, because we believe in empowerment and choice we do our best to + provide usable self-service forms of our products and services for + personal use. +

+ +

+ If at any time you feel that our Licensing is in conflict with our mission + or values, please let us know. +

+ +

Terms of Service

+ +

+ We want to make the world a better place. Everyone has a different + definition of what "a better place" means, so the purpose of our terms is + to rule out some things that we think makes the world (and particularly + our world) a worse place: +

+ +

+ You agree that you will use the Greenlock™ service, code, libraries, + documentation, etc (provided by us) primarily for + securing network connections for yourself, your customers, on your and + your customer's devices on internets, intranets, and... other nets. +

+ +

+ You agree that you will take reasonable measures to keep up-to-date with + security releases. +

+ +

+ You agree to not use our products or services in a way that would cause + unusual or undue burden on our servers or services, our partners servers + or services, or our customers servers or services, or in a way that harms + or misrepresents the reputation or brand value (including causing brand + confusion) of the aforementioned parties (or really anybody). +

+ +

+ This is not to say that you can't publicly have a negative opinion, but + don't bite the hand that feeds and don't be vicious or misrepresentative. +

+ +

+ If you have a use case that may be in violation of these terms + (particularly the part about undue burden), but you feel contributes to + making the world a better place, we're here to help (assuming it also + aligns with our values). Although it may not be appropriate to use our + services, but perhaps we can help you with a solution based on our + no-cost, low-cost or open source products. +

+ +

+ If at any time you feel that our Terms of Service are in conflict with our + mission or values, please let us know. +

+ +

Trademark

+ +

+ "Greenlock" and the "green G lock" mark are Trademarks of + AJ ONeal. +

+ +

+ We'll be coming out with a brand guide as to how you should use the marks. + In the meantime: don't change the proportions, colors (excepting the case + of greyscale and black and white). +

+ +

+ It is appropriate to use the trademark in a way that promotes the brand + with proper attribution, linking to the official project repositories, + etc. +

+ +

+ It is appropriate use the name greenlock in a plugin for Greenlock™, + as long as it is clear that it is a community contribution. +

+ +

+ If you create a "hard" fork of our code or any products or services, you + should give your fork its own name, and not use ours. That sound, we + gladly welcome your suggestiosn and pull requests. +

+ +

+ If you mirror our code you should make it clear that it is a mirror and + link to the official repository. in association with usand the disclose + that you use Greenlock +

+ +

+ If at any time you feel that our Trademark policies are in conflict with + our values, please let us know. +

+ +

Privacy Policy

+ +

What we collect and (more importantly) Why:

+ +

Name:

+

+ In the cases that we collect your name, it's because we want to know how + to address you. All four of us want to be personable if and when we reach + out. +

+ +

Email:

+

+ There are three main purposes for which we may use your email address: +

+ +

+ 1. A one-time outreach to ask if you were able to do what you intended to + do. We want to make a great product. Although open source projects + traditionally have a reactive approach to communication (i.e. you + file a bug and wait for a response), we believe that creating sustainable + open source requires a proactive approach. +

+ +

+ 2. Security and legal notifications. It's important that we have a way to + contact you if we've made a mistake or discover a mistake that needs to be + addressed. This may include vulnerabilities as well as mandatory upgrades + (such as when a significant change to the Let's Encrypt API is made). + Making sure that our products work and are secure aligns with our values + and contributes to our brand identity. +

+ +

+ 3. Opt-in updates. Many of you want to know when we have significant + feature updates or when we have something that we believe is really + valuable to share. We've created an opt-in avenue for that. And you can + always opt-out as well. +

+ +

Telemetry:

+

+ We believe that the current open source model needs improvement - it often + relies heavily on large centralized platforms which aggregate a lot of + user information for the platform without appropriately targeting the + relationship between authors and users of projcts (i.e. npm, github, etc). + We believe that making open source sustainable means a greater focus on + empowering authors and users. We've learned from other projects (Caddy, + Heroku, and others) which use telemetry as part of a proactive approach to + open source and we believe that it can be a great avenue for us to be + proactive as well. +

+ +

+ We may use telemetry about operating system, browser, node version, code + version, and other system-level information to better understand how we + can serve our users (you) and proactively solve problems that we might not + otherwise hear about. For example, if we see many page visits in a certain + browser (or installs with a new version of node), but few successful + registrations, we know that something is wrong. +

+ +

Other:

+

+ We also use Google Analytics on our web sites for basic functionality. + Other than that, nothing else comes to mind right now. As we consider what + we will do in the future, it will be measured against our mission and + values. We never want to come across as spammy or forceful. We want to do + things that help us build our brand, acknowledge our customers; things + that are proactive, and that promote sustainable source. +

+ +

+ If at any time you feel that our Privacy policy is in conflict with our + mission or values, please let us know. +

+ +
+
+

Copyright 2018 AJ ONeal

+ + diff --git a/styles/main.css b/styles/main.css index 43dda6b..7ba1fb9 100644 --- a/styles/main.css +++ b/styles/main.css @@ -1,115 +1,114 @@ .column-row { - display: flex; - flex-direction: column; - text-align: center; - align-items: center; + display: flex; + flex-direction: column; + text-align: center; + align-items: center; } body { - position: relative; - margin-top: 5.777777778em; - min-height: 36em; - font-size: 18px; - font-family: 'Source Sans Pro', sans-serif; - font-stretch: normal; - line-height: 1.33; - letter-spacing: -0.4px; - color: #1a1a1a; - opacity: 0; + position: relative; + margin-top: 5.777777778em; + min-height: 36em; + font-size: 18px; + font-family: "Source Sans Pro", sans-serif; + font-stretch: normal; + line-height: 1.33; + letter-spacing: -0.4px; + color: #1a1a1a; + opacity: 0; } h1 { - font-size: 2.666666667em; - max-width: 8em; - text-align: center; + font-size: 2.666666667em; + max-width: 8em; + text-align: center; } input { - font-size: 1em; - padding: 0.444444444em; - border: solid #d9d9d9 1px; - border-radius: 2px; - font-family: inherit; + font-size: 1em; + padding: 0.444444444em; + border: solid #d9d9d9 1px; + border-radius: 2px; + font-family: inherit; } button { - padding: 0.444444444em 1.2em; - font-size: 1em; - background-color: #5bc17f; - border: solid 1px #5bc17f; - border-radius: 2px; - font-weight: normal; - font-stretch: normal; - letter-spacing: -0.4px; - font-family: inherit; - text-align: center; - color: white; - height: 40px; - line-height: 1.13; + padding: 0.444444444em 1.2em; + font-size: 1em; + background-color: #5bc17f; + border: solid 1px #5bc17f; + border-radius: 2px; + font-weight: normal; + font-stretch: normal; + letter-spacing: -0.4px; + font-family: inherit; + text-align: center; + color: white; + height: 40px; + line-height: 1.13; } .acme-advanced-fields { - position: absolute; - bottom: 0; - padding: 1em; - text-align: center; + position: absolute; + bottom: 0; + padding: 1em; + text-align: center; } .domain-subtext { - font-size: 0.833333333em; - color: #666; - text-align: center; - margin: 0.5em; + font-size: 0.833333333em; + color: #666; + text-align: center; + margin: 0.5em; } input#acme-domains:before { - content: "Secure | https://"; + content: "Secure | https://"; } .domain-psuedo-input { - display: inline-block; - margin-right: .6666667em; - border: solid #d9d9d9 1px; - border-radius: 2px; - padding: 0.44444444em; - color: #d9d9d9; + display: inline-block; + margin-right: 0.6666667em; + border: solid #d9d9d9 1px; + border-radius: 2px; + padding: 0.44444444em; + color: #d9d9d9; } input#acme-domains { - border: none; - padding: 0; - padding-right: 0; - width: 17.2222222em; - color: #222; + border: none; + padding: 0; + padding-right: 0; + width: 17.2222222em; + color: #222; } input#acme-domains:focus { - outline: none; + outline: none; } span.secure-green { - color: #5bc17f; + color: #5bc17f; } .why-you-need { - width: 26.555556em; + width: 26.555556em; } body.js-app-ready { - transition: opacity 0.2s; - opacity: 1; + transition: opacity 0.2s; + opacity: 1; } .acme-advanced-fields > * { - margin: 0 0.5em; + margin: 0 0.5em; } .js-javascript-warning { - border: solid 1px red; - background-color: #ffc0cb40; - border-radius: 2px; - margin: 0.6em; - padding: 0.5em 1em; - width: 30em; - } - \ No newline at end of file + border: solid 1px red; + background-color: #ffc0cb40; + border-radius: 2px; + margin: 0.6em; + padding: 0.5em 1em; + width: 30em; +}