WIP halfway there

This commit is contained in:
AJ ONeal 2019-10-04 17:35:59 -06:00
parent 6c11446e2f
commit e75c503356
34 changed files with 3849 additions and 3196 deletions

1
.gitignore vendored
View File

@ -1,3 +1,4 @@
.env
dist/ dist/
*.gz *.gz
.*.sw* .*.sw*

17
fixtures/account.jwk.json Normal file
View File

@ -0,0 +1,17 @@
{
"private": {
"kty": "EC",
"crv": "P-256",
"d": "HB1OvdHfLnIy2mYYO9cLU4BqP36CeyS8OsDf3OnYP-M",
"x": "uLh0RLpAmKyyHCf2zOaF18IIuBiJEiZ8Mu3xPZ7ZxN8",
"y": "vVl_cCXK0_GlCaCT5Yg750LUd8eRU6tySEdQFLM62NQ",
"kid": "UuuZa_56jCM2douUq1riGyRphPtRvCPkxtkg0bP-pNs"
},
"public": {
"kty": "EC",
"crv": "P-256",
"x": "uLh0RLpAmKyyHCf2zOaF18IIuBiJEiZ8Mu3xPZ7ZxN8",
"y": "vVl_cCXK0_GlCaCT5Yg750LUd8eRU6tySEdQFLM62NQ",
"kid": "UuuZa_56jCM2douUq1riGyRphPtRvCPkxtkg0bP-pNs"
}
}

View File

@ -0,0 +1,13 @@
{
"key": {
"kty": "EC",
"crv": "P-256",
"x": "uLh0RLpAmKyyHCf2zOaF18IIuBiJEiZ8Mu3xPZ7ZxN8",
"y": "vVl_cCXK0_GlCaCT5Yg750LUd8eRU6tySEdQFLM62NQ",
"kid": "https://acme-staging-v02.api.letsencrypt.org/acme/acct/11265299"
},
"contact": [],
"initialIp": "66.219.236.169",
"createdAt": "2019-10-04T22:54:28.569489074Z",
"status": "valid"
}

20
fixtures/server.jwk.json Normal file
View File

@ -0,0 +1,20 @@
{
"private": {
"kty": "RSA",
"n": "ud6agEF9P6H66ciYgvZ_FakyZKossq5i6J2D4wIcJBnem5X63t7u3E7Rpc7rgVB5MElUNZmBoVO3VbaVJpiG0tS5zxkOZcj_k6C_5LXBdTHinG0bFZHtV6Wapf5fJ4PXNp71AHWv09qz4swJzz6_Rp_7ovNpivVsdVHfd8g9HqH3sjouwfIGfo-1LLm0F4NM12AJZISFt_03knhbvtd5x4ASorBiENPPnv2s7SA5kFT1Seeu-iUCq8PlKi-HMbNrLeM2E3wYySQPSSDt6UXRTvIzW_8upXRvaVThJk3wWjx-qt1CUIFoZBh2RsmiujWFFc6ORXb3GlF3U4LaMt3YEw",
"e": "AQAB",
"d": "YCzN9yVr4Jw5D_UK7WEMuzGUcMAZZs-TQFgY4UK7Ovbj18_QQrhKElb6Zfhepcf1HUYkO6PVjpuZ1tEl9hWgVcFa781AROyvSj04beiaVMDeSCCwjgW3MM3w6olnxTOUDaBMl9NNiqq0v9riDImkQbAQbe3To-KAH2ig4AMNlSZJAhmI2zAMiJhQE_pAcCxc-bQ5oNO-WSU0GRHWdMJSXp9mFgoBhVPDYGW-dmnoFzuNWssxlSqGXY-8a2YOuiunK6XM5_80c1eQqmy-k1InUIViR_wljskc8UiH6xa8BCznZYacgSz4PnvKsiKWKQQ1eliIucV3MC6BzMD3N8EWqQ",
"p": "8NUtOIglu0dvDGmEB7QC5eC02Y2jZKnoxHSPKMAEPxQ0131_2aL49IzADWoTvae3NBPzU7ol3RwJo_GvS967OysfOr6Od699p1FSLwLfK89aql7_uVPJh4Q43H-W_NtRHKUkv0OmkDiwa4WqBQTVfREdPQ3NJT7vIY-cqH_AMRc",
"q": "xZNIl9NRl3b0_V8Y-7_6_foIu9Sx5ILv2XV7WONDx2jp4vuT7byLm1UWdYPBbxLyd5TAvWqtyvaRtVNyplrD0PyyPK3NxqVJde0uzScAU-bf25DeK30V22Xo7IEZiPZoizrjtzGnS6VVNJmZ-Ictz3xmWIudw5d5XDH12fFRlmU",
"dp": "F1Ld9UqiNNf_NjmF0uUpHrA7c5JXD6mw5E3Ri4XFI4LGd1QtLJuu9qgm9WWfkc-LW5zPBP3TKu3LNThz3KougdV0SdEopQi255xllC34BRso0bUvmPg3XUt94kTtD4ICAf8wZuGbYP5Mf61LQP8t2dXtefs7Me89Y4ewCVWN_HM",
"dq": "oPuT35lgVtCnZ7dPrPjNMpnC-gCg_fcuJPqTiWaLuHQkdjzUWJYTDnqy9Qdo2e8PPx4mOXAtsT1clekrdp5oBOWQ-N4I172fcIXUZ3ZKzxJD_iw4yih-YajUs7exLabQoflWx9KeZIWPOm-ZRCYoznGnFqiT4GWQje1rS6xT9P0",
"qi": "aXkK-w4Npw0BpUEzQ1PURVGm5y5cKIdd-CfEYwub19rronI9EEvuQHoqR7ODtZ_mlIIffHmHaM3ug50fJDB9QDOG4Ioc5S4YxVURT58Ps8at-dQAAP1UgSlV3vhXh4WZRaDECUI_728U3fxQqH78bJsy81mU8MtGU8LR_eTMXx8",
"kid": "1hxSLs31DwbGo532keMUL9eY8L6gWyYlbcr0TtiV7qk"
},
"public": {
"kty": "RSA",
"n": "ud6agEF9P6H66ciYgvZ_FakyZKossq5i6J2D4wIcJBnem5X63t7u3E7Rpc7rgVB5MElUNZmBoVO3VbaVJpiG0tS5zxkOZcj_k6C_5LXBdTHinG0bFZHtV6Wapf5fJ4PXNp71AHWv09qz4swJzz6_Rp_7ovNpivVsdVHfd8g9HqH3sjouwfIGfo-1LLm0F4NM12AJZISFt_03knhbvtd5x4ASorBiENPPnv2s7SA5kFT1Seeu-iUCq8PlKi-HMbNrLeM2E3wYySQPSSDt6UXRTvIzW_8upXRvaVThJk3wWjx-qt1CUIFoZBh2RsmiujWFFc6ORXb3GlF3U4LaMt3YEw",
"e": "AQAB",
"kid": "1hxSLs31DwbGo532keMUL9eY8L6gWyYlbcr0TtiV7qk"
}
}

View File

@ -2,40 +2,40 @@
/* This Source Code Form is subject to the terms of the Mozilla Public /* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this * License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
(function(exports) { 'use strict';
'use strict'; /* globals Promise */
/* globals Promise */
var ACME = (exports.ACME = {}); var ACME = module.exports;
//var Keypairs = exports.Keypairs || {}; //var Keypairs = exports.Keypairs || {};
//var CSR = exports.CSR; //var CSR = exports.CSR;
var Enc = exports.Enc || {}; var Enc = require('omnibuffer');
var Crypto = exports.Crypto || {}; var sha2 = require('./node/sha2.js');
var http = require('./node/http.js');
ACME.formatPemChain = function formatPemChain(str) { ACME.formatPemChain = function formatPemChain(str) {
return ( return (
str str
.trim() .trim()
.replace(/[\r\n]+/g, '\n') .replace(/[\r\n]+/g, '\n')
.replace(/\-\n\-/g, '-\n\n-') + '\n' .replace(/\-\n\-/g, '-\n\n-') + '\n'
); );
}; };
ACME.splitPemChain = function splitPemChain(str) { ACME.splitPemChain = function splitPemChain(str) {
return str return str
.trim() .trim()
.split(/[\r\n]{2,}/g) .split(/[\r\n]{2,}/g)
.map(function(str) { .map(function(str) {
return str + '\n'; return str + '\n';
}); });
}; };
// http-01: GET https://example.org/.well-known/acme-challenge/{{token}} => {{keyAuth}} // http-01: GET https://example.org/.well-known/acme-challenge/{{token}} => {{keyAuth}}
// dns-01: TXT _acme-challenge.example.org. => "{{urlSafeBase64(sha256(keyAuth))}}" // dns-01: TXT _acme-challenge.example.org. => "{{urlSafeBase64(sha256(keyAuth))}}"
ACME.challengePrefixes = { ACME.challengePrefixes = {
'http-01': '/.well-known/acme-challenge', 'http-01': '/.well-known/acme-challenge',
'dns-01': '_acme-challenge' 'dns-01': '_acme-challenge'
}; };
ACME.challengeTests = { ACME.challengeTests = {
'http-01': function(me, auth) { 'http-01': function(me, auth) {
return me.http01(auth).then(function(keyAuth) { return me.http01(auth).then(function(keyAuth) {
var err; var err;
@ -88,13 +88,13 @@
return Promise.reject(err); return Promise.reject(err);
}); });
} }
}; };
ACME._directory = function(me) { ACME._directory = function(me) {
// GET-as-GET ok // GET-as-GET ok
return me.request({ method: 'GET', url: me.directoryUrl, json: true }); return me.request({ method: 'GET', url: me.directoryUrl, json: true });
}; };
ACME._getNonce = function(me) { ACME._getNonce = function(me) {
// GET-as-GET, HEAD-as-HEAD ok // GET-as-GET, HEAD-as-HEAD ok
var nonce; var nonce;
while (true) { while (true) {
@ -116,12 +116,12 @@
.then(function(resp) { .then(function(resp) {
return resp.headers['replay-nonce']; return resp.headers['replay-nonce'];
}); });
}; };
ACME._setNonce = function(me, nonce) { ACME._setNonce = function(me, nonce) {
me._nonces.unshift({ nonce: nonce, createdAt: Date.now() }); me._nonces.unshift({ nonce: nonce, createdAt: Date.now() });
}; };
// ACME RFC Section 7.3 Account Creation // ACME RFC Section 7.3 Account Creation
/* /*
{ {
"protected": base64url({ "protected": base64url({
"alg": "ES256", "alg": "ES256",
@ -140,7 +140,7 @@
"signature": "RZPOnYoPs1PhjszF...-nh6X1qtOFPB519I" "signature": "RZPOnYoPs1PhjszF...-nh6X1qtOFPB519I"
} }
*/ */
ACME._registerAccount = function(me, options) { ACME._registerAccount = function(me, options) {
if (me.debug) { if (me.debug) {
console.debug('[acme-v2] accounts.create'); console.debug('[acme-v2] accounts.create');
} }
@ -180,9 +180,7 @@
kid: options.externalAccount.id, kid: options.externalAccount.id,
url: me._directoryUrls.newAccount url: me._directoryUrls.newAccount
}, },
payload: Enc.binToBuf( payload: Enc.strToBuf(JSON.stringify(pair.public))
JSON.stringify(pair.public)
)
}).then(function(jws) { }).then(function(jws) {
body.externalAccountBinding = jws; body.externalAccountBinding = jws;
return body; return body;
@ -196,14 +194,12 @@
options: options, options: options,
url: me._directoryUrls.newAccount, url: me._directoryUrls.newAccount,
protected: { kid: false, jwk: pair.public }, protected: { kid: false, jwk: pair.public },
payload: Enc.binToBuf(payload) payload: Enc.strToBuf(payload)
}) })
.then(function(resp) { .then(function(resp) {
var account = resp.body; var account = resp.body;
if ( if (2 !== Math.floor(resp.statusCode / 100)) {
2 !== Math.floor(resp.statusCode / 100)
) {
throw new Error( throw new Error(
'account error: ' + 'account error: ' +
JSON.stringify(resp.body) JSON.stringify(resp.body)
@ -275,8 +271,8 @@
); );
} }
}); });
}; };
/* /*
POST /acme/new-order HTTP/1.1 POST /acme/new-order HTTP/1.1
Host: example.com Host: example.com
Content-Type: application/jose+json Content-Type: application/jose+json
@ -296,7 +292,7 @@
"signature": "H6ZXtGjTZyUnPeKn...wEA4TklBdh3e454g" "signature": "H6ZXtGjTZyUnPeKn...wEA4TklBdh3e454g"
} }
*/ */
ACME._getChallenges = function(me, options, authUrl) { ACME._getChallenges = function(me, options, authUrl) {
if (me.debug) { if (me.debug) {
console.debug('\n[DEBUG] getChallenges\n'); console.debug('\n[DEBUG] getChallenges\n');
} }
@ -310,14 +306,14 @@
}).then(function(resp) { }).then(function(resp) {
return resp.body; return resp.body;
}); });
}; };
ACME._wait = function wait(ms) { ACME._wait = function wait(ms) {
return new Promise(function(resolve) { return new Promise(function(resolve) {
setTimeout(resolve, ms || 1100); setTimeout(resolve, ms || 1100);
}); });
}; };
ACME._testChallengeOptions = function() { ACME._testChallengeOptions = function() {
var chToken = ACME._prnd(16); var chToken = ACME._prnd(16);
return [ return [
{ {
@ -346,8 +342,8 @@
token: 'test-' + chToken + '-3' token: 'test-' + chToken + '-3'
} }
]; ];
}; };
ACME._testChallenges = function(me, options) { ACME._testChallenges = function(me, options) {
var CHECK_DELAY = 0; var CHECK_DELAY = 0;
return Promise.all( return Promise.all(
options.domains.map(function(identifierValue) { options.domains.map(function(identifierValue) {
@ -447,8 +443,8 @@
); );
}); });
}); });
}; };
ACME._chooseChallenge = function(options, results) { ACME._chooseChallenge = function(options, results) {
// For each of the challenge types that we support // For each of the challenge types that we support
var challenge; var challenge;
options.challengeTypes.some(function(chType) { options.challengeTypes.some(function(chType) {
@ -463,8 +459,8 @@
}); });
return challenge; return challenge;
}; };
ACME._challengeToAuth = function(me, options, request, challenge, dryrun) { ACME._challengeToAuth = function(me, options, request, challenge, dryrun) {
// we don't poison the dns cache with our dummy request // we don't poison the dns cache with our dummy request
var dnsPrefix = ACME.challengePrefixes['dns-01']; var dnsPrefix = ACME.challengePrefixes['dns-01'];
if (dryrun) { if (dryrun) {
@ -494,9 +490,7 @@
auth.hostname = auth.identifier.value; auth.hostname = auth.identifier.value;
// because I'm not 100% clear if the wildcard identifier does or doesn't have the leading *. in all cases // because I'm not 100% clear if the wildcard identifier does or doesn't have the leading *. in all cases
auth.altname = ACME._untame(auth.identifier.value, auth.wildcard); auth.altname = ACME._untame(auth.identifier.value, auth.wildcard);
return ACME._importKeypair(me, options.accountKeypair).then(function( return ACME._importKeypair(me, options.accountKeypair).then(function(pair) {
pair
) {
return me.Keypairs.thumbprint({ jwk: pair.public }).then(function( return me.Keypairs.thumbprint({ jwk: pair.public }).then(function(
thumb thumb
) { ) {
@ -511,28 +505,30 @@
ACME.challengePrefixes['http-01'] + ACME.challengePrefixes['http-01'] +
'/' + '/' +
auth.token; auth.token;
auth.dnsHost = auth.dnsHost = dnsPrefix + '.' + auth.hostname.replace('*.', '');
dnsPrefix + '.' + auth.hostname.replace('*.', '');
return Crypto._sha('sha256', auth.keyAuthorization).then( return sha2
function(hash) { .sum(256, auth.keyAuthorization)
auth.dnsAuthorization = hash; .then(function(hash) {
return Enc.bufToUrlBase64(new Uint8Array(hash));
})
.then(function(hash64) {
auth.dnsAuthorization = hash64;
return auth; return auth;
}
);
}); });
}); });
}; });
};
ACME._untame = function(name, wild) { ACME._untame = function(name, wild) {
if (wild) { if (wild) {
name = '*.' + name.replace('*.', ''); name = '*.' + name.replace('*.', '');
} }
return name; return name;
}; };
// https://tools.ietf.org/html/draft-ietf-acme-acme-10#section-7.5.1 // https://tools.ietf.org/html/draft-ietf-acme-acme-10#section-7.5.1
ACME._postChallenge = function(me, options, auth) { ACME._postChallenge = function(me, options, auth) {
var RETRY_INTERVAL = me.retryInterval || 1000; var RETRY_INTERVAL = me.retryInterval || 1000;
var DEAUTH_INTERVAL = me.deauthWait || 10 * 1000; var DEAUTH_INTERVAL = me.deauthWait || 10 * 1000;
var MAX_POLL = me.retryPoll || 8; var MAX_POLL = me.retryPoll || 8;
@ -567,7 +563,7 @@
options: options, options: options,
url: auth.url, url: auth.url,
protected: { kid: options._kid }, protected: { kid: options._kid },
payload: Enc.binToBuf(JSON.stringify({ status: 'deactivated' })) payload: Enc.strToBuf(JSON.stringify({ status: 'deactivated' }))
}).then(function(resp) { }).then(function(resp) {
if (me.debug) { if (me.debug) {
console.debug('deactivate challenge: resp.body:'); console.debug('deactivate challenge: resp.body:');
@ -616,9 +612,7 @@
if (me.debug) { if (me.debug) {
console.debug('poll: again'); console.debug('poll: again');
} }
return ACME._wait(RETRY_INTERVAL).then( return ACME._wait(RETRY_INTERVAL).then(respondToChallenge);
respondToChallenge
);
} }
if ('valid' === resp.body.status) { if ('valid' === resp.body.status) {
@ -666,7 +660,7 @@
options: options, options: options,
url: auth.url, url: auth.url,
protected: { kid: options._kid }, protected: { kid: options._kid },
payload: Enc.binToBuf(JSON.stringify({})) payload: Enc.strToBuf(JSON.stringify({}))
}).then(function(resp) { }).then(function(resp) {
if (me.debug) { if (me.debug) {
console.debug('respond to challenge: resp.body:'); console.debug('respond to challenge: resp.body:');
@ -679,8 +673,8 @@
} }
return respondToChallenge(); return respondToChallenge();
}; };
ACME._setChallenge = function(me, options, auth) { ACME._setChallenge = function(me, options, auth) {
return new Promise(function(resolve, reject) { return new Promise(function(resolve, reject) {
var challengers = options.challenges || {}; var challengers = options.challenges || {};
var challenger = var challenger =
@ -739,13 +733,14 @@
} }
return ACME._wait(DELAY); return ACME._wait(DELAY);
}); });
}; };
ACME._finalizeOrder = function(me, options, validatedDomains) { ACME._finalizeOrder = function(me, options, validatedDomains) {
if (me.debug) { if (me.debug) {
console.debug('finalizeOrder:'); console.debug('finalizeOrder:');
} }
return ACME._generateCsrWeb64(me, options, validatedDomains).then( return ACME._generateCsrWeb64(me, options, validatedDomains).then(function(
function(csr) { csr
) {
var body = { csr: csr }; var body = { csr: csr };
var payload = JSON.stringify(body); var payload = JSON.stringify(body);
@ -757,7 +752,7 @@
options: options, options: options,
url: options._finalize, url: options._finalize,
protected: { kid: options._kid }, protected: { kid: options._kid },
payload: Enc.binToBuf(payload) payload: Enc.strToBuf(payload)
}).then(function(resp) { }).then(function(resp) {
if (me.debug) { if (me.debug) {
console.debug('order finalized: resp.body:'); console.debug('order finalized: resp.body:');
@ -859,15 +854,14 @@
} }
return pollCert(); return pollCert();
} });
); };
}; // _kid
// _kid // registerAccount
// registerAccount // postChallenge
// postChallenge // finalizeOrder
// finalizeOrder // getCertificate
// getCertificate ACME._getCertificate = function(me, options) {
ACME._getCertificate = function(me, options) {
if (me.debug) { if (me.debug) {
console.debug('[acme-v2] DEBUG get cert 1'); console.debug('[acme-v2] DEBUG get cert 1');
} }
@ -985,7 +979,7 @@
options: options, options: options,
url: me._directoryUrls.newOrder, url: me._directoryUrls.newOrder,
protected: { kid: options._kid }, protected: { kid: options._kid },
payload: Enc.binToBuf(payload) payload: Enc.strToBuf(payload)
}).then(function(resp) { }).then(function(resp) {
var location = resp.headers.location; var location = resp.headers.location;
var setAuths; var setAuths;
@ -1023,8 +1017,9 @@
return; return;
} }
return ACME._getChallenges(me, options, authUrl).then( return ACME._getChallenges(me, options, authUrl).then(function(
function(results) { results
) {
// var domain = options.domains[i]; // results.identifier.value // var domain = options.domains[i]; // results.identifier.value
// If it's already valid, we're golden it regardless // If it's already valid, we're golden it regardless
@ -1036,10 +1031,7 @@
return setNext(); return setNext();
} }
var challenge = ACME._chooseChallenge( var challenge = ACME._chooseChallenge(options, results);
options,
results
);
if (!challenge) { if (!challenge) {
// For example, wildcards require dns-01 and, if we don't have that, we have to bail // For example, wildcards require dns-01 and, if we don't have that, we have to bail
return Promise.reject( return Promise.reject(
@ -1059,14 +1051,11 @@
false false
).then(function(auth) { ).then(function(auth) {
auths.push(auth); auths.push(auth);
return ACME._setChallenge( return ACME._setChallenge(me, options, auth).then(
me, setNext
options,
auth
).then(setNext);
});
}
); );
});
});
} }
function checkNext() { function checkNext() {
@ -1115,11 +1104,7 @@
return ident.value; return ident.value;
}); });
return ACME._finalizeOrder( return ACME._finalizeOrder(me, options, validatedDomains);
me,
options,
validatedDomains
);
}) })
.then(function(order) { .then(function(order) {
if (me.debug) { if (me.debug) {
@ -1159,8 +1144,8 @@
}); });
}); });
}); });
}; };
ACME._generateCsrWeb64 = function(me, options, validatedDomains) { ACME._generateCsrWeb64 = function(me, options, validatedDomains) {
var csr; var csr;
if (options.csr) { if (options.csr) {
csr = options.csr; csr = options.csr;
@ -1193,17 +1178,16 @@
return Enc.bufToUrlBase64(der); return Enc.bufToUrlBase64(der);
}); });
}); });
}; };
ACME.create = function create(me) { ACME.create = function create(me) {
if (!me) { if (!me) {
me = {}; me = {};
} }
// me.debug = true; // me.debug = true;
me.challengePrefixes = ACME.challengePrefixes; me.challengePrefixes = ACME.challengePrefixes;
me.Keypairs = me.Keypairs = me.Keypairs || require('./keypairs.js');
me.Keypairs || exports.Keypairs || require('keypairs').Keypairs; me.CSR = me.CSR || require('./csr.js');
me.CSR = me.CSR || exports.CSR || require('CSR').CSR;
me._nonces = []; me._nonces = [];
me._canUse = {}; me._canUse = {};
if (!me._baseUrl) { if (!me._baseUrl) {
@ -1278,10 +1262,10 @@
} }
}; };
return me; return me;
}; };
// Handle nonce, signing, and request altogether // Handle nonce, signing, and request altogether
ACME._jwsRequest = function(me, bigopts) { ACME._jwsRequest = function(me, bigopts) {
return ACME._getNonce(me).then(function(nonce) { return ACME._getNonce(me).then(function(nonce) {
bigopts.protected.nonce = nonce; bigopts.protected.nonce = nonce;
bigopts.protected.url = bigopts.url; bigopts.protected.url = bigopts.url;
@ -1306,9 +1290,9 @@
return ACME._request(me, { url: bigopts.url, json: jws }); return ACME._request(me, { url: bigopts.url, json: jws });
}); });
}); });
}; };
// Handle some ACME-specific defaults // Handle some ACME-specific defaults
ACME._request = function(me, opts) { ACME._request = function(me, opts) {
if (!opts.headers) { if (!opts.headers) {
opts.headers = {}; opts.headers = {};
} }
@ -1326,9 +1310,9 @@
} }
return resp; return resp;
}); });
}; };
// A very generic, swappable request lib // A very generic, swappable request lib
ACME._defaultRequest = function(opts) { ACME._defaultRequest = function(opts) {
// Note: normally we'd have to supply a User-Agent string, but not here in a browser // Note: normally we'd have to supply a User-Agent string, but not here in a browser
if (!opts.headers) { if (!opts.headers) {
opts.headers = {}; opts.headers = {};
@ -1346,35 +1330,11 @@
} }
} }
opts.cors = true; opts.cors = true;
return window.fetch(opts.url, opts).then(function(resp) {
var headers = {};
var result = {
statusCode: resp.status,
headers: headers,
toJSON: function() {
return this;
}
};
Array.from(resp.headers.entries()).forEach(function(h) {
headers[h[0]] = h[1];
});
if (!headers['content-type']) {
return result;
}
if (/json/.test(headers['content-type'])) {
return resp.json().then(function(json) {
result.body = json;
return result;
});
}
return resp.text().then(function(txt) {
result.body = txt;
return result;
});
});
};
ACME._importKeypair = function(me, kp) { return http.request(opts);
};
ACME._importKeypair = function(me, kp) {
var jwk = kp.privateKeyJwk; var jwk = kp.privateKeyJwk;
var p; var p;
if (jwk) { if (jwk) {
@ -1398,9 +1358,9 @@
} }
return pair; return pair;
}); });
}; };
/* /*
TODO TODO
Per-Order State Params Per-Order State Params
_kty _kty
@ -1412,15 +1372,15 @@ Per-Order State Params
_authorizations _authorizations
*/ */
ACME._toWebsafeBase64 = function(b64) { ACME._toWebsafeBase64 = function(b64) {
return b64 return b64
.replace(/\+/g, '-') .replace(/\+/g, '-')
.replace(/\//g, '_') .replace(/\//g, '_')
.replace(/=/g, ''); .replace(/=/g, '');
}; };
// In v8 this is crypto random, but we're just using it for pseudorandom // In v8 this is crypto random, but we're just using it for pseudorandom
ACME._prnd = function(n) { ACME._prnd = function(n) {
var rnd = ''; var rnd = '';
while (rnd.length / 2 < n) { while (rnd.length / 2 < n) {
var num = Math.random() var num = Math.random()
@ -1433,11 +1393,11 @@ Per-Order State Params
rnd += pairs.map(ACME._toHex).join(''); rnd += pairs.map(ACME._toHex).join('');
} }
return rnd.substr(0, n * 2); return rnd.substr(0, n * 2);
}; };
ACME._toHex = function(pair) { ACME._toHex = function(pair) {
return parseInt(pair, 10).toString(16); return parseInt(pair, 10).toString(16);
}; };
ACME._dns01 = function(me, auth) { ACME._dns01 = function(me, auth) {
return new me.request({ return new me.request({
url: me._baseUrl + '/api/dns/' + auth.dnsHost + '?type=TXT' url: me._baseUrl + '/api/dns/' + auth.dnsHost + '?type=TXT'
}).then(function(resp) { }).then(function(resp) {
@ -1458,16 +1418,16 @@ Per-Order State Params
}) })
}; };
}); });
}; };
ACME._http01 = function(me, auth) { ACME._http01 = function(me, auth) {
var url = encodeURIComponent(auth.challengeUrl); var url = encodeURIComponent(auth.challengeUrl);
return new me.request({ return new me.request({
url: me._baseUrl + '/api/http?url=' + url url: me._baseUrl + '/api/http?url=' + url
}).then(function(resp) { }).then(function(resp) {
return resp.body; return resp.body;
}); });
}; };
ACME._removeChallenge = function(me, options, auth) { ACME._removeChallenge = function(me, options, auth) {
var challengers = options.challenges || {}; var challengers = options.challenges || {};
var removeChallenge = var removeChallenge =
(challengers[auth.type] && challengers[auth.type].remove) || (challengers[auth.type] && challengers[auth.type].remove) ||
@ -1490,28 +1450,4 @@ Per-Order State Params
} }
removeChallenge(auth.request.identifier, auth.token, function() {}); removeChallenge(auth.request.identifier, auth.token, function() {});
} }
}; };
Enc.bufToUrlBase64 = function(u8) {
return Enc.bufToBase64(u8)
.replace(/\+/g, '-')
.replace(/\//g, '_')
.replace(/=/g, '');
};
Enc.bufToBase64 = function(u8) {
var bin = '';
u8.forEach(function(i) {
bin += String.fromCharCode(i);
});
return btoa(bin);
};
Crypto._sha = function(sha, str) {
var encoder = new TextEncoder();
var data = encoder.encode(str);
sha = 'SHA-' + sha.replace(/^sha-?/i, '');
return window.crypto.subtle.digest(sha, data).then(function(hash) {
return Enc.bufToUrlBase64(new Uint8Array(hash));
});
};
})('undefined' === typeof window ? module.exports : window);

View File

@ -1,147 +0,0 @@
(function(exports) {
'use strict';
if (!exports.ASN1) {
exports.ASN1 = {};
}
if (!exports.Enc) {
exports.Enc = {};
}
if (!exports.PEM) {
exports.PEM = {};
}
var ASN1 = exports.ASN1;
var Enc = exports.Enc;
var PEM = exports.PEM;
//
// Packer
//
// Almost every ASN.1 type that's important for CSR
// can be represented generically with only a few rules.
exports.ASN1 = function ASN1(/*type, hexstrings...*/) {
var args = Array.prototype.slice.call(arguments);
var typ = args.shift();
var str = args
.join('')
.replace(/\s+/g, '')
.toLowerCase();
var len = str.length / 2;
var lenlen = 0;
var hex = typ;
// We can't have an odd number of hex chars
if (len !== Math.round(len)) {
throw new Error('invalid hex');
}
// The first byte of any ASN.1 sequence is the type (Sequence, Integer, etc)
// The second byte is either the size of the value, or the size of its size
// 1. If the second byte is < 0x80 (128) it is considered the size
// 2. If it is > 0x80 then it describes the number of bytes of the size
// ex: 0x82 means the next 2 bytes describe the size of the value
// 3. The special case of exactly 0x80 is "indefinite" length (to end-of-file)
if (len > 127) {
lenlen += 1;
while (len > 255) {
lenlen += 1;
len = len >> 8;
}
}
if (lenlen) {
hex += Enc.numToHex(0x80 + lenlen);
}
return hex + Enc.numToHex(str.length / 2) + str;
};
// The Integer type has some special rules
ASN1.UInt = function UINT() {
var str = Array.prototype.slice.call(arguments).join('');
var first = parseInt(str.slice(0, 2), 16);
// If the first byte is 0x80 or greater, the number is considered negative
// Therefore we add a '00' prefix if the 0x80 bit is set
if (0x80 & first) {
str = '00' + str;
}
return ASN1('02', str);
};
// The Bit String type also has a special rule
ASN1.BitStr = function BITSTR() {
var str = Array.prototype.slice.call(arguments).join('');
// '00' is a mask of how many bits of the next byte to ignore
return ASN1('03', '00' + str);
};
ASN1.pack = function(arr) {
var typ = Enc.numToHex(arr[0]);
var str = '';
if (Array.isArray(arr[1])) {
arr[1].forEach(function(a) {
str += ASN1.pack(a);
});
} else if ('string' === typeof arr[1]) {
str = arr[1];
} else {
throw new Error('unexpected array');
}
if ('03' === typ) {
return ASN1.BitStr(str);
} else if ('02' === typ) {
return ASN1.UInt(str);
} else {
return ASN1(typ, str);
}
};
Object.keys(ASN1).forEach(function(k) {
exports.ASN1[k] = ASN1[k];
});
ASN1 = exports.ASN1;
PEM.packBlock = function(opts) {
// TODO allow for headers?
return (
'-----BEGIN ' +
opts.type +
'-----\n' +
Enc.bufToBase64(opts.bytes)
.match(/.{1,64}/g)
.join('\n') +
'\n' +
'-----END ' +
opts.type +
'-----'
);
};
Enc.bufToBase64 = function(u8) {
var bin = '';
u8.forEach(function(i) {
bin += String.fromCharCode(i);
});
return btoa(bin);
};
Enc.hexToBuf = function(hex) {
var arr = [];
hex.match(/.{2}/g).forEach(function(h) {
arr.push(parseInt(h, 16));
});
return 'undefined' !== typeof Uint8Array ? new Uint8Array(arr) : arr;
};
Enc.numToHex = function(d) {
d = d.toString(16);
if (d.length % 2) {
return '0' + d;
}
return d;
};
})('undefined' !== typeof window ? window : module.exports);

View File

@ -1,222 +0,0 @@
// Copyright 2018 AJ ONeal. All rights reserved
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
(function(exports) {
'use strict';
if (!exports.ASN1) {
exports.ASN1 = {};
}
if (!exports.Enc) {
exports.Enc = {};
}
if (!exports.PEM) {
exports.PEM = {};
}
var ASN1 = exports.ASN1;
var Enc = exports.Enc;
var PEM = exports.PEM;
//
// Parser
//
// Although I've only seen 9 max in https certificates themselves,
// but each domain list could have up to 100
ASN1.ELOOPN = 102;
ASN1.ELOOP =
'uASN1.js Error: iterated over ' +
ASN1.ELOOPN +
'+ elements (probably a malformed file)';
// I've seen https certificates go 29 deep
ASN1.EDEEPN = 60;
ASN1.EDEEP =
'uASN1.js Error: element nested ' +
ASN1.EDEEPN +
'+ layers deep (probably a malformed file)';
// Container Types are Sequence 0x30, Container Array? (0xA0, 0xA1)
// Value Types are Boolean 0x01, Integer 0x02, Null 0x05, Object ID 0x06, String 0x0C, 0x16, 0x13, 0x1e Value Array? (0x82)
// Bit String (0x03) and Octet String (0x04) may be values or containers
// Sometimes Bit String is used as a container (RSA Pub Spki)
ASN1.CTYPES = [0x30, 0x31, 0xa0, 0xa1];
ASN1.VTYPES = [0x01, 0x02, 0x05, 0x06, 0x0c, 0x82];
ASN1.parse = function parseAsn1Helper(buf) {
//var ws = ' ';
function parseAsn1(buf, depth, eager) {
if (depth.length >= ASN1.EDEEPN) {
throw new Error(ASN1.EDEEP);
}
var index = 2; // we know, at minimum, data starts after type (0) and lengthSize (1)
var asn1 = { type: buf[0], lengthSize: 0, length: buf[1] };
var child;
var iters = 0;
var adjust = 0;
var adjustedLen;
// Determine how many bytes the length uses, and what it is
if (0x80 & asn1.length) {
asn1.lengthSize = 0x7f & asn1.length;
// I think that buf->hex->int solves the problem of Endianness... not sure
asn1.length = parseInt(
Enc.bufToHex(buf.slice(index, index + asn1.lengthSize)),
16
);
index += asn1.lengthSize;
}
// High-order bit Integers have a leading 0x00 to signify that they are positive.
// Bit Streams use the first byte to signify padding, which x.509 doesn't use.
if (
0x00 === buf[index] &&
(0x02 === asn1.type || 0x03 === asn1.type)
) {
// However, 0x00 on its own is a valid number
if (asn1.length > 1) {
index += 1;
adjust = -1;
}
}
adjustedLen = asn1.length + adjust;
//console.warn(depth.join(ws) + '0x' + Enc.numToHex(asn1.type), index, 'len:', asn1.length, asn1);
function parseChildren(eager) {
asn1.children = [];
//console.warn('1 len:', (2 + asn1.lengthSize + asn1.length), 'idx:', index, 'clen:', 0);
while (
iters < ASN1.ELOOPN &&
index < 2 + asn1.length + asn1.lengthSize
) {
iters += 1;
depth.length += 1;
child = parseAsn1(
buf.slice(index, index + adjustedLen),
depth,
eager
);
depth.length -= 1;
// The numbers don't match up exactly and I don't remember why...
// probably something with adjustedLen or some such, but the tests pass
index += 2 + child.lengthSize + child.length;
//console.warn('2 len:', (2 + asn1.lengthSize + asn1.length), 'idx:', index, 'clen:', (2 + child.lengthSize + child.length));
if (index > 2 + asn1.lengthSize + asn1.length) {
if (!eager) {
console.error(
JSON.stringify(asn1, ASN1._replacer, 2)
);
}
throw new Error(
'Parse error: child value length (' +
child.length +
') is greater than remaining parent length (' +
(asn1.length - index) +
' = ' +
asn1.length +
' - ' +
index +
')'
);
}
asn1.children.push(child);
//console.warn(depth.join(ws) + '0x' + Enc.numToHex(asn1.type), index, 'len:', asn1.length, asn1);
}
if (index !== 2 + asn1.lengthSize + asn1.length) {
//console.warn('index:', index, 'length:', (2 + asn1.lengthSize + asn1.length));
throw new Error('premature end-of-file');
}
if (iters >= ASN1.ELOOPN) {
throw new Error(ASN1.ELOOP);
}
delete asn1.value;
return asn1;
}
// Recurse into types that are _always_ containers
if (-1 !== ASN1.CTYPES.indexOf(asn1.type)) {
return parseChildren(eager);
}
// Return types that are _always_ values
asn1.value = buf.slice(index, index + adjustedLen);
if (-1 !== ASN1.VTYPES.indexOf(asn1.type)) {
return asn1;
}
// For ambigious / unknown types, recurse and return on failure
// (and return child array size to zero)
try {
return parseChildren(true);
} catch (e) {
asn1.children.length = 0;
return asn1;
}
}
var asn1 = parseAsn1(buf, []);
var len = buf.byteLength || buf.length;
if (len !== 2 + asn1.lengthSize + asn1.length) {
throw new Error(
'Length of buffer does not match length of ASN.1 sequence.'
);
}
return asn1;
};
ASN1._replacer = function(k, v) {
if ('type' === k) {
return '0x' + Enc.numToHex(v);
}
if (v && 'value' === k) {
return '0x' + Enc.bufToHex(v.data || v);
}
return v;
};
// don't replace the full parseBlock, if it exists
PEM.parseBlock =
PEM.parseBlock ||
function(str) {
var der = str
.split(/\n/)
.filter(function(line) {
return !/-----/.test(line);
})
.join('');
return { bytes: Enc.base64ToBuf(der) };
};
Enc.base64ToBuf = function(b64) {
return Enc.binToBuf(atob(b64));
};
Enc.binToBuf = function(bin) {
var arr = bin.split('').map(function(ch) {
return ch.charCodeAt(0);
});
return 'undefined' !== typeof Uint8Array ? new Uint8Array(arr) : arr;
};
Enc.bufToHex = function(u8) {
var hex = [];
var i, h;
var len = u8.byteLength || u8.length;
for (i = 0; i < len; i += 1) {
h = u8[i].toString(16);
if (h.length % 2) {
h = '0' + h;
}
hex.push(h);
}
return hex.join('').toLowerCase();
};
Enc.numToHex = function(d) {
d = d.toString(16);
if (d.length % 2) {
return '0' + d;
}
return d;
};
})('undefined' !== typeof window ? window : module.exports);

1
lib/asn1/README.md Normal file
View File

@ -0,0 +1 @@
Disabiguation: `Any`. There was once an actual ASN.1 type with the literal name 'Any'. It was deprecated in 1994 and the `Any` in the API simply means "give any value"

11
lib/asn1/index.js Normal file
View File

@ -0,0 +1,11 @@
'use strict';
var ASN1 = module.exports;
var packer = require('./packer.js');
var parser = require('./parser.js');
Object.keys(parser).forEach(function(key) {
ASN1[key] = parser[key];
});
Object.keys(packer).forEach(function(key) {
ASN1[key] = packer[key];
});

91
lib/asn1/packer.js Normal file
View File

@ -0,0 +1,91 @@
'use strict';
var ASN1 = module.exports;
var Enc = require('omnibuffer');
//
// Packer
//
// Almost every ASN.1 type that's important for CSR
// can be represented generically with only a few rules.
function Any(/*type, hexstrings...*/) {
var args = Array.prototype.slice.call(arguments);
var typ = args.shift();
var str = args
.join('')
.replace(/\s+/g, '')
.toLowerCase();
var len = str.length / 2;
var lenlen = 0;
var hex = typ;
// We can't have an odd number of hex chars
if (len !== Math.round(len)) {
throw new Error('invalid hex');
}
// The first byte of any ASN.1 sequence is the type (Sequence, Integer, etc)
// The second byte is either the size of the value, or the size of its size
// 1. If the second byte is < 0x80 (128) it is considered the size
// 2. If it is > 0x80 then it describes the number of bytes of the size
// ex: 0x82 means the next 2 bytes describe the size of the value
// 3. The special case of exactly 0x80 is "indefinite" length (to end-of-file)
if (len > 127) {
lenlen += 1;
while (len > 255) {
lenlen += 1;
len = len >> 8;
}
}
if (lenlen) {
hex += Enc.numToHex(0x80 + lenlen);
}
return hex + Enc.numToHex(str.length / 2) + str;
}
ASN1.Any = Any;
// The Integer type has some special rules
ASN1.UInt = function UINT() {
var str = Array.prototype.slice.call(arguments).join('');
var first = parseInt(str.slice(0, 2), 16);
// If the first byte is 0x80 or greater, the number is considered negative
// Therefore we add a '00' prefix if the 0x80 bit is set
if (0x80 & first) {
str = '00' + str;
}
return Any('02', str);
};
// The Bit String type also has a special rule
ASN1.BitStr = function BITSTR() {
var str = Array.prototype.slice.call(arguments).join('');
// '00' is a mask of how many bits of the next byte to ignore
return Any('03', '00' + str);
};
ASN1.pack = function(arr) {
var typ = Enc.numToHex(arr[0]);
var str = '';
if (Array.isArray(arr[1])) {
arr[1].forEach(function(a) {
str += ASN1.pack(a);
});
} else if ('string' === typeof arr[1]) {
str = arr[1];
} else {
throw new Error('unexpected array');
}
if ('03' === typ) {
return ASN1.BitStr(str);
} else if ('02' === typ) {
return ASN1.UInt(str);
} else {
return Any(typ, str);
}
};

159
lib/asn1/parser.js Normal file
View File

@ -0,0 +1,159 @@
// Copyright 2018 AJ ONeal. All rights reserved
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
'use strict';
var ASN1 = module.exports;
var Enc = require('omnibuffer');
//
// Parser
//
// Although I've only seen 9 max in https certificates themselves,
// but each domain list could have up to 100
ASN1.ELOOPN = 102;
ASN1.ELOOP =
'uASN1.js Error: iterated over ' +
ASN1.ELOOPN +
'+ elements (probably a malformed file)';
// I've seen https certificates go 29 deep
ASN1.EDEEPN = 60;
ASN1.EDEEP =
'uASN1.js Error: element nested ' +
ASN1.EDEEPN +
'+ layers deep (probably a malformed file)';
// Container Types are Sequence 0x30, Container Array? (0xA0, 0xA1)
// Value Types are Boolean 0x01, Integer 0x02, Null 0x05, Object ID 0x06, String 0x0C, 0x16, 0x13, 0x1e Value Array? (0x82)
// Bit String (0x03) and Octet String (0x04) may be values or containers
// Sometimes Bit String is used as a container (RSA Pub Spki)
ASN1.CTYPES = [0x30, 0x31, 0xa0, 0xa1];
ASN1.VTYPES = [0x01, 0x02, 0x05, 0x06, 0x0c, 0x82];
ASN1.parse = function parseAsn1Helper(buf) {
//var ws = ' ';
function parseAsn1(buf, depth, eager) {
if (depth.length >= ASN1.EDEEPN) {
throw new Error(ASN1.EDEEP);
}
var index = 2; // we know, at minimum, data starts after type (0) and lengthSize (1)
var asn1 = { type: buf[0], lengthSize: 0, length: buf[1] };
var child;
var iters = 0;
var adjust = 0;
var adjustedLen;
// Determine how many bytes the length uses, and what it is
if (0x80 & asn1.length) {
asn1.lengthSize = 0x7f & asn1.length;
// I think that buf->hex->int solves the problem of Endianness... not sure
asn1.length = parseInt(
Enc.bufToHex(buf.slice(index, index + asn1.lengthSize)),
16
);
index += asn1.lengthSize;
}
// High-order bit Integers have a leading 0x00 to signify that they are positive.
// Bit Streams use the first byte to signify padding, which x.509 doesn't use.
if (0x00 === buf[index] && (0x02 === asn1.type || 0x03 === asn1.type)) {
// However, 0x00 on its own is a valid number
if (asn1.length > 1) {
index += 1;
adjust = -1;
}
}
adjustedLen = asn1.length + adjust;
//console.warn(depth.join(ws) + '0x' + Enc.numToHex(asn1.type), index, 'len:', asn1.length, asn1);
function parseChildren(eager) {
asn1.children = [];
//console.warn('1 len:', (2 + asn1.lengthSize + asn1.length), 'idx:', index, 'clen:', 0);
while (
iters < ASN1.ELOOPN &&
index < 2 + asn1.length + asn1.lengthSize
) {
iters += 1;
depth.length += 1;
child = parseAsn1(
buf.slice(index, index + adjustedLen),
depth,
eager
);
depth.length -= 1;
// The numbers don't match up exactly and I don't remember why...
// probably something with adjustedLen or some such, but the tests pass
index += 2 + child.lengthSize + child.length;
//console.warn('2 len:', (2 + asn1.lengthSize + asn1.length), 'idx:', index, 'clen:', (2 + child.lengthSize + child.length));
if (index > 2 + asn1.lengthSize + asn1.length) {
if (!eager) {
console.error(JSON.stringify(asn1, ASN1._replacer, 2));
}
throw new Error(
'Parse error: child value length (' +
child.length +
') is greater than remaining parent length (' +
(asn1.length - index) +
' = ' +
asn1.length +
' - ' +
index +
')'
);
}
asn1.children.push(child);
//console.warn(depth.join(ws) + '0x' + Enc.numToHex(asn1.type), index, 'len:', asn1.length, asn1);
}
if (index !== 2 + asn1.lengthSize + asn1.length) {
//console.warn('index:', index, 'length:', (2 + asn1.lengthSize + asn1.length));
throw new Error('premature end-of-file');
}
if (iters >= ASN1.ELOOPN) {
throw new Error(ASN1.ELOOP);
}
delete asn1.value;
return asn1;
}
// Recurse into types that are _always_ containers
if (-1 !== ASN1.CTYPES.indexOf(asn1.type)) {
return parseChildren(eager);
}
// Return types that are _always_ values
asn1.value = buf.slice(index, index + adjustedLen);
if (-1 !== ASN1.VTYPES.indexOf(asn1.type)) {
return asn1;
}
// For ambigious / unknown types, recurse and return on failure
// (and return child array size to zero)
try {
return parseChildren(true);
} catch (e) {
asn1.children.length = 0;
return asn1;
}
}
var asn1 = parseAsn1(buf, []);
var len = buf.byteLength || buf.length;
if (len !== 2 + asn1.lengthSize + asn1.length) {
throw new Error(
'Length of buffer does not match length of ASN.1 sequence.'
);
}
return asn1;
};
ASN1._replacer = function(k, v) {
if ('type' === k) {
return '0x' + Enc.numToHex(v);
}
if (v && 'value' === k) {
return '0x' + Enc.bufToHex(v.data || v);
}
return v;
};

57
lib/browser/ecdsa.js Normal file
View File

@ -0,0 +1,57 @@
'use strict';
var native = module.exports;
// XXX received from caller
var EC = native;
native.generate = function(opts) {
var wcOpts = {};
if (!opts) {
opts = {};
}
if (!opts.kty) {
opts.kty = 'EC';
}
// ECDSA has only the P curves and an associated bitlength
wcOpts.name = 'ECDSA';
if (!opts.namedCurve) {
opts.namedCurve = 'P-256';
}
wcOpts.namedCurve = opts.namedCurve; // true for supported curves
if (/256/.test(wcOpts.namedCurve)) {
wcOpts.namedCurve = 'P-256';
wcOpts.hash = { name: 'SHA-256' };
} else if (/384/.test(wcOpts.namedCurve)) {
wcOpts.namedCurve = 'P-384';
wcOpts.hash = { name: 'SHA-384' };
} else {
return Promise.Reject(
new Error(
"'" +
wcOpts.namedCurve +
"' is not an NIST approved ECDSA namedCurve. " +
" Please choose either 'P-256' or 'P-384'. " +
// XXX received from caller
EC._stance
)
);
}
var extractable = true;
return window.crypto.subtle
.generateKey(wcOpts, extractable, ['sign', 'verify'])
.then(function(result) {
return window.crypto.subtle
.exportKey('jwk', result.privateKey)
.then(function(privJwk) {
privJwk.key_ops = undefined;
privJwk.ext = undefined;
return {
private: privJwk,
// XXX received from caller
public: EC.neuter({ jwk: privJwk })
};
});
});
};

32
lib/browser/http.js Normal file
View File

@ -0,0 +1,32 @@
'use strict';
var http = module.exports;
http.request = function(opts) {
return window.fetch(opts.url, opts).then(function(resp) {
var headers = {};
var result = {
statusCode: resp.status,
headers: headers,
toJSON: function() {
return this;
}
};
Array.from(resp.headers.entries()).forEach(function(h) {
headers[h[0]] = h[1];
});
if (!headers['content-type']) {
return result;
}
if (/json/.test(headers['content-type'])) {
return resp.json().then(function(json) {
result.body = json;
return result;
});
}
return resp.text().then(function(txt) {
result.body = txt;
return result;
});
});
};

108
lib/browser/keypairs.js Normal file
View File

@ -0,0 +1,108 @@
'use strict';
var Keypairs = module.exports;
Keypairs._sign = function(opts, payload) {
return Keypairs._import(opts).then(function(privkey) {
if ('string' === typeof payload) {
payload = new TextEncoder().encode(payload);
}
return window.crypto.subtle
.sign(
{
name: Keypairs._getName(opts),
hash: { name: 'SHA-' + Keypairs._getBits(opts) }
},
privkey,
payload
)
.then(function(signature) {
signature = new Uint8Array(signature); // ArrayBuffer -> u8
// This will come back into play for CSRs, but not for JOSE
if ('EC' === opts.jwk.kty && /x509|asn1/i.test(opts.format)) {
return Keypairs._ecdsaJoseSigToAsn1Sig(signature);
} else {
// jose/jws/jwt
return signature;
}
});
});
};
Keypairs._import = function(opts) {
return Promise.resolve().then(function() {
var ops;
// all private keys just happen to have a 'd'
if (opts.jwk.d) {
ops = ['sign'];
} else {
ops = ['verify'];
}
// gotta mark it as extractable, as if it matters
opts.jwk.ext = true;
opts.jwk.key_ops = ops;
return window.crypto.subtle
.importKey(
'jwk',
opts.jwk,
{
name: Keypairs._getName(opts),
namedCurve: opts.jwk.crv,
hash: { name: 'SHA-' + Keypairs._getBits(opts) }
},
true,
ops
)
.then(function(privkey) {
delete opts.jwk.ext;
return privkey;
});
});
};
// ECDSA JOSE / JWS / JWT signatures differ from "normal" ASN1/X509 ECDSA signatures
// https://tools.ietf.org/html/rfc7518#section-3.4
Keypairs._ecdsaJoseSigToAsn1Sig = function(bufsig) {
// it's easier to do the manipulation in the browser with an array
bufsig = Array.from(bufsig);
var hlen = bufsig.length / 2; // should be even
var r = bufsig.slice(0, hlen);
var s = bufsig.slice(hlen);
// unpad positive ints less than 32 bytes wide
while (!r[0]) {
r = r.slice(1);
}
while (!s[0]) {
s = s.slice(1);
}
// pad (or re-pad) ambiguously non-negative BigInts, up to 33 bytes wide
if (0x80 & r[0]) {
r.unshift(0);
}
if (0x80 & s[0]) {
s.unshift(0);
}
var len = 2 + r.length + 2 + s.length;
var head = [0x30];
// hard code 0x80 + 1 because it won't be longer than
// two SHA512 plus two pad bytes (130 bytes <= 256)
if (len >= 0x80) {
head.push(0x81);
}
head.push(len);
return Uint8Array.from(
head.concat([0x02, r.length], r, [0x02, s.length], s)
);
};
Keypairs._getName = function(opts) {
if (/EC/i.test(opts.jwk.kty)) {
return 'ECDSA';
} else {
return 'RSASSA-PKCS1-v1_5';
}
};

59
lib/browser/rsa.js Normal file
View File

@ -0,0 +1,59 @@
'use strict';
var native = module.exports;
// XXX added by caller: _stance, neuter
var RSA = native;
native.generate = function(opts) {
var wcOpts = {};
if (!opts) {
opts = {};
}
if (!opts.kty) {
opts.kty = 'RSA';
}
// Support PSS? I don't think it's used for Let's Encrypt
wcOpts.name = 'RSASSA-PKCS1-v1_5';
if (!opts.modulusLength) {
opts.modulusLength = 2048;
}
wcOpts.modulusLength = opts.modulusLength;
if (wcOpts.modulusLength >= 2048 && wcOpts.modulusLength < 3072) {
// erring on the small side... for no good reason
wcOpts.hash = { name: 'SHA-256' };
} else if (wcOpts.modulusLength >= 3072 && wcOpts.modulusLength < 4096) {
wcOpts.hash = { name: 'SHA-384' };
} else if (wcOpts.modulusLength < 4097) {
wcOpts.hash = { name: 'SHA-512' };
} else {
// Public key thumbprints should be paired with a hash of similar length,
// so anything above SHA-512's keyspace would be left under-represented anyway.
return Promise.Reject(
new Error(
"'" +
wcOpts.modulusLength +
"' is not within the safe and universally" +
' acceptable range of 2048-4096. Typically you should pick 2048, 3072, or 4096, though other values' +
' divisible by 8 are allowed. ' +
RSA._stance
)
);
}
// TODO maybe allow this to be set to any of the standard values?
wcOpts.publicExponent = new Uint8Array([0x01, 0x00, 0x01]);
var extractable = true;
return window.crypto.subtle
.generateKey(wcOpts, extractable, ['sign', 'verify'])
.then(function(result) {
return window.crypto.subtle
.exportKey('jwk', result.privateKey)
.then(function(privJwk) {
return {
private: privJwk,
public: RSA.neuter({ jwk: privJwk })
};
});
});
};

13
lib/browser/sha2.js Normal file
View File

@ -0,0 +1,13 @@
'use strict';
var sha2 = module.exports;
var encoder = new TextEncoder();
sha2.sum = function(alg, str) {
var data = str;
if ('string' === typeof data) {
data = encoder.encode(str);
}
var sha = 'SHA-' + String(alg).replace(/^sha-?/i, '');
return window.crypto.subtle.digest(sha, data);
};

View File

@ -2,18 +2,21 @@
/* This Source Code Form is subject to the terms of the Mozilla Public /* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this * License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
(function(exports) { 'use strict';
'use strict'; /*global Promise*/
/*global Promise*/
var ASN1 = exports.ASN1; var ASN1 = require('./asn1/parser.js'); // DER, actually
var Enc = exports.Enc; var Asn1 = ASN1.Any;
var PEM = exports.PEM; var BitStr = ASN1.BitStr;
var X509 = exports.x509; var UInt = ASN1.UInt;
var Keypairs = exports.Keypairs; var Asn1Parser = require('./asn1/packer.js'); // DER, actually
var Enc = require('omnibuffer');
var PEM = require('./pem.js');
var X509 = require('./x509.js');
var Keypairs = require('./keypairs');
// TODO find a way that the prior node-ish way of `module.exports = function () {}` isn't broken // TODO find a way that the prior node-ish way of `module.exports = function () {}` isn't broken
var CSR = (exports.CSR = function(opts) { var CSR = (exports.CSR = function(opts) {
// We're using a Promise here to be compatible with the browser version // We're using a Promise here to be compatible with the browser version
// which will probably use the webcrypto API for some of the conversions // which will probably use the webcrypto API for some of the conversions
return CSR._prepare(opts).then(function(opts) { return CSR._prepare(opts).then(function(opts) {
@ -21,9 +24,9 @@
return CSR._encode(opts, bytes); return CSR._encode(opts, bytes);
}); });
}); });
}); });
CSR._prepare = function(opts) { CSR._prepare = function(opts) {
return Promise.resolve().then(function() { return Promise.resolve().then(function() {
var Keypairs; var Keypairs;
opts = JSON.parse(JSON.stringify(opts)); opts = JSON.parse(JSON.stringify(opts));
@ -43,8 +46,7 @@
!opts.domains.every(function(d) { !opts.domains.every(function(d) {
// allow punycode? xn-- // allow punycode? xn--
if ( if (
'string' === 'string' === typeof d /*&& /\./.test(d) && !/--/.test(d)*/
typeof d /*&& /\./.test(d) && !/--/.test(d)*/
) { ) {
return true; return true;
} }
@ -81,9 +83,9 @@
return opts; return opts;
}); });
}); });
}; };
CSR._encode = function(opts, bytes) { CSR._encode = function(opts, bytes) {
if ('der' === (opts.encoding || '').toLowerCase()) { if ('der' === (opts.encoding || '').toLowerCase()) {
return bytes; return bytes;
} }
@ -91,19 +93,19 @@
type: 'CERTIFICATE REQUEST', type: 'CERTIFICATE REQUEST',
bytes: bytes /* { jwk: jwk, domains: opts.domains } */ bytes: bytes /* { jwk: jwk, domains: opts.domains } */
}); });
}; };
CSR.create = function createCsr(opts) { CSR.create = function createCsr(opts) {
var hex = CSR.request(opts.jwk, opts.domains); var hex = CSR.request(opts.jwk, opts.domains);
return CSR._sign(opts.jwk, hex).then(function(csr) { return CSR._sign(opts.jwk, hex).then(function(csr) {
return Enc.hexToBuf(csr); return Enc.hexToBuf(csr);
}); });
}; };
// //
// EC / RSA // EC / RSA
// //
CSR.request = function createCsrBodyEc(jwk, domains) { CSR.request = function createCsrBodyEc(jwk, domains) {
var asn1pub; var asn1pub;
if (/^EC/i.test(jwk.kty)) { if (/^EC/i.test(jwk.kty)) {
asn1pub = X509.packCsrEcPublicKey(jwk); asn1pub = X509.packCsrEcPublicKey(jwk);
@ -111,9 +113,9 @@
asn1pub = X509.packCsrRsaPublicKey(jwk); asn1pub = X509.packCsrRsaPublicKey(jwk);
} }
return X509.packCsr(asn1pub, domains); return X509.packCsr(asn1pub, domains);
}; };
CSR._sign = function csrEcSig(jwk, request) { CSR._sign = function csrEcSig(jwk, request) {
// Took some tips from https://gist.github.com/codermapuche/da4f96cdb6d5ff53b7ebc156ec46a10a // Took some tips from https://gist.github.com/codermapuche/da4f96cdb6d5ff53b7ebc156ec46a10a
// TODO will have to convert web ECDSA signatures to PEM ECDSA signatures (but RSA should be the same) // TODO will have to convert web ECDSA signatures to PEM ECDSA signatures (but RSA should be the same)
// TODO have a consistent non-private way to sign // TODO have a consistent non-private way to sign
@ -127,43 +129,43 @@
kty: jwk.kty kty: jwk.kty
}); });
}); });
}; };
CSR._toDer = function encode(opts) { CSR._toDer = function encode(opts) {
var sty; var sty;
if (/^EC/i.test(opts.kty)) { if (/^EC/i.test(opts.kty)) {
// 1.2.840.10045.4.3.2 ecdsaWithSHA256 (ANSI X9.62 ECDSA algorithm with SHA256) // 1.2.840.10045.4.3.2 ecdsaWithSHA256 (ANSI X9.62 ECDSA algorithm with SHA256)
sty = ASN1('30', ASN1('06', '2a8648ce3d040302')); sty = Asn1('30', Asn1('06', '2a8648ce3d040302'));
} else { } else {
// 1.2.840.113549.1.1.11 sha256WithRSAEncryption (PKCS #1) // 1.2.840.113549.1.1.11 sha256WithRSAEncryption (PKCS #1)
sty = ASN1('30', ASN1('06', '2a864886f70d01010b'), ASN1('05')); sty = Asn1('30', Asn1('06', '2a864886f70d01010b'), Asn1('05'));
} }
return ASN1( return Asn1(
'30', '30',
// The Full CSR Request Body // The Full CSR Request Body
opts.request, opts.request,
// The Signature Type // The Signature Type
sty, sty,
// The Signature // The Signature
ASN1.BitStr(Enc.bufToHex(opts.signature)) BitStr(Enc.bufToHex(opts.signature))
); );
}; };
X509.packCsr = function(asn1pubkey, domains) { X509.packCsr = function(asn1pubkey, domains) {
return ASN1( return Asn1(
'30', '30',
// Version (0) // Version (0)
ASN1.UInt('00'), UInt('00'),
// 2.5.4.3 commonName (X.520 DN component) // 2.5.4.3 commonName (X.520 DN component)
ASN1( Asn1(
'30', '30',
ASN1( Asn1(
'31', '31',
ASN1( Asn1(
'30', '30',
ASN1('06', '550403'), Asn1('06', '550403'),
ASN1('0c', Enc.utf8ToHex(domains[0])) Asn1('0c', Enc.utf8ToHex(domains[0]))
) )
) )
), ),
@ -172,30 +174,27 @@
asn1pubkey, asn1pubkey,
// Request Body // Request Body
ASN1( Asn1(
'a0', 'a0',
ASN1( Asn1(
'30', '30',
// 1.2.840.113549.1.9.14 extensionRequest (PKCS #9 via CRMF) // 1.2.840.113549.1.9.14 extensionRequest (PKCS #9 via CRMF)
ASN1('06', '2a864886f70d01090e'), Asn1('06', '2a864886f70d01090e'),
ASN1( Asn1(
'31', '31',
ASN1( Asn1(
'30', '30',
ASN1( Asn1(
'30', '30',
// 2.5.29.17 subjectAltName (X.509 extension) // 2.5.29.17 subjectAltName (X.509 extension)
ASN1('06', '551d11'), Asn1('06', '551d11'),
ASN1( Asn1(
'04', '04',
ASN1( Asn1(
'30', '30',
domains domains
.map(function(d) { .map(function(d) {
return ASN1( return Asn1('82', Enc.utf8ToHex(d));
'82',
Enc.utf8ToHex(d)
);
}) })
.join('') .join('')
) )
@ -206,11 +205,11 @@
) )
) )
); );
}; };
// TODO finish this later // TODO finish this later
// we want to parse the domains, the public key, and verify the signature // we want to parse the domains, the public key, and verify the signature
CSR._info = function(der) { CSR._info = function(der) {
// standard base64 PEM // standard base64 PEM
if ('string' === typeof der && '-' === der[0]) { if ('string' === typeof der && '-' === der[0]) {
der = PEM.parseBlock(der).bytes; der = PEM.parseBlock(der).bytes;
@ -220,7 +219,7 @@
der = Enc.base64ToBuf(der); der = Enc.base64ToBuf(der);
} }
// not supporting binary-encoded bas64 // not supporting binary-encoded bas64
var c = ASN1.parse(der); var c = Asn1Parser.parse(der);
var kty; var kty;
// A cert has 3 parts: cert, signature meta, signature // A cert has 3 parts: cert, signature meta, signature
if (c.children.length !== 3) { if (c.children.length !== 3) {
@ -232,10 +231,10 @@
if (sig.children.length) { if (sig.children.length) {
// ASN1/X509 EC // ASN1/X509 EC
sig = sig.children[0]; sig = sig.children[0];
sig = ASN1( sig = Asn1(
'30', '30',
ASN1.UInt(Enc.bufToHex(sig.children[0].value)), UInt(Enc.bufToHex(sig.children[0].value)),
ASN1.UInt(Enc.bufToHex(sig.children[1].value)) UInt(Enc.bufToHex(sig.children[1].value))
); );
sig = Enc.hexToBuf(sig); sig = Enc.hexToBuf(sig);
kty = 'EC'; kty = 'EC';
@ -300,9 +299,7 @@
var domains = req.children[3].children var domains = req.children[3].children
.filter(function(seq) { .filter(function(seq) {
// 1.2.840.113549.1.9.14 extensionRequest (PKCS #9 via CRMF) // 1.2.840.113549.1.9.14 extensionRequest (PKCS #9 via CRMF)
if ( if ('2a864886f70d01090e' === Enc.bufToHex(seq.children[0].value)) {
'2a864886f70d01090e' === Enc.bufToHex(seq.children[0].value)
) {
return true; return true;
} }
}) })
@ -315,12 +312,12 @@
} }
}) })
.map(function(seq2) { .map(function(seq2) {
return seq2.children[1].children[0].children.map( return seq2.children[1].children[0].children.map(function(
function(name) { name
) {
// TODO utf8 // TODO utf8
return Enc.bufToBin(name.value); return Enc.bufToBin(name.value);
} });
);
})[0]; })[0];
})[0]; })[0];
@ -330,73 +327,4 @@
jwk: pub, jwk: pub,
signature: sig signature: sig
}; };
}; };
X509.packCsrRsaPublicKey = function(jwk) {
// Sequence the key
var n = ASN1.UInt(Enc.base64ToHex(jwk.n));
var e = ASN1.UInt(Enc.base64ToHex(jwk.e));
var asn1pub = ASN1('30', n, e);
// Add the CSR pub key header
return ASN1(
'30',
ASN1('30', ASN1('06', '2a864886f70d010101'), ASN1('05')),
ASN1.BitStr(asn1pub)
);
};
X509.packCsrEcPublicKey = function(jwk) {
var ecOid = X509._oids[jwk.crv];
if (!ecOid) {
throw new Error(
"Unsupported namedCurve '" +
jwk.crv +
"'. Supported types are " +
Object.keys(X509._oids)
);
}
var cmp = '04'; // 04 == x+y, 02 == x-only
var hxy = '';
// Placeholder. I'm not even sure if compression should be supported.
if (!jwk.y) {
cmp = '02';
}
hxy += Enc.base64ToHex(jwk.x);
if (jwk.y) {
hxy += Enc.base64ToHex(jwk.y);
}
// 1.2.840.10045.2.1 ecPublicKey
return ASN1(
'30',
ASN1('30', ASN1('06', '2a8648ce3d0201'), ASN1('06', ecOid)),
ASN1.BitStr(cmp + hxy)
);
};
X509._oids = {
// 1.2.840.10045.3.1.7 prime256v1
// (ANSI X9.62 named elliptic curve) (06 08 - 2A 86 48 CE 3D 03 01 07)
'P-256': '2a8648ce3d030107',
// 1.3.132.0.34 P-384 (06 05 - 2B 81 04 00 22)
// (SEC 2 recommended EC domain secp256r1)
'P-384': '2b81040022'
// requires more logic and isn't a recommended standard
// 1.3.132.0.35 P-521 (06 05 - 2B 81 04 00 23)
// (SEC 2 alternate P-521)
//, 'P-521': '2B 81 04 00 23'
};
// don't replace the full parseBlock, if it exists
PEM.parseBlock =
PEM.parseBlock ||
function(str) {
var der = str
.split(/\n/)
.filter(function(line) {
return !/-----/.test(line);
})
.join('');
return { bytes: Enc.base64ToBuf(der) };
};
})('undefined' === typeof window ? module.exports : window);

View File

@ -1,73 +1,34 @@
/*global Promise*/ /*global Promise*/
(function(exports) { 'use strict';
'use strict';
var EC = (exports.Eckles = {}); var EC = module.exports;
var x509 = exports.x509; var native = require('./node/ecdsa.js');
if ('undefined' !== typeof module) {
module.exports = EC;
}
var PEM = exports.PEM;
var SSH = exports.SSH;
var Enc = {};
var textEncoder = new TextEncoder();
EC._stance = // TODO SSH
var SSH;
var x509 = require('./x509.js');
var PEM = require('./pem.js');
//var SSH = require('./ssh-keys.js');
var Enc = require('omnibuffer');
var sha2 = require('./node/sha2.js');
// 1.2.840.10045.3.1.7
// prime256v1 (ANSI X9.62 named elliptic curve)
var OBJ_ID_EC = '06 08 2A8648CE3D030107'.replace(/\s+/g, '').toLowerCase();
// 1.3.132.0.34
// secp384r1 (SECG (Certicom) named elliptic curve)
var OBJ_ID_EC_384 = '06 05 2B81040022'.replace(/\s+/g, '').toLowerCase();
EC._stance =
"We take the stance that if you're knowledgeable enough to" + "We take the stance that if you're knowledgeable enough to" +
" properly and securely use non-standard crypto then you shouldn't need Bluecrypt anyway."; " properly and securely use non-standard crypto then you shouldn't need Bluecrypt anyway.";
EC._universal = native._stance = EC._stance;
EC._universal =
'Bluecrypt only supports crypto with standard cross-browser and cross-platform support.'; 'Bluecrypt only supports crypto with standard cross-browser and cross-platform support.';
EC.generate = function(opts) { EC.generate = native.generate;
var wcOpts = {};
if (!opts) {
opts = {};
}
if (!opts.kty) {
opts.kty = 'EC';
}
// ECDSA has only the P curves and an associated bitlength EC.export = function(opts) {
wcOpts.name = 'ECDSA';
if (!opts.namedCurve) {
opts.namedCurve = 'P-256';
}
wcOpts.namedCurve = opts.namedCurve; // true for supported curves
if (/256/.test(wcOpts.namedCurve)) {
wcOpts.namedCurve = 'P-256';
wcOpts.hash = { name: 'SHA-256' };
} else if (/384/.test(wcOpts.namedCurve)) {
wcOpts.namedCurve = 'P-384';
wcOpts.hash = { name: 'SHA-384' };
} else {
return Promise.Reject(
new Error(
"'" +
wcOpts.namedCurve +
"' is not an NIST approved ECDSA namedCurve. " +
" Please choose either 'P-256' or 'P-384'. " +
EC._stance
)
);
}
var extractable = true;
return window.crypto.subtle
.generateKey(wcOpts, extractable, ['sign', 'verify'])
.then(function(result) {
return window.crypto.subtle
.exportKey('jwk', result.privateKey)
.then(function(privJwk) {
privJwk.key_ops = undefined;
privJwk.ext = undefined;
return {
private: privJwk,
public: EC.neuter({ jwk: privJwk })
};
});
});
};
EC.export = function(opts) {
return Promise.resolve().then(function() { return Promise.resolve().then(function() {
if (!opts || !opts.jwk || 'object' !== typeof opts.jwk) { if (!opts || !opts.jwk || 'object' !== typeof opts.jwk) {
throw new Error('must pass { jwk: jwk } as a JSON object'); throw new Error('must pass { jwk: jwk } as a JSON object');
@ -144,16 +105,86 @@
); );
} }
}); });
}; };
EC.pack = function(opts) { native.export = EC.export;
return Promise.resolve().then(function() {
return EC.exportSync(opts);
});
};
// Chopping off the private parts is now part of the public API. EC.import = function(opts) {
// I thought it sounded a little too crude at first, but it really is the best name in every possible way. return Promise.resolve().then(function() {
EC.neuter = function(opts) { if (!opts || !opts.pem || 'string' !== typeof opts.pem) {
throw new Error('must pass { pem: pem } as a string');
}
if (0 === opts.pem.indexOf('ecdsa-sha2-')) {
return SSH.parseSsh(opts.pem);
}
var pem = opts.pem;
var u8 = PEM.parseBlock(pem).bytes;
var hex = Enc.bufToHex(u8);
var jwk = { kty: 'EC', crv: null, x: null, y: null };
//console.log();
if (
-1 !== hex.indexOf(OBJ_ID_EC) ||
-1 !== hex.indexOf(OBJ_ID_EC_384)
) {
if (-1 !== hex.indexOf(OBJ_ID_EC_384)) {
jwk.crv = 'P-384';
} else {
jwk.crv = 'P-256';
}
// PKCS8
if (0x02 === u8[3] && 0x30 === u8[6] && 0x06 === u8[8]) {
//console.log("PKCS8", u8[3].toString(16), u8[6].toString(16), u8[8].toString(16));
jwk = x509.parsePkcs8(u8, jwk);
// EC-only
} else if (0x02 === u8[2] && 0x04 === u8[5] && 0xa0 === u8[39]) {
//console.log("EC---", u8[2].toString(16), u8[5].toString(16), u8[39].toString(16));
jwk = x509.parseSec1(u8, jwk);
// EC-only
} else if (0x02 === u8[3] && 0x04 === u8[6] && 0xa0 === u8[56]) {
//console.log("EC---", u8[3].toString(16), u8[6].toString(16), u8[56].toString(16));
jwk = x509.parseSec1(u8, jwk);
// SPKI/PKIK (Public)
} else if (0x30 === u8[2] && 0x06 === u8[4] && 0x06 === u8[13]) {
//console.log("SPKI-", u8[2].toString(16), u8[4].toString(16), u8[13].toString(16));
jwk = x509.parseSpki(u8, jwk);
// Error
} else {
//console.log("PKCS8", u8[3].toString(16), u8[6].toString(16), u8[8].toString(16));
//console.log("EC---", u8[2].toString(16), u8[5].toString(16), u8[39].toString(16));
//console.log("EC---", u8[3].toString(16), u8[6].toString(16), u8[56].toString(16));
//console.log("SPKI-", u8[2].toString(16), u8[4].toString(16), u8[13].toString(16));
throw new Error('unrecognized key format');
}
} else {
throw new Error('Supported key types are P-256 and P-384');
}
if (opts.public) {
if (true !== opts.public) {
throw new Error(
'options.public must be either `true` or `false` not (' +
typeof opts.public +
") '" +
opts.public +
"'"
);
}
delete jwk.d;
}
return jwk;
});
};
native.import = EC.import;
EC.pack = function(opts) {
return Promise.resolve().then(function() {
return EC.export(opts);
});
};
// Chopping off the private parts is now part of the public API.
// I thought it sounded a little too crude at first, but it really is the best name in every possible way.
EC.neuter = function(opts) {
// trying to find the best balance of an immutable copy with custom attributes // trying to find the best balance of an immutable copy with custom attributes
var jwk = {}; var jwk = {};
Object.keys(opts.jwk).forEach(function(k) { Object.keys(opts.jwk).forEach(function(k) {
@ -167,34 +198,32 @@
jwk[k] = JSON.parse(JSON.stringify(opts.jwk[k])); jwk[k] = JSON.parse(JSON.stringify(opts.jwk[k]));
}); });
return jwk; return jwk;
}; };
native.neuter = EC.neuter;
// https://stackoverflow.com/questions/42588786/how-to-fingerprint-a-jwk // https://stackoverflow.com/questions/42588786/how-to-fingerprint-a-jwk
EC.__thumbprint = function(jwk) { EC.__thumbprint = function(jwk) {
// Use the same entropy for SHA as for key // Use the same entropy for SHA as for key
var alg = 'SHA-256'; var alg = 'SHA-256';
if (/384/.test(jwk.crv)) { if (/384/.test(jwk.crv)) {
alg = 'SHA-384'; alg = 'SHA-384';
} }
return window.crypto.subtle var payload =
.digest(
{ name: alg },
textEncoder.encode(
'{"crv":"' + '{"crv":"' +
jwk.crv + jwk.crv +
'","kty":"EC","x":"' + '","kty":"EC","x":"' +
jwk.x + jwk.x +
'","y":"' + '","y":"' +
jwk.y + jwk.y +
'"}' '"}';
) console.log('[debug] EC', alg, payload);
) return sha2.sum(alg, payload).then(function(hash) {
.then(function(hash) { console.log('[debug] EC hash', hash);
return Enc.bufToUrlBase64(new Uint8Array(hash)); return Enc.bufToUrlBase64(Uint8Array.from(hash));
}); });
}; };
EC.thumbprint = function(opts) { EC.thumbprint = function(opts) {
return Promise.resolve().then(function() { return Promise.resolve().then(function() {
var jwk; var jwk;
if ('EC' === opts.kty) { if ('EC' === opts.kty) {
@ -202,26 +231,10 @@
} else if (opts.jwk) { } else if (opts.jwk) {
jwk = opts.jwk; jwk = opts.jwk;
} else { } else {
return EC.import(opts).then(function(jwk) { return native.import(opts).then(function(jwk) {
return EC.__thumbprint(jwk); return EC.__thumbprint(jwk);
}); });
} }
return EC.__thumbprint(jwk); return EC.__thumbprint(jwk);
}); });
}; };
Enc.bufToUrlBase64 = function(u8) {
return Enc.bufToBase64(u8)
.replace(/\+/g, '-')
.replace(/\//g, '_')
.replace(/=/g, '');
};
Enc.bufToBase64 = function(u8) {
var bin = '';
u8.forEach(function(i) {
bin += String.fromCharCode(i);
});
return btoa(bin);
};
})('undefined' !== typeof module ? module.exports : window);

View File

@ -1,18 +1,18 @@
/*global Promise*/ /*global Promise*/
(function(exports) { 'use strict';
'use strict';
var Keypairs = (exports.Keypairs = {}); var Keypairs = module.exports;
var Rasha = exports.Rasha; var Rasha = require('./rsa.js');
var Eckles = exports.Eckles; var Eckles = require('./ecdsa.js');
var Enc = exports.Enc || {}; var native = require('./node/keypairs.js');
var Enc = require('omnibuffer');
Keypairs._stance = Keypairs._stance =
"We take the stance that if you're knowledgeable enough to" + "We take the stance that if you're knowledgeable enough to" +
" properly and securely use non-standard crypto then you shouldn't need Bluecrypt anyway."; " properly and securely use non-standard crypto then you shouldn't need Bluecrypt anyway.";
Keypairs._universal = Keypairs._universal =
'Bluecrypt only supports crypto with standard cross-browser and cross-platform support.'; 'Bluecrypt only supports crypto with standard cross-browser and cross-platform support.';
Keypairs.generate = function(opts) { Keypairs.generate = function(opts) {
opts = opts || {}; opts = opts || {};
var p; var p;
if (!opts.kty) { if (!opts.kty) {
@ -37,29 +37,29 @@
); );
} }
return p.then(function(pair) { return p.then(function(pair) {
return Keypairs.thumbprint({ jwk: pair.public }).then(function( return Keypairs.thumbprint({ jwk: pair.public }).then(function(thumb) {
thumb
) {
pair.private.kid = thumb; // maybe not the same id on the private key? pair.private.kid = thumb; // maybe not the same id on the private key?
pair.public.kid = thumb; pair.public.kid = thumb;
return pair; return pair;
}); });
}); });
}; };
Keypairs.export = function(opts) { Keypairs.export = function(opts) {
return Eckles.export(opts).catch(function(err) { return Eckles.export(opts).catch(function(err) {
return Rasha.export(opts).catch(function() { return Rasha.export(opts).catch(function() {
return Promise.reject(err); return Promise.reject(err);
}); });
}); });
}; };
// XXX
native.export = Keypairs.export;
/** /**
* Chopping off the private parts is now part of the public API. * Chopping off the private parts is now part of the public API.
* I thought it sounded a little too crude at first, but it really is the best name in every possible way. * I thought it sounded a little too crude at first, but it really is the best name in every possible way.
*/ */
Keypairs.neuter = function(opts) { Keypairs.neuter = function(opts) {
/** trying to find the best balance of an immutable copy with custom attributes */ /** trying to find the best balance of an immutable copy with custom attributes */
var jwk = {}; var jwk = {};
Object.keys(opts.jwk).forEach(function(k) { Object.keys(opts.jwk).forEach(function(k) {
@ -73,19 +73,21 @@
jwk[k] = JSON.parse(JSON.stringify(opts.jwk[k])); jwk[k] = JSON.parse(JSON.stringify(opts.jwk[k]));
}); });
return jwk; return jwk;
}; };
Keypairs.thumbprint = function(opts) { Keypairs.thumbprint = function(opts) {
return Promise.resolve().then(function() { return Promise.resolve().then(function() {
if (/EC/i.test(opts.jwk.kty)) { if (/EC/i.test(opts.jwk.kty)) {
console.log('[debug] EC thumbprint');
return Eckles.thumbprint(opts); return Eckles.thumbprint(opts);
} else { } else {
console.log('[debug] RSA thumbprint');
return Rasha.thumbprint(opts); return Rasha.thumbprint(opts);
} }
}); });
}; };
Keypairs.publish = function(opts) { Keypairs.publish = function(opts) {
if ('object' !== typeof opts.jwk || !opts.jwk.kty) { if ('object' !== typeof opts.jwk || !opts.jwk.kty) {
throw new Error('invalid jwk: ' + JSON.stringify(opts.jwk)); throw new Error('invalid jwk: ' + JSON.stringify(opts.jwk));
} }
@ -115,16 +117,16 @@
jwk.kid = thumb; jwk.kid = thumb;
return jwk; return jwk;
}); });
}; };
// JWT a.k.a. JWS with Claims using Compact Serialization // JWT a.k.a. JWS with Claims using Compact Serialization
Keypairs.signJwt = function(opts) { Keypairs.signJwt = function(opts) {
return Keypairs.thumbprint({ jwk: opts.jwk }).then(function(thumb) { return Keypairs.thumbprint({ jwk: opts.jwk }).then(function(thumb) {
var header = opts.header || {}; var header = opts.header || {};
var claims = JSON.parse(JSON.stringify(opts.claims || {})); var claims = JSON.parse(JSON.stringify(opts.claims || {}));
header.typ = 'JWT'; header.typ = 'JWT';
if (!header.kid) { if (!header.kid && false !== header.kid) {
header.kid = thumb; header.kid = thumb;
} }
if (!header.alg && opts.alg) { if (!header.alg && opts.alg) {
@ -170,15 +172,13 @@
return [jws.protected, jws.payload, jws.signature].join('.'); return [jws.protected, jws.payload, jws.signature].join('.');
}); });
}); });
}; };
Keypairs.signJws = function(opts) { Keypairs.signJws = function(opts) {
return Keypairs.thumbprint(opts).then(function(thumb) { return Keypairs.thumbprint(opts).then(function(thumb) {
function alg() { function alg() {
if (!opts.jwk) { if (!opts.jwk) {
throw new Error( throw new Error("opts.jwk must exist and must declare 'typ'");
"opts.jwk must exist and must declare 'typ'"
);
} }
if (opts.jwk.alg) { if (opts.jwk.alg) {
return opts.jwk.alg; return opts.jwk.alg;
@ -229,12 +229,11 @@
payload = Enc.binToBuf(payload); payload = Enc.binToBuf(payload);
} }
// node specifies RSA-SHAxxx even when it's actually ecdsa (it's all encoded x509 shasums anyway)
var protected64 = Enc.strToUrlBase64(protectedHeader); var protected64 = Enc.strToUrlBase64(protectedHeader);
var payload64 = Enc.bufToUrlBase64(payload); var payload64 = Enc.bufToUrlBase64(payload);
var msg = protected64 + '.' + payload64; var msg = protected64 + '.' + payload64;
return Keypairs._sign(opts, msg).then(function(buf) { return native._sign(opts, msg).then(function(buf) {
var signedMsg = { var signedMsg = {
protected: protected64, protected: protected64,
payload: payload64, payload: payload64,
@ -254,38 +253,9 @@
}); });
} }
}); });
}; };
Keypairs._sign = function(opts, payload) { Keypairs._getBits = function(opts) {
return Keypairs._import(opts).then(function(privkey) {
if ('string' === typeof payload) {
payload = new TextEncoder().encode(payload);
}
return window.crypto.subtle
.sign(
{
name: Keypairs._getName(opts),
hash: { name: 'SHA-' + Keypairs._getBits(opts) }
},
privkey,
payload
)
.then(function(signature) {
signature = new Uint8Array(signature); // ArrayBuffer -> u8
// This will come back into play for CSRs, but not for JOSE
if (
'EC' === opts.jwk.kty &&
/x509|asn1/i.test(opts.format)
) {
return Keypairs._ecdsaJoseSigToAsn1Sig(signature);
} else {
// jose/jws/jwt
return signature;
}
});
});
};
Keypairs._getBits = function(opts) {
if (opts.alg) { if (opts.alg) {
return opts.alg.replace(/[a-z\-]/gi, ''); return opts.alg.replace(/[a-z\-]/gi, '');
} }
@ -301,83 +271,11 @@
} }
return '256'; return '256';
}; };
Keypairs._getName = function(opts) { // XXX
if (/EC/i.test(opts.jwk.kty)) { native._getBits = Keypairs._getBits;
return 'ECDSA';
} else {
return 'RSASSA-PKCS1-v1_5';
}
};
Keypairs._import = function(opts) {
return Promise.resolve().then(function() {
var ops;
// all private keys just happen to have a 'd'
if (opts.jwk.d) {
ops = ['sign'];
} else {
ops = ['verify'];
}
// gotta mark it as extractable, as if it matters
opts.jwk.ext = true;
opts.jwk.key_ops = ops;
return window.crypto.subtle function setTime(time) {
.importKey(
'jwk',
opts.jwk,
{
name: Keypairs._getName(opts),
namedCurve: opts.jwk.crv,
hash: { name: 'SHA-' + Keypairs._getBits(opts) }
},
true,
ops
)
.then(function(privkey) {
delete opts.jwk.ext;
return privkey;
});
});
};
// ECDSA JOSE / JWS / JWT signatures differ from "normal" ASN1/X509 ECDSA signatures
// https://tools.ietf.org/html/rfc7518#section-3.4
Keypairs._ecdsaJoseSigToAsn1Sig = function(bufsig) {
// it's easier to do the manipulation in the browser with an array
bufsig = Array.from(bufsig);
var hlen = bufsig.length / 2; // should be even
var r = bufsig.slice(0, hlen);
var s = bufsig.slice(hlen);
// unpad positive ints less than 32 bytes wide
while (!r[0]) {
r = r.slice(1);
}
while (!s[0]) {
s = s.slice(1);
}
// pad (or re-pad) ambiguously non-negative BigInts, up to 33 bytes wide
if (0x80 & r[0]) {
r.unshift(0);
}
if (0x80 & s[0]) {
s.unshift(0);
}
var len = 2 + r.length + 2 + s.length;
var head = [0x30];
// hard code 0x80 + 1 because it won't be longer than
// two SHA512 plus two pad bytes (130 bytes <= 256)
if (len >= 0x80) {
head.push(0x81);
}
head.push(len);
return Uint8Array.from(
head.concat([0x02, r.length], r, [0x02, s.length], s)
);
};
function setTime(time) {
if ('number' === typeof time) { if ('number' === typeof time) {
return time; return time;
} }
@ -411,22 +309,21 @@
} }
return now + mult * num; return now + mult * num;
} }
Enc.hexToBuf = function(hex) { Enc.hexToBuf = function(hex) {
var arr = []; var arr = [];
hex.match(/.{2}/g).forEach(function(h) { hex.match(/.{2}/g).forEach(function(h) {
arr.push(parseInt(h, 16)); arr.push(parseInt(h, 16));
}); });
return 'undefined' !== typeof Uint8Array ? new Uint8Array(arr) : arr; return 'undefined' !== typeof Uint8Array ? new Uint8Array(arr) : arr;
}; };
Enc.strToUrlBase64 = function(str) { Enc.strToUrlBase64 = function(str) {
return Enc.bufToUrlBase64(Enc.binToBuf(str)); return Enc.bufToUrlBase64(Enc.binToBuf(str));
}; };
Enc.binToBuf = function(bin) { Enc.binToBuf = function(bin) {
var arr = bin.split('').map(function(ch) { var arr = bin.split('').map(function(ch) {
return ch.charCodeAt(0); return ch.charCodeAt(0);
}); });
return 'undefined' !== typeof Uint8Array ? new Uint8Array(arr) : arr; return 'undefined' !== typeof Uint8Array ? new Uint8Array(arr) : arr;
}; };
})('undefined' !== typeof module ? module.exports : window);

113
lib/node/ecdsa.js Normal file
View File

@ -0,0 +1,113 @@
'use strict';
var native = module.exports;
// XXX provided by caller: import, export
var EC = native;
// TODO SSH
native.generate = function(opts) {
return Promise.resolve().then(function() {
var typ = 'ec';
var format = opts.format;
var encoding = opts.encoding;
var priv;
var pub = 'spki';
if (!format) {
format = 'jwk';
}
if (-1 !== ['spki', 'pkcs8', 'ssh'].indexOf(format)) {
format = 'pkcs8';
}
if ('pem' === format) {
format = 'sec1';
encoding = 'pem';
} else if ('der' === format) {
format = 'sec1';
encoding = 'der';
}
if ('jwk' === format || 'json' === format) {
format = 'jwk';
encoding = 'json';
} else {
priv = format;
}
if (!encoding) {
encoding = 'pem';
}
if (priv) {
priv = { type: priv, format: encoding };
pub = { type: pub, format: encoding };
} else {
// jwk
priv = { type: 'sec1', format: 'pem' };
pub = { type: 'spki', format: 'pem' };
}
return new Promise(function(resolve, reject) {
return require('crypto').generateKeyPair(
typ,
{
namedCurve: opts.crv || opts.namedCurve || 'P-256',
privateKeyEncoding: priv,
publicKeyEncoding: pub
},
function(err, pubkey, privkey) {
if (err) {
reject(err);
}
resolve({
private: privkey,
public: pubkey
});
}
);
}).then(function(keypair) {
if ('jwk' === format) {
return Promise.all([
native.import({
pem: keypair.private,
format: priv.type
}),
native.import({
pem: keypair.public,
format: pub.type,
public: true
})
]).then(function(pair) {
return {
private: pair[0],
public: pair[1]
};
});
}
if ('ssh' !== opts.format) {
return keypair;
}
return native
.import({
pem: keypair.public,
format: format,
public: true
})
.then(function(jwk) {
return EC.export({
jwk: jwk,
format: opts.format,
public: true
}).then(function(pub) {
return {
private: keypair.private,
public: pub
};
});
});
});
});
};

View File

@ -0,0 +1,53 @@
// Copyright 2016-2018 AJ ONeal. All rights reserved
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
'use strict';
module.exports = function (bitlen, exp) {
var k = require('node-forge').pki.rsa
.generateKeyPair({ bits: bitlen || 2048, e: exp || 0x10001 }).privateKey;
var jwk = {
kty: "RSA"
, n: _toUrlBase64(k.n)
, e: _toUrlBase64(k.e)
, d: _toUrlBase64(k.d)
, p: _toUrlBase64(k.p)
, q: _toUrlBase64(k.q)
, dp: _toUrlBase64(k.dP)
, dq: _toUrlBase64(k.dQ)
, qi: _toUrlBase64(k.qInv)
};
return {
private: jwk
, public: {
kty: jwk.kty
, n: jwk.n
, e: jwk.e
}
};
};
function _toUrlBase64(fbn) {
var hex = fbn.toRadix(16);
if (hex.length % 2) {
// Invalid hex string
hex = '0' + hex;
}
while ('00' === hex.slice(0, 2)) {
hex = hex.slice(2);
}
return Buffer.from(hex, 'hex').toString('base64')
.replace(/\+/g, "-")
.replace(/\//g, "_")
.replace(/=/g,"")
;
}
if (require.main === module) {
var keypair = module.exports(2048, 0x10001);
console.info(keypair.private);
console.warn(keypair.public);
//console.info(keypair.privateKeyJwk);
//console.warn(keypair.publicKeyJwk);
}

View File

@ -0,0 +1,23 @@
// Copyright 2016-2018 AJ ONeal. All rights reserved
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
'use strict';
module.exports = function (bitlen, exp) {
var keypair = require('crypto').generateKeyPairSync(
'rsa'
, { modulusLength: bitlen
, publicExponent: exp
, privateKeyEncoding: { type: 'pkcs1', format: 'pem' }
, publicKeyEncoding: { type: 'pkcs1', format: 'pem' }
}
);
var result = { privateKeyPem: keypair.privateKey.trim() };
return result;
};
if (require.main === module) {
var keypair = module.exports(2048, 0x10001);
console.info(keypair.privateKeyPem);
}

View File

@ -0,0 +1,22 @@
// Copyright 2016-2018 AJ ONeal. All rights reserved
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
'use strict';
module.exports = function (bitlen, exp) {
var ursa;
try {
ursa = require('ursa');
} catch(e) {
ursa = require('ursa-optional');
}
var keypair = ursa.generatePrivateKey(bitlen, exp);
var result = { privateKeyPem: keypair.toPrivatePem().toString('ascii').trim() };
return result;
};
if (require.main === module) {
var keypair = module.exports(2048, 0x10001);
console.info(keypair.privateKeyPem);
}

View File

@ -0,0 +1,64 @@
// Copyright 2016-2018 AJ ONeal. All rights reserved
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
'use strict';
var oldver = false;
module.exports = function (bitlen, exp) {
bitlen = parseInt(bitlen, 10) || 2048;
exp = parseInt(exp, 10) || 65537;
try {
return require('./generate-privkey-node.js')(bitlen, exp);
} catch(e) {
if (!/generateKeyPairSync is not a function/.test(e.message)) {
throw e;
}
try {
return require('./generate-privkey-ursa.js')(bitlen, exp);
} catch(e) {
if (e.code !== 'MODULE_NOT_FOUND') {
console.error("[rsa-compat] Unexpected error when using 'ursa':");
console.error(e);
}
if (!oldver) {
oldver = true;
console.warn("[WARN] rsa-compat: Your version of node does not have crypto.generateKeyPair()");
console.warn("[WARN] rsa-compat: Please update to node >= v10.12 or 'npm install --save ursa node-forge'");
console.warn("[WARN] rsa-compat: Using node-forge as a fallback may be unacceptably slow.");
if (/arm|mips/i.test(require('os').arch)) {
console.warn("================================================================");
console.warn(" WARNING");
console.warn("================================================================");
console.warn("");
console.warn("WARNING: You are generating an RSA key using pure JavaScript on");
console.warn(" a VERY SLOW cpu. This could take DOZENS of minutes!");
console.warn("");
console.warn(" We recommend installing node >= v10.12, or 'gcc' and 'ursa'");
console.warn("");
console.warn("EXAMPLE:");
console.warn("");
console.warn(" sudo apt-get install build-essential && npm install ursa");
console.warn("");
console.warn("================================================================");
}
}
try {
return require('./generate-privkey-forge.js')(bitlen, exp);
} catch(e) {
if (e.code !== 'MODULE_NOT_FOUND') {
throw e;
}
console.error("[ERROR] rsa-compat: could not generate a private key.");
console.error("None of crypto.generateKeyPair, ursa, nor node-forge are present");
}
}
}
};
if (require.main === module) {
var keypair = module.exports(2048, 0x10001);
console.info(keypair.privateKeyPem);
}

19
lib/node/http.js Normal file
View File

@ -0,0 +1,19 @@
'use strict';
var http = module.exports;
var promisify = require('util').promisify;
var request = promisify(require('@root/request'));
http.request = function(opts) {
if (!opts.headers) {
opts.headers = {};
}
if (
!Object.keys(opts.headers).some(function(key) {
return 'user-agent' === key.toLowerCase();
})
) {
// TODO opts.headers['User-Agent'] = 'TODO';
}
return request(opts);
};

84
lib/node/keypairs.js Normal file
View File

@ -0,0 +1,84 @@
'use strict';
var Keypairs = module.exports;
var crypto = require('crypto');
Keypairs._sign = function(opts, payload) {
return Keypairs._import(opts).then(function(pem) {
payload = Buffer.from(payload);
// node specifies RSA-SHAxxx even when it's actually ecdsa (it's all encoded x509 shasums anyway)
// TODO opts.alg = (protect||header).alg
var nodeAlg = 'SHA' + Keypairs._getBits(opts);
var binsig = crypto
.createSign(nodeAlg)
.update(payload)
.sign(pem);
if ('EC' === opts.jwk.kty) {
// ECDSA JWT signatures differ from "normal" ECDSA signatures
// https://tools.ietf.org/html/rfc7518#section-3.4
binsig = Keypairs._ecdsaAsn1SigToJoseSig(binsig);
}
return binsig;
});
};
Keypairs._import = function(opts) {
if (opts.pem && opts.jwk) {
return Promise.resolve(opts.pem);
} else {
// XXX added by caller
return Keypairs.export({ jwk: opts.jwk });
}
};
Keypairs._ecdsaAsn1SigToJoseSig = function(binsig) {
// should have asn1 sequence header of 0x30
if (0x30 !== binsig[0]) {
throw new Error('Impossible EC SHA head marker');
}
var index = 2; // first ecdsa "R" header byte
var len = binsig[1];
var lenlen = 0;
// Seek length of length if length is greater than 127 (i.e. two 512-bit / 64-byte R and S values)
if (0x80 & len) {
lenlen = len - 0x80; // should be exactly 1
len = binsig[2]; // should be <= 130 (two 64-bit SHA-512s, plus padding)
index += lenlen;
}
// should be of BigInt type
if (0x02 !== binsig[index]) {
throw new Error('Impossible EC SHA R marker');
}
index += 1;
var rlen = binsig[index];
var bits = 32;
if (rlen > 49) {
bits = 64;
} else if (rlen > 33) {
bits = 48;
}
var r = binsig.slice(index + 1, index + 1 + rlen).toString('hex');
var slen = binsig[index + 1 + rlen + 1]; // skip header and read length
var s = binsig.slice(index + 1 + rlen + 1 + 1).toString('hex');
if (2 * slen !== s.length) {
throw new Error('Impossible EC SHA S length');
}
// There may be one byte of padding on either
while (r.length < 2 * bits) {
r = '00' + r;
}
while (s.length < 2 * bits) {
s = '00' + s;
}
if (2 * (bits + 1) === r.length) {
r = r.slice(2);
}
if (2 * (bits + 1) === s.length) {
s = s.slice(2);
}
return Buffer.concat([Buffer.from(r, 'hex'), Buffer.from(s, 'hex')]);
};

154
lib/node/rsa.js Normal file
View File

@ -0,0 +1,154 @@
'use strict';
var native = module.exports;
// XXX provided by caller: export
var RSA = native;
var PEM = require('../pem.js');
var x509 = require('../x509.js');
var ASN1 = require('../asn1/parser.js');
native.generate = function(opts) {
opts.kty = 'RSA';
return native._generate(opts).then(function(pair) {
var format = opts.format;
var encoding = opts.encoding;
// The easy way
if ('json' === format && !encoding) {
format = 'jwk';
encoding = 'json';
}
if (
('jwk' === format || !format) &&
('json' === encoding || !encoding)
) {
return pair;
}
if ('jwk' === format || 'json' === encoding) {
throw new Error(
"format '" +
format +
"' is incompatible with encoding '" +
encoding +
"'"
);
}
// The... less easy way
/*
var priv;
var pub;
if ('spki' === format || 'pkcs8' === format) {
format = 'pkcs8';
pub = 'spki';
}
if ('pem' === format) {
format = 'pkcs1';
encoding = 'pem';
} else if ('der' === format) {
format = 'pkcs1';
encoding = 'der';
}
priv = format;
pub = pub || format;
if (!encoding) {
encoding = 'pem';
}
if (priv) {
priv = { type: priv, format: encoding };
pub = { type: pub, format: encoding };
} else {
// jwk
priv = { type: 'pkcs1', format: 'pem' };
pub = { type: 'pkcs1', format: 'pem' };
}
*/
if (('pem' === format || 'der' === format) && !encoding) {
encoding = format;
format = 'pkcs1';
}
var exOpts = { jwk: pair.private, format: format, encoding: encoding };
return RSA.export(exOpts).then(function(priv) {
exOpts.public = true;
if ('pkcs8' === exOpts.format) {
exOpts.format = 'spki';
}
return RSA.export(exOpts).then(function(pub) {
return { private: priv, public: pub };
});
});
});
};
native._generate = function(opts) {
if (!opts) {
opts = {};
}
return new Promise(function(resolve, reject) {
try {
var modlen = opts.modulusLength || 2048;
var exp = opts.publicExponent || 0x10001;
var pair = require('./generate-privkey.js')(modlen, exp);
if (pair.private) {
resolve(pair);
return;
}
pair = toJwks(pair);
resolve({ private: pair.private, public: pair.public });
} catch (e) {
reject(e);
}
});
};
// PKCS1 to JWK only
function toJwks(oldpair) {
var block = PEM.parseBlock(oldpair.privateKeyPem);
var asn1 = ASN1.parse(block.bytes);
var jwk = { kty: 'RSA', n: null, e: null };
jwk = x509.parsePkcs1(block.bytes, asn1, jwk);
return { private: jwk, public: RSA.neuter({ jwk: jwk }) };
}
// TODO
var Enc = require('omnibuffer');
x509.parsePkcs1 = function parseRsaPkcs1(buf, asn1, jwk) {
if (
!asn1.children.every(function(el) {
return 0x02 === el.type;
})
) {
throw new Error(
'not an RSA PKCS#1 public or private key (not all ints)'
);
}
if (2 === asn1.children.length) {
jwk.n = Enc.bufToUrlBase64(asn1.children[0].value);
jwk.e = Enc.bufToUrlBase64(asn1.children[1].value);
return jwk;
} else if (asn1.children.length >= 9) {
// the standard allows for "otherPrimeInfos", hence at least 9
jwk.n = Enc.bufToUrlBase64(asn1.children[1].value);
jwk.e = Enc.bufToUrlBase64(asn1.children[2].value);
jwk.d = Enc.bufToUrlBase64(asn1.children[3].value);
jwk.p = Enc.bufToUrlBase64(asn1.children[4].value);
jwk.q = Enc.bufToUrlBase64(asn1.children[5].value);
jwk.dp = Enc.bufToUrlBase64(asn1.children[6].value);
jwk.dq = Enc.bufToUrlBase64(asn1.children[7].value);
jwk.qi = Enc.bufToUrlBase64(asn1.children[8].value);
return jwk;
} else {
throw new Error(
'not an RSA PKCS#1 public or private key (wrong number of ints)'
);
}
};

17
lib/node/sha2.js Normal file
View File

@ -0,0 +1,17 @@
/* global Promise */
'use strict';
var sha2 = module.exports;
var crypto = require('crypto');
sha2.sum = function(alg, str) {
return Promise.resolve().then(function() {
var sha = 'sha' + String(alg).replace(/^sha-?/i, '');
// utf8 is the default for strings
var buf = Buffer.from(str);
return crypto
.createHash(sha)
.update(buf)
.digest();
});
};

33
lib/pem.js Normal file
View File

@ -0,0 +1,33 @@
'use strict';
var PEM = module.exports;
var Enc = require('omnibuffer');
PEM.packBlock = function(opts) {
// TODO allow for headers?
return (
'-----BEGIN ' +
opts.type +
'-----\n' +
Enc.bufToBase64(opts.bytes)
.match(/.{1,64}/g)
.join('\n') +
'\n' +
'-----END ' +
opts.type +
'-----'
);
};
// don't replace the full parseBlock, if it exists
PEM.parseBlock =
PEM.parseBlock ||
function(str) {
var der = str
.split(/\n/)
.filter(function(line) {
return !/-----/.test(line);
})
.join('');
return { bytes: Enc.base64ToBuf(der) };
};

View File

@ -1,82 +1,26 @@
/*global Promise*/ /*global Promise*/
(function(exports) { 'use strict';
'use strict';
var RSA = (exports.Rasha = {}); var RSA = module.exports;
var x509 = exports.x509; var native = require('./node/rsa.js');
if ('undefined' !== typeof module) { var x509 = require('./x509.js');
module.exports = RSA; var PEM = require('./pem.js');
} //var SSH = require('./ssh-keys.js');
var PEM = exports.PEM; var sha2 = require('./node/sha2.js');
var SSH = exports.SSH; var Enc = require('omnibuffer');
var Enc = {};
var textEncoder = new TextEncoder();
RSA._stance = RSA._universal =
'Bluecrypt only supports crypto with standard cross-browser and cross-platform support.';
RSA._stance =
"We take the stance that if you're knowledgeable enough to" + "We take the stance that if you're knowledgeable enough to" +
" properly and securely use non-standard crypto then you shouldn't need Bluecrypt anyway."; " properly and securely use non-standard crypto then you shouldn't need Bluecrypt anyway.";
RSA._universal = native._stance = RSA._stance;
'Bluecrypt only supports crypto with standard cross-browser and cross-platform support.';
RSA.generate = function(opts) {
var wcOpts = {};
if (!opts) {
opts = {};
}
if (!opts.kty) {
opts.kty = 'RSA';
}
// Support PSS? I don't think it's used for Let's Encrypt RSA.generate = native.generate;
wcOpts.name = 'RSASSA-PKCS1-v1_5';
if (!opts.modulusLength) {
opts.modulusLength = 2048;
}
wcOpts.modulusLength = opts.modulusLength;
if (wcOpts.modulusLength >= 2048 && wcOpts.modulusLength < 3072) {
// erring on the small side... for no good reason
wcOpts.hash = { name: 'SHA-256' };
} else if (
wcOpts.modulusLength >= 3072 &&
wcOpts.modulusLength < 4096
) {
wcOpts.hash = { name: 'SHA-384' };
} else if (wcOpts.modulusLength < 4097) {
wcOpts.hash = { name: 'SHA-512' };
} else {
// Public key thumbprints should be paired with a hash of similar length,
// so anything above SHA-512's keyspace would be left under-represented anyway.
return Promise.Reject(
new Error(
"'" +
wcOpts.modulusLength +
"' is not within the safe and universally" +
' acceptable range of 2048-4096. Typically you should pick 2048, 3072, or 4096, though other values' +
' divisible by 8 are allowed. ' +
RSA._stance
)
);
}
// TODO maybe allow this to be set to any of the standard values?
wcOpts.publicExponent = new Uint8Array([0x01, 0x00, 0x01]);
var extractable = true; // Chopping off the private parts is now part of the public API.
return window.crypto.subtle // I thought it sounded a little too crude at first, but it really is the best name in every possible way.
.generateKey(wcOpts, extractable, ['sign', 'verify']) RSA.neuter = function(opts) {
.then(function(result) {
return window.crypto.subtle
.exportKey('jwk', result.privateKey)
.then(function(privJwk) {
return {
private: privJwk,
public: RSA.neuter({ jwk: privJwk })
};
});
});
};
// Chopping off the private parts is now part of the public API.
// I thought it sounded a little too crude at first, but it really is the best name in every possible way.
RSA.neuter = function(opts) {
// trying to find the best balance of an immutable copy with custom attributes // trying to find the best balance of an immutable copy with custom attributes
var jwk = {}; var jwk = {};
Object.keys(opts.jwk).forEach(function(k) { Object.keys(opts.jwk).forEach(function(k) {
@ -90,10 +34,11 @@
jwk[k] = JSON.parse(JSON.stringify(opts.jwk[k])); jwk[k] = JSON.parse(JSON.stringify(opts.jwk[k]));
}); });
return jwk; return jwk;
}; };
native.neuter = RSA.neuter;
// https://stackoverflow.com/questions/42588786/how-to-fingerprint-a-jwk // https://stackoverflow.com/questions/42588786/how-to-fingerprint-a-jwk
RSA.__thumbprint = function(jwk) { RSA.__thumbprint = function(jwk) {
// Use the same entropy for SHA as for key // Use the same entropy for SHA as for key
var len = Math.floor(jwk.n.length * 0.75); var len = Math.floor(jwk.n.length * 0.75);
var alg = 'SHA-256'; var alg = 'SHA-256';
@ -104,19 +49,14 @@
} else if (len >= 383) { } else if (len >= 383) {
alg = 'SHA-384'; alg = 'SHA-384';
} }
return window.crypto.subtle return sha2
.digest( .sum(alg, '{"e":"' + jwk.e + '","kty":"RSA","n":"' + jwk.n + '"}')
{ name: alg },
textEncoder.encode(
'{"e":"' + jwk.e + '","kty":"RSA","n":"' + jwk.n + '"}'
)
)
.then(function(hash) { .then(function(hash) {
return Enc.bufToUrlBase64(new Uint8Array(hash)); return Enc.bufToUrlBase64(Uint8Array.from(hash));
}); });
}; };
RSA.thumbprint = function(opts) { RSA.thumbprint = function(opts) {
return Promise.resolve().then(function() { return Promise.resolve().then(function() {
var jwk; var jwk;
if ('EC' === opts.kty) { if ('EC' === opts.kty) {
@ -130,9 +70,9 @@
} }
return RSA.__thumbprint(jwk); return RSA.__thumbprint(jwk);
}); });
}; };
RSA.export = function(opts) { RSA.export = function(opts) {
return Promise.resolve().then(function() { return Promise.resolve().then(function() {
if (!opts || !opts.jwk || 'object' !== typeof opts.jwk) { if (!opts || !opts.jwk || 'object' !== typeof opts.jwk) {
throw new Error('must pass { jwk: jwk }'); throw new Error('must pass { jwk: jwk }');
@ -140,10 +80,7 @@
var jwk = JSON.parse(JSON.stringify(opts.jwk)); var jwk = JSON.parse(JSON.stringify(opts.jwk));
var format = opts.format; var format = opts.format;
var pub = opts.public; var pub = opts.public;
if ( if (pub || -1 !== ['spki', 'pkix', 'ssh', 'rfc4716'].indexOf(format)) {
pub ||
-1 !== ['spki', 'pkix', 'ssh', 'rfc4716'].indexOf(format)
) {
jwk = RSA.neuter({ jwk: jwk }); jwk = RSA.neuter({ jwk: jwk });
} }
if ('RSA' !== jwk.kty) { if ('RSA' !== jwk.kty) {
@ -208,28 +145,14 @@
); );
} }
}); });
}; };
RSA.pack = function(opts) { native.export = RSA.export;
RSA.pack = function(opts) {
// wrapped in a promise for API compatibility // wrapped in a promise for API compatibility
// with the forthcoming browser version // with the forthcoming browser version
// (and potential future native node capability) // (and potential future native node capability)
return Promise.resolve().then(function() { return Promise.resolve().then(function() {
return RSA.export(opts); return RSA.export(opts);
}); });
}; };
Enc.bufToUrlBase64 = function(u8) {
return Enc.bufToBase64(u8)
.replace(/\+/g, '-')
.replace(/\//g, '_')
.replace(/=/g, '');
};
Enc.bufToBase64 = function(u8) {
var bin = '';
u8.forEach(function(i) {
bin += String.fromCharCode(i);
});
return btoa(bin);
};
})('undefined' !== typeof module ? module.exports : window);

View File

@ -1,23 +1,23 @@
(function(exports) { 'use strict';
'use strict';
var x509 = (exports.x509 = {}); var x509 = module.exports;
var ASN1 = exports.ASN1; var ASN1 = require('./asn1/packer.js');
var Enc = exports.Enc; var Asn1 = ASN1.Any;
var UInt = ASN1.UInt;
var BitStr = ASN1.BitStr;
var Enc = require('omnibuffer');
// 1.2.840.10045.3.1.7 // 1.2.840.10045.3.1.7
// prime256v1 (ANSI X9.62 named elliptic curve) // prime256v1 (ANSI X9.62 named elliptic curve)
var OBJ_ID_EC = '06 08 2A8648CE3D030107'.replace(/\s+/g, '').toLowerCase(); var OBJ_ID_EC = '06 08 2A8648CE3D030107'.replace(/\s+/g, '').toLowerCase();
// 1.3.132.0.34 // 1.3.132.0.34
// secp384r1 (SECG (Certicom) named elliptic curve) // secp384r1 (SECG (Certicom) named elliptic curve)
var OBJ_ID_EC_384 = '06 05 2B81040022'.replace(/\s+/g, '').toLowerCase(); var OBJ_ID_EC_384 = '06 05 2B81040022'.replace(/\s+/g, '').toLowerCase();
// 1.2.840.10045.2.1 // 1.2.840.10045.2.1
// ecPublicKey (ANSI X9.62 public key type) // ecPublicKey (ANSI X9.62 public key type)
var OBJ_ID_EC_PUB = '06 07 2A8648CE3D0201' var OBJ_ID_EC_PUB = '06 07 2A8648CE3D0201'.replace(/\s+/g, '').toLowerCase();
.replace(/\s+/g, '')
.toLowerCase();
x509.parseSec1 = function parseEcOnlyPrivkey(u8, jwk) { x509.parseSec1 = function parseEcOnlyPrivkey(u8, jwk) {
var index = 7; var index = 7;
var len = 32; var len = 32;
var olen = OBJ_ID_EC.length / 2; var olen = OBJ_ID_EC.length / 2;
@ -55,33 +55,33 @@
y: Enc.bufToUrlBase64(y) y: Enc.bufToUrlBase64(y)
//, yh: Enc.bufToHex(y) //, yh: Enc.bufToHex(y)
}; };
}; };
x509.packPkcs1 = function(jwk) { x509.packPkcs1 = function(jwk) {
var n = ASN1.UInt(Enc.base64ToHex(jwk.n)); var n = UInt(Enc.base64ToHex(jwk.n));
var e = ASN1.UInt(Enc.base64ToHex(jwk.e)); var e = UInt(Enc.base64ToHex(jwk.e));
if (!jwk.d) { if (!jwk.d) {
return Enc.hexToBuf(ASN1('30', n, e)); return Enc.hexToBuf(Asn1('30', n, e));
} }
return Enc.hexToBuf( return Enc.hexToBuf(
ASN1( Asn1(
'30', '30',
ASN1.UInt('00'), UInt('00'),
n, n,
e, e,
ASN1.UInt(Enc.base64ToHex(jwk.d)), UInt(Enc.base64ToHex(jwk.d)),
ASN1.UInt(Enc.base64ToHex(jwk.p)), UInt(Enc.base64ToHex(jwk.p)),
ASN1.UInt(Enc.base64ToHex(jwk.q)), UInt(Enc.base64ToHex(jwk.q)),
ASN1.UInt(Enc.base64ToHex(jwk.dp)), UInt(Enc.base64ToHex(jwk.dp)),
ASN1.UInt(Enc.base64ToHex(jwk.dq)), UInt(Enc.base64ToHex(jwk.dq)),
ASN1.UInt(Enc.base64ToHex(jwk.qi)) UInt(Enc.base64ToHex(jwk.qi))
) )
); );
}; };
x509.parsePkcs8 = function parseEcPkcs8(u8, jwk) { x509.parsePkcs8 = function parseEcPkcs8(u8, jwk) {
var index = 24 + OBJ_ID_EC.length / 2; var index = 24 + OBJ_ID_EC.length / 2;
var len = 32; var len = 32;
if ('P-384' === jwk.crv) { if ('P-384' === jwk.crv) {
@ -116,9 +116,9 @@
y: Enc.bufToUrlBase64(y) y: Enc.bufToUrlBase64(y)
//, yh: Enc.bufToHex(y) //, yh: Enc.bufToHex(y)
}; };
}; };
x509.parseSpki = function parsePem(u8, jwk) { x509.parseSpki = function parsePem(u8, jwk) {
var ci = 16 + OBJ_ID_EC.length / 2; var ci = 16 + OBJ_ID_EC.length / 2;
var len = 32; var len = 32;
@ -146,45 +146,41 @@
y: Enc.bufToUrlBase64(y) y: Enc.bufToUrlBase64(y)
//, yh: Enc.bufToHex(y) //, yh: Enc.bufToHex(y)
}; };
}; };
x509.parsePkix = x509.parseSpki; x509.parsePkix = x509.parseSpki;
x509.packSec1 = function(jwk) { x509.packSec1 = function(jwk) {
var d = Enc.base64ToHex(jwk.d); var d = Enc.base64ToHex(jwk.d);
var x = Enc.base64ToHex(jwk.x); var x = Enc.base64ToHex(jwk.x);
var y = Enc.base64ToHex(jwk.y); var y = Enc.base64ToHex(jwk.y);
var objId = 'P-256' === jwk.crv ? OBJ_ID_EC : OBJ_ID_EC_384; var objId = 'P-256' === jwk.crv ? OBJ_ID_EC : OBJ_ID_EC_384;
return Enc.hexToBuf( return Enc.hexToBuf(
ASN1( Asn1(
'30', '30',
ASN1.UInt('01'), UInt('01'),
ASN1('04', d), Asn1('04', d),
ASN1('A0', objId), Asn1('A0', objId),
ASN1('A1', ASN1.BitStr('04' + x + y)) Asn1('A1', BitStr('04' + x + y))
) )
); );
}; };
/** /**
* take a private jwk and creates a der from it * take a private jwk and creates a der from it
* @param {*} jwk * @param {*} jwk
*/ */
x509.packPkcs8 = function(jwk) { x509.packPkcs8 = function(jwk) {
if ('RSA' === jwk.kty) { if ('RSA' === jwk.kty) {
if (!jwk.d) { if (!jwk.d) {
// Public RSA // Public RSA
return Enc.hexToBuf( return Enc.hexToBuf(
ASN1( Asn1(
'30', '30',
ASN1( Asn1('30', Asn1('06', '2a864886f70d010101'), Asn1('05')),
BitStr(
Asn1(
'30', '30',
ASN1('06', '2a864886f70d010101'), UInt(Enc.base64ToHex(jwk.n)),
ASN1('05') UInt(Enc.base64ToHex(jwk.e))
),
ASN1.BitStr(
ASN1(
'30',
ASN1.UInt(Enc.base64ToHex(jwk.n)),
ASN1.UInt(Enc.base64ToHex(jwk.e))
) )
) )
) )
@ -193,23 +189,23 @@
// Private RSA // Private RSA
return Enc.hexToBuf( return Enc.hexToBuf(
ASN1( Asn1(
'30', '30',
ASN1.UInt('00'), UInt('00'),
ASN1('30', ASN1('06', '2a864886f70d010101'), ASN1('05')), Asn1('30', Asn1('06', '2a864886f70d010101'), Asn1('05')),
ASN1( Asn1(
'04', '04',
ASN1( Asn1(
'30', '30',
ASN1.UInt('00'), UInt('00'),
ASN1.UInt(Enc.base64ToHex(jwk.n)), UInt(Enc.base64ToHex(jwk.n)),
ASN1.UInt(Enc.base64ToHex(jwk.e)), UInt(Enc.base64ToHex(jwk.e)),
ASN1.UInt(Enc.base64ToHex(jwk.d)), UInt(Enc.base64ToHex(jwk.d)),
ASN1.UInt(Enc.base64ToHex(jwk.p)), UInt(Enc.base64ToHex(jwk.p)),
ASN1.UInt(Enc.base64ToHex(jwk.q)), UInt(Enc.base64ToHex(jwk.q)),
ASN1.UInt(Enc.base64ToHex(jwk.dp)), UInt(Enc.base64ToHex(jwk.dp)),
ASN1.UInt(Enc.base64ToHex(jwk.dq)), UInt(Enc.base64ToHex(jwk.dq)),
ASN1.UInt(Enc.base64ToHex(jwk.qi)) UInt(Enc.base64ToHex(jwk.qi))
) )
) )
) )
@ -221,40 +217,40 @@
var y = Enc.base64ToHex(jwk.y); var y = Enc.base64ToHex(jwk.y);
var objId = 'P-256' === jwk.crv ? OBJ_ID_EC : OBJ_ID_EC_384; var objId = 'P-256' === jwk.crv ? OBJ_ID_EC : OBJ_ID_EC_384;
return Enc.hexToBuf( return Enc.hexToBuf(
ASN1( Asn1(
'30', '30',
ASN1.UInt('00'), UInt('00'),
ASN1('30', OBJ_ID_EC_PUB, objId), Asn1('30', OBJ_ID_EC_PUB, objId),
ASN1( Asn1(
'04', '04',
ASN1( Asn1(
'30', '30',
ASN1.UInt('01'), UInt('01'),
ASN1('04', d), Asn1('04', d),
ASN1('A1', ASN1.BitStr('04' + x + y)) Asn1('A1', BitStr('04' + x + y))
) )
) )
) )
); );
}; };
x509.packSpki = function(jwk) { x509.packSpki = function(jwk) {
if (/EC/i.test(jwk.kty)) { if (/EC/i.test(jwk.kty)) {
return x509.packSpkiEc(jwk); return x509.packSpkiEc(jwk);
} }
return x509.packSpkiRsa(jwk); return x509.packSpkiRsa(jwk);
}; };
x509.packSpkiRsa = function(jwk) { x509.packSpkiRsa = function(jwk) {
if (!jwk.d) { if (!jwk.d) {
// Public RSA // Public RSA
return Enc.hexToBuf( return Enc.hexToBuf(
ASN1( Asn1(
'30', '30',
ASN1('30', ASN1('06', '2a864886f70d010101'), ASN1('05')), Asn1('30', Asn1('06', '2a864886f70d010101'), Asn1('05')),
ASN1.BitStr( BitStr(
ASN1( Asn1(
'30', '30',
ASN1.UInt(Enc.base64ToHex(jwk.n)), UInt(Enc.base64ToHex(jwk.n)),
ASN1.UInt(Enc.base64ToHex(jwk.e)) UInt(Enc.base64ToHex(jwk.e))
) )
) )
) )
@ -263,39 +259,89 @@
// Private RSA // Private RSA
return Enc.hexToBuf( return Enc.hexToBuf(
ASN1( Asn1(
'30', '30',
ASN1.UInt('00'), UInt('00'),
ASN1('30', ASN1('06', '2a864886f70d010101'), ASN1('05')), Asn1('30', Asn1('06', '2a864886f70d010101'), Asn1('05')),
ASN1( Asn1(
'04', '04',
ASN1( Asn1(
'30', '30',
ASN1.UInt('00'), UInt('00'),
ASN1.UInt(Enc.base64ToHex(jwk.n)), UInt(Enc.base64ToHex(jwk.n)),
ASN1.UInt(Enc.base64ToHex(jwk.e)), UInt(Enc.base64ToHex(jwk.e)),
ASN1.UInt(Enc.base64ToHex(jwk.d)), UInt(Enc.base64ToHex(jwk.d)),
ASN1.UInt(Enc.base64ToHex(jwk.p)), UInt(Enc.base64ToHex(jwk.p)),
ASN1.UInt(Enc.base64ToHex(jwk.q)), UInt(Enc.base64ToHex(jwk.q)),
ASN1.UInt(Enc.base64ToHex(jwk.dp)), UInt(Enc.base64ToHex(jwk.dp)),
ASN1.UInt(Enc.base64ToHex(jwk.dq)), UInt(Enc.base64ToHex(jwk.dq)),
ASN1.UInt(Enc.base64ToHex(jwk.qi)) UInt(Enc.base64ToHex(jwk.qi))
) )
) )
) )
); );
}; };
x509.packSpkiEc = function(jwk) { x509.packSpkiEc = function(jwk) {
var x = Enc.base64ToHex(jwk.x); var x = Enc.base64ToHex(jwk.x);
var y = Enc.base64ToHex(jwk.y); var y = Enc.base64ToHex(jwk.y);
var objId = 'P-256' === jwk.crv ? OBJ_ID_EC : OBJ_ID_EC_384; var objId = 'P-256' === jwk.crv ? OBJ_ID_EC : OBJ_ID_EC_384;
return Enc.hexToBuf( return Enc.hexToBuf(
ASN1( Asn1('30', Asn1('30', OBJ_ID_EC_PUB, objId), BitStr('04' + x + y))
'30',
ASN1('30', OBJ_ID_EC_PUB, objId),
ASN1.BitStr('04' + x + y)
)
); );
}; };
x509.packPkix = x509.packSpki; x509.packPkix = x509.packSpki;
})('undefined' !== typeof module ? module.exports : window);
x509.packCsrRsaPublicKey = function(jwk) {
// Sequence the key
var n = UInt(Enc.base64ToHex(jwk.n));
var e = UInt(Enc.base64ToHex(jwk.e));
var asn1pub = Asn1('30', n, e);
// Add the CSR pub key header
return Asn1(
'30',
Asn1('30', Asn1('06', '2a864886f70d010101'), Asn1('05')),
BitStr(asn1pub)
);
};
x509.packCsrEcPublicKey = function(jwk) {
var ecOid = x509._oids[jwk.crv];
if (!ecOid) {
throw new Error(
"Unsupported namedCurve '" +
jwk.crv +
"'. Supported types are " +
Object.keys(x509._oids)
);
}
var cmp = '04'; // 04 == x+y, 02 == x-only
var hxy = '';
// Placeholder. I'm not even sure if compression should be supported.
if (!jwk.y) {
cmp = '02';
}
hxy += Enc.base64ToHex(jwk.x);
if (jwk.y) {
hxy += Enc.base64ToHex(jwk.y);
}
// 1.2.840.10045.2.1 ecPublicKey
return Asn1(
'30',
Asn1('30', Asn1('06', '2a8648ce3d0201'), Asn1('06', ecOid)),
BitStr(cmp + hxy)
);
};
x509._oids = {
// 1.2.840.10045.3.1.7 prime256v1
// (ANSI X9.62 named elliptic curve) (06 08 - 2A 86 48 CE 3D 03 01 07)
'P-256': '2a8648ce3d030107',
// 1.3.132.0.34 P-384 (06 05 - 2B 81 04 00 22)
// (SEC 2 recommended EC domain secp256r1)
'P-384': '2b81040022'
// requires more logic and isn't a recommended standard
// 1.3.132.0.35 P-521 (06 05 - 2B 81 04 00 23)
// (SEC 2 alternate P-521)
//, 'P-521': '2B 81 04 00 23'
};

6
package-lock.json generated
View File

@ -171,6 +171,12 @@
"hexdump.js": "git+https://git.coolaj86.com/coolaj86/hexdump.js#v1.0.4" "hexdump.js": "git+https://git.coolaj86.com/coolaj86/hexdump.js#v1.0.4"
} }
}, },
"dotenv": {
"version": "8.1.0",
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-8.1.0.tgz",
"integrity": "sha512-GUE3gqcDCaMltj2++g6bRQ5rBJWtkWTmqmD0fo1RnnMuUqHNCt2oTPeDnS9n6fKYvlhn7AeBkb38lymBtWBQdA==",
"dev": true
},
"ee-first": { "ee-first": {
"version": "1.1.1", "version": "1.1.1",
"resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",

View File

@ -2,8 +2,15 @@
"name": "acme", "name": "acme",
"version": "2.0.0-wip.0", "version": "2.0.0-wip.0",
"description": "Free SSL certificates through Let's Encrypt, right in your browser", "description": "Free SSL certificates through Let's Encrypt, right in your browser",
"main": "bluecrypt-acme.js",
"homepage": "https://rootprojects.org/acme/", "homepage": "https://rootprojects.org/acme/",
"main": "lib/acme.js",
"browser": {
"./lib/node/sha2.js": "./lib/browser/sha2.js",
"./lib/node/http.js": "./lib/browser/http.js",
"./lib/node/ecdsa.js": "./lib/browser/ecdsa.js",
"./lib/node/rsa.js": "./lib/browser/rsa.js",
"./lib/node/keypairs.js": "./lib/browser/keypairs.js"
},
"directories": { "directories": {
"lib": "lib" "lib": "lib"
}, },
@ -38,6 +45,7 @@
"@root/request": "^1.3.10", "@root/request": "^1.3.10",
"dig.js": "^1.3.9", "dig.js": "^1.3.9",
"dns-suite": "^1.2.12", "dns-suite": "^1.2.12",
"dotenv": "^8.1.0",
"express": "^4.16.4", "express": "^4.16.4",
"uglify-js": "^3.6.0" "uglify-js": "^3.6.0"
} }

101
tests/index.js Normal file
View File

@ -0,0 +1,101 @@
'use strict';
var ACME = require('../');
var Keypairs = require('../lib/keypairs.js');
var acme = ACME.create({});
var config = {
env: process.env.ENV,
email: process.env.SUBSCRIBER_EMAIL,
domain: process.env.BASE_DOMAIN
};
config.debug = !/^PROD/i.test(config.env);
async function happyPath() {
var domains = randomDomains();
var agreed = false;
var metadata = await acme.init(
'https://acme-staging-v02.api.letsencrypt.org/directory'
);
// Ready to use, show page
if (config.debug) {
console.info('ACME.js initialized');
console.info(metadata);
console.info('');
console.info();
}
// EC for account (but RSA for cert, for testing both)
var accountKeypair = await Keypairs.generate({ kty: 'EC' });
if (config.debug) {
console.info('Account Key Created');
console.info(JSON.stringify(accountKeypair, null, 2));
console.info('');
console.info();
}
var account = await acme.accounts.create({
agreeToTerms: agree,
// TODO detect jwk/pem/der?
accountKeypair: { privateKeyJwk: accountKeypair.private },
email: config.email
});
// TODO top-level agree
function agree(tos) {
if (config.debug) {
console.info('Agreeing to Terms of Service:');
console.info(tos);
console.info('');
console.info();
}
agreed = true;
return Promise.resolve(tos);
}
if (config.debug) {
console.info('New Subscriber Account');
console.info(JSON.stringify(account, null, 2));
console.info();
console.info();
}
if (!agreed) {
throw new Error('Failed to ask the user to agree to terms');
}
var serverKeypair = await Keypairs.generate({ kty: 'RSA' });
if (config.debug) {
console.info('Server Key Created');
console.info(JSON.stringify(serverKeypair, null, 2));
console.info('');
console.info();
}
}
happyPath()
.then(function() {
console.info('success');
})
.catch(function(err) {
console.error('Error:');
console.error(err.stack);
});
function randomDomains() {
var rnd = random();
return ['foo-acmejs', 'bar-acmejs', '*.baz-acmejs', 'baz-acmejs'].map(
function(pre) {
return pre + '-' + rnd + '.' + config.domain;
}
);
}
function random() {
return parseInt(
Math.random()
.toString()
.slice(2, 99),
10
)
.toString(16)
.slice(0, 4);
}