|
|
@ -58,11 +58,36 @@ BACME.nonce = function () { |
|
|
|
}; |
|
|
|
|
|
|
|
BACME.accounts = {}; |
|
|
|
BACME.accounts.generateKeypair = function () { |
|
|
|
|
|
|
|
// type = ECDSA
|
|
|
|
// bitlength = 256
|
|
|
|
BACME.accounts.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" }; |
|
|
|
} |
|
|
|
|
|
|
|
// https://github.com/diafygi/webcrypto-examples#ecdsa---generatekey
|
|
|
|
var extractable = true; |
|
|
|
return webCrypto.subtle.generateKey( |
|
|
|
{ name: "ECDSA", namedCurve: "P-256" } |
|
|
|
wcOpts |
|
|
|
, extractable |
|
|
|
, [ 'sign', 'verify' ] |
|
|
|
).then(function (result) { |
|
|
@ -71,11 +96,11 @@ BACME.accounts.generateKeypair = function () { |
|
|
|
return webCrypto.subtle.exportKey( |
|
|
|
"jwk" |
|
|
|
, result.privateKey |
|
|
|
).then(function (jwk) { |
|
|
|
).then(function (privJwk) { |
|
|
|
|
|
|
|
accountJwk = jwk; |
|
|
|
accountJwk = privJwk; |
|
|
|
console.log('private jwk:'); |
|
|
|
console.log(JSON.stringify(jwk, null, 2)); |
|
|
|
console.log(JSON.stringify(privJwk, null, 2)); |
|
|
|
|
|
|
|
return webCrypto.subtle.exportKey( |
|
|
|
"pkcs8" |
|
|
@ -84,7 +109,8 @@ BACME.accounts.generateKeypair = function () { |
|
|
|
console.log('pkcs8:'); |
|
|
|
console.log(Array.from(new Uint8Array(keydata))); |
|
|
|
|
|
|
|
return accountKeypair; |
|
|
|
return privJwk; |
|
|
|
//return accountKeypair;
|
|
|
|
}); |
|
|
|
}) |
|
|
|
}); |
|
|
@ -97,63 +123,137 @@ BACME._jsto64 = function (json) { |
|
|
|
|
|
|
|
var textEncoder = new TextEncoder(); |
|
|
|
|
|
|
|
// email = john.doe@gmail.com
|
|
|
|
BACME.accounts.sign = function (email) { |
|
|
|
var payload64 = BACME._jsto64( |
|
|
|
{ termsOfServiceAgreed: true |
|
|
|
, onlyReturnExisting: false |
|
|
|
, contact: [ 'mailto:' + email ] |
|
|
|
} |
|
|
|
); |
|
|
|
|
|
|
|
var protected64 = BACME._jsto64( |
|
|
|
{ nonce: nonce |
|
|
|
, url: accountUrl |
|
|
|
, alg: 'ES256' |
|
|
|
, jwk: { |
|
|
|
kty: accountJwk.kty |
|
|
|
, crv: accountJwk.crv |
|
|
|
, x: accountJwk.x |
|
|
|
, y: accountJwk.y |
|
|
|
} |
|
|
|
} |
|
|
|
); |
|
|
|
|
|
|
|
// Note: this function hashes before signing so send data, not the hash
|
|
|
|
return window.crypto.subtle.sign( |
|
|
|
{ name: "ECDSA", hash: { name: "SHA-256" } } |
|
|
|
, accountKeypair.privateKey |
|
|
|
, textEncoder.encode(protected64 + '.' + payload64) |
|
|
|
).then(function (signature) { |
|
|
|
|
|
|
|
// convert buffer to urlsafe base64
|
|
|
|
var sig64 = btoa(Array.prototype.map.call(new Uint8Array(signature), function (ch) { |
|
|
|
return String.fromCharCode(ch); |
|
|
|
}).join('')).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/g, ''); |
|
|
|
BACME._importKey = function (jwk) { |
|
|
|
var alg; // I think the 256 refers to the hash
|
|
|
|
var wcOpts = {}; |
|
|
|
var extractable = false; |
|
|
|
|
|
|
|
// ECDSA
|
|
|
|
if (/^EC/i.test(jwk.kty)) { |
|
|
|
wcOpts.name = 'ECDSA'; |
|
|
|
wcOpts.namedCurve = jwk.crv; |
|
|
|
alg = 'ES256'; |
|
|
|
} |
|
|
|
|
|
|
|
// RSA
|
|
|
|
if (/^RS/i.test(jwk.kty)) { |
|
|
|
wcOpts.name = 'RSASSA-PKCS1-v1_5'; |
|
|
|
wcOpts.hash = { name: "SHA-256" }; |
|
|
|
alg = 'RS256'; |
|
|
|
} |
|
|
|
|
|
|
|
return window.crypto.subtle.importKey( |
|
|
|
"jwk" |
|
|
|
, jwk |
|
|
|
, wcOpts |
|
|
|
, extractable |
|
|
|
, [ "sign"/*, "verify"*/ ] |
|
|
|
).then(function (keypair) { |
|
|
|
return { |
|
|
|
wcKey: keypair |
|
|
|
, meta: { |
|
|
|
alg: alg |
|
|
|
, name: wcOpts.name |
|
|
|
, hash: wcOpts.hash |
|
|
|
} |
|
|
|
, jwk: jwk |
|
|
|
}; |
|
|
|
}); |
|
|
|
}; |
|
|
|
BACME._sign = function (opts) { |
|
|
|
var wcPrivKey = opts.abstractKey.wcKey; |
|
|
|
var wcOpts = opts.abstractKey.meta; |
|
|
|
var alg = opts.abstractKey.meta.alg; // I think the 256 refers to the hash
|
|
|
|
var signHash; |
|
|
|
|
|
|
|
console.log('kty', opts.abstractKey.jwk.kty); |
|
|
|
signHash = { name: "SHA-" + alg.replace(/[a-z]+/ig, '') }; |
|
|
|
|
|
|
|
var msg = textEncoder.encode(opts.protected64 + '.' + opts.payload64); |
|
|
|
console.log('msg:', msg); |
|
|
|
return window.crypto.subtle.sign( |
|
|
|
{ name: wcOpts.name, hash: signHash } |
|
|
|
, wcPrivKey |
|
|
|
, msg |
|
|
|
).then(function (signature) { |
|
|
|
//console.log('sig1:', signature);
|
|
|
|
//console.log('sig2:', new Uint8Array(signature));
|
|
|
|
//console.log('sig3:', Array.prototype.slice.call(new Uint8Array(signature)));
|
|
|
|
// convert buffer to urlsafe base64
|
|
|
|
var sig64 = btoa(Array.prototype.map.call(new Uint8Array(signature), function (ch) { |
|
|
|
return String.fromCharCode(ch); |
|
|
|
}).join('')).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/g, ''); |
|
|
|
|
|
|
|
console.log('[1] URL-safe Base64 Signature:'); |
|
|
|
console.log(sig64); |
|
|
|
|
|
|
|
var signedMsg = { |
|
|
|
protected: opts.protected64 |
|
|
|
, payload: opts.payload64 |
|
|
|
, signature: sig64 |
|
|
|
}; |
|
|
|
|
|
|
|
console.log('URL-safe Base64 Signature:'); |
|
|
|
console.log(sig64); |
|
|
|
console.log('Signed Base64 Msg:'); |
|
|
|
console.log(JSON.stringify(signedMsg, null, 2)); |
|
|
|
|
|
|
|
signedAccount = { |
|
|
|
protected: protected64 |
|
|
|
, payload: payload64 |
|
|
|
, signature: sig64 |
|
|
|
}; |
|
|
|
console.log('Signed Base64 Account:'); |
|
|
|
console.log(JSON.stringify(signedAccount, null, 2)); |
|
|
|
}); |
|
|
|
return signedMsg; |
|
|
|
}); |
|
|
|
}; |
|
|
|
// email = john.doe@gmail.com
|
|
|
|
// jwk = { ... }
|
|
|
|
// agree = true
|
|
|
|
BACME.accounts.sign = function (opts) { |
|
|
|
|
|
|
|
return BACME._importKey(opts.jwk).then(function (abstractKey) { |
|
|
|
|
|
|
|
var payloadJson = |
|
|
|
{ termsOfServiceAgreed: opts.agree |
|
|
|
, onlyReturnExisting: false |
|
|
|
, contact: opts.contacts || [ 'mailto:' + opts.email ] |
|
|
|
}; |
|
|
|
console.log('payload:'); |
|
|
|
console.log(payloadJson); |
|
|
|
var payload64 = BACME._jsto64( |
|
|
|
payloadJson |
|
|
|
); |
|
|
|
|
|
|
|
// TODO RSA
|
|
|
|
var protectedJson = |
|
|
|
{ nonce: opts.nonce |
|
|
|
, url: accountUrl |
|
|
|
, alg: abstractKey.meta.alg |
|
|
|
, jwk: { |
|
|
|
kty: opts.jwk.kty |
|
|
|
, crv: opts.jwk.crv |
|
|
|
, x: opts.jwk.x |
|
|
|
, y: opts.jwk.y |
|
|
|
} |
|
|
|
}; |
|
|
|
console.log('protected:'); |
|
|
|
console.log(protectedJson); |
|
|
|
var protected64 = BACME._jsto64( |
|
|
|
protectedJson |
|
|
|
); |
|
|
|
|
|
|
|
// Note: this function hashes before signing so send data, not the hash
|
|
|
|
return BACME._sign({ |
|
|
|
abstractKey: abstractKey |
|
|
|
, payload64: payload64 |
|
|
|
, protected64: protected64 |
|
|
|
}); |
|
|
|
}); |
|
|
|
}; |
|
|
|
|
|
|
|
var account; |
|
|
|
var accountId; |
|
|
|
|
|
|
|
BACME.accounts.set = function () { |
|
|
|
BACME.accounts.set = function (opts) { |
|
|
|
nonce = null; |
|
|
|
return window.fetch(accountUrl, { |
|
|
|
mode: 'cors' |
|
|
|
, method: 'POST' |
|
|
|
, headers: { 'Content-Type': 'application/jose+json' } |
|
|
|
, body: JSON.stringify(signedAccount) |
|
|
|
, body: JSON.stringify(opts.signedAccount) |
|
|
|
}).then(function (resp) { |
|
|
|
BACME._logHeaders(resp); |
|
|
|
nonce = resp.headers.get('replay-nonce'); |
|
|
@ -163,11 +263,18 @@ BACME.accounts.set = function () { |
|
|
|
|
|
|
|
if (!resp.headers.get('content-type')) { |
|
|
|
console.log('Body: <none>'); |
|
|
|
return; |
|
|
|
|
|
|
|
return { kid: accountId }; |
|
|
|
} |
|
|
|
|
|
|
|
return resp.json().then(function (result) { |
|
|
|
if (/^Error/i.test(result.detail)) { |
|
|
|
return Promise.reject(new Error(result.detail)); |
|
|
|
} |
|
|
|
result.kid = accountId; |
|
|
|
BACME._logBody(result); |
|
|
|
|
|
|
|
return result; |
|
|
|
}); |
|
|
|
}); |
|
|
|
}; |
|
|
@ -178,37 +285,29 @@ var signedOrder; |
|
|
|
BACME.orders = {}; |
|
|
|
|
|
|
|
// identifiers = [ { type: 'dns', value: 'example.com' }, { type: 'dns', value: '*.example.com' } ]
|
|
|
|
BACME.orders.sign = function (identifiers) { |
|
|
|
var payload64 = jsto64({ identifiers: identifiers }); |
|
|
|
// signedAccount
|
|
|
|
BACME.orders.sign = function (opts) { |
|
|
|
var payload64 = BACME._jsto64({ identifiers: opts.identifiers }); |
|
|
|
|
|
|
|
var protected64 = jsto64( |
|
|
|
{ nonce: nonce, alg: 'ES256', url: orderUrl, kid: accountId } |
|
|
|
var protected64 = BACME._jsto64( |
|
|
|
{ nonce: nonce, alg: 'ES256', url: orderUrl, kid: opts.kid } |
|
|
|
); |
|
|
|
|
|
|
|
return window.crypto.subtle.sign( |
|
|
|
{ name: "ECDSA", hash: { name: "SHA-256" } } |
|
|
|
, accountKeypair.privateKey |
|
|
|
, textEncoder.encode(protected64 + '.' + payload64) |
|
|
|
).then(function (signature) { |
|
|
|
|
|
|
|
// convert buffer to urlsafe base64
|
|
|
|
var sig64 = btoa(Array.prototype.map.call(new Uint8Array(signature), function (ch) { |
|
|
|
return String.fromCharCode(ch); |
|
|
|
}).join('')).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/g, ''); |
|
|
|
|
|
|
|
console.log('URL-safe Base64 Signature:'); |
|
|
|
console.log(sig64); |
|
|
|
|
|
|
|
signedOrder = { |
|
|
|
protected: protected64 |
|
|
|
, payload: payload64 |
|
|
|
, signature: sig64 |
|
|
|
}; |
|
|
|
console.log('Signed Base64 Order:'); |
|
|
|
console.log(JSON.stringify(signedAccount, null, 2)); |
|
|
|
|
|
|
|
return signedOrder; |
|
|
|
}); |
|
|
|
return BACME._importKey(opts.jwk).then(function (abstractKey) { |
|
|
|
console.log('abstractKey:'); |
|
|
|
console.log(abstractKey); |
|
|
|
return BACME._sign({ |
|
|
|
abstractKey: abstractKey |
|
|
|
, payload64: payload64 |
|
|
|
, protected64: protected64 |
|
|
|
}).then(function (sig) { |
|
|
|
if (!sig) { |
|
|
|
throw new Error('sig is undefined... nonsense!'); |
|
|
|
} |
|
|
|
console.log('newsig', sig); |
|
|
|
return sig; |
|
|
|
}); |
|
|
|
}); |
|
|
|
}; |
|
|
|
|
|
|
|
var order; |
|
|
@ -216,25 +315,26 @@ var currentOrderUrl; |
|
|
|
var authorizationUrls; |
|
|
|
var finalizeUrl; |
|
|
|
|
|
|
|
BACME.orders.create = function () { |
|
|
|
BACME.orders.create = function (opts) { |
|
|
|
nonce = null; |
|
|
|
return window.fetch(orderUrl, { |
|
|
|
mode: 'cors' |
|
|
|
, method: 'POST' |
|
|
|
, headers: { 'Content-Type': 'application/jose+json' } |
|
|
|
, body: JSON.stringify(signedOrder) |
|
|
|
, body: JSON.stringify(opts.signedOrder) |
|
|
|
}).then(function (resp) { |
|
|
|
console.log('Headers:'); |
|
|
|
Array.from(resp.headers.entries()).forEach(function (h) { console.log(h[0] + ': ' + h[1]); }); |
|
|
|
BACME._logHeaders(resp); |
|
|
|
currentOrderUrl = resp.headers.get('location'); |
|
|
|
nonce = resp.headers.get('replay-nonce'); |
|
|
|
console.log('Next nonce:', nonce); |
|
|
|
|
|
|
|
return resp.json().then(function (result) { |
|
|
|
if (/^Error/i.test(result.detail)) { |
|
|
|
return Promise.reject(new Error(result.detail)); |
|
|
|
} |
|
|
|
authorizationUrls = result.authorizations; |
|
|
|
finalizeUrl = result.finalize; |
|
|
|
console.log('Body:'); |
|
|
|
console.log(JSON.stringify(result, null, 2)); |
|
|
|
BACME._logBody(result); |
|
|
|
|
|
|
|
return result; |
|
|
|
}); |
|
|
@ -242,6 +342,22 @@ BACME.orders.create = function () { |
|
|
|
}; |
|
|
|
|
|
|
|
BACME.challenges = {}; |
|
|
|
BACME.challenges.all = function () { |
|
|
|
var challenges = []; |
|
|
|
|
|
|
|
function next() { |
|
|
|
if (!authorizationUrls.length) { |
|
|
|
return challenges; |
|
|
|
} |
|
|
|
|
|
|
|
return BACME.challenges.view().then(function (challenge) { |
|
|
|
challenges.push(challenge); |
|
|
|
return next(); |
|
|
|
}); |
|
|
|
} |
|
|
|
|
|
|
|
return next(); |
|
|
|
}; |
|
|
|
BACME.challenges.view = function () { |
|
|
|
var authzUrl = authorizationUrls.pop(); |
|
|
|
var token; |
|
|
@ -273,10 +389,19 @@ var httpPath; |
|
|
|
var dnsAuth; |
|
|
|
var dnsRecord; |
|
|
|
|
|
|
|
BACME.thumbprint = function () { |
|
|
|
BACME.thumbprint = function (opts) { |
|
|
|
// https://stackoverflow.com/questions/42588786/how-to-fingerprint-a-jwk
|
|
|
|
|
|
|
|
var accountPublicStr = '{' + ['crv', 'kty', 'x', 'y'].map(function (key) { |
|
|
|
var accountJwk = opts.jwk; |
|
|
|
var keys; |
|
|
|
|
|
|
|
if (/^EC/i.test(opts.jwk.kty)) { |
|
|
|
keys = [ 'e', 'kty', 'n' ]; |
|
|
|
} else if (/^RS/i.test(opts.jwk.kty)) { |
|
|
|
keys = [ 'crv', 'kty', 'x', 'y' ]; |
|
|
|
} |
|
|
|
|
|
|
|
var accountPublicStr = '{' + keys.map(function (key) { |
|
|
|
return '"' + key + '":"' + accountJwk[key] + '"'; |
|
|
|
}).join(',') + '}'; |
|
|
|
|
|
|
@ -338,11 +463,11 @@ BACME.challenges['dns-01'] = function () { |
|
|
|
var challengePollUrl; |
|
|
|
|
|
|
|
BACME.challenges.accept = function () { |
|
|
|
var payload64 = jsto64( |
|
|
|
var payload64 = BACME._jsto64( |
|
|
|
{} |
|
|
|
); |
|
|
|
|
|
|
|
var protected64 = jsto64( |
|
|
|
var protected64 = BACME._jsto64( |
|
|
|
{ nonce: nonce, alg: 'ES256', url: challengeUrl, kid: accountId } |
|
|
|
); |
|
|
|
|
|
|
@ -371,8 +496,7 @@ BACME.challenges.accept = function () { |
|
|
|
, body: JSON.stringify(body) |
|
|
|
} |
|
|
|
).then(function (resp) { |
|
|
|
console.log('Headers:'); |
|
|
|
Array.from(resp.headers.entries()).forEach(function (h) { console.log(h[0] + ': ' + h[1]); }); |
|
|
|
BACME._logHeaders(resp); |
|
|
|
nonce = resp.headers.get('replay-nonce'); |
|
|
|
|
|
|
|
return resp.json().then(function (reply) { |
|
|
@ -435,11 +559,11 @@ BACME.orders.generateCsr = function (keypair, domains) { |
|
|
|
var certificateUrl; |
|
|
|
|
|
|
|
BACME.orders.finalize = function () { |
|
|
|
var payload64 = jsto64( |
|
|
|
var payload64 = BACME._jsto64( |
|
|
|
{ csr: csr } |
|
|
|
); |
|
|
|
|
|
|
|
var protected64 = jsto64( |
|
|
|
var protected64 = BACME._jsto64( |
|
|
|
{ nonce: nonce, alg: 'ES256', url: finalizeUrl, kid: accountId } |
|
|
|
); |
|
|
|
|
|
|
|