ソースを参照

progress on bacme

v1
AJ ONeal 6年前
コミット
4fd5fd8bd9
  1. 1
      .gitignore
  2. 3
      index.html
  3. 8
      install.sh
  4. 3
      js/app.js
  5. 478
      js/bacme.js

1
.gitignore

@ -1 +1,2 @@
js/pkijs.org
js/browser-csr

3
index.html

@ -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>

8
install.sh

@ -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

3
js/app.js

@ -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';
}());

478
js/bacme.js

@ -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));
読み込み中…
キャンセル
保存