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,15 +2,15 @@
/* 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 (
@ -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)
@ -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,15 +505,17 @@
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;
} });
);
}); });
}); });
}; };
@ -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:');
@ -744,8 +738,9 @@
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,8 +854,7 @@
} }
return pollCert(); return pollCert();
} });
);
}; };
// _kid // _kid
// registerAccount // registerAccount
@ -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) {
@ -1201,9 +1186,8 @@
} }
// 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) {
@ -1346,32 +1330,8 @@
} }
} }
opts.cors = true; opts.cors = true;
return window.fetch(opts.url, opts).then(function(resp) {
var headers = {}; return http.request(opts);
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) { ACME._importKeypair = function(me, kp) {
@ -1491,27 +1451,3 @@ 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,15 +2,18 @@
/* 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) {
@ -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;
} }
@ -133,37 +135,37 @@
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('')
) )
@ -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];
@ -331,72 +328,3 @@
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,71 +1,32 @@
/*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; // TODO SSH
} var SSH;
var PEM = exports.PEM;
var SSH = exports.SSH; var x509 = require('./x509.js');
var Enc = {}; var PEM = require('./pem.js');
var textEncoder = new TextEncoder(); //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 = 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.";
native._stance = EC._stance;
EC._universal = 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
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) { EC.export = function(opts) {
return Promise.resolve().then(function() { return Promise.resolve().then(function() {
@ -145,9 +106,79 @@
} }
}); });
}; };
native.export = EC.export;
EC.import = function(opts) {
return Promise.resolve().then(function() {
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) { EC.pack = function(opts) {
return Promise.resolve().then(function() { return Promise.resolve().then(function() {
return EC.exportSync(opts); return EC.export(opts);
}); });
}; };
@ -168,6 +199,7 @@
}); });
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) {
@ -176,21 +208,18 @@
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));
}); });
}; };
@ -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,11 +1,11 @@
/*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" +
@ -37,9 +37,7 @@
); );
} }
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;
@ -54,6 +52,8 @@
}); });
}); });
}; };
// 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.
@ -78,8 +78,10 @@
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);
} }
}); });
@ -124,7 +126,7 @@
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) {
@ -176,9 +178,7 @@
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,
@ -256,35 +255,6 @@
}); });
}; };
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._getBits = function(opts) { Keypairs._getBits = function(opts) {
if (opts.alg) { if (opts.alg) {
return opts.alg.replace(/[a-z\-]/gi, ''); return opts.alg.replace(/[a-z\-]/gi, '');
@ -302,80 +272,8 @@
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
.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) { function setTime(time) {
if ('number' === typeof time) { if ('number' === typeof time) {
@ -429,4 +327,3 @@
}); });
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,78 +1,22 @@
/*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._universal =
'Bluecrypt only supports crypto with standard cross-browser and cross-platform support.';
RSA._stance = 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;
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 })
};
});
});
};
// 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.
@ -91,6 +35,7 @@
}); });
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) {
@ -104,15 +49,10 @@
} 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));
}); });
}; };
@ -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) {
@ -209,6 +146,8 @@
} }
}); });
}; };
native.export = RSA.export;
RSA.pack = function(opts) { 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
@ -217,19 +156,3 @@
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,9 +1,11 @@
(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)
@ -13,9 +15,7 @@
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;
@ -58,25 +58,25 @@
}; };
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))
) )
); );
}; };
@ -155,12 +155,12 @@
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))
) )
); );
}; };
@ -173,18 +173,14 @@
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,17 +217,17 @@
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))
) )
) )
) )
@ -247,14 +243,14 @@
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,23 +259,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))
) )
) )
) )
@ -290,12 +286,62 @@
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);
}