AJ ONeal
6 years ago
5 changed files with 491 additions and 2 deletions
@ -1 +1,2 @@ |
|||||
js/pkijs.org |
js/pkijs.org |
||||
|
js/browser-csr |
||||
|
@ -1,6 +1,14 @@ |
|||||
|
#!/bin/bash |
||||
|
|
||||
mkdir -p js/pkijs.org/v1.3.33/ |
mkdir -p js/pkijs.org/v1.3.33/ |
||||
pushd 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/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_schema.js |
||||
wget -c https://raw.githubusercontent.com/PeculiarVentures/PKI.js/41b63af760cacb565dd850fb3466ada4ca163eff/org/pkijs/x509_simpl.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 |
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 () { |
(function () { |
||||
'use strict'; |
'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'; |
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