create acocunt and order, and view challenges

This commit is contained in:
AJ ONeal 2018-05-02 08:02:22 +00:00
parent 5d4f71ba8e
commit ec9a2606f6
2 changed files with 313 additions and 103 deletions

100
js/app.js
View File

@ -5,6 +5,8 @@
var $qsa = function (s) { return window.document.querySelectorAll(s); }; var $qsa = function (s) { return window.document.querySelectorAll(s); };
var info = {}; var info = {};
var steps = {}; var steps = {};
var nonce;
var kid;
var i = 1; var i = 1;
//$qs('.js-acme-directory-url').value = 'https://acme-v02.api.letsencrypt.org/directory'; //$qs('.js-acme-directory-url').value = 'https://acme-v02.api.letsencrypt.org/directory';
@ -16,11 +18,13 @@
}); });
} }
$qs('.js-acme-form-domains').addEventListener('submit', function (ev) { $qsa('.js-acme-form').forEach(function ($el) {
$el.addEventListener('submit', function (ev) {
ev.preventDefault(); ev.preventDefault();
steps[i].submit(ev); steps[i].submit(ev);
i += 1; i += 1;
}); });
});
steps[1] = function () { steps[1] = function () {
hideForms(); hideForms();
@ -33,8 +37,12 @@
return BACME.directory($qs('.js-acme-directory-url').value).then(function (directory) { return BACME.directory($qs('.js-acme-directory-url').value).then(function (directory) {
$qs('.js-acme-tos-url').href = directory.meta.termsOfService; $qs('.js-acme-tos-url').href = directory.meta.termsOfService;
return BACME.nonce().then(function (_nonce) {
nonce = _nonce;
steps[i](); steps[i]();
}); });
});
}; };
steps[2] = function () { steps[2] = function () {
@ -42,16 +50,94 @@
$qs('.js-acme-form-account').hidden = false; $qs('.js-acme-form-account').hidden = false;
}; };
steps[2].submit = function () { steps[2].submit = function () {
info.contact = [ 'mailto:' + $qs('.js-acme-account-email').value ]; var email = $qs('.js-acme-account-email').value.toLowerCase().trim();
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;
// TODO // TODO
// create account key // options for
// create account // * regenerate key
// capture email // * ECDSA / RSA / bitlength
// submit challenges
// populate challenges in table // TODO ping with version and account creation
var jwk = JSON.parse(localStorage.getItem('account:' + email) || 'null');
var p;
function createKeypair() {
return BACME.accounts.generateKeypair({
type: 'ECDSA'
, bitlength: '256'
}).then(function (jwk) {
localStorage.setItem('account:' + email, JSON.stringify(jwk));
return jwk;
})
}
if (jwk) {
p = Promise.resolve(jwk);
} else {
p = createKeypair();
}
function createAccount(jwk) {
console.log('account jwk:');
console.log(jwk);
delete jwk.key_ops;
return BACME.accounts.sign({
jwk: jwk
, contacts: [ 'mailto:' + email ]
, agree: info.agree
, nonce: nonce
, kid: kid
}).then(function (signedAccount) {
return BACME.accounts.set({
signedAccount: signedAccount
}).then(function (account) {
console.log('account:');
console.log(account);
kid = account.kid;
return kid;
});
});
}
return p.then(function (_jwk) {
jwk = _jwk;
kid = JSON.parse(localStorage.getItem('account-kid:' + email) || 'null');
var p2
// TODO save account id rather than always retrieving it
if (kid) {
p2 = Promise.resolve(kid);
} else {
p2 = createAccount(jwk);
}
return p2.then(function (_kid) {
kid = _kid;
return BACME.orders.sign({
jwk: jwk
, identifiers: info.identifiers
, kid: kid
}).then(function (signedOrder) {
return BACME.orders.create({
signedOrder: signedOrder
}).then(function (/*challengeIndexes*/) {
return BACME.challenges.all().then(function (challenges) {
console.log('challenges:');
console.log(challenges);
// TODO populate challenges in table
steps[i](); steps[i]();
});
});
});
});
}).catch(function (err) {
console.error('Step \'' + i + '\' Error:');
console.error(err);
});
}; };
steps[3] = function () { steps[3] = function () {

View File

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