progress on bacme
This commit is contained in:
parent
549771a0bc
commit
4fd5fd8bd9
|
@ -1 +1,2 @@
|
|||
js/pkijs.org
|
||||
js/browser-csr
|
||||
|
|
|
@ -14,6 +14,9 @@
|
|||
<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>
|
||||
|
||||
<script src="./js/bacme.js"></script>
|
||||
<script src="./js/app.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
@ -1,6 +1,14 @@
|
|||
#!/bin/bash
|
||||
|
||||
mkdir -p js/pkijs.org/v1.3.33/
|
||||
pushd js/pkijs.org/v1.3.33/
|
||||
wget -c https://raw.githubusercontent.com/PeculiarVentures/PKI.js/41b63af760cacb565dd850fb3466ada4ca163eff/org/pkijs/common.js
|
||||
wget -c https://raw.githubusercontent.com/PeculiarVentures/PKI.js/41b63af760cacb565dd850fb3466ada4ca163eff/org/pkijs/x509_schema.js
|
||||
wget -c https://raw.githubusercontent.com/PeculiarVentures/PKI.js/41b63af760cacb565dd850fb3466ada4ca163eff/org/pkijs/x509_simpl.js
|
||||
wget -c https://raw.githubusercontent.com/PeculiarVentures/ASN1.js/f7181c21c61e53a940ea24373ab489ad86d51bc1/org/pkijs/asn1.js
|
||||
popd
|
||||
|
||||
mkdir -p js/browser-csr/v1.0.0-alpha/
|
||||
pushd js/browser-csr/v1.0.0-alpha/
|
||||
wget -c https://git.coolaj86.com/coolaj86/browser-csr.js/raw/commit/c513a862a4e016794da800f0c2eec858b80837ab/csr.js
|
||||
popd
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
(function () {
|
||||
'use strict';
|
||||
|
||||
console.log("Hello, World!");
|
||||
|
||||
//window.document.querySelector('.js-acme-directory-url').value = 'https://acme-v02.api.letsencrypt.org/directory';
|
||||
window.document.querySelector('.js-acme-directory-url').value = 'https://acme-staging-v02.api.letsencrypt.org/directory';
|
||||
}());
|
||||
|
|
|
@ -0,0 +1,478 @@
|
|||
(function (exports) {
|
||||
'use strict';
|
||||
|
||||
var BACME = exports.BACME = {};
|
||||
var webFetch = exports.fetch;
|
||||
var webCrypto = exports.crypto;
|
||||
|
||||
var directoryUrl = 'https://acme-staging-v02.api.letsencrypt.org/directory';
|
||||
var directory;
|
||||
|
||||
var nonceUrl = directory.newNonce || 'https://acme-staging-v02.api.letsencrypt.org/acme/new-nonce';
|
||||
var nonce;
|
||||
|
||||
var accountKeypair;
|
||||
var accountJwk;
|
||||
|
||||
var accountUrl = directory.newAccount;
|
||||
var signedAccount;
|
||||
|
||||
BACME.challengePrefixes = {
|
||||
'http-01': '/.well-known/acme-challenge'
|
||||
, 'dns-01': '_acme-challenge'
|
||||
};
|
||||
|
||||
BACME._logHeaders = function (resp) {
|
||||
console.log('Headers:');
|
||||
Array.from(resp.headers.entries()).forEach(function (h) { console.log(h[0] + ': ' + h[1]); });
|
||||
};
|
||||
|
||||
BACME._logBody = function (body) {
|
||||
console.log('Body:');
|
||||
console.log(JSON.stringify(body, null, 2));
|
||||
console.log('');
|
||||
};
|
||||
|
||||
BACME.directory = function (url) {
|
||||
return webFetch(directoryUrl, { mode: 'cors' }).then(function (resp) {
|
||||
BACME._logHeaders(resp);
|
||||
return resp.json().then(function (body) {
|
||||
directory = body;
|
||||
BACME._logBody(body);
|
||||
return body;
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
BACME.nonce = function () {
|
||||
return webFetch(nonceUrl, { mode: 'cors' }).then(function (resp) {
|
||||
BACME._logHeaders(resp);
|
||||
nonce = resp.headers.get('replay-nonce');
|
||||
console.log('Nonce:', nonce);
|
||||
// resp.body is empty
|
||||
return resp.headers.get('replay-nonce');
|
||||
});
|
||||
};
|
||||
|
||||
BACME.accounts = {};
|
||||
BACME.accounts.generateKeypair = function () {
|
||||
// https://github.com/diafygi/webcrypto-examples#ecdsa---generatekey
|
||||
var extractable = true;
|
||||
return webCrypto.subtle.generateKey(
|
||||
{ name: "ECDSA", namedCurve: "P-256" }
|
||||
, extractable
|
||||
, [ 'sign', 'verify' ]
|
||||
).then(function (result) {
|
||||
accountKeypair = result;
|
||||
|
||||
return webCrypto.subtle.exportKey(
|
||||
"jwk"
|
||||
, result.privateKey
|
||||
).then(function (jwk) {
|
||||
|
||||
accountJwk = jwk;
|
||||
console.log('private jwk:');
|
||||
console.log(JSON.stringify(jwk, null, 2));
|
||||
|
||||
return webCrypto.subtle.exportKey(
|
||||
"pkcs8"
|
||||
, result.privateKey
|
||||
).then(function (keydata) {
|
||||
console.log('pkcs8:');
|
||||
console.log(Array.from(new Uint8Array(keydata)));
|
||||
|
||||
return accountKeypair;
|
||||
});
|
||||
})
|
||||
});
|
||||
};
|
||||
|
||||
// json to url-safe base64
|
||||
BACME._jsto64 = function (json) {
|
||||
return btoa(JSON.stringify(json)).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/g, '');
|
||||
};
|
||||
|
||||
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, '');
|
||||
|
||||
console.log('URL-safe Base64 Signature:');
|
||||
console.log(sig64);
|
||||
|
||||
signedAccount = {
|
||||
protected: protected64
|
||||
, payload: payload64
|
||||
, signature: sig64
|
||||
};
|
||||
console.log('Signed Base64 Account:');
|
||||
console.log(JSON.stringify(signedAccount, null, 2));
|
||||
});
|
||||
};
|
||||
|
||||
var account;
|
||||
var accountId;
|
||||
|
||||
BACME.accounts.set = function () {
|
||||
nonce = null;
|
||||
return window.fetch(accountUrl, {
|
||||
mode: 'cors'
|
||||
, method: 'POST'
|
||||
, headers: { 'Content-Type': 'application/jose+json' }
|
||||
, body: JSON.stringify(signedAccount)
|
||||
}).then(function (resp) {
|
||||
BACME._logHeaders(resp);
|
||||
nonce = resp.headers.get('replay-nonce');
|
||||
accountId = resp.headers.get('location');
|
||||
console.log('Next nonce:', nonce);
|
||||
console.log('Location/kid:', accountId);
|
||||
|
||||
if (!resp.headers.get('content-type')) {
|
||||
console.log('Body: <none>');
|
||||
return;
|
||||
}
|
||||
|
||||
return resp.json().then(function (result) {
|
||||
BACME._logBody(result);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
var orderUrl = directory.newOrder || "https://acme-staging-v02.api.letsencrypt.org/acme/new-order";
|
||||
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 });
|
||||
|
||||
var protected64 = jsto64(
|
||||
{ nonce: nonce, alg: 'ES256', url: orderUrl, kid: accountId }
|
||||
);
|
||||
|
||||
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;
|
||||
});
|
||||
};
|
||||
|
||||
var order;
|
||||
var currentOrderUrl;
|
||||
var authorizationUrls;
|
||||
var finalizeUrl;
|
||||
|
||||
BACME.orders.create = function () {
|
||||
nonce = null;
|
||||
return window.fetch(orderUrl, {
|
||||
mode: 'cors'
|
||||
, method: 'POST'
|
||||
, headers: { 'Content-Type': 'application/jose+json' }
|
||||
, body: JSON.stringify(signedOrder)
|
||||
}).then(function (resp) {
|
||||
console.log('Headers:');
|
||||
Array.from(resp.headers.entries()).forEach(function (h) { console.log(h[0] + ': ' + h[1]); });
|
||||
currentOrderUrl = resp.headers.get('location');
|
||||
nonce = resp.headers.get('replay-nonce');
|
||||
console.log('Next nonce:', nonce);
|
||||
|
||||
return resp.json().then(function (result) {
|
||||
authorizationUrls = result.authorizations;
|
||||
finalizeUrl = result.finalize;
|
||||
console.log('Body:');
|
||||
console.log(JSON.stringify(result, null, 2));
|
||||
|
||||
return result;
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
BACME.challenges = {};
|
||||
BACME.challenges.view = function () {
|
||||
var authzUrl = authorizationUrls.pop();
|
||||
var token;
|
||||
var challengeDomain;
|
||||
var challengeUrl;
|
||||
|
||||
return window.fetch(authzUrl, {
|
||||
mode: 'cors'
|
||||
}).then(function (resp) {
|
||||
BACME._logHeaders(resp);
|
||||
|
||||
return resp.json().then(function (result) {
|
||||
// Note: select the challenge you wish to use
|
||||
var challenge = result.challenges.slice(0).pop();
|
||||
token = challenge.token;
|
||||
challengeUrl = challenge.url;
|
||||
challengeDomain = result.identifier.value;
|
||||
|
||||
BACME._logBody(result);
|
||||
|
||||
return { token: challenge.token, url: challenge.url, domain: result.identifier.value, challenges: result.challenges };
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
var thumbprint;
|
||||
var keyAuth;
|
||||
var httpPath;
|
||||
var dnsAuth;
|
||||
var dnsRecord;
|
||||
|
||||
BACME.thumbprint = function () {
|
||||
// https://stackoverflow.com/questions/42588786/how-to-fingerprint-a-jwk
|
||||
|
||||
var accountPublicStr = '{' + ['crv', 'kty', 'x', 'y'].map(function (key) {
|
||||
return '"' + key + '":"' + accountJwk[key] + '"';
|
||||
}).join(',') + '}';
|
||||
|
||||
return window.crypto.subtle.digest(
|
||||
{ name: "SHA-256" } // SHA-256 is spec'd, non-optional
|
||||
, textEncoder.encode(accountPublicStr)
|
||||
).then(function(hash){
|
||||
thumbprint = btoa(Array.prototype.map.call(new Uint8Array(hash), function (ch) {
|
||||
return String.fromCharCode(ch);
|
||||
}).join('')).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/g, '');
|
||||
|
||||
console.log('Thumbprint:');
|
||||
console.log(thumbprint);
|
||||
|
||||
return thumbprint;
|
||||
});
|
||||
};
|
||||
|
||||
BACME.challenges['http-01'] = function () {
|
||||
// The contents of the key authorization file
|
||||
keyAuth = token + '.' + thumbprint;
|
||||
|
||||
// Where the key authorization file goes
|
||||
httpPath = 'http://' + challengeDomain + '/.well-known/acme-challenge/' + token;
|
||||
|
||||
console.log("echo '" + keyAuth + "' > '" + httpPath + "'");
|
||||
|
||||
return {
|
||||
path: httpPath
|
||||
, value: keyAuth
|
||||
};
|
||||
});
|
||||
BACME.challenges['dns-01'] = function () {
|
||||
return window.crypto.subtle.digest(
|
||||
{ name: "SHA-256", }
|
||||
, textEncoder.encode(keyAuth)
|
||||
).then(function(hash){
|
||||
dnsAuth = btoa(Array.prototype.map.call(new Uint8Array(hash), function (ch) {
|
||||
return String.fromCharCode(ch);
|
||||
}).join('')).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/g, '');
|
||||
|
||||
dnsRecord = '_acme-challenge.' + challengeDomain;
|
||||
|
||||
console.log('DNS TXT Auth:');
|
||||
// The name of the record
|
||||
console.log(dnsRecord);
|
||||
// The TXT record value
|
||||
console.log(dnsAuth);
|
||||
|
||||
return {
|
||||
type: 'TXT'
|
||||
, host: dnsRecord
|
||||
, answer: dnsAuth;
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
var challengePollUrl;
|
||||
|
||||
BACME.challenges.accept = function () {
|
||||
var payload64 = jsto64(
|
||||
{}
|
||||
);
|
||||
|
||||
var protected64 = jsto64(
|
||||
{ nonce: nonce, alg: 'ES256', url: challengeUrl, kid: accountId }
|
||||
);
|
||||
|
||||
nonce = null;
|
||||
return window.crypto.subtle.sign(
|
||||
{ name: "ECDSA", hash: { name: "SHA-256" } }
|
||||
, accountKeypair.privateKey
|
||||
, textEncoder.encode(protected64 + '.' + payload64)
|
||||
).then(function (signature) {
|
||||
|
||||
var sig64 = btoa(Array.prototype.map.call(new Uint8Array(signature), function (ch) {
|
||||
return String.fromCharCode(ch);
|
||||
}).join('')).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/g, '');
|
||||
|
||||
var body = {
|
||||
protected: protected64
|
||||
, payload: payload64
|
||||
, signature: sig64
|
||||
};
|
||||
|
||||
return window.fetch(
|
||||
challengeUrl
|
||||
, { mode: 'cors'
|
||||
, method: 'POST'
|
||||
, headers: { 'Content-Type': 'application/jose+json' }
|
||||
, body: JSON.stringify(body)
|
||||
}
|
||||
).then(function (resp) {
|
||||
console.log('Headers:');
|
||||
Array.from(resp.headers.entries()).forEach(function (h) { console.log(h[0] + ': ' + h[1]); });
|
||||
nonce = resp.headers.get('replay-nonce');
|
||||
|
||||
return resp.json().then(function (reply) {
|
||||
challengePollUrl = reply.url;
|
||||
|
||||
console.log('Challenge ACK:');
|
||||
console.log(JSON.stringify(reply));
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
BACME.challenges.check = function () {
|
||||
return window.fetch(challengePollUrl, { mode: 'cors' }).then(function (resp) {
|
||||
BACME._logHeaders(resp);
|
||||
nonce = resp.headers.get('replay-nonce');
|
||||
|
||||
return resp.json().then(function (reply) {
|
||||
challengePollUrl = reply.url;
|
||||
|
||||
BACME._logBody(reply);
|
||||
|
||||
return reply;
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
var domainKeypair;
|
||||
var domainJwk;
|
||||
|
||||
BACME.domains = {};
|
||||
// TODO factor out from BACME.accounts.generateKeypair
|
||||
BACME.domains.generateKeypair = function () {
|
||||
var extractable = true;
|
||||
return window.crypto.subtle.generateKey(
|
||||
{ name: "ECDSA", namedCurve: "P-256" }
|
||||
, extractable
|
||||
, [ 'sign', 'verify' ]
|
||||
).then(function (result) {
|
||||
domainKeypair = result;
|
||||
|
||||
return window.crypto.subtle.exportKey(
|
||||
"jwk"
|
||||
, result.privateKey
|
||||
).then(function (jwk) {
|
||||
|
||||
domainJwk = jwk;
|
||||
console.log('private jwk:');
|
||||
console.log(JSON.stringify(jwk, null, 2));
|
||||
|
||||
return domainKeypair;
|
||||
})
|
||||
});
|
||||
};
|
||||
|
||||
BACME.order.generateCsr = function (keypair, domains) {
|
||||
return Promise.resolve(CSR.generate(keypair, domains));
|
||||
};
|
||||
|
||||
var certificateUrl;
|
||||
|
||||
BACME.order.finalize = function () {
|
||||
var payload64 = jsto64(
|
||||
{ csr: csr }
|
||||
);
|
||||
|
||||
var protected64 = jsto64(
|
||||
{ nonce: nonce, alg: 'ES256', url: finalizeUrl, kid: accountId }
|
||||
);
|
||||
|
||||
nonce = null;
|
||||
return window.crypto.subtle.sign(
|
||||
{ name: "ECDSA", hash: { name: "SHA-256" } }
|
||||
, accountKeypair.privateKey
|
||||
, textEncoder.encode(protected64 + '.' + payload64)
|
||||
).then(function (signature) {
|
||||
|
||||
var sig64 = btoa(Array.prototype.map.call(new Uint8Array(signature), function (ch) {
|
||||
return String.fromCharCode(ch);
|
||||
}).join('')).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/g, '');
|
||||
|
||||
var body = {
|
||||
protected: protected64
|
||||
, payload: payload64
|
||||
, signature: sig64
|
||||
};
|
||||
|
||||
return window.fetch(
|
||||
finalizeUrl
|
||||
, { mode: 'cors'
|
||||
, method: 'POST'
|
||||
, headers: { 'Content-Type': 'application/jose+json' }
|
||||
, body: JSON.stringify(body)
|
||||
}
|
||||
).then(function (resp) {
|
||||
BACME._logHeaders(resp);
|
||||
nonce = resp.headers.get('replay-nonce');
|
||||
|
||||
return resp.json().then(function (reply) {
|
||||
certificateUrl = reply.certificate;
|
||||
BACME._logBody(reply);
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
}(window));
|
Loading…
Reference in New Issue