WIP almost working
This commit is contained in:
parent
11020cbf27
commit
2ca4c984f8
|
@ -33,13 +33,8 @@
|
||||||
<link rel="preload" href="./fonts/6xK3dSBYKcSV-LCoeQqfX1RYOo3qOK7l.woff2" as="font" crossorigin="anonymous">
|
<link rel="preload" href="./fonts/6xK3dSBYKcSV-LCoeQqfX1RYOo3qOK7l.woff2" as="font" crossorigin="anonymous">
|
||||||
|
|
||||||
<link rel="preload" href="./fonts/HI_SiYsKILxRpg3hIP6sJ7fM7PqlPevW.woff2" as="font" crossorigin="anonymous">
|
<link rel="preload" href="./fonts/HI_SiYsKILxRpg3hIP6sJ7fM7PqlPevW.woff2" as="font" crossorigin="anonymous">
|
||||||
<link rel="preload" href="./js/bacme.js" as="script">
|
<link rel="preload" href="./js/bluecrypt-acme.js" as="script">
|
||||||
<link rel="preload" href="./js/app.js" as="script">
|
<link rel="preload" href="./js/greenlock.js" as="script">
|
||||||
<link rel="preload" href="./js/pkijs.org/v1.3.33/common.js" as="script">
|
|
||||||
<link rel="preload" href="./js/pkijs.org/v1.3.33/asn1.js" as="script">
|
|
||||||
<link rel="preload" href="./js/pkijs.org/v1.3.33/x509_schema.js" as="script">
|
|
||||||
<link rel="preload" href="./js/pkijs.org/v1.3.33/x509_simpl.js" as="script">
|
|
||||||
<link rel="preload" href="./js/browser-csr/v1.0.0-alpha/csr.js" as="script">
|
|
||||||
|
|
||||||
</head>
|
</head>
|
||||||
<body hidden>
|
<body hidden>
|
||||||
|
@ -342,14 +337,8 @@
|
||||||
<br>
|
<br>
|
||||||
|
|
||||||
|
|
||||||
<script src="./js/bacme.js"></script>
|
<script src="./js/bluecrypt-acme.js"></script>
|
||||||
<script src="./js/app.js"></script>
|
<script src="./js/greenlock.js"></script>
|
||||||
|
|
||||||
<script src="./js/pkijs.org/v1.3.33/common.js"></script>
|
|
||||||
<script src="./js/pkijs.org/v1.3.33/asn1.js"></script>
|
|
||||||
<script src="./js/pkijs.org/v1.3.33/x509_schema.js"></script>
|
|
||||||
<script src="./js/pkijs.org/v1.3.33/x509_simpl.js"></script>
|
|
||||||
<script src="./js/browser-csr/v1.0.0-alpha/csr.js"></script>
|
|
||||||
|
|
||||||
<!-- Global site tag (gtag.js) - Google Analytics -->
|
<!-- Global site tag (gtag.js) - Google Analytics -->
|
||||||
<script async src="https://www.googletagmanager.com/gtag/js?id=UA-118745161-2"></script>
|
<script async src="https://www.googletagmanager.com/gtag/js?id=UA-118745161-2"></script>
|
||||||
|
|
|
@ -2,8 +2,7 @@
|
||||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
/* 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
|
* 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/. */
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||||
;
|
;(function (exports) {
|
||||||
(function (exports) {
|
|
||||||
|
|
||||||
var Enc = exports.Enc = {};
|
var Enc = exports.Enc = {};
|
||||||
|
|
||||||
|
@ -1962,8 +1961,13 @@ ACME._getChallenges = function (me, options, authUrl) {
|
||||||
, payload: ''
|
, payload: ''
|
||||||
, url: authUrl
|
, url: authUrl
|
||||||
}).then(function (resp) {
|
}).then(function (resp) {
|
||||||
|
// Pre-emptive rather than lazy for interfaces that need to show the challenges to the user first
|
||||||
|
return ACME._challengesToAuth(me, options, resp.body, false).then(function (auths) {
|
||||||
|
resp.body._rawChallenges = resp.body.challenges;
|
||||||
|
resp.body.challenges = auths;
|
||||||
return resp.body;
|
return resp.body;
|
||||||
});
|
});
|
||||||
|
});
|
||||||
};
|
};
|
||||||
ACME._wait = function wait(ms) {
|
ACME._wait = function wait(ms) {
|
||||||
return new Promise(function (resolve) {
|
return new Promise(function (resolve) {
|
||||||
|
@ -1987,12 +1991,6 @@ ACME._testChallengeOptions = function () {
|
||||||
"token": "test-" + chToken + "-1",
|
"token": "test-" + chToken + "-1",
|
||||||
"_wildcard": true
|
"_wildcard": true
|
||||||
}
|
}
|
||||||
, {
|
|
||||||
"type": "tls-sni-01",
|
|
||||||
"status": "pending",
|
|
||||||
"url": "https://acme-staging-v02.example.com/2",
|
|
||||||
"token": "test-" + chToken + "-2"
|
|
||||||
}
|
|
||||||
, {
|
, {
|
||||||
"type": "tls-alpn-01",
|
"type": "tls-alpn-01",
|
||||||
"status": "pending",
|
"status": "pending",
|
||||||
|
@ -2010,11 +2008,29 @@ ACME._testChallenges = function (me, options) {
|
||||||
challenges = challenges.filter(function (ch) { return ch._wildcard; });
|
challenges = challenges.filter(function (ch) { return ch._wildcard; });
|
||||||
}
|
}
|
||||||
|
|
||||||
var challenge = ACME._chooseChallenge(options, { challenges: challenges });
|
// The dry-run comes first in the spirit of "fail fast"
|
||||||
if (!challenge) {
|
// (and protecting against challenge failure rate limits)
|
||||||
|
var dryrun = true;
|
||||||
|
var resp = {
|
||||||
|
body: {
|
||||||
|
identifier: {
|
||||||
|
type: "dns"
|
||||||
|
, value: identifierValue.replace(/^\*\./, '')
|
||||||
|
}
|
||||||
|
, challenges: challenges
|
||||||
|
, expires: new Date(Date.now() + (60 * 1000)).toISOString()
|
||||||
|
, wildcard: identifierValue.includes('*.') || undefined
|
||||||
|
}
|
||||||
|
};
|
||||||
|
return ACME._challengesToAuth(me, options, resp.body, dryrun).then(function (auths) {
|
||||||
|
resp.body._rawChallenges = resp.body.challenges;
|
||||||
|
resp.body.challenges = auths;
|
||||||
|
|
||||||
|
var auth = ACME._chooseAuth(options, resp.body.challenges);
|
||||||
|
if (!auth) {
|
||||||
// For example, wildcards require dns-01 and, if we don't have that, we have to bail
|
// For example, wildcards require dns-01 and, if we don't have that, we have to bail
|
||||||
var enabled = Object.keys(options.challenges).join(', ') || 'none';
|
var enabled = Object.keys(options.challenges).join(', ') || 'none';
|
||||||
var suitable = challenges.map(function (r) { return r.type; }).join(', ') || 'none';
|
var suitable = resp.body.challenges.map(function (r) { return r.type; }).join(', ') || 'none';
|
||||||
return Promise.reject(new Error(
|
return Promise.reject(new Error(
|
||||||
"None of the challenge types that you've enabled ( " + enabled + " )"
|
"None of the challenge types that you've enabled ( " + enabled + " )"
|
||||||
+ " are suitable for validating the domain you've selected (" + identifierValue + ")."
|
+ " are suitable for validating the domain you've selected (" + identifierValue + ")."
|
||||||
|
@ -2027,32 +2043,16 @@ ACME._testChallenges = function (me, options) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
if ('dns-01' === challenge.type) {
|
if ('dns-01' === auth.type) {
|
||||||
// Give the nameservers a moment to propagate
|
// Give the nameservers a moment to propagate
|
||||||
CHECK_DELAY = 1.5 * 1000;
|
CHECK_DELAY = 1.5 * 1000;
|
||||||
}
|
}
|
||||||
|
|
||||||
return Promise.resolve().then(function () {
|
|
||||||
var results = {
|
|
||||||
identifier: {
|
|
||||||
type: "dns"
|
|
||||||
, value: identifierValue.replace(/^\*\./, '')
|
|
||||||
}
|
|
||||||
, challenges: [ challenge ]
|
|
||||||
, expires: new Date(Date.now() + (60 * 1000)).toISOString()
|
|
||||||
, wildcard: identifierValue.includes('*.') || undefined
|
|
||||||
};
|
|
||||||
|
|
||||||
// The dry-run comes first in the spirit of "fail fast"
|
|
||||||
// (and protecting against challenge failure rate limits)
|
|
||||||
var dryrun = true;
|
|
||||||
return ACME._challengeToAuth(me, options, results, challenge, dryrun).then(function (auth) {
|
|
||||||
if (!me._canUse[auth.type]) { return; }
|
if (!me._canUse[auth.type]) { return; }
|
||||||
return ACME._setChallenge(me, options, auth).then(function () {
|
return ACME._setChallenge(me, options, auth).then(function () {
|
||||||
return auth;
|
return auth;
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
|
||||||
})).then(function (auths) {
|
})).then(function (auths) {
|
||||||
auths = auths.filter(Boolean);
|
auths = auths.filter(Boolean);
|
||||||
if (!auths.length) { /*skip actual test*/ return; }
|
if (!auths.length) { /*skip actual test*/ return; }
|
||||||
|
@ -2067,59 +2067,43 @@ ACME._testChallenges = function (me, options) {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
ACME._chooseChallenge = function(options, results) {
|
ACME._chooseAuth = function(options, auths) {
|
||||||
// For each of the challenge types that we support
|
// For each of the challenge types that we support
|
||||||
var challenge;
|
var auth;
|
||||||
var challengeTypes = Object.keys(options.challenges);
|
var challengeTypes = Object.keys(options.challenges);
|
||||||
// ordered from most to least preferred
|
// ordered from most to least preferred
|
||||||
challengeTypes = [ 'tls-alpn-01', 'http-01', 'dns-01' ].filter(function (chType) {
|
challengeTypes = (options.challengePriority||[ 'tls-alpn-01', 'http-01', 'dns-01' ]).filter(function (chType) {
|
||||||
return challengeTypes.includes(chType);
|
return challengeTypes.includes(chType);
|
||||||
});
|
});
|
||||||
|
|
||||||
/*
|
|
||||||
// Lot's of error checking to inform the user of mistakes
|
|
||||||
if (!(options.challengeTypes||[]).length) {
|
|
||||||
options.challengeTypes = Object.keys(options.challenges||{});
|
|
||||||
}
|
|
||||||
if (!options.challengeTypes.length) {
|
|
||||||
options.challengeTypes = [ options.challengeType ].filter(Boolean);
|
|
||||||
}
|
|
||||||
if (options.challengeType) {
|
|
||||||
options.challengeTypes.sort(function (a, b) {
|
|
||||||
if (a === options.challengeType) { return -1; }
|
|
||||||
if (b === options.challengeType) { return 1; }
|
|
||||||
return 0;
|
|
||||||
});
|
|
||||||
if (options.challengeType !== options.challengeTypes[0]) {
|
|
||||||
return Promise.reject(new Error("options.challengeType is '" + options.challengeType + "',"
|
|
||||||
+ " which does not exist in the supplied types '" + options.challengeTypes.join(',') + "'"));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// TODO check that all challengeTypes are represented in challenges
|
|
||||||
if (!options.challengeTypes.length) {
|
|
||||||
return Promise.reject(new Error("options.challengeTypes (string array) must be specified"
|
|
||||||
+ " (and in order of preferential priority)."));
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
challengeTypes.some(function (chType) {
|
challengeTypes.some(function (chType) {
|
||||||
// And for each of the challenge types that are allowed
|
// And for each of the challenge types that are allowed
|
||||||
return results.challenges.some(function (ch) {
|
return auths.some(function (ch) {
|
||||||
// Check to see if there are any matches
|
// Check to see if there are any matches
|
||||||
if (ch.type === chType) {
|
if (ch.type === chType) {
|
||||||
challenge = ch;
|
auth = ch;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
return challenge;
|
return auth;
|
||||||
};
|
};
|
||||||
ACME._challengeToAuth = function (me, options, request, challenge, dryrun) {
|
ACME._challengesToAuth = function (me, options, request, dryrun) {
|
||||||
// we don't poison the dns cache with our dummy request
|
// we don't poison the dns cache with our dummy request
|
||||||
var dnsPrefix = ACME.challengePrefixes['dns-01'];
|
var dnsPrefix = ACME.challengePrefixes['dns-01'];
|
||||||
if (dryrun) {
|
if (dryrun) {
|
||||||
dnsPrefix = dnsPrefix.replace('acme-challenge', 'greenlock-dryrun-' + ACME._prnd(4));
|
dnsPrefix = dnsPrefix.replace('acme-challenge', 'greenlock-dryrun-' + ACME._prnd(4));
|
||||||
}
|
}
|
||||||
|
var challengeTypes = Object.keys(options.challenges);
|
||||||
|
|
||||||
|
return ACME._importKeypair(me, 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 = {};
|
var auth = {};
|
||||||
|
|
||||||
|
@ -2141,20 +2125,31 @@ ACME._challengeToAuth = function (me, options, request, challenge, dryrun) {
|
||||||
auth.hostname = auth.identifier.value;
|
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
|
// 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.altname = ACME._untame(auth.identifier.value, auth.wildcard);
|
||||||
return ACME._importKeypair(me, options.accountKeypair).then(function (pair) {
|
|
||||||
return me.Keypairs.thumbprint({ jwk: pair.public }).then(function (thumb) {
|
|
||||||
auth.thumbprint = thumb;
|
auth.thumbprint = thumb;
|
||||||
// keyAuthorization = token || '.' || base64url(JWK_Thumbprint(accountKey))
|
// keyAuthorization = token || '.' || base64url(JWK_Thumbprint(accountKey))
|
||||||
auth.keyAuthorization = challenge.token + '.' + auth.thumbprint;
|
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
|
// conflicts with ACME challenge id url is already in use, so we call this challengeUrl instead
|
||||||
// TODO auth.http01Url ?
|
// TODO auth.http01Url ?
|
||||||
auth.challengeUrl = 'http://' + auth.identifier.value + ACME.challengePrefixes['http-01'] + '/' + auth.token;
|
auth.challengeUrl = 'http://' + auth.identifier.value + ACME.challengePrefixes['http-01'] + '/' + auth.token;
|
||||||
auth.dnsHost = dnsPrefix + '.' + auth.hostname.replace('*.', '');
|
return auth;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ('dns-01' !== auth.type) {
|
||||||
|
return auth;
|
||||||
|
}
|
||||||
|
|
||||||
return Crypto._sha('sha256', auth.keyAuthorization).then(function (hash) {
|
return Crypto._sha('sha256', auth.keyAuthorization).then(function (hash) {
|
||||||
|
auth.dnsHost = dnsPrefix + '.' + auth.hostname.replace('*.', '');
|
||||||
auth.dnsAuthorization = hash;
|
auth.dnsAuthorization = hash;
|
||||||
|
auth.keyAuthorizationDigest = hash;
|
||||||
return auth;
|
return auth;
|
||||||
});
|
});
|
||||||
|
})).then(function (auths) {
|
||||||
|
return auths.filter(Boolean);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
@ -2241,18 +2236,21 @@ ACME._postChallenge = function (me, options, auth) {
|
||||||
return resp.body;
|
return resp.body;
|
||||||
}
|
}
|
||||||
|
|
||||||
var errmsg;
|
var err;
|
||||||
if (!resp.body.status) {
|
if (resp.body.error && resp.body.error.detail) {
|
||||||
errmsg = "[acme-v2] (E_STATE_EMPTY) empty challenge state for '" + altname + "':";
|
err = new Error("[acme-v2] " + auth.altname + " state:" + resp.body.status + " " + resp.body.error.detail);
|
||||||
}
|
err.auth = auth;
|
||||||
else if ('invalid' === resp.body.status) {
|
err.altname = auth.altname;
|
||||||
errmsg = "[acme-v2] (E_STATE_INVALID) challenge state for '" + altname + "': '" + resp.body.status + "'";
|
err.type = auth.type;
|
||||||
}
|
err.urn = resp.body.error.type;
|
||||||
else {
|
err.code = ('invalid' === resp.body.status) ? 'E_CHALLENGE_INVALID' : 'E_CHALLENGE_UNKNOWN';
|
||||||
errmsg = "[acme-v2] (E_STATE_UKN) challenge state for '" + altname + "': '" + resp.body.status + "'";
|
err.uri = resp.body.url;
|
||||||
|
} else {
|
||||||
|
err = new Error("[acme-v2] " + auth.altname + " (E_STATE_UKN): " + JSON.stringify(resp.body, null, 2));
|
||||||
|
err.code = 'E_CHALLENGE_UNKNOWN';
|
||||||
}
|
}
|
||||||
|
|
||||||
return Promise.reject(new Error(errmsg));
|
return Promise.reject(err);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2312,34 +2310,32 @@ ACME._setChallenge = function (me, options, auth) {
|
||||||
ACME._setChallengesAll = function (me, options) {
|
ACME._setChallengesAll = function (me, options) {
|
||||||
var order = options.order;
|
var order = options.order;
|
||||||
var setAuths = order.authorizations.slice(0);
|
var setAuths = order.authorizations.slice(0);
|
||||||
var challenges = order.challenges;
|
var claims = order.claims.slice(0);
|
||||||
var validAuths = [];
|
var validAuths = [];
|
||||||
var auths = [];
|
var auths = [];
|
||||||
|
|
||||||
function setNext() {
|
function setNext() {
|
||||||
var authUrl = setAuths.shift();
|
var authUrl = setAuths.shift();
|
||||||
var results = challenges.shift();
|
var claim = claims.shift();
|
||||||
if (!authUrl) { return; }
|
if (!authUrl) { return; }
|
||||||
|
|
||||||
// var domain = options.domains[i]; // results.identifier.value
|
// var domain = options.domains[i]; // claim.identifier.value
|
||||||
|
|
||||||
// If it's already valid, we're golden it regardless
|
// If it's already valid, we're golden it regardless
|
||||||
if (results.challenges.some(function (ch) { return 'valid' === ch.status; })) {
|
if (claim.challenges.some(function (ch) { return 'valid' === ch.status; })) {
|
||||||
return setNext();
|
return setNext();
|
||||||
}
|
}
|
||||||
|
|
||||||
var challenge = ACME._chooseChallenge(options, results);
|
var auth = ACME._chooseAuth(options, claim.challenges);
|
||||||
if (!challenge) {
|
if (!auth) {
|
||||||
// For example, wildcards require dns-01 and, if we don't have that, we have to bail
|
// For example, wildcards require dns-01 and, if we don't have that, we have to bail
|
||||||
return Promise.reject(new Error(
|
return Promise.reject(new Error(
|
||||||
"Server didn't offer any challenge we can handle for '" + options.domains.join() + "'."
|
"Server didn't offer any challenge we can handle for '" + options.domains.join() + "'."
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
return ACME._challengeToAuth(me, options, results, challenge, false).then(function (auth) {
|
|
||||||
auths.push(auth);
|
auths.push(auth);
|
||||||
return ACME._setChallenge(me, options, auth).then(setNext);
|
return ACME._setChallenge(me, options, auth).then(setNext);
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function checkNext() {
|
function checkNext() {
|
||||||
|
@ -2358,6 +2354,7 @@ ACME._setChallengesAll = function (me, options) {
|
||||||
}).then(checkNext);
|
}).then(checkNext);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Actually sets the challenge via ACME
|
||||||
function challengeNext() {
|
function challengeNext() {
|
||||||
var auth = validAuths.shift();
|
var auth = validAuths.shift();
|
||||||
if (!auth) { return; }
|
if (!auth) { return; }
|
||||||
|
@ -2519,7 +2516,7 @@ ACME._createOrder = function (me, options) {
|
||||||
if (me.debug) { console.debug('[ordered]', location); } // the account id url
|
if (me.debug) { console.debug('[ordered]', location); } // the account id url
|
||||||
if (me.debug) { console.debug(resp); }
|
if (me.debug) { console.debug(resp); }
|
||||||
|
|
||||||
if (!options.authorizations) {
|
if (!order.authorizations) {
|
||||||
return Promise.reject(new Error(
|
return Promise.reject(new Error(
|
||||||
"[acme-v2.js] authorizations were not fetched for '" + options.domains.join() + "':\n"
|
"[acme-v2.js] authorizations were not fetched for '" + options.domains.join() + "':\n"
|
||||||
+ JSON.stringify(resp.body)
|
+ JSON.stringify(resp.body)
|
||||||
|
@ -2528,25 +2525,24 @@ ACME._createOrder = function (me, options) {
|
||||||
|
|
||||||
return order;
|
return order;
|
||||||
}).then(function (order) {
|
}).then(function (order) {
|
||||||
var challenges = [];
|
var claims = [];
|
||||||
if (me.debug) { console.debug("[acme-v2] POST newOrder has authorizations"); }
|
if (me.debug) { console.debug("[acme-v2] POST newOrder has authorizations"); }
|
||||||
var challengeAuths = order.authorizations.slice(0);
|
var challengeAuths = order.authorizations.slice(0);
|
||||||
|
|
||||||
function getNext() {
|
function getNext() {
|
||||||
var authUrl = challengeAuths.shift();
|
var authUrl = challengeAuths.shift();
|
||||||
if (!authUrl) { return challenges; }
|
if (!authUrl) { return claims; }
|
||||||
|
|
||||||
return ACME._getChallenges(me, options, authUrl).then(function (results) {
|
return ACME._getChallenges(me, options, authUrl).then(function (claim) {
|
||||||
// var domain = options.domains[i]; // results.identifier.value
|
// var domain = options.domains[i]; // claim.identifier.value
|
||||||
challenges.push(results);
|
claims.push(claim);
|
||||||
return getNext();
|
return getNext();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return getNext().then(function () {
|
return getNext().then(function () {
|
||||||
order.challenges = challenges;
|
order.claims = claims;
|
||||||
options.order = order;
|
options.order = order;
|
||||||
console.log('DEBUG 2 order (too much info for challenges?):', order);
|
|
||||||
return order;
|
return order;
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -2684,10 +2680,12 @@ ACME.create = function create(me) {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
me.orders = {
|
me.orders = {
|
||||||
create: function (options) {
|
// create + get challlenges
|
||||||
|
request: function (options) {
|
||||||
return ACME._createOrder(me, options);
|
return ACME._createOrder(me, options);
|
||||||
}
|
}
|
||||||
, finalize: function (options) {
|
// set challenges, check challenges, finalize order, return order
|
||||||
|
, complete: function (options) {
|
||||||
return ACME._finalizeOrder(me, options);
|
return ACME._finalizeOrder(me, options);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
@ -2,6 +2,7 @@
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
/*global URLSearchParams,Headers*/
|
/*global URLSearchParams,Headers*/
|
||||||
|
var PromiseA = window.Promise;
|
||||||
var VERSION = '2';
|
var VERSION = '2';
|
||||||
// ACME recommends ECDSA P-256, but RSA 2048 is still required by some old servers (like what replicated.io uses )
|
// 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)
|
// ECDSA P-384, P-521, and RSA 3072, 4096 are NOT recommend standards (and not properly supported)
|
||||||
|
@ -15,11 +16,32 @@
|
||||||
var $qs = function (s) { return window.document.querySelector(s); };
|
var $qs = function (s) { return window.document.querySelector(s); };
|
||||||
var $qsa = function (s) { return window.document.querySelectorAll(s); };
|
var $qsa = function (s) { return window.document.querySelectorAll(s); };
|
||||||
var acme;
|
var acme;
|
||||||
var accountStuff;
|
|
||||||
var info = {};
|
var info = {};
|
||||||
var steps = {};
|
var steps = {};
|
||||||
var i = 1;
|
var i = 1;
|
||||||
var apiUrl = 'https://acme-{{env}}.api.letsencrypt.org/directory';
|
var apiUrl = 'https://acme-{{env}}.api.letsencrypt.org/directory';
|
||||||
|
var challenges = {
|
||||||
|
'http-01': {
|
||||||
|
set: function (auth) {
|
||||||
|
console.log('Chose http-01 for', auth.altname, auth);
|
||||||
|
return Promise.resolve();
|
||||||
|
}
|
||||||
|
, remove: function (auth) {
|
||||||
|
console.log('Can remove http-01 for', auth.altname, auth);
|
||||||
|
return Promise.resolve();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
, 'dns-01': {
|
||||||
|
set: function (auth) {
|
||||||
|
console.log('Chose dns-01 for', auth.altname, auth);
|
||||||
|
return Promise.resolve();
|
||||||
|
}
|
||||||
|
, remove: function (auth) {
|
||||||
|
console.log('Can remove dns-01 for', auth.altname, auth);
|
||||||
|
return Promise.resolve();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
function updateApiType() {
|
function updateApiType() {
|
||||||
console.log("type updated");
|
console.log("type updated");
|
||||||
|
@ -59,8 +81,29 @@
|
||||||
i += 1;
|
i += 1;
|
||||||
|
|
||||||
return PromiseA.resolve(steps[j].submit(ev)).catch(function (err) {
|
return PromiseA.resolve(steps[j].submit(ev)).catch(function (err) {
|
||||||
|
var ourfault = true;
|
||||||
console.error(err);
|
console.error(err);
|
||||||
window.alert("Something went wrong. It's our fault not yours. Please email aj@rootprojects.org and let him know that 'step " + j + "' failed.");
|
console.error(Object.keys(err));
|
||||||
|
if ('E_CHALLENGE_INVALID' === err.code) {
|
||||||
|
if ('dns-01' === err.type) {
|
||||||
|
ourfault = false;
|
||||||
|
window.alert("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;
|
||||||
|
window.alert("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));
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -178,7 +221,8 @@
|
||||||
$qs('.js-acme-form-domains').hidden = false;
|
$qs('.js-acme-form-domains').hidden = false;
|
||||||
};
|
};
|
||||||
steps[1].submit = function () {
|
steps[1].submit = function () {
|
||||||
info.identifiers = $qs('.js-acme-domains').value.split(/\s*,\s*/g).map(function (hostname) {
|
info.domains = $qs('.js-acme-domains').value.replace(/https?:\/\//g, ' ').replace(/,/g, ' ').trim().split(/\s+/g);
|
||||||
|
info.identifiers = info.domains.map(function (hostname) {
|
||||||
return { type: 'dns', value: hostname.toLowerCase().trim() };
|
return { type: 'dns', value: hostname.toLowerCase().trim() };
|
||||||
}).slice(0,1); //Disable multiple values for now. We'll just take the first and work with it.
|
}).slice(0,1); //Disable multiple values for now. We'll just take the first and work with it.
|
||||||
info.identifiers.sort(function (a, b) {
|
info.identifiers.sort(function (a, b) {
|
||||||
|
@ -204,12 +248,14 @@
|
||||||
steps[2].submit = function () {
|
steps[2].submit = function () {
|
||||||
var email = $qs('.js-acme-account-email').value.toLowerCase().trim();
|
var email = $qs('.js-acme-account-email').value.toLowerCase().trim();
|
||||||
|
|
||||||
|
info.email = email;
|
||||||
info.contact = [ 'mailto:' + email ];
|
info.contact = [ 'mailto:' + email ];
|
||||||
info.agree = $qs('.js-acme-account-tos').checked;
|
info.agree = $qs('.js-acme-account-tos').checked;
|
||||||
//info.greenlockAgree = $qs('.js-gl-tos').checked;
|
//info.greenlockAgree = $qs('.js-gl-tos').checked;
|
||||||
|
info.domains = info.identifiers.map(function (ident) { return ident.value; });
|
||||||
|
|
||||||
// TODO ping with version and account creation
|
// TODO ping with version and account creation
|
||||||
setTimeout(saveContact, 100, email, info.identifiers.map(function (ident) { return ident.value; }));
|
setTimeout(saveContact, 100, email, info.domains);
|
||||||
|
|
||||||
function checkTos(tos) {
|
function checkTos(tos) {
|
||||||
if (info.agree) {
|
if (info.agree) {
|
||||||
|
@ -227,10 +273,10 @@
|
||||||
, accountKeypair: { privateKeyJwk: jwk }
|
, accountKeypair: { privateKeyJwk: jwk }
|
||||||
}).then(function (account) {
|
}).then(function (account) {
|
||||||
console.log("account created result:", account);
|
console.log("account created result:", account);
|
||||||
accountStuff.account = account;
|
info.account = account;
|
||||||
accountStuff.privateJwk = jwk;
|
info.privateJwk = jwk;
|
||||||
accountStuff.email = email;
|
info.email = email;
|
||||||
accountStuff.acme = acme; // TODO XXX remove
|
info.acme = acme; // TODO XXX remove
|
||||||
}).catch(function (err) {
|
}).catch(function (err) {
|
||||||
console.error("A bad thing happened:");
|
console.error("A bad thing happened:");
|
||||||
console.error(err);
|
console.error(err);
|
||||||
|
@ -241,55 +287,47 @@
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}).then(function () {
|
}).then(function () {
|
||||||
var jwk = accountStuff.privateJwk;
|
var jwk = info.privateJwk;
|
||||||
var account = accountStuff.account;
|
var account = info.account;
|
||||||
|
|
||||||
return acme.orders.create({
|
return acme.orders.request({
|
||||||
account: account
|
account: account
|
||||||
, accountKeypair: { privateKeyJwk: jwk }
|
, accountKeypair: { privateKeyJwk: jwk }
|
||||||
, identifiers: info.identifiers
|
, domains: info.domains
|
||||||
|
, challenges: challenges
|
||||||
}).then(function (order) {
|
}).then(function (order) {
|
||||||
return acme.orders.create({
|
info.order = order;
|
||||||
signedOrder: signedOrder
|
|
||||||
}).then(function (order) {
|
var claims = order.claims;
|
||||||
accountStuff.order = order;
|
|
||||||
var claims = order.challenges;
|
|
||||||
console.log('claims:');
|
console.log('claims:');
|
||||||
console.log(claims);
|
console.log(claims);
|
||||||
|
|
||||||
var obj = { 'dns-01': [], 'http-01': [], 'wildcard': [] };
|
var obj = { 'dns-01': [], 'http-01': [], 'wildcard': [] };
|
||||||
info.challenges = obj;
|
info.challenges = obj;
|
||||||
|
/*
|
||||||
var map = {
|
var map = {
|
||||||
'http-01': '.js-acme-verification-http-01'
|
'http-01': '.js-acme-verification-http-01'
|
||||||
, 'dns-01': '.js-acme-verification-dns-01'
|
, 'dns-01': '.js-acme-verification-dns-01'
|
||||||
, 'wildcard': '.js-acme-verification-wildcard'
|
, 'wildcard': '.js-acme-verification-wildcard'
|
||||||
};
|
};
|
||||||
options.challengePriority = [ 'http-01', 'dns-01' ];
|
*/
|
||||||
|
|
||||||
// TODO make Promise-friendly
|
claims.forEach(function (claim) {
|
||||||
return PromiseA.all(claims.map(function (claim) {
|
console.log("Challenge (claim):");
|
||||||
|
console.log(claim);
|
||||||
var hostname = claim.identifier.value;
|
var hostname = claim.identifier.value;
|
||||||
return PromiseA.all(claim.challenges.map(function (c) {
|
claim.challenges.forEach(function (c) {
|
||||||
var keyAuth = BACME.challenges['http-01']({
|
var auth = c;
|
||||||
token: c.token
|
|
||||||
, thumbprint: thumbprint
|
|
||||||
, challengeDomain: hostname
|
|
||||||
});
|
|
||||||
return BACME.challenges['dns-01']({
|
|
||||||
keyAuth: keyAuth.value
|
|
||||||
, challengeDomain: hostname
|
|
||||||
}).then(function (dnsAuth) {
|
|
||||||
var data = {
|
var data = {
|
||||||
type: c.type
|
type: c.type
|
||||||
, hostname: hostname
|
, hostname: hostname
|
||||||
, url: c.url
|
, url: c.url
|
||||||
, token: c.token
|
, token: c.token
|
||||||
, keyAuthorization: keyAuth
|
, httpPath: auth.challengeUrl
|
||||||
, httpPath: keyAuth.path
|
, httpAuth: auth.keyAuthorization
|
||||||
, httpAuth: keyAuth.value
|
, dnsType: 'TXT'
|
||||||
, dnsType: dnsAuth.type
|
, dnsHost: auth.dnsHost
|
||||||
, dnsHost: dnsAuth.host
|
, dnsAnswer: auth.keyAuthorizationDigest
|
||||||
, dnsAnswer: dnsAuth.answer
|
|
||||||
};
|
};
|
||||||
|
|
||||||
console.log('');
|
console.log('');
|
||||||
|
@ -299,9 +337,10 @@
|
||||||
console.log(data);
|
console.log(data);
|
||||||
console.log('');
|
console.log('');
|
||||||
|
|
||||||
|
var verification;
|
||||||
if (claim.wildcard) {
|
if (claim.wildcard) {
|
||||||
obj.wildcard.push(data);
|
obj.wildcard.push(data);
|
||||||
let verification = $qs(".js-acme-verification-wildcard");
|
verification = $qs(".js-acme-verification-wildcard");
|
||||||
verification.querySelector(".js-acme-ver-hostname").innerHTML = data.hostname;
|
verification.querySelector(".js-acme-ver-hostname").innerHTML = data.hostname;
|
||||||
verification.querySelector(".js-acme-ver-txt-host").innerHTML = data.dnsHost;
|
verification.querySelector(".js-acme-ver-txt-host").innerHTML = data.dnsHost;
|
||||||
verification.querySelector(".js-acme-ver-txt-value").innerHTML = data.dnsAnswer;
|
verification.querySelector(".js-acme-ver-txt-value").innerHTML = data.dnsAnswer;
|
||||||
|
@ -311,7 +350,7 @@
|
||||||
obj[data.type].push(data);
|
obj[data.type].push(data);
|
||||||
|
|
||||||
if ('dns-01' === data.type) {
|
if ('dns-01' === data.type) {
|
||||||
let verification = $qs(".js-acme-verification-dns-01");
|
verification = $qs(".js-acme-verification-dns-01");
|
||||||
verification.querySelector(".js-acme-ver-hostname").innerHTML = data.hostname;
|
verification.querySelector(".js-acme-ver-hostname").innerHTML = data.hostname;
|
||||||
verification.querySelector(".js-acme-ver-txt-host").innerHTML = data.dnsHost;
|
verification.querySelector(".js-acme-ver-txt-host").innerHTML = data.dnsHost;
|
||||||
verification.querySelector(".js-acme-ver-txt-value").innerHTML = data.dnsAnswer;
|
verification.querySelector(".js-acme-ver-txt-value").innerHTML = data.dnsAnswer;
|
||||||
|
@ -324,11 +363,8 @@
|
||||||
$qs(".js-download-verify-link").download = data.httpPath.split("/").slice(-1);
|
$qs(".js-download-verify-link").download = data.httpPath.split("/").slice(-1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
});
|
});
|
||||||
|
});
|
||||||
}));
|
|
||||||
})).then(function () {
|
|
||||||
|
|
||||||
// hide wildcard if no wildcard
|
// hide wildcard if no wildcard
|
||||||
// hide http-01 and dns-01 if only wildcard
|
// hide http-01 and dns-01 if only wildcard
|
||||||
|
@ -344,13 +380,11 @@
|
||||||
console.log("MAGIC STEP NUMBER in 2 is:", i);
|
console.log("MAGIC STEP NUMBER in 2 is:", i);
|
||||||
steps[i]();
|
steps[i]();
|
||||||
});
|
});
|
||||||
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}).catch(function (err) {
|
}).catch(function (err) {
|
||||||
console.error('Step \'\' Error:');
|
console.error('Step \'\' Error:');
|
||||||
console.error(err, err.stack);
|
console.error(err, err.stack);
|
||||||
window.alert("An error happened at Step " + i + ", but it's not your fault. Email aj@rootprojects.org and let him know.");
|
window.alert("An error happened (but it's not your fault)."
|
||||||
|
+ " Email aj@rootprojects.org to let him know that 'order and get challenges' failed.");
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -360,58 +394,44 @@
|
||||||
$qs('.js-acme-form-challenges').hidden = false;
|
$qs('.js-acme-form-challenges').hidden = false;
|
||||||
};
|
};
|
||||||
steps[3].submit = function () {
|
steps[3].submit = function () {
|
||||||
options.challengeTypes = [ 'dns-01' ];
|
var challengePriority = [ 'dns-01' ];
|
||||||
if ('http-01' === $qs('.js-acme-challenge-type:checked').value) {
|
if ('http-01' === $qs('.js-acme-challenge-type:checked').value) {
|
||||||
options.challengeTypes.unshift('http-01');
|
challengePriority.unshift('http-01');
|
||||||
}
|
}
|
||||||
console.log('primary challenge type is:', options.challengeTypes[0]);
|
console.log('primary challenge type is:', challengePriority[0]);
|
||||||
|
|
||||||
return getAccountKeypair(email).then(function (jwk) {
|
steps[i]();
|
||||||
|
return getAccountKeypair(info.email).then(function (jwk) {
|
||||||
// for now just show the next page immediately (its a spinner)
|
// for now just show the next page immediately (its a spinner)
|
||||||
// TODO put a test challenge in the list
|
// TODO put a test challenge in the list
|
||||||
// TODO warn about wait-time if DNS
|
// TODO warn about wait-time if DNS
|
||||||
steps[i]();
|
return getServerKeypair().then(function (serverJwk) {
|
||||||
return getServerKeypair().then(function () {
|
return acme.orders.complete({
|
||||||
return acme.orders.finalize({
|
account: info.account
|
||||||
account: accountStuff.account
|
|
||||||
, accountKeypair: { privateKeyJwk: jwk }
|
, accountKeypair: { privateKeyJwk: jwk }
|
||||||
, order: accountStuff.order
|
, order: info.order
|
||||||
, domainKeypair: 'TODO'
|
, domains: info.domains
|
||||||
});
|
, domainKeypair: { privateKeyJwk: serverJwk }
|
||||||
|
, challengePriority: challengePriority
|
||||||
|
, challenges: challenges
|
||||||
}).then(function (certs) {
|
}).then(function (certs) {
|
||||||
|
return Keypairs.export({ jwk: serverJwk }).then(function (keyPem) {
|
||||||
console.log('WINNING!');
|
console.log('WINNING!');
|
||||||
console.log(certs);
|
console.log(certs);
|
||||||
$qs('#js-fullchain').innerHTML = certs;
|
$qs('#js-fullchain').innerHTML = [
|
||||||
|
certs.cert.trim() + "\n"
|
||||||
|
, certs.chain + "\n"
|
||||||
|
].join("\n");
|
||||||
$qs("#js-download-fullchain-link").href =
|
$qs("#js-download-fullchain-link").href =
|
||||||
"data:text/octet-stream;base64," + window.btoa(certs);
|
"data:text/octet-stream;base64," + window.btoa(certs);
|
||||||
|
|
||||||
var wcOpts;
|
$qs('#js-privkey').innerHTML = keyPem;
|
||||||
var pemName;
|
|
||||||
if (/^R/.test(info.serverJwk.kty)) {
|
|
||||||
pemName = 'RSA';
|
|
||||||
wcOpts = { name: "RSASSA-PKCS1-v1_5", hash: { name: "SHA-256" } };
|
|
||||||
} else {
|
|
||||||
pemName = 'EC';
|
|
||||||
wcOpts = { name: "ECDSA", namedCurve: "P-256" };
|
|
||||||
}
|
|
||||||
return crypto.subtle.importKey(
|
|
||||||
"jwk"
|
|
||||||
, info.serverJwk
|
|
||||||
, wcOpts
|
|
||||||
, true
|
|
||||||
, ["sign"]
|
|
||||||
).then(function (privateKey) {
|
|
||||||
return window.crypto.subtle.exportKey("pkcs8", privateKey);
|
|
||||||
}).then (function (keydata) {
|
|
||||||
var pem = spkiToPEM(keydata, pemName);
|
|
||||||
$qs('#js-privkey').innerHTML = pem;
|
|
||||||
$qs("#js-download-privkey-link").href =
|
$qs("#js-download-privkey-link").href =
|
||||||
"data:text/octet-stream;base64," + window.btoa(pem);
|
"data:text/octet-stream;base64," + window.btoa(keyPem);
|
||||||
steps[i]();
|
submitForm();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}).then(function () {
|
|
||||||
return submitForm();
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -424,11 +444,7 @@
|
||||||
steps[4].submit = function () {
|
steps[4].submit = function () {
|
||||||
console.log('Congrats! Auto advancing...');
|
console.log('Congrats! Auto advancing...');
|
||||||
|
|
||||||
|
|
||||||
}).catch(function (err) {
|
|
||||||
console.error(err.toString());
|
|
||||||
window.alert("An error happened in the final step, but it's not your fault. Email aj@rootprojects.org and let him know.");
|
window.alert("An error happened in the final step, but it's not your fault. Email aj@rootprojects.org and let him know.");
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
steps[5] = function () {
|
steps[5] = function () {
|
||||||
|
@ -436,6 +452,8 @@
|
||||||
hideForms();
|
hideForms();
|
||||||
$qs('.js-acme-form-download').hidden = false;
|
$qs('.js-acme-form-download').hidden = false;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// The kickoff
|
||||||
steps[1]();
|
steps[1]();
|
||||||
|
|
||||||
var params = new URLSearchParams(window.location.search);
|
var params = new URLSearchParams(window.location.search);
|
||||||
|
@ -481,8 +499,8 @@
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
return testRsaSupport().then(function () {
|
return testEcdsaSupport().then(function () {
|
||||||
console.info('[crypto] RSA is supported');
|
console.info('[crypto] ECDSA is supported');
|
||||||
}).catch(function (err) {
|
}).catch(function (err) {
|
||||||
console.error('[crypto] could not use either RSA nor EC.');
|
console.error('[crypto] could not use either RSA nor EC.');
|
||||||
console.error(err);
|
console.error(err);
|
||||||
|
|
14
index.html
14
index.html
|
@ -23,15 +23,10 @@
|
||||||
</style>
|
</style>
|
||||||
<link rel="preload" href="./app/fonts/6xKydSBYKcSV-LCoeQqfX1RYOo3ig4vwlxdu.woff2" as="font" crossorigin="anonymous">
|
<link rel="preload" href="./app/fonts/6xKydSBYKcSV-LCoeQqfX1RYOo3ig4vwlxdu.woff2" as="font" crossorigin="anonymous">
|
||||||
<link rel="preload" href="./app/fonts/6xK3dSBYKcSV-LCoeQqfX1RYOo3qOK7l.woff2" as="font" crossorigin="anonymous">
|
<link rel="preload" href="./app/fonts/6xK3dSBYKcSV-LCoeQqfX1RYOo3qOK7l.woff2" as="font" crossorigin="anonymous">
|
||||||
|
|
||||||
<link rel="prefetch" href="./app/fonts/HI_SiYsKILxRpg3hIP6sJ7fM7PqlPevW.woff2" as="font" crossorigin="anonymous">
|
<link rel="prefetch" href="./app/fonts/HI_SiYsKILxRpg3hIP6sJ7fM7PqlPevW.woff2" as="font" crossorigin="anonymous">
|
||||||
<link rel="prefetch" href="./app/js/app.js">
|
<link rel="prefetch" href="./app/js/bluecrypt-acme.js" as="script">
|
||||||
<link rel="prefetch" href="./app/js/bacme.js">
|
<link rel="prefetch" href="./app/js/greenlock.js" as="script">
|
||||||
<link rel="prefetch" href="./app/js/pkijs.org/v1.3.33/common.js">
|
<link rel="prefetch" href="./js/app.js" as="script">
|
||||||
<link rel="prefetch" href="./app/js/pkijs.org/v1.3.33/asn1.js">
|
|
||||||
<link rel="prefetch" href="./app/js/pkijs.org/v1.3.33/x509_schema.js">
|
|
||||||
<link rel="prefetch" href="./app/js/pkijs.org/v1.3.33/x509_simpl.js">
|
|
||||||
<link rel="prefetch" href="./app/js/browser-csr/v1.0.0-alpha/csr.js">
|
|
||||||
</head>
|
</head>
|
||||||
<body class="js-app-ready">
|
<body class="js-app-ready">
|
||||||
<script>
|
<script>
|
||||||
|
@ -47,7 +42,8 @@
|
||||||
</div>
|
</div>
|
||||||
<div class="column-row">
|
<div class="column-row">
|
||||||
<div class="js-javascript-warning">
|
<div class="js-javascript-warning">
|
||||||
Greenlock will process the CSR in the browser and request the certificates directly from letsencrypt.org. Please enable Javascript before continuing.
|
Greenlock will process the CSR in the browser and request the certificates directly from letsencrypt.org.
|
||||||
|
Please enable Javascript before continuing.
|
||||||
</div>
|
</div>
|
||||||
<form id="js-acme-form" action="./app/" method=>
|
<form id="js-acme-form" action="./app/" method=>
|
||||||
<div class="domain-psuedo-input">
|
<div class="domain-psuedo-input">
|
||||||
|
|
Loading…
Reference in New Issue