Browse Source

progress on bacme

master
AJ ONeal 2 years ago
parent
commit
4fd5fd8bd9
5 changed files with 491 additions and 2 deletions
  1. +1
    -0
      .gitignore
  2. +3
    -0
      index.html
  3. +8
    -0
      install.sh
  4. +1
    -2
      js/app.js
  5. +478
    -0
      js/bacme.js

+ 1
- 0
.gitignore View File

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

+ 3
- 0
index.html View File

@@ -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
- 0
install.sh View File

@@ -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
- 2
js/app.js View File

@@ -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
- 0
js/bacme.js View File

@@ -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…
Cancel
Save