|
@ -4,6 +4,8 @@ |
|
|
var BACME = exports.BACME = {}; |
|
|
var BACME = exports.BACME = {}; |
|
|
var webFetch = exports.fetch; |
|
|
var webFetch = exports.fetch; |
|
|
var webCrypto = exports.crypto; |
|
|
var webCrypto = exports.crypto; |
|
|
|
|
|
var Promise = window.Promise; |
|
|
|
|
|
var CSR = window.CSR; |
|
|
|
|
|
|
|
|
var directoryUrl = 'https://acme-staging-v02.api.letsencrypt.org/directory'; |
|
|
var directoryUrl = 'https://acme-staging-v02.api.letsencrypt.org/directory'; |
|
|
var directory; |
|
|
var directory; |
|
@ -15,7 +17,6 @@ var accountKeypair; |
|
|
var accountJwk; |
|
|
var accountJwk; |
|
|
|
|
|
|
|
|
var accountUrl; |
|
|
var accountUrl; |
|
|
var signedAccount; |
|
|
|
|
|
|
|
|
|
|
|
BACME.challengePrefixes = { |
|
|
BACME.challengePrefixes = { |
|
|
'http-01': '/.well-known/acme-challenge' |
|
|
'http-01': '/.well-known/acme-challenge' |
|
@ -23,38 +24,38 @@ BACME.challengePrefixes = { |
|
|
}; |
|
|
}; |
|
|
|
|
|
|
|
|
BACME._logHeaders = function (resp) { |
|
|
BACME._logHeaders = function (resp) { |
|
|
console.log('Headers:'); |
|
|
console.log('Headers:'); |
|
|
Array.from(resp.headers.entries()).forEach(function (h) { console.log(h[0] + ': ' + h[1]); }); |
|
|
Array.from(resp.headers.entries()).forEach(function (h) { console.log(h[0] + ': ' + h[1]); }); |
|
|
}; |
|
|
}; |
|
|
|
|
|
|
|
|
BACME._logBody = function (body) { |
|
|
BACME._logBody = function (body) { |
|
|
console.log('Body:'); |
|
|
console.log('Body:'); |
|
|
console.log(JSON.stringify(body, null, 2)); |
|
|
console.log(JSON.stringify(body, null, 2)); |
|
|
console.log(''); |
|
|
console.log(''); |
|
|
}; |
|
|
}; |
|
|
|
|
|
|
|
|
BACME.directory = function (opts) { |
|
|
BACME.directory = function (opts) { |
|
|
return webFetch(opts.directoryUrl || directoryUrl, { mode: 'cors' }).then(function (resp) { |
|
|
return webFetch(opts.directoryUrl || directoryUrl, { mode: 'cors' }).then(function (resp) { |
|
|
BACME._logHeaders(resp); |
|
|
BACME._logHeaders(resp); |
|
|
return resp.json().then(function (body) { |
|
|
return resp.json().then(function (body) { |
|
|
directory = body; |
|
|
directory = body; |
|
|
nonceUrl = directory.newNonce || 'https://acme-staging-v02.api.letsencrypt.org/acme/new-nonce'; |
|
|
nonceUrl = directory.newNonce || 'https://acme-staging-v02.api.letsencrypt.org/acme/new-nonce'; |
|
|
accountUrl = directory.newAccount || 'https://acme-staging-v02.api.letsencrypt.org/acme/new-account'; |
|
|
accountUrl = directory.newAccount || 'https://acme-staging-v02.api.letsencrypt.org/acme/new-account'; |
|
|
orderUrl = directory.newOrder || "https://acme-staging-v02.api.letsencrypt.org/acme/new-order"; |
|
|
orderUrl = directory.newOrder || "https://acme-staging-v02.api.letsencrypt.org/acme/new-order"; |
|
|
BACME._logBody(body); |
|
|
BACME._logBody(body); |
|
|
return body; |
|
|
return body; |
|
|
}); |
|
|
}); |
|
|
}); |
|
|
}); |
|
|
}; |
|
|
}; |
|
|
|
|
|
|
|
|
BACME.nonce = function () { |
|
|
BACME.nonce = function () { |
|
|
return webFetch(nonceUrl, { mode: 'cors' }).then(function (resp) { |
|
|
return webFetch(nonceUrl, { mode: 'cors' }).then(function (resp) { |
|
|
BACME._logHeaders(resp); |
|
|
BACME._logHeaders(resp); |
|
|
nonce = resp.headers.get('replay-nonce'); |
|
|
nonce = resp.headers.get('replay-nonce'); |
|
|
console.log('Nonce:', nonce); |
|
|
console.log('Nonce:', nonce); |
|
|
// resp.body is empty
|
|
|
// resp.body is empty
|
|
|
return resp.headers.get('replay-nonce'); |
|
|
return resp.headers.get('replay-nonce'); |
|
|
}); |
|
|
}); |
|
|
}; |
|
|
}; |
|
|
|
|
|
|
|
|
BACME.accounts = {}; |
|
|
BACME.accounts = {}; |
|
@ -62,66 +63,38 @@ BACME.accounts = {}; |
|
|
// type = ECDSA
|
|
|
// type = ECDSA
|
|
|
// bitlength = 256
|
|
|
// bitlength = 256
|
|
|
BACME.accounts.generateKeypair = function (opts) { |
|
|
BACME.accounts.generateKeypair = function (opts) { |
|
|
var wcOpts = {}; |
|
|
return BACME.generateKeypair(opts).then(function (result) { |
|
|
|
|
|
accountKeypair = result; |
|
|
|
|
|
|
|
|
// ECDSA has only the P curves and an associated bitlength
|
|
|
return webCrypto.subtle.exportKey( |
|
|
if (/^EC/i.test(opts.type)) { |
|
|
"jwk" |
|
|
wcOpts.name = 'ECDSA'; |
|
|
, result.privateKey |
|
|
if (/256/.test(opts.bitlength)) { |
|
|
).then(function (privJwk) { |
|
|
wcOpts.namedCurve = 'P-256'; |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
// RSA-PSS is another option, but I don't think it's used for Let's Encrypt
|
|
|
|
|
|
// I think the hash is only necessary for signing, not generation or import
|
|
|
|
|
|
if (/^RS/i.test(opts.type)) { |
|
|
|
|
|
wcOpts.name = 'RSASSA-PKCS1-v1_5'; |
|
|
|
|
|
wcOpts.modulusLength = opts.bitlength; |
|
|
|
|
|
if (opts.bitlength < 2048) { |
|
|
|
|
|
wcOpts.modulusLength = opts.bitlength * 8; |
|
|
|
|
|
} |
|
|
|
|
|
wcOpts.publicExponent = new Uint8Array([0x01, 0x00, 0x01]); |
|
|
|
|
|
wcOpts.hash = { name: "SHA-256" }; |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
// https://github.com/diafygi/webcrypto-examples#ecdsa---generatekey
|
|
|
|
|
|
var extractable = true; |
|
|
|
|
|
return webCrypto.subtle.generateKey( |
|
|
|
|
|
wcOpts |
|
|
|
|
|
, extractable |
|
|
|
|
|
, [ 'sign', 'verify' ] |
|
|
|
|
|
).then(function (result) { |
|
|
|
|
|
accountKeypair = result; |
|
|
|
|
|
|
|
|
|
|
|
return webCrypto.subtle.exportKey( |
|
|
|
|
|
"jwk" |
|
|
|
|
|
, result.privateKey |
|
|
|
|
|
).then(function (privJwk) { |
|
|
|
|
|
|
|
|
|
|
|
accountJwk = privJwk; |
|
|
accountJwk = privJwk; |
|
|
console.log('private jwk:'); |
|
|
console.log('private jwk:'); |
|
|
console.log(JSON.stringify(privJwk, null, 2)); |
|
|
console.log(JSON.stringify(privJwk, null, 2)); |
|
|
|
|
|
|
|
|
return privJwk; |
|
|
return privJwk; |
|
|
/* |
|
|
/* |
|
|
return webCrypto.subtle.exportKey( |
|
|
return webCrypto.subtle.exportKey( |
|
|
"pkcs8" |
|
|
"pkcs8" |
|
|
, result.privateKey |
|
|
, result.privateKey |
|
|
).then(function (keydata) { |
|
|
).then(function (keydata) { |
|
|
console.log('pkcs8:'); |
|
|
console.log('pkcs8:'); |
|
|
console.log(Array.from(new Uint8Array(keydata))); |
|
|
console.log(Array.from(new Uint8Array(keydata))); |
|
|
|
|
|
|
|
|
return privJwk; |
|
|
return privJwk; |
|
|
//return accountKeypair;
|
|
|
//return accountKeypair;
|
|
|
}); |
|
|
}); |
|
|
*/ |
|
|
*/ |
|
|
}) |
|
|
}); |
|
|
}); |
|
|
}); |
|
|
}; |
|
|
}; |
|
|
|
|
|
|
|
|
// json to url-safe base64
|
|
|
// json to url-safe base64
|
|
|
BACME._jsto64 = function (json) { |
|
|
BACME._jsto64 = function (json) { |
|
|
return btoa(JSON.stringify(json)).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/g, ''); |
|
|
return btoa(JSON.stringify(json)).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/g, ''); |
|
|
}; |
|
|
}; |
|
|
|
|
|
|
|
|
var textEncoder = new TextEncoder(); |
|
|
var textEncoder = new TextEncoder(); |
|
@ -158,7 +131,7 @@ BACME._importKey = function (jwk) { |
|
|
e: priv.e |
|
|
e: priv.e |
|
|
, kty: priv.kty |
|
|
, kty: priv.kty |
|
|
, n: priv.n |
|
|
, n: priv.n |
|
|
} |
|
|
}; |
|
|
if (!priv.p) { |
|
|
if (!priv.p) { |
|
|
priv = null; |
|
|
priv = null; |
|
|
} |
|
|
} |
|
@ -167,7 +140,7 @@ BACME._importKey = function (jwk) { |
|
|
return window.crypto.subtle.importKey( |
|
|
return window.crypto.subtle.importKey( |
|
|
"jwk" |
|
|
"jwk" |
|
|
, pub |
|
|
, pub |
|
|
, wcOpts |
|
|
, wcOpts |
|
|
, extractable |
|
|
, extractable |
|
|
, [ "verify" ] |
|
|
, [ "verify" ] |
|
|
).then(function (publicKey) { |
|
|
).then(function (publicKey) { |
|
@ -271,8 +244,8 @@ BACME.accounts.sign = function (opts) { |
|
|
protectedJson |
|
|
protectedJson |
|
|
); |
|
|
); |
|
|
|
|
|
|
|
|
// Note: this function hashes before signing so send data, not the hash
|
|
|
// Note: this function hashes before signing so send data, not the hash
|
|
|
return BACME._sign({ |
|
|
return BACME._sign({ |
|
|
abstractKey: abstractKey |
|
|
abstractKey: abstractKey |
|
|
, payload64: payload64 |
|
|
, payload64: payload64 |
|
|
, protected64: protected64 |
|
|
, protected64: protected64 |
|
@ -280,30 +253,29 @@ BACME.accounts.sign = function (opts) { |
|
|
}); |
|
|
}); |
|
|
}; |
|
|
}; |
|
|
|
|
|
|
|
|
var account; |
|
|
|
|
|
var accountId; |
|
|
var accountId; |
|
|
|
|
|
|
|
|
BACME.accounts.set = function (opts) { |
|
|
BACME.accounts.set = function (opts) { |
|
|
nonce = null; |
|
|
nonce = null; |
|
|
return window.fetch(accountUrl, { |
|
|
return window.fetch(accountUrl, { |
|
|
mode: 'cors' |
|
|
mode: 'cors' |
|
|
, method: 'POST' |
|
|
, method: 'POST' |
|
|
, headers: { 'Content-Type': 'application/jose+json' } |
|
|
, headers: { 'Content-Type': 'application/jose+json' } |
|
|
, body: JSON.stringify(opts.signedAccount) |
|
|
, body: JSON.stringify(opts.signedAccount) |
|
|
}).then(function (resp) { |
|
|
}).then(function (resp) { |
|
|
BACME._logHeaders(resp); |
|
|
BACME._logHeaders(resp); |
|
|
nonce = resp.headers.get('replay-nonce'); |
|
|
nonce = resp.headers.get('replay-nonce'); |
|
|
accountId = resp.headers.get('location'); |
|
|
accountId = resp.headers.get('location'); |
|
|
console.log('Next nonce:', nonce); |
|
|
console.log('Next nonce:', nonce); |
|
|
console.log('Location/kid:', accountId); |
|
|
console.log('Location/kid:', accountId); |
|
|
|
|
|
|
|
|
if (!resp.headers.get('content-type')) { |
|
|
if (!resp.headers.get('content-type')) { |
|
|
console.log('Body: <none>'); |
|
|
console.log('Body: <none>'); |
|
|
|
|
|
|
|
|
return { kid: accountId }; |
|
|
return { kid: accountId }; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
return resp.json().then(function (result) { |
|
|
return resp.json().then(function (result) { |
|
|
if (/^Error/i.test(result.detail)) { |
|
|
if (/^Error/i.test(result.detail)) { |
|
|
return Promise.reject(new Error(result.detail)); |
|
|
return Promise.reject(new Error(result.detail)); |
|
|
} |
|
|
} |
|
@ -311,21 +283,20 @@ BACME.accounts.set = function (opts) { |
|
|
BACME._logBody(result); |
|
|
BACME._logBody(result); |
|
|
|
|
|
|
|
|
return result; |
|
|
return result; |
|
|
}); |
|
|
}); |
|
|
}); |
|
|
}); |
|
|
}; |
|
|
}; |
|
|
|
|
|
|
|
|
var orderUrl; |
|
|
var orderUrl; |
|
|
var signedOrder; |
|
|
|
|
|
|
|
|
|
|
|
BACME.orders = {}; |
|
|
BACME.orders = {}; |
|
|
|
|
|
|
|
|
// identifiers = [ { type: 'dns', value: 'example.com' }, { type: 'dns', value: '*.example.com' } ]
|
|
|
// identifiers = [ { type: 'dns', value: 'example.com' }, { type: 'dns', value: '*.example.com' } ]
|
|
|
// signedAccount
|
|
|
// signedAccount
|
|
|
BACME.orders.sign = function (opts) { |
|
|
BACME.orders.sign = function (opts) { |
|
|
var payload64 = BACME._jsto64({ identifiers: opts.identifiers }); |
|
|
var payload64 = BACME._jsto64({ identifiers: opts.identifiers }); |
|
|
|
|
|
|
|
|
return BACME._importKey(opts.jwk).then(function (abstractKey) { |
|
|
return BACME._importKey(opts.jwk).then(function (abstractKey) { |
|
|
var protected64 = BACME._jsto64( |
|
|
var protected64 = BACME._jsto64( |
|
|
{ nonce: nonce, alg: abstractKey.meta.alg/*'ES256'*/, url: orderUrl, kid: opts.kid } |
|
|
{ nonce: nonce, alg: abstractKey.meta.alg/*'ES256'*/, url: orderUrl, kid: opts.kid } |
|
|
); |
|
|
); |
|
@ -345,36 +316,35 @@ BACME.orders.sign = function (opts) { |
|
|
}); |
|
|
}); |
|
|
}; |
|
|
}; |
|
|
|
|
|
|
|
|
var order; |
|
|
|
|
|
var currentOrderUrl; |
|
|
var currentOrderUrl; |
|
|
var authorizationUrls; |
|
|
var authorizationUrls; |
|
|
var finalizeUrl; |
|
|
var finalizeUrl; |
|
|
|
|
|
|
|
|
BACME.orders.create = function (opts) { |
|
|
BACME.orders.create = function (opts) { |
|
|
nonce = null; |
|
|
nonce = null; |
|
|
return window.fetch(orderUrl, { |
|
|
return window.fetch(orderUrl, { |
|
|
mode: 'cors' |
|
|
mode: 'cors' |
|
|
, method: 'POST' |
|
|
, method: 'POST' |
|
|
, headers: { 'Content-Type': 'application/jose+json' } |
|
|
, headers: { 'Content-Type': 'application/jose+json' } |
|
|
, body: JSON.stringify(opts.signedOrder) |
|
|
, body: JSON.stringify(opts.signedOrder) |
|
|
}).then(function (resp) { |
|
|
}).then(function (resp) { |
|
|
BACME._logHeaders(resp); |
|
|
BACME._logHeaders(resp); |
|
|
currentOrderUrl = resp.headers.get('location'); |
|
|
currentOrderUrl = resp.headers.get('location'); |
|
|
nonce = resp.headers.get('replay-nonce'); |
|
|
nonce = resp.headers.get('replay-nonce'); |
|
|
console.log('Next nonce:', nonce); |
|
|
console.log('Next nonce:', nonce); |
|
|
|
|
|
|
|
|
return resp.json().then(function (result) { |
|
|
return resp.json().then(function (result) { |
|
|
if (/^Error/i.test(result.detail)) { |
|
|
if (/^Error/i.test(result.detail)) { |
|
|
return Promise.reject(new Error(result.detail)); |
|
|
return Promise.reject(new Error(result.detail)); |
|
|
} |
|
|
} |
|
|
authorizationUrls = result.authorizations; |
|
|
authorizationUrls = result.authorizations; |
|
|
finalizeUrl = result.finalize; |
|
|
finalizeUrl = result.finalize; |
|
|
BACME._logBody(result); |
|
|
BACME._logBody(result); |
|
|
|
|
|
|
|
|
result.url = currentOrderUrl; |
|
|
result.url = currentOrderUrl; |
|
|
return result; |
|
|
return result; |
|
|
}); |
|
|
}); |
|
|
}); |
|
|
}); |
|
|
}; |
|
|
}; |
|
|
|
|
|
|
|
|
BACME.challenges = {}; |
|
|
BACME.challenges = {}; |
|
@ -395,22 +365,22 @@ BACME.challenges.all = function () { |
|
|
return next(); |
|
|
return next(); |
|
|
}; |
|
|
}; |
|
|
BACME.challenges.view = function () { |
|
|
BACME.challenges.view = function () { |
|
|
var authzUrl = authorizationUrls.pop(); |
|
|
var authzUrl = authorizationUrls.pop(); |
|
|
var token; |
|
|
var token; |
|
|
var challengeDomain; |
|
|
var challengeDomain; |
|
|
var challengeUrl; |
|
|
var challengeUrl; |
|
|
|
|
|
|
|
|
return window.fetch(authzUrl, { |
|
|
return window.fetch(authzUrl, { |
|
|
mode: 'cors' |
|
|
mode: 'cors' |
|
|
}).then(function (resp) { |
|
|
}).then(function (resp) { |
|
|
BACME._logHeaders(resp); |
|
|
BACME._logHeaders(resp); |
|
|
|
|
|
|
|
|
return resp.json().then(function (result) { |
|
|
return resp.json().then(function (result) { |
|
|
// Note: select the challenge you wish to use
|
|
|
// Note: select the challenge you wish to use
|
|
|
var challenge = result.challenges.slice(0).pop(); |
|
|
var challenge = result.challenges.slice(0).pop(); |
|
|
token = challenge.token; |
|
|
token = challenge.token; |
|
|
challengeUrl = challenge.url; |
|
|
challengeUrl = challenge.url; |
|
|
challengeDomain = result.identifier.value; |
|
|
challengeDomain = result.identifier.value; |
|
|
|
|
|
|
|
|
BACME._logBody(result); |
|
|
BACME._logBody(result); |
|
|
|
|
|
|
|
@ -424,8 +394,8 @@ BACME.challenges.view = function () { |
|
|
//, url: challenge.url
|
|
|
//, url: challenge.url
|
|
|
//, domain: result.identifier.value,
|
|
|
//, domain: result.identifier.value,
|
|
|
}; |
|
|
}; |
|
|
}); |
|
|
}); |
|
|
}); |
|
|
}); |
|
|
}; |
|
|
}; |
|
|
|
|
|
|
|
|
var thumbprint; |
|
|
var thumbprint; |
|
@ -435,7 +405,7 @@ var dnsAuth; |
|
|
var dnsRecord; |
|
|
var dnsRecord; |
|
|
|
|
|
|
|
|
BACME.thumbprint = function (opts) { |
|
|
BACME.thumbprint = function (opts) { |
|
|
// https://stackoverflow.com/questions/42588786/how-to-fingerprint-a-jwk
|
|
|
// https://stackoverflow.com/questions/42588786/how-to-fingerprint-a-jwk
|
|
|
|
|
|
|
|
|
var accountJwk = opts.jwk; |
|
|
var accountJwk = opts.jwk; |
|
|
var keys; |
|
|
var keys; |
|
@ -446,34 +416,34 @@ BACME.thumbprint = function (opts) { |
|
|
keys = [ 'e', 'kty', 'n' ]; |
|
|
keys = [ 'e', 'kty', 'n' ]; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
var accountPublicStr = '{' + keys.map(function (key) { |
|
|
var accountPublicStr = '{' + keys.map(function (key) { |
|
|
return '"' + key + '":"' + accountJwk[key] + '"'; |
|
|
return '"' + key + '":"' + accountJwk[key] + '"'; |
|
|
}).join(',') + '}'; |
|
|
}).join(',') + '}'; |
|
|
|
|
|
|
|
|
return window.crypto.subtle.digest( |
|
|
return window.crypto.subtle.digest( |
|
|
{ name: "SHA-256" } // SHA-256 is spec'd, non-optional
|
|
|
{ name: "SHA-256" } // SHA-256 is spec'd, non-optional
|
|
|
, textEncoder.encode(accountPublicStr) |
|
|
, textEncoder.encode(accountPublicStr) |
|
|
).then(function (hash) { |
|
|
).then(function (hash) { |
|
|
thumbprint = btoa(Array.prototype.map.call(new Uint8Array(hash), function (ch) { |
|
|
thumbprint = btoa(Array.prototype.map.call(new Uint8Array(hash), function (ch) { |
|
|
return String.fromCharCode(ch); |
|
|
return String.fromCharCode(ch); |
|
|
}).join('')).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/g, ''); |
|
|
}).join('')).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/g, ''); |
|
|
|
|
|
|
|
|
console.log('Thumbprint:'); |
|
|
console.log('Thumbprint:'); |
|
|
console.log(opts); |
|
|
console.log(opts); |
|
|
console.log(accountPublicStr); |
|
|
console.log(accountPublicStr); |
|
|
console.log(thumbprint); |
|
|
console.log(thumbprint); |
|
|
|
|
|
|
|
|
return thumbprint; |
|
|
return thumbprint; |
|
|
}); |
|
|
}); |
|
|
}; |
|
|
}; |
|
|
|
|
|
|
|
|
// { token, thumbprint, challengeDomain }
|
|
|
// { token, thumbprint, challengeDomain }
|
|
|
BACME.challenges['http-01'] = function (opts) { |
|
|
BACME.challenges['http-01'] = function (opts) { |
|
|
// The contents of the key authorization file
|
|
|
// The contents of the key authorization file
|
|
|
keyAuth = opts.token + '.' + opts.thumbprint; |
|
|
keyAuth = opts.token + '.' + opts.thumbprint; |
|
|
|
|
|
|
|
|
// Where the key authorization file goes
|
|
|
// Where the key authorization file goes
|
|
|
httpPath = 'http://' + opts.challengeDomain + '/.well-known/acme-challenge/' + opts.token; |
|
|
httpPath = 'http://' + opts.challengeDomain + '/.well-known/acme-challenge/' + opts.token; |
|
|
|
|
|
|
|
|
console.log("echo '" + keyAuth + "' > '" + httpPath + "'"); |
|
|
console.log("echo '" + keyAuth + "' > '" + httpPath + "'"); |
|
|
|
|
|
|
|
@ -487,28 +457,28 @@ BACME.challenges['http-01'] = function (opts) { |
|
|
BACME.challenges['dns-01'] = function (opts) { |
|
|
BACME.challenges['dns-01'] = function (opts) { |
|
|
console.log('opts.keyAuth for DNS:'); |
|
|
console.log('opts.keyAuth for DNS:'); |
|
|
console.log(opts.keyAuth); |
|
|
console.log(opts.keyAuth); |
|
|
return window.crypto.subtle.digest( |
|
|
return window.crypto.subtle.digest( |
|
|
{ name: "SHA-256", } |
|
|
{ name: "SHA-256", } |
|
|
, textEncoder.encode(opts.keyAuth) |
|
|
, textEncoder.encode(opts.keyAuth) |
|
|
).then(function (hash) { |
|
|
).then(function (hash) { |
|
|
dnsAuth = btoa(Array.prototype.map.call(new Uint8Array(hash), function (ch) { |
|
|
dnsAuth = btoa(Array.prototype.map.call(new Uint8Array(hash), function (ch) { |
|
|
return String.fromCharCode(ch); |
|
|
return String.fromCharCode(ch); |
|
|
}).join('')).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/g, ''); |
|
|
}).join('')).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/g, ''); |
|
|
|
|
|
|
|
|
dnsRecord = '_acme-challenge.' + opts.challengeDomain; |
|
|
dnsRecord = '_acme-challenge.' + opts.challengeDomain; |
|
|
|
|
|
|
|
|
console.log('DNS TXT Auth:'); |
|
|
console.log('DNS TXT Auth:'); |
|
|
// The name of the record
|
|
|
// The name of the record
|
|
|
console.log(dnsRecord); |
|
|
console.log(dnsRecord); |
|
|
// The TXT record value
|
|
|
// The TXT record value
|
|
|
console.log(dnsAuth); |
|
|
console.log(dnsAuth); |
|
|
|
|
|
|
|
|
return { |
|
|
return { |
|
|
type: 'TXT' |
|
|
type: 'TXT' |
|
|
, host: dnsRecord |
|
|
, host: dnsRecord |
|
|
, answer: dnsAuth |
|
|
, answer: dnsAuth |
|
|
}; |
|
|
}; |
|
|
}); |
|
|
}); |
|
|
}; |
|
|
}; |
|
|
|
|
|
|
|
|
var challengePollUrl; |
|
|
var challengePollUrl; |
|
@ -516,84 +486,108 @@ var challengePollUrl; |
|
|
// { jwk, challengeUrl, accountId (kid) }
|
|
|
// { jwk, challengeUrl, accountId (kid) }
|
|
|
BACME.challenges.accept = function (opts) { |
|
|
BACME.challenges.accept = function (opts) { |
|
|
var payload64 = BACME._jsto64( |
|
|
var payload64 = BACME._jsto64( |
|
|
{} |
|
|
{} |
|
|
); |
|
|
); |
|
|
|
|
|
|
|
|
return BACME._importKey(opts.jwk).then(function (abstractKey) { |
|
|
return BACME._importKey(opts.jwk).then(function (abstractKey) { |
|
|
var protected64 = BACME._jsto64( |
|
|
var protected64 = BACME._jsto64( |
|
|
{ nonce: nonce, alg: abstractKey.meta.alg/*'ES256'*/, url: opts.challengeUrl, kid: opts.accountId } |
|
|
{ nonce: nonce, alg: abstractKey.meta.alg/*'ES256'*/, url: opts.challengeUrl, kid: opts.accountId } |
|
|
); |
|
|
); |
|
|
return BACME._sign({ |
|
|
return BACME._sign({ |
|
|
abstractKey: abstractKey |
|
|
abstractKey: abstractKey |
|
|
, payload64: payload64 |
|
|
, payload64: payload64 |
|
|
, protected64: protected64 |
|
|
, protected64: protected64 |
|
|
}); |
|
|
}); |
|
|
}).then(function (signedAccept) { |
|
|
}).then(function (signedAccept) { |
|
|
|
|
|
|
|
|
nonce = null; |
|
|
nonce = null; |
|
|
return window.fetch( |
|
|
return window.fetch( |
|
|
opts.challengeUrl |
|
|
opts.challengeUrl |
|
|
, { mode: 'cors' |
|
|
, { mode: 'cors' |
|
|
, method: 'POST' |
|
|
, method: 'POST' |
|
|
, headers: { 'Content-Type': 'application/jose+json' } |
|
|
, headers: { 'Content-Type': 'application/jose+json' } |
|
|
, body: JSON.stringify(signedAccept) |
|
|
, body: JSON.stringify(signedAccept) |
|
|
} |
|
|
} |
|
|
).then(function (resp) { |
|
|
).then(function (resp) { |
|
|
BACME._logHeaders(resp); |
|
|
BACME._logHeaders(resp); |
|
|
nonce = resp.headers.get('replay-nonce'); |
|
|
nonce = resp.headers.get('replay-nonce'); |
|
|
console.log("ACCEPT NONCE:", nonce); |
|
|
console.log("ACCEPT NONCE:", nonce); |
|
|
|
|
|
|
|
|
return resp.json().then(function (reply) { |
|
|
return resp.json().then(function (reply) { |
|
|
challengePollUrl = reply.url; |
|
|
challengePollUrl = reply.url; |
|
|
|
|
|
|
|
|
console.log('Challenge ACK:'); |
|
|
console.log('Challenge ACK:'); |
|
|
console.log(JSON.stringify(reply)); |
|
|
console.log(JSON.stringify(reply)); |
|
|
return reply; |
|
|
return reply; |
|
|
}); |
|
|
}); |
|
|
}); |
|
|
}); |
|
|
}); |
|
|
}); |
|
|
}; |
|
|
}; |
|
|
|
|
|
|
|
|
BACME.challenges.check = function (opts) { |
|
|
BACME.challenges.check = function (opts) { |
|
|
return window.fetch(opts.challengePollUrl, { mode: 'cors' }).then(function (resp) { |
|
|
return window.fetch(opts.challengePollUrl, { mode: 'cors' }).then(function (resp) { |
|
|
BACME._logHeaders(resp); |
|
|
BACME._logHeaders(resp); |
|
|
|
|
|
|
|
|
return resp.json().then(function (reply) { |
|
|
return resp.json().then(function (reply) { |
|
|
challengePollUrl = reply.url; |
|
|
challengePollUrl = reply.url; |
|
|
|
|
|
|
|
|
BACME._logBody(reply); |
|
|
BACME._logBody(reply); |
|
|
|
|
|
|
|
|
return reply; |
|
|
return reply; |
|
|
}); |
|
|
}); |
|
|
}); |
|
|
}); |
|
|
}; |
|
|
}; |
|
|
|
|
|
|
|
|
var domainKeypair; |
|
|
var domainKeypair; |
|
|
var domainJwk; |
|
|
var domainJwk; |
|
|
|
|
|
|
|
|
|
|
|
BACME.generateKeypair = function (opts) { |
|
|
|
|
|
var wcOpts = {}; |
|
|
|
|
|
|
|
|
|
|
|
// ECDSA has only the P curves and an associated bitlength
|
|
|
|
|
|
if (/^EC/i.test(opts.type)) { |
|
|
|
|
|
wcOpts.name = 'ECDSA'; |
|
|
|
|
|
if (/256/.test(opts.bitlength)) { |
|
|
|
|
|
wcOpts.namedCurve = 'P-256'; |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
// RSA-PSS is another option, but I don't think it's used for Let's Encrypt
|
|
|
|
|
|
// I think the hash is only necessary for signing, not generation or import
|
|
|
|
|
|
if (/^RS/i.test(opts.type)) { |
|
|
|
|
|
wcOpts.name = 'RSASSA-PKCS1-v1_5'; |
|
|
|
|
|
wcOpts.modulusLength = opts.bitlength; |
|
|
|
|
|
if (opts.bitlength < 2048) { |
|
|
|
|
|
wcOpts.modulusLength = opts.bitlength * 8; |
|
|
|
|
|
} |
|
|
|
|
|
wcOpts.publicExponent = new Uint8Array([0x01, 0x00, 0x01]); |
|
|
|
|
|
wcOpts.hash = { name: "SHA-256" }; |
|
|
|
|
|
} |
|
|
|
|
|
var extractable = true; |
|
|
|
|
|
return window.crypto.subtle.generateKey( |
|
|
|
|
|
{ name: "ECDSA", namedCurve: "P-256" } |
|
|
|
|
|
, extractable |
|
|
|
|
|
, [ 'sign', 'verify' ] |
|
|
|
|
|
); |
|
|
|
|
|
}; |
|
|
BACME.domains = {}; |
|
|
BACME.domains = {}; |
|
|
// TODO factor out from BACME.accounts.generateKeypair
|
|
|
// TODO factor out from BACME.accounts.generateKeypair even more
|
|
|
BACME.domains.generateKeypair = function () { |
|
|
BACME.domains.generateKeypair = function (opts) { |
|
|
var extractable = true; |
|
|
return BACME.generateKeypair(opts).then(function (result) { |
|
|
return window.crypto.subtle.generateKey( |
|
|
domainKeypair = result; |
|
|
{ name: "ECDSA", namedCurve: "P-256" } |
|
|
|
|
|
, extractable |
|
|
return window.crypto.subtle.exportKey( |
|
|
, [ 'sign', 'verify' ] |
|
|
"jwk" |
|
|
).then(function (result) { |
|
|
, result.privateKey |
|
|
domainKeypair = result; |
|
|
).then(function (privJwk) { |
|
|
|
|
|
|
|
|
return window.crypto.subtle.exportKey( |
|
|
domainJwk = privJwk; |
|
|
"jwk" |
|
|
console.log('private jwk:'); |
|
|
, result.privateKey |
|
|
console.log(JSON.stringify(privJwk, null, 2)); |
|
|
).then(function (jwk) { |
|
|
|
|
|
|
|
|
return privJwk; |
|
|
domainJwk = jwk; |
|
|
}); |
|
|
console.log('private jwk:'); |
|
|
}); |
|
|
console.log(JSON.stringify(jwk, null, 2)); |
|
|
|
|
|
|
|
|
|
|
|
return domainKeypair; |
|
|
|
|
|
}) |
|
|
|
|
|
}); |
|
|
|
|
|
}; |
|
|
}; |
|
|
|
|
|
|
|
|
// { serverJwk, domains }
|
|
|
// { serverJwk, domains }
|
|
@ -607,41 +601,41 @@ var certificateUrl; |
|
|
|
|
|
|
|
|
// { csr, jwk, finalizeUrl, accountId }
|
|
|
// { csr, jwk, finalizeUrl, accountId }
|
|
|
BACME.orders.finalize = function (opts) { |
|
|
BACME.orders.finalize = function (opts) { |
|
|
var payload64 = BACME._jsto64( |
|
|
var payload64 = BACME._jsto64( |
|
|
{ csr: opts.csr } |
|
|
{ csr: opts.csr } |
|
|
); |
|
|
); |
|
|
|
|
|
|
|
|
return BACME._importKey(opts.jwk).then(function (abstractKey) { |
|
|
return BACME._importKey(opts.jwk).then(function (abstractKey) { |
|
|
var protected64 = BACME._jsto64( |
|
|
var protected64 = BACME._jsto64( |
|
|
{ nonce: nonce, alg: abstractKey.meta.alg/*'ES256'*/, url: opts.finalizeUrl, kid: opts.accountId } |
|
|
{ nonce: nonce, alg: abstractKey.meta.alg/*'ES256'*/, url: opts.finalizeUrl, kid: opts.accountId } |
|
|
); |
|
|
); |
|
|
return BACME._sign({ |
|
|
return BACME._sign({ |
|
|
abstractKey: abstractKey |
|
|
abstractKey: abstractKey |
|
|
, payload64: payload64 |
|
|
, payload64: payload64 |
|
|
, protected64: protected64 |
|
|
, protected64: protected64 |
|
|
}); |
|
|
}); |
|
|
}).then(function (signedFinal) { |
|
|
}).then(function (signedFinal) { |
|
|
|
|
|
|
|
|
nonce = null; |
|
|
nonce = null; |
|
|
return window.fetch( |
|
|
return window.fetch( |
|
|
opts.finalizeUrl |
|
|
opts.finalizeUrl |
|
|
, { mode: 'cors' |
|
|
, { mode: 'cors' |
|
|
, method: 'POST' |
|
|
, method: 'POST' |
|
|
, headers: { 'Content-Type': 'application/jose+json' } |
|
|
, headers: { 'Content-Type': 'application/jose+json' } |
|
|
, body: JSON.stringify(signedFinal) |
|
|
, body: JSON.stringify(signedFinal) |
|
|
} |
|
|
} |
|
|
).then(function (resp) { |
|
|
).then(function (resp) { |
|
|
BACME._logHeaders(resp); |
|
|
BACME._logHeaders(resp); |
|
|
nonce = resp.headers.get('replay-nonce'); |
|
|
nonce = resp.headers.get('replay-nonce'); |
|
|
|
|
|
|
|
|
return resp.json().then(function (reply) { |
|
|
return resp.json().then(function (reply) { |
|
|
certificateUrl = reply.certificate; |
|
|
certificateUrl = reply.certificate; |
|
|
BACME._logBody(reply); |
|
|
BACME._logBody(reply); |
|
|
|
|
|
|
|
|
return reply; |
|
|
return reply; |
|
|
}); |
|
|
}); |
|
|
}); |
|
|
}); |
|
|
}); |
|
|
}); |
|
|
}; |
|
|
}; |
|
|
|
|
|
|
|
|
BACME.orders.receive = function (opts) { |
|
|
BACME.orders.receive = function (opts) { |
|
|