AJ ONeal
il y a 6 ans
5 fichiers modifiés avec 491 ajouts et 2 suppressions
@ -1 +1,2 @@ |
|||
js/pkijs.org |
|||
js/browser-csr |
|||
|
@ -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)); |
Chargement…
Référencer dans un nouveau ticket