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"
}
}

File diff suppressed because it is too large Load Diff

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,401 +2,329 @@
/* 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) {
return CSR.create(opts).then(function(bytes) { return CSR.create(opts).then(function(bytes) {
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));
// We do a bit of extra error checking for user convenience // We do a bit of extra error checking for user convenience
if (!opts) { if (!opts) {
throw new Error( throw new Error(
'You must pass options with key and domains to rsacsr' 'You must pass options with key and domains to rsacsr'
); );
}
if (!Array.isArray(opts.domains) || 0 === opts.domains.length) {
new Error('You must pass options.domains as a non-empty array');
}
// I need to check that 例.中国 is a valid domain name
if (
!opts.domains.every(function(d) {
// allow punycode? xn--
if (
'string' ===
typeof d /*&& /\./.test(d) && !/--/.test(d)*/
) {
return true;
}
})
) {
throw new Error('You must pass options.domains as strings');
}
if (opts.jwk) {
return opts;
}
if (opts.key && opts.key.kty) {
opts.jwk = opts.key;
return opts;
}
if (!opts.pem && !opts.key) {
throw new Error('You must pass options.key as a JSON web key');
}
Keypairs = exports.Keypairs;
if (!exports.Keypairs) {
throw new Error(
'Keypairs.js is an optional dependency for PEM-to-JWK.\n' +
"Install it if you'd like to use it:\n" +
'\tnpm install --save rasha\n' +
'Otherwise supply a jwk as the private key.'
);
}
return Keypairs.import({ pem: opts.pem || opts.key }).then(function(
pair
) {
opts.jwk = pair.private;
return opts;
});
});
};
CSR._encode = function(opts, bytes) {
if ('der' === (opts.encoding || '').toLowerCase()) {
return bytes;
} }
return PEM.packBlock({ if (!Array.isArray(opts.domains) || 0 === opts.domains.length) {
type: 'CERTIFICATE REQUEST', new Error('You must pass options.domains as a non-empty array');
bytes: bytes /* { jwk: jwk, domains: opts.domains } */
});
};
CSR.create = function createCsr(opts) {
var hex = CSR.request(opts.jwk, opts.domains);
return CSR._sign(opts.jwk, hex).then(function(csr) {
return Enc.hexToBuf(csr);
});
};
//
// EC / RSA
//
CSR.request = function createCsrBodyEc(jwk, domains) {
var asn1pub;
if (/^EC/i.test(jwk.kty)) {
asn1pub = X509.packCsrEcPublicKey(jwk);
} else {
asn1pub = X509.packCsrRsaPublicKey(jwk);
} }
return X509.packCsr(asn1pub, domains);
};
CSR._sign = function csrEcSig(jwk, request) { // I need to check that 例.中国 is a valid domain name
// Took some tips from https://gist.github.com/codermapuche/da4f96cdb6d5ff53b7ebc156ec46a10a if (
// TODO will have to convert web ECDSA signatures to PEM ECDSA signatures (but RSA should be the same) !opts.domains.every(function(d) {
// TODO have a consistent non-private way to sign // allow punycode? xn--
return Keypairs._sign( if (
{ jwk: jwk, format: 'x509' }, 'string' === typeof d /*&& /\./.test(d) && !/--/.test(d)*/
Enc.hexToBuf(request) ) {
).then(function(sig) { return true;
return CSR._toDer({ }
request: request, })
signature: sig, ) {
kty: jwk.kty throw new Error('You must pass options.domains as strings');
});
});
};
CSR._toDer = function encode(opts) {
var sty;
if (/^EC/i.test(opts.kty)) {
// 1.2.840.10045.4.3.2 ecdsaWithSHA256 (ANSI X9.62 ECDSA algorithm with SHA256)
sty = ASN1('30', ASN1('06', '2a8648ce3d040302'));
} else {
// 1.2.840.113549.1.1.11 sha256WithRSAEncryption (PKCS #1)
sty = ASN1('30', ASN1('06', '2a864886f70d01010b'), ASN1('05'));
} }
return ASN1(
if (opts.jwk) {
return opts;
}
if (opts.key && opts.key.kty) {
opts.jwk = opts.key;
return opts;
}
if (!opts.pem && !opts.key) {
throw new Error('You must pass options.key as a JSON web key');
}
Keypairs = exports.Keypairs;
if (!exports.Keypairs) {
throw new Error(
'Keypairs.js is an optional dependency for PEM-to-JWK.\n' +
"Install it if you'd like to use it:\n" +
'\tnpm install --save rasha\n' +
'Otherwise supply a jwk as the private key.'
);
}
return Keypairs.import({ pem: opts.pem || opts.key }).then(function(
pair
) {
opts.jwk = pair.private;
return opts;
});
});
};
CSR._encode = function(opts, bytes) {
if ('der' === (opts.encoding || '').toLowerCase()) {
return bytes;
}
return PEM.packBlock({
type: 'CERTIFICATE REQUEST',
bytes: bytes /* { jwk: jwk, domains: opts.domains } */
});
};
CSR.create = function createCsr(opts) {
var hex = CSR.request(opts.jwk, opts.domains);
return CSR._sign(opts.jwk, hex).then(function(csr) {
return Enc.hexToBuf(csr);
});
};
//
// EC / RSA
//
CSR.request = function createCsrBodyEc(jwk, domains) {
var asn1pub;
if (/^EC/i.test(jwk.kty)) {
asn1pub = X509.packCsrEcPublicKey(jwk);
} else {
asn1pub = X509.packCsrRsaPublicKey(jwk);
}
return X509.packCsr(asn1pub, domains);
};
CSR._sign = function csrEcSig(jwk, request) {
// 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 have a consistent non-private way to sign
return Keypairs._sign(
{ jwk: jwk, format: 'x509' },
Enc.hexToBuf(request)
).then(function(sig) {
return CSR._toDer({
request: request,
signature: sig,
kty: jwk.kty
});
});
};
CSR._toDer = function encode(opts) {
var sty;
if (/^EC/i.test(opts.kty)) {
// 1.2.840.10045.4.3.2 ecdsaWithSHA256 (ANSI X9.62 ECDSA algorithm with SHA256)
sty = Asn1('30', Asn1('06', '2a8648ce3d040302'));
} else {
// 1.2.840.113549.1.1.11 sha256WithRSAEncryption (PKCS #1)
sty = Asn1('30', Asn1('06', '2a864886f70d01010b'), Asn1('05'));
}
return Asn1(
'30',
// The Full CSR Request Body
opts.request,
// The Signature Type
sty,
// The Signature
BitStr(Enc.bufToHex(opts.signature))
);
};
X509.packCsr = function(asn1pubkey, domains) {
return Asn1(
'30',
// Version (0)
UInt('00'),
// 2.5.4.3 commonName (X.520 DN component)
Asn1(
'30', '30',
// The Full CSR Request Body Asn1(
opts.request, '31',
// The Signature Type Asn1(
sty,
// The Signature
ASN1.BitStr(Enc.bufToHex(opts.signature))
);
};
X509.packCsr = function(asn1pubkey, domains) {
return ASN1(
'30',
// Version (0)
ASN1.UInt('00'),
// 2.5.4.3 commonName (X.520 DN component)
ASN1(
'30',
ASN1(
'31',
ASN1(
'30',
ASN1('06', '550403'),
ASN1('0c', Enc.utf8ToHex(domains[0]))
)
)
),
// Public Key (RSA or EC)
asn1pubkey,
// Request Body
ASN1(
'a0',
ASN1(
'30', '30',
// 1.2.840.113549.1.9.14 extensionRequest (PKCS #9 via CRMF) Asn1('06', '550403'),
ASN1('06', '2a864886f70d01090e'), Asn1('0c', Enc.utf8ToHex(domains[0]))
ASN1( )
'31', )
ASN1( ),
// Public Key (RSA or EC)
asn1pubkey,
// Request Body
Asn1(
'a0',
Asn1(
'30',
// 1.2.840.113549.1.9.14 extensionRequest (PKCS #9 via CRMF)
Asn1('06', '2a864886f70d01090e'),
Asn1(
'31',
Asn1(
'30',
Asn1(
'30', '30',
ASN1( // 2.5.29.17 subjectAltName (X.509 extension)
'30', Asn1('06', '551d11'),
// 2.5.29.17 subjectAltName (X.509 extension) Asn1(
ASN1('06', '551d11'), '04',
ASN1( Asn1(
'04', '30',
ASN1( domains
'30', .map(function(d) {
domains return Asn1('82', Enc.utf8ToHex(d));
.map(function(d) { })
return ASN1( .join('')
'82',
Enc.utf8ToHex(d)
);
})
.join('')
)
) )
) )
) )
) )
) )
) )
); )
}; );
};
// 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;
} }
// jose urlBase64 not-PEM // jose urlBase64 not-PEM
if ('string' === typeof der) { if ('string' === typeof der) {
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) {
throw new Error( throw new Error(
"doesn't look like a certificate request: expected 3 parts of header" "doesn't look like a certificate request: expected 3 parts of header"
);
}
var sig = c.children[2];
if (sig.children.length) {
// ASN1/X509 EC
sig = sig.children[0];
sig = ASN1(
'30',
ASN1.UInt(Enc.bufToHex(sig.children[0].value)),
ASN1.UInt(Enc.bufToHex(sig.children[1].value))
);
sig = Enc.hexToBuf(sig);
kty = 'EC';
} else {
// Raw RSA Sig
sig = sig.value;
kty = 'RSA';
}
//c.children[1]; // signature type
var req = c.children[0];
// TODO utf8
if (4 !== req.children.length) {
throw new Error(
"doesn't look like a certificate request: expected 4 parts to request"
);
}
// 0 null
// 1 commonName / subject
var sub = Enc.bufToBin(
req.children[1].children[0].children[0].children[1].value
); );
// 3 public key (type, key) }
//console.log('oid', Enc.bufToHex(req.children[2].children[0].children[0].value)); var sig = c.children[2];
var pub; if (sig.children.length) {
// TODO reuse ASN1 parser for these? // ASN1/X509 EC
if ('EC' === kty) { sig = sig.children[0];
// throw away compression byte sig = Asn1(
pub = req.children[2].children[1].value.slice(1); '30',
pub = { kty: kty, x: pub.slice(0, 32), y: pub.slice(32) }; UInt(Enc.bufToHex(sig.children[0].value)),
while (0 === pub.x[0]) { UInt(Enc.bufToHex(sig.children[1].value))
pub.x = pub.x.slice(1); );
} sig = Enc.hexToBuf(sig);
while (0 === pub.y[0]) { kty = 'EC';
pub.y = pub.y.slice(1); } else {
} // Raw RSA Sig
if ((pub.x.length || pub.x.byteLength) > 48) { sig = sig.value;
pub.crv = 'P-521'; kty = 'RSA';
} else if ((pub.x.length || pub.x.byteLength) > 32) { }
pub.crv = 'P-384'; //c.children[1]; // signature type
} else { var req = c.children[0];
pub.crv = 'P-256'; // TODO utf8
} if (4 !== req.children.length) {
pub.x = Enc.bufToUrlBase64(pub.x); throw new Error(
pub.y = Enc.bufToUrlBase64(pub.y); "doesn't look like a certificate request: expected 4 parts to request"
} else { );
pub = req.children[2].children[1].children[0]; }
pub = { // 0 null
kty: kty, // 1 commonName / subject
n: pub.children[0].value, var sub = Enc.bufToBin(
e: pub.children[1].value req.children[1].children[0].children[0].children[1].value
}; );
while (0 === pub.n[0]) { // 3 public key (type, key)
pub.n = pub.n.slice(1); //console.log('oid', Enc.bufToHex(req.children[2].children[0].children[0].value));
} var pub;
while (0 === pub.e[0]) { // TODO reuse ASN1 parser for these?
pub.e = pub.e.slice(1); if ('EC' === kty) {
} // throw away compression byte
pub.n = Enc.bufToUrlBase64(pub.n); pub = req.children[2].children[1].value.slice(1);
pub.e = Enc.bufToUrlBase64(pub.e); pub = { kty: kty, x: pub.slice(0, 32), y: pub.slice(32) };
while (0 === pub.x[0]) {
pub.x = pub.x.slice(1);
} }
// 4 extensions while (0 === pub.y[0]) {
var domains = req.children[3].children pub.y = pub.y.slice(1);
.filter(function(seq) { }
// 1.2.840.113549.1.9.14 extensionRequest (PKCS #9 via CRMF) if ((pub.x.length || pub.x.byteLength) > 48) {
if ( pub.crv = 'P-521';
'2a864886f70d01090e' === Enc.bufToHex(seq.children[0].value) } else if ((pub.x.length || pub.x.byteLength) > 32) {
) { pub.crv = 'P-384';
return true; } else {
} pub.crv = 'P-256';
}) }
.map(function(seq) { pub.x = Enc.bufToUrlBase64(pub.x);
return seq.children[1].children[0].children pub.y = Enc.bufToUrlBase64(pub.y);
.filter(function(seq2) { } else {
// subjectAltName (X.509 extension) pub = req.children[2].children[1].children[0];
if ('551d11' === Enc.bufToHex(seq2.children[0].value)) { pub = {
return true; kty: kty,
} n: pub.children[0].value,
}) e: pub.children[1].value
.map(function(seq2) {
return seq2.children[1].children[0].children.map(
function(name) {
// TODO utf8
return Enc.bufToBin(name.value);
}
);
})[0];
})[0];
return {
subject: sub,
altnames: domains,
jwk: pub,
signature: sig
}; };
}; while (0 === pub.n[0]) {
pub.n = pub.n.slice(1);
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 while (0 === pub.e[0]) {
var hxy = ''; pub.e = pub.e.slice(1);
// Placeholder. I'm not even sure if compression should be supported.
if (!jwk.y) {
cmp = '02';
} }
hxy += Enc.base64ToHex(jwk.x); pub.n = Enc.bufToUrlBase64(pub.n);
if (jwk.y) { pub.e = Enc.bufToUrlBase64(pub.e);
hxy += Enc.base64ToHex(jwk.y); }
} // 4 extensions
var domains = req.children[3].children
// 1.2.840.10045.2.1 ecPublicKey .filter(function(seq) {
return ASN1( // 1.2.840.113549.1.9.14 extensionRequest (PKCS #9 via CRMF)
'30', if ('2a864886f70d01090e' === Enc.bufToHex(seq.children[0].value)) {
ASN1('30', ASN1('06', '2a8648ce3d0201'), ASN1('06', ecOid)), return true;
ASN1.BitStr(cmp + hxy) }
); })
}; .map(function(seq) {
X509._oids = { return seq.children[1].children[0].children
// 1.2.840.10045.3.1.7 prime256v1 .filter(function(seq2) {
// (ANSI X9.62 named elliptic curve) (06 08 - 2A 86 48 CE 3D 03 01 07) // subjectAltName (X.509 extension)
'P-256': '2a8648ce3d030107', if ('551d11' === Enc.bufToHex(seq2.children[0].value)) {
// 1.3.132.0.34 P-384 (06 05 - 2B 81 04 00 22) return true;
// (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(''); .map(function(seq2) {
return { bytes: Enc.base64ToBuf(der) }; return seq2.children[1].children[0].children.map(function(
}; name
})('undefined' === typeof window ? module.exports : window); ) {
// TODO utf8
return Enc.bufToBin(name.value);
});
})[0];
})[0];
return {
subject: sub,
altnames: domains,
jwk: pub,
signature: sig
};
};

View File

@ -1,227 +1,240 @@
/*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
"We take the stance that if you're knowledgeable enough to" + var SSH;
" properly and securely use non-standard crypto then you shouldn't need Bluecrypt anyway.";
EC._universal = var x509 = require('./x509.js');
'Bluecrypt only supports crypto with standard cross-browser and cross-platform support.'; var PEM = require('./pem.js');
EC.generate = function(opts) { //var SSH = require('./ssh-keys.js');
var wcOpts = {}; var Enc = require('omnibuffer');
if (!opts) { var sha2 = require('./node/sha2.js');
opts = {};
// 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" +
" properly and securely use non-standard crypto then you shouldn't need Bluecrypt anyway.";
native._stance = EC._stance;
EC._universal =
'Bluecrypt only supports crypto with standard cross-browser and cross-platform support.';
EC.generate = native.generate;
EC.export = function(opts) {
return Promise.resolve().then(function() {
if (!opts || !opts.jwk || 'object' !== typeof opts.jwk) {
throw new Error('must pass { jwk: jwk } as a JSON object');
} }
if (!opts.kty) { var jwk = JSON.parse(JSON.stringify(opts.jwk));
opts.kty = 'EC'; var format = opts.format;
if (
opts.public ||
-1 !== ['spki', 'pkix', 'ssh', 'rfc4716'].indexOf(format)
) {
jwk.d = null;
} }
if ('EC' !== jwk.kty) {
// ECDSA has only the P curves and an associated bitlength throw new Error("options.jwk.kty must be 'EC' for EC keys");
wcOpts.name = 'ECDSA';
if (!opts.namedCurve) {
opts.namedCurve = 'P-256';
} }
wcOpts.namedCurve = opts.namedCurve; // true for supported curves if (!jwk.d) {
if (/256/.test(wcOpts.namedCurve)) { if (!format || -1 !== ['spki', 'pkix'].indexOf(format)) {
wcOpts.namedCurve = 'P-256'; format = 'spki';
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() {
if (!opts || !opts.jwk || 'object' !== typeof opts.jwk) {
throw new Error('must pass { jwk: jwk } as a JSON object');
}
var jwk = JSON.parse(JSON.stringify(opts.jwk));
var format = opts.format;
if (
opts.public ||
-1 !== ['spki', 'pkix', 'ssh', 'rfc4716'].indexOf(format)
) {
jwk.d = null;
}
if ('EC' !== jwk.kty) {
throw new Error("options.jwk.kty must be 'EC' for EC keys");
}
if (!jwk.d) {
if (!format || -1 !== ['spki', 'pkix'].indexOf(format)) {
format = 'spki';
} else if (-1 !== ['ssh', 'rfc4716'].indexOf(format)) {
format = 'ssh';
} else {
throw new Error(
"options.format must be 'spki' or 'ssh' for public EC keys, not (" +
typeof format +
') ' +
format
);
}
} else {
if (!format || 'sec1' === format) {
format = 'sec1';
} else if ('pkcs8' !== format) {
throw new Error(
"options.format must be 'sec1' or 'pkcs8' for private EC keys, not '" +
format +
"'"
);
}
}
if (-1 === ['P-256', 'P-384'].indexOf(jwk.crv)) {
throw new Error(
"options.jwk.crv must be either P-256 or P-384 for EC keys, not '" +
jwk.crv +
"'"
);
}
if (!jwk.y) {
throw new Error(
'options.jwk.y must be a urlsafe base64-encoded either P-256 or P-384'
);
}
if ('sec1' === format) {
return PEM.packBlock({
type: 'EC PRIVATE KEY',
bytes: x509.packSec1(jwk)
});
} else if ('pkcs8' === format) {
return PEM.packBlock({
type: 'PRIVATE KEY',
bytes: x509.packPkcs8(jwk)
});
} else if (-1 !== ['spki', 'pkix'].indexOf(format)) {
return PEM.packBlock({
type: 'PUBLIC KEY',
bytes: x509.packSpki(jwk)
});
} else if (-1 !== ['ssh', 'rfc4716'].indexOf(format)) { } else if (-1 !== ['ssh', 'rfc4716'].indexOf(format)) {
return SSH.packSsh(jwk); format = 'ssh';
} else { } else {
throw new Error( throw new Error(
'Sanity Error: reached unreachable code block with format: ' + "options.format must be 'spki' or 'ssh' for public EC keys, not (" +
typeof format +
') ' +
format format
); );
} }
}); } else {
}; if (!format || 'sec1' === format) {
EC.pack = function(opts) { format = 'sec1';
return Promise.resolve().then(function() { } else if ('pkcs8' !== format) {
return EC.exportSync(opts); throw new Error(
}); "options.format must be 'sec1' or 'pkcs8' for private EC keys, not '" +
}; format +
"'"
// 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
var jwk = {};
Object.keys(opts.jwk).forEach(function(k) {
if ('undefined' === typeof opts.jwk[k]) {
return;
} }
// ignore EC private parts
if ('d' === k) {
return;
}
jwk[k] = JSON.parse(JSON.stringify(opts.jwk[k]));
});
return jwk;
};
// https://stackoverflow.com/questions/42588786/how-to-fingerprint-a-jwk
EC.__thumbprint = function(jwk) {
// Use the same entropy for SHA as for key
var alg = 'SHA-256';
if (/384/.test(jwk.crv)) {
alg = 'SHA-384';
} }
return window.crypto.subtle if (-1 === ['P-256', 'P-384'].indexOf(jwk.crv)) {
.digest( throw new Error(
{ name: alg }, "options.jwk.crv must be either P-256 or P-384 for EC keys, not '" +
textEncoder.encode( jwk.crv +
'{"crv":"' + "'"
jwk.crv + );
'","kty":"EC","x":"' + }
jwk.x + if (!jwk.y) {
'","y":"' + throw new Error(
jwk.y + 'options.jwk.y must be a urlsafe base64-encoded either P-256 or P-384'
'"}' );
) }
)
.then(function(hash) { if ('sec1' === format) {
return Enc.bufToUrlBase64(new Uint8Array(hash)); return PEM.packBlock({
type: 'EC PRIVATE KEY',
bytes: x509.packSec1(jwk)
}); });
}; } else if ('pkcs8' === format) {
return PEM.packBlock({
type: 'PRIVATE KEY',
bytes: x509.packPkcs8(jwk)
});
} else if (-1 !== ['spki', 'pkix'].indexOf(format)) {
return PEM.packBlock({
type: 'PUBLIC KEY',
bytes: x509.packSpki(jwk)
});
} else if (-1 !== ['ssh', 'rfc4716'].indexOf(format)) {
return SSH.packSsh(jwk);
} else {
throw new Error(
'Sanity Error: reached unreachable code block with format: ' +
format
);
}
});
};
native.export = EC.export;
EC.thumbprint = function(opts) { EC.import = function(opts) {
return Promise.resolve().then(function() { return Promise.resolve().then(function() {
var jwk; if (!opts || !opts.pem || 'string' !== typeof opts.pem) {
if ('EC' === opts.kty) { throw new Error('must pass { pem: pem } as a string');
jwk = opts; }
} else if (opts.jwk) { if (0 === opts.pem.indexOf('ecdsa-sha2-')) {
jwk = opts.jwk; 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 { } else {
return EC.import(opts).then(function(jwk) { jwk.crv = 'P-256';
return EC.__thumbprint(jwk);
});
} }
return EC.__thumbprint(jwk);
});
};
Enc.bufToUrlBase64 = function(u8) { // PKCS8
return Enc.bufToBase64(u8) if (0x02 === u8[3] && 0x30 === u8[6] && 0x06 === u8[8]) {
.replace(/\+/g, '-') //console.log("PKCS8", u8[3].toString(16), u8[6].toString(16), u8[8].toString(16));
.replace(/\//g, '_') jwk = x509.parsePkcs8(u8, jwk);
.replace(/=/g, ''); // 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;
Enc.bufToBase64 = function(u8) { EC.pack = function(opts) {
var bin = ''; return Promise.resolve().then(function() {
u8.forEach(function(i) { return EC.export(opts);
bin += String.fromCharCode(i); });
}); };
return btoa(bin);
}; // Chopping off the private parts is now part of the public API.
})('undefined' !== typeof module ? module.exports : window); // 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
var jwk = {};
Object.keys(opts.jwk).forEach(function(k) {
if ('undefined' === typeof opts.jwk[k]) {
return;
}
// ignore EC private parts
if ('d' === k) {
return;
}
jwk[k] = JSON.parse(JSON.stringify(opts.jwk[k]));
});
return jwk;
};
native.neuter = EC.neuter;
// https://stackoverflow.com/questions/42588786/how-to-fingerprint-a-jwk
EC.__thumbprint = function(jwk) {
// Use the same entropy for SHA as for key
var alg = 'SHA-256';
if (/384/.test(jwk.crv)) {
alg = 'SHA-384';
}
var payload =
'{"crv":"' +
jwk.crv +
'","kty":"EC","x":"' +
jwk.x +
'","y":"' +
jwk.y +
'"}';
console.log('[debug] EC', alg, payload);
return sha2.sum(alg, payload).then(function(hash) {
console.log('[debug] EC hash', hash);
return Enc.bufToUrlBase64(Uint8Array.from(hash));
});
};
EC.thumbprint = function(opts) {
return Promise.resolve().then(function() {
var jwk;
if ('EC' === opts.kty) {
jwk = opts;
} else if (opts.jwk) {
jwk = opts.jwk;
} else {
return native.import(opts).then(function(jwk) {
return EC.__thumbprint(jwk);
});
}
return EC.__thumbprint(jwk);
});
};

View File

@ -1,432 +1,329 @@
/*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) {
opts.kty = opts.type; opts.kty = opts.type;
} }
if (!opts.kty) { if (!opts.kty) {
opts.kty = 'EC'; opts.kty = 'EC';
} }
if (/^EC/i.test(opts.kty)) { if (/^EC/i.test(opts.kty)) {
p = Eckles.generate(opts); p = Eckles.generate(opts);
} else if (/^RSA$/i.test(opts.kty)) { } else if (/^RSA$/i.test(opts.kty)) {
p = Rasha.generate(opts); p = Rasha.generate(opts);
} else { } else {
return Promise.Reject( return Promise.Reject(
new Error( new Error(
"'" +
opts.kty +
"' is not a well-supported key type." +
Keypairs._universal +
" Please choose 'EC', or 'RSA' if you have good reason to."
)
);
}
return p.then(function(pair) {
return Keypairs.thumbprint({ jwk: pair.public }).then(function(
thumb
) {
pair.private.kid = thumb; // maybe not the same id on the private key?
pair.public.kid = thumb;
return pair;
});
});
};
Keypairs.export = function(opts) {
return Eckles.export(opts).catch(function(err) {
return Rasha.export(opts).catch(function() {
return Promise.reject(err);
});
});
};
/**
* 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.
*/
Keypairs.neuter = function(opts) {
/** trying to find the best balance of an immutable copy with custom attributes */
var jwk = {};
Object.keys(opts.jwk).forEach(function(k) {
if ('undefined' === typeof opts.jwk[k]) {
return;
}
// ignore RSA and EC private parts
if (-1 !== ['d', 'p', 'q', 'dp', 'dq', 'qi'].indexOf(k)) {
return;
}
jwk[k] = JSON.parse(JSON.stringify(opts.jwk[k]));
});
return jwk;
};
Keypairs.thumbprint = function(opts) {
return Promise.resolve().then(function() {
if (/EC/i.test(opts.jwk.kty)) {
return Eckles.thumbprint(opts);
} else {
return Rasha.thumbprint(opts);
}
});
};
Keypairs.publish = function(opts) {
if ('object' !== typeof opts.jwk || !opts.jwk.kty) {
throw new Error('invalid jwk: ' + JSON.stringify(opts.jwk));
}
/** returns a copy */
var jwk = Keypairs.neuter(opts);
if (jwk.exp) {
jwk.exp = setTime(jwk.exp);
} else {
if (opts.exp) {
jwk.exp = setTime(opts.exp);
} else if (opts.expiresIn) {
jwk.exp = Math.round(Date.now() / 1000) + opts.expiresIn;
} else if (opts.expiresAt) {
jwk.exp = opts.expiresAt;
}
}
if (!jwk.use && false !== jwk.use) {
jwk.use = 'sig';
}
if (jwk.kid) {
return Promise.resolve(jwk);
}
return Keypairs.thumbprint({ jwk: jwk }).then(function(thumb) {
jwk.kid = thumb;
return jwk;
});
};
// JWT a.k.a. JWS with Claims using Compact Serialization
Keypairs.signJwt = function(opts) {
return Keypairs.thumbprint({ jwk: opts.jwk }).then(function(thumb) {
var header = opts.header || {};
var claims = JSON.parse(JSON.stringify(opts.claims || {}));
header.typ = 'JWT';
if (!header.kid) {
header.kid = thumb;
}
if (!header.alg && opts.alg) {
header.alg = opts.alg;
}
if (!claims.iat && (false === claims.iat || false === opts.iat)) {
claims.iat = undefined;
} else if (!claims.iat) {
claims.iat = Math.round(Date.now() / 1000);
}
if (opts.exp) {
claims.exp = setTime(opts.exp);
} else if (
!claims.exp &&
(false === claims.exp || false === opts.exp)
) {
claims.exp = undefined;
} else if (!claims.exp) {
throw new Error(
"opts.claims.exp should be the expiration date as seconds, human form (i.e. '1h' or '15m') or false"
);
}
if (opts.iss) {
claims.iss = opts.iss;
}
if (!claims.iss && (false === claims.iss || false === opts.iss)) {
claims.iss = undefined;
} else if (!claims.iss) {
throw new Error(
'opts.claims.iss should be in the form of https://example.com/, a secure OIDC base url'
);
}
return Keypairs.signJws({
jwk: opts.jwk,
pem: opts.pem,
protected: header,
header: undefined,
payload: claims
}).then(function(jws) {
return [jws.protected, jws.payload, jws.signature].join('.');
});
});
};
Keypairs.signJws = function(opts) {
return Keypairs.thumbprint(opts).then(function(thumb) {
function alg() {
if (!opts.jwk) {
throw new Error(
"opts.jwk must exist and must declare 'typ'"
);
}
if (opts.jwk.alg) {
return opts.jwk.alg;
}
var typ = 'RSA' === opts.jwk.kty ? 'RS' : 'ES';
return typ + Keypairs._getBits(opts);
}
function sign() {
var protect = opts.protected;
var payload = opts.payload;
// Compute JWS signature
var protectedHeader = '';
// Because unprotected headers are allowed, regrettably...
// https://stackoverflow.com/a/46288694
if (false !== protect) {
if (!protect) {
protect = {};
}
if (!protect.alg) {
protect.alg = alg();
}
// There's a particular request where ACME / Let's Encrypt explicitly doesn't use a kid
if (false === protect.kid) {
protect.kid = undefined;
} else if (!protect.kid) {
protect.kid = thumb;
}
protectedHeader = JSON.stringify(protect);
}
// Not sure how to handle the empty case since ACME POST-as-GET must be empty
//if (!payload) {
// throw new Error("opts.payload should be JSON, string, or ArrayBuffer (it may be empty, but that must be explicit)");
//}
// Trying to detect if it's a plain object (not Buffer, ArrayBuffer, Array, Uint8Array, etc)
if (
payload &&
'string' !== typeof payload &&
'undefined' === typeof payload.byteLength &&
'undefined' === typeof payload.buffer
) {
payload = JSON.stringify(payload);
}
// Converting to a buffer, even if it was just converted to a string
if ('string' === typeof 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 payload64 = Enc.bufToUrlBase64(payload);
var msg = protected64 + '.' + payload64;
return Keypairs._sign(opts, msg).then(function(buf) {
var signedMsg = {
protected: protected64,
payload: payload64,
signature: Enc.bufToUrlBase64(buf)
};
return signedMsg;
});
}
if (opts.jwk) {
return sign();
} else {
return Keypairs.import({ pem: opts.pem }).then(function(pair) {
opts.jwk = pair.private;
return sign();
});
}
});
};
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) {
if (opts.alg) {
return opts.alg.replace(/[a-z\-]/gi, '');
}
// base64 len to byte len
var len = Math.floor((opts.jwk.n || '').length * 0.75);
// TODO this may be a bug
// need to confirm that the padding is no more or less than 1 byte
if (/521/.test(opts.jwk.crv) || len >= 511) {
return '512';
} else if (/384/.test(opts.jwk.crv) || len >= 383) {
return '384';
}
return '256';
};
Keypairs._getName = function(opts) {
if (/EC/i.test(opts.jwk.kty)) {
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) {
if ('number' === typeof time) {
return time;
}
var t = time.match(/^(\-?\d+)([dhms])$/i);
if (!t || !t[0]) {
throw new Error(
"'" + "'" +
time + opts.kty +
"' should be datetime in seconds or human-readable format (i.e. 3d, 1h, 15m, 30s" "' is not a well-supported key type." +
); Keypairs._universal +
} " Please choose 'EC', or 'RSA' if you have good reason to."
)
);
}
return p.then(function(pair) {
return Keypairs.thumbprint({ jwk: pair.public }).then(function(thumb) {
pair.private.kid = thumb; // maybe not the same id on the private key?
pair.public.kid = thumb;
return pair;
});
});
};
var now = Math.round(Date.now() / 1000); Keypairs.export = function(opts) {
var num = parseInt(t[1], 10); return Eckles.export(opts).catch(function(err) {
var unit = t[2]; return Rasha.export(opts).catch(function() {
var mult = 1; return Promise.reject(err);
switch (unit) { });
// fancy fallthrough, what fun! });
case 'd': };
mult *= 24; // XXX
/*falls through*/ native.export = Keypairs.export;
case 'h':
mult *= 60;
/*falls through*/
case 'm':
mult *= 60;
/*falls through*/
case 's':
mult *= 1;
}
return now + mult * num; /**
* 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.
*/
Keypairs.neuter = function(opts) {
/** trying to find the best balance of an immutable copy with custom attributes */
var jwk = {};
Object.keys(opts.jwk).forEach(function(k) {
if ('undefined' === typeof opts.jwk[k]) {
return;
}
// ignore RSA and EC private parts
if (-1 !== ['d', 'p', 'q', 'dp', 'dq', 'qi'].indexOf(k)) {
return;
}
jwk[k] = JSON.parse(JSON.stringify(opts.jwk[k]));
});
return jwk;
};
Keypairs.thumbprint = function(opts) {
return Promise.resolve().then(function() {
if (/EC/i.test(opts.jwk.kty)) {
console.log('[debug] EC thumbprint');
return Eckles.thumbprint(opts);
} else {
console.log('[debug] RSA thumbprint');
return Rasha.thumbprint(opts);
}
});
};
Keypairs.publish = function(opts) {
if ('object' !== typeof opts.jwk || !opts.jwk.kty) {
throw new Error('invalid jwk: ' + JSON.stringify(opts.jwk));
} }
Enc.hexToBuf = function(hex) { /** returns a copy */
var arr = []; var jwk = Keypairs.neuter(opts);
hex.match(/.{2}/g).forEach(function(h) {
arr.push(parseInt(h, 16)); if (jwk.exp) {
jwk.exp = setTime(jwk.exp);
} else {
if (opts.exp) {
jwk.exp = setTime(opts.exp);
} else if (opts.expiresIn) {
jwk.exp = Math.round(Date.now() / 1000) + opts.expiresIn;
} else if (opts.expiresAt) {
jwk.exp = opts.expiresAt;
}
}
if (!jwk.use && false !== jwk.use) {
jwk.use = 'sig';
}
if (jwk.kid) {
return Promise.resolve(jwk);
}
return Keypairs.thumbprint({ jwk: jwk }).then(function(thumb) {
jwk.kid = thumb;
return jwk;
});
};
// JWT a.k.a. JWS with Claims using Compact Serialization
Keypairs.signJwt = function(opts) {
return Keypairs.thumbprint({ jwk: opts.jwk }).then(function(thumb) {
var header = opts.header || {};
var claims = JSON.parse(JSON.stringify(opts.claims || {}));
header.typ = 'JWT';
if (!header.kid && false !== header.kid) {
header.kid = thumb;
}
if (!header.alg && opts.alg) {
header.alg = opts.alg;
}
if (!claims.iat && (false === claims.iat || false === opts.iat)) {
claims.iat = undefined;
} else if (!claims.iat) {
claims.iat = Math.round(Date.now() / 1000);
}
if (opts.exp) {
claims.exp = setTime(opts.exp);
} else if (
!claims.exp &&
(false === claims.exp || false === opts.exp)
) {
claims.exp = undefined;
} else if (!claims.exp) {
throw new Error(
"opts.claims.exp should be the expiration date as seconds, human form (i.e. '1h' or '15m') or false"
);
}
if (opts.iss) {
claims.iss = opts.iss;
}
if (!claims.iss && (false === claims.iss || false === opts.iss)) {
claims.iss = undefined;
} else if (!claims.iss) {
throw new Error(
'opts.claims.iss should be in the form of https://example.com/, a secure OIDC base url'
);
}
return Keypairs.signJws({
jwk: opts.jwk,
pem: opts.pem,
protected: header,
header: undefined,
payload: claims
}).then(function(jws) {
return [jws.protected, jws.payload, jws.signature].join('.');
}); });
return 'undefined' !== typeof Uint8Array ? new Uint8Array(arr) : arr; });
}; };
Enc.strToUrlBase64 = function(str) {
return Enc.bufToUrlBase64(Enc.binToBuf(str)); Keypairs.signJws = function(opts) {
}; return Keypairs.thumbprint(opts).then(function(thumb) {
Enc.binToBuf = function(bin) { function alg() {
var arr = bin.split('').map(function(ch) { if (!opts.jwk) {
return ch.charCodeAt(0); throw new Error("opts.jwk must exist and must declare 'typ'");
}); }
return 'undefined' !== typeof Uint8Array ? new Uint8Array(arr) : arr; if (opts.jwk.alg) {
}; return opts.jwk.alg;
})('undefined' !== typeof module ? module.exports : window); }
var typ = 'RSA' === opts.jwk.kty ? 'RS' : 'ES';
return typ + Keypairs._getBits(opts);
}
function sign() {
var protect = opts.protected;
var payload = opts.payload;
// Compute JWS signature
var protectedHeader = '';
// Because unprotected headers are allowed, regrettably...
// https://stackoverflow.com/a/46288694
if (false !== protect) {
if (!protect) {
protect = {};
}
if (!protect.alg) {
protect.alg = alg();
}
// There's a particular request where ACME / Let's Encrypt explicitly doesn't use a kid
if (false === protect.kid) {
protect.kid = undefined;
} else if (!protect.kid) {
protect.kid = thumb;
}
protectedHeader = JSON.stringify(protect);
}
// Not sure how to handle the empty case since ACME POST-as-GET must be empty
//if (!payload) {
// throw new Error("opts.payload should be JSON, string, or ArrayBuffer (it may be empty, but that must be explicit)");
//}
// Trying to detect if it's a plain object (not Buffer, ArrayBuffer, Array, Uint8Array, etc)
if (
payload &&
'string' !== typeof payload &&
'undefined' === typeof payload.byteLength &&
'undefined' === typeof payload.buffer
) {
payload = JSON.stringify(payload);
}
// Converting to a buffer, even if it was just converted to a string
if ('string' === typeof payload) {
payload = Enc.binToBuf(payload);
}
var protected64 = Enc.strToUrlBase64(protectedHeader);
var payload64 = Enc.bufToUrlBase64(payload);
var msg = protected64 + '.' + payload64;
return native._sign(opts, msg).then(function(buf) {
var signedMsg = {
protected: protected64,
payload: payload64,
signature: Enc.bufToUrlBase64(buf)
};
return signedMsg;
});
}
if (opts.jwk) {
return sign();
} else {
return Keypairs.import({ pem: opts.pem }).then(function(pair) {
opts.jwk = pair.private;
return sign();
});
}
});
};
Keypairs._getBits = function(opts) {
if (opts.alg) {
return opts.alg.replace(/[a-z\-]/gi, '');
}
// base64 len to byte len
var len = Math.floor((opts.jwk.n || '').length * 0.75);
// TODO this may be a bug
// need to confirm that the padding is no more or less than 1 byte
if (/521/.test(opts.jwk.crv) || len >= 511) {
return '512';
} else if (/384/.test(opts.jwk.crv) || len >= 383) {
return '384';
}
return '256';
};
// XXX
native._getBits = Keypairs._getBits;
function setTime(time) {
if ('number' === typeof time) {
return time;
}
var t = time.match(/^(\-?\d+)([dhms])$/i);
if (!t || !t[0]) {
throw new Error(
"'" +
time +
"' should be datetime in seconds or human-readable format (i.e. 3d, 1h, 15m, 30s"
);
}
var now = Math.round(Date.now() / 1000);
var num = parseInt(t[1], 10);
var unit = t[2];
var mult = 1;
switch (unit) {
// fancy fallthrough, what fun!
case 'd':
mult *= 24;
/*falls through*/
case 'h':
mult *= 60;
/*falls through*/
case 'm':
mult *= 60;
/*falls through*/
case 's':
mult *= 1;
}
return now + mult * num;
}
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.strToUrlBase64 = function(str) {
return Enc.bufToUrlBase64(Enc.binToBuf(str));
};
Enc.binToBuf = function(bin) {
var arr = bin.split('').map(function(ch) {
return ch.charCodeAt(0);
});
return 'undefined' !== typeof Uint8Array ? new Uint8Array(arr) : arr;
};

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,235 +1,158 @@
/*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 sha2 = require('./node/sha2.js');
var Enc = require('omnibuffer');
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" +
" properly and securely use non-standard crypto then you shouldn't need Bluecrypt anyway.";
native._stance = RSA._stance;
RSA.generate = native.generate;
// 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
var jwk = {};
Object.keys(opts.jwk).forEach(function(k) {
if ('undefined' === typeof opts.jwk[k]) {
return;
}
// ignore RSA private parts
if (-1 !== ['d', 'p', 'q', 'dp', 'dq', 'qi'].indexOf(k)) {
return;
}
jwk[k] = JSON.parse(JSON.stringify(opts.jwk[k]));
});
return jwk;
};
native.neuter = RSA.neuter;
// https://stackoverflow.com/questions/42588786/how-to-fingerprint-a-jwk
RSA.__thumbprint = function(jwk) {
// Use the same entropy for SHA as for key
var len = Math.floor(jwk.n.length * 0.75);
var alg = 'SHA-256';
// TODO this may be a bug
// need to confirm that the padding is no more or less than 1 byte
if (len >= 511) {
alg = 'SHA-512';
} else if (len >= 383) {
alg = 'SHA-384';
} }
var PEM = exports.PEM; return sha2
var SSH = exports.SSH; .sum(alg, '{"e":"' + jwk.e + '","kty":"RSA","n":"' + jwk.n + '"}')
var Enc = {}; .then(function(hash) {
var textEncoder = new TextEncoder(); return Enc.bufToUrlBase64(Uint8Array.from(hash));
});
};
RSA._stance = RSA.thumbprint = function(opts) {
"We take the stance that if you're knowledgeable enough to" + return Promise.resolve().then(function() {
" properly and securely use non-standard crypto then you shouldn't need Bluecrypt anyway."; var jwk;
RSA._universal = if ('EC' === opts.kty) {
'Bluecrypt only supports crypto with standard cross-browser and cross-platform support.'; jwk = opts;
RSA.generate = function(opts) { } else if (opts.jwk) {
var wcOpts = {}; jwk = opts.jwk;
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 { } else {
// Public key thumbprints should be paired with a hash of similar length, return RSA.import(opts).then(function(jwk) {
// so anything above SHA-512's keyspace would be left under-represented anyway. return RSA.__thumbprint(jwk);
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.
// 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
var jwk = {};
Object.keys(opts.jwk).forEach(function(k) {
if ('undefined' === typeof opts.jwk[k]) {
return;
}
// ignore RSA private parts
if (-1 !== ['d', 'p', 'q', 'dp', 'dq', 'qi'].indexOf(k)) {
return;
}
jwk[k] = JSON.parse(JSON.stringify(opts.jwk[k]));
});
return jwk;
};
// https://stackoverflow.com/questions/42588786/how-to-fingerprint-a-jwk
RSA.__thumbprint = function(jwk) {
// Use the same entropy for SHA as for key
var len = Math.floor(jwk.n.length * 0.75);
var alg = 'SHA-256';
// TODO this may be a bug
// need to confirm that the padding is no more or less than 1 byte
if (len >= 511) {
alg = 'SHA-512';
} else if (len >= 383) {
alg = 'SHA-384';
} }
return window.crypto.subtle return RSA.__thumbprint(jwk);
.digest( });
{ name: alg }, };
textEncoder.encode(
'{"e":"' + jwk.e + '","kty":"RSA","n":"' + jwk.n + '"}'
)
)
.then(function(hash) {
return Enc.bufToUrlBase64(new Uint8Array(hash));
});
};
RSA.thumbprint = function(opts) { RSA.export = function(opts) {
return Promise.resolve().then(function() { return Promise.resolve().then(function() {
var jwk; if (!opts || !opts.jwk || 'object' !== typeof opts.jwk) {
if ('EC' === opts.kty) { throw new Error('must pass { jwk: jwk }');
jwk = opts; }
} else if (opts.jwk) { var jwk = JSON.parse(JSON.stringify(opts.jwk));
jwk = opts.jwk; var format = opts.format;
} else { var pub = opts.public;
return RSA.import(opts).then(function(jwk) { if (pub || -1 !== ['spki', 'pkix', 'ssh', 'rfc4716'].indexOf(format)) {
return RSA.__thumbprint(jwk); jwk = RSA.neuter({ jwk: jwk });
}); }
} if ('RSA' !== jwk.kty) {
return RSA.__thumbprint(jwk); throw new Error("options.jwk.kty must be 'RSA' for RSA keys");
}); }
}; if (!jwk.p) {
// TODO test for n and e
RSA.export = function(opts) { pub = true;
return Promise.resolve().then(function() { if (!format || 'pkcs1' === format) {
if (!opts || !opts.jwk || 'object' !== typeof opts.jwk) { format = 'pkcs1';
throw new Error('must pass { jwk: jwk }');
}
var jwk = JSON.parse(JSON.stringify(opts.jwk));
var format = opts.format;
var pub = opts.public;
if (
pub ||
-1 !== ['spki', 'pkix', 'ssh', 'rfc4716'].indexOf(format)
) {
jwk = RSA.neuter({ jwk: jwk });
}
if ('RSA' !== jwk.kty) {
throw new Error("options.jwk.kty must be 'RSA' for RSA keys");
}
if (!jwk.p) {
// TODO test for n and e
pub = true;
if (!format || 'pkcs1' === format) {
format = 'pkcs1';
} else if (-1 !== ['spki', 'pkix'].indexOf(format)) {
format = 'spki';
} else if (-1 !== ['ssh', 'rfc4716'].indexOf(format)) {
format = 'ssh';
} else {
throw new Error(
"options.format must be 'spki', 'pkcs1', or 'ssh' for public RSA keys, not (" +
typeof format +
') ' +
format
);
}
} else {
// TODO test for all necessary keys (d, p, q ...)
if (!format || 'pkcs1' === format) {
format = 'pkcs1';
} else if ('pkcs8' !== format) {
throw new Error(
"options.format must be 'pkcs1' or 'pkcs8' for private RSA keys"
);
}
}
if ('pkcs1' === format) {
if (jwk.d) {
return PEM.packBlock({
type: 'RSA PRIVATE KEY',
bytes: x509.packPkcs1(jwk)
});
} else {
return PEM.packBlock({
type: 'RSA PUBLIC KEY',
bytes: x509.packPkcs1(jwk)
});
}
} else if ('pkcs8' === format) {
return PEM.packBlock({
type: 'PRIVATE KEY',
bytes: x509.packPkcs8(jwk)
});
} else if (-1 !== ['spki', 'pkix'].indexOf(format)) { } else if (-1 !== ['spki', 'pkix'].indexOf(format)) {
return PEM.packBlock({ format = 'spki';
type: 'PUBLIC KEY',
bytes: x509.packSpki(jwk)
});
} else if (-1 !== ['ssh', 'rfc4716'].indexOf(format)) { } else if (-1 !== ['ssh', 'rfc4716'].indexOf(format)) {
return SSH.pack({ jwk: jwk, comment: opts.comment }); format = 'ssh';
} else { } else {
throw new Error( throw new Error(
'Sanity Error: reached unreachable code block with format: ' + "options.format must be 'spki', 'pkcs1', or 'ssh' for public RSA keys, not (" +
typeof format +
') ' +
format format
); );
} }
}); } else {
}; // TODO test for all necessary keys (d, p, q ...)
RSA.pack = function(opts) { if (!format || 'pkcs1' === format) {
// wrapped in a promise for API compatibility format = 'pkcs1';
// with the forthcoming browser version } else if ('pkcs8' !== format) {
// (and potential future native node capability) throw new Error(
return Promise.resolve().then(function() { "options.format must be 'pkcs1' or 'pkcs8' for private RSA keys"
return RSA.export(opts); );
}); }
}; }
Enc.bufToUrlBase64 = function(u8) { if ('pkcs1' === format) {
return Enc.bufToBase64(u8) if (jwk.d) {
.replace(/\+/g, '-') return PEM.packBlock({
.replace(/\//g, '_') type: 'RSA PRIVATE KEY',
.replace(/=/g, ''); bytes: x509.packPkcs1(jwk)
}; });
} else {
return PEM.packBlock({
type: 'RSA PUBLIC KEY',
bytes: x509.packPkcs1(jwk)
});
}
} else if ('pkcs8' === format) {
return PEM.packBlock({
type: 'PRIVATE KEY',
bytes: x509.packPkcs8(jwk)
});
} else if (-1 !== ['spki', 'pkix'].indexOf(format)) {
return PEM.packBlock({
type: 'PUBLIC KEY',
bytes: x509.packSpki(jwk)
});
} else if (-1 !== ['ssh', 'rfc4716'].indexOf(format)) {
return SSH.pack({ jwk: jwk, comment: opts.comment });
} else {
throw new Error(
'Sanity Error: reached unreachable code block with format: ' +
format
);
}
});
};
native.export = RSA.export;
Enc.bufToBase64 = function(u8) { RSA.pack = function(opts) {
var bin = ''; // wrapped in a promise for API compatibility
u8.forEach(function(i) { // with the forthcoming browser version
bin += String.fromCharCode(i); // (and potential future native node capability)
}); return Promise.resolve().then(function() {
return btoa(bin); return RSA.export(opts);
}; });
})('undefined' !== typeof module ? module.exports : window); };

View File

@ -1,260 +1,186 @@
(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;
if ('P-384' === jwk.crv) { if ('P-384' === jwk.crv) {
olen = OBJ_ID_EC_384.length / 2; olen = OBJ_ID_EC_384.length / 2;
index = 8; index = 8;
len = 48; len = 48;
} }
if (len !== u8[index - 1]) { if (len !== u8[index - 1]) {
throw new Error('Unexpected bitlength ' + len); throw new Error('Unexpected bitlength ' + len);
} }
// private part is d // private part is d
var d = u8.slice(index, index + len); var d = u8.slice(index, index + len);
// compression bit index // compression bit index
var ci = index + len + 2 + olen + 2 + 3; var ci = index + len + 2 + olen + 2 + 3;
var c = u8[ci]; var c = u8[ci];
var x, y; var x, y;
if (0x04 === c) { if (0x04 === c) {
y = u8.slice(ci + 1 + len, ci + 1 + len + len); y = u8.slice(ci + 1 + len, ci + 1 + len + len);
} else if (0x02 !== c) { } else if (0x02 !== c) {
throw new Error('not a supported EC private key'); throw new Error('not a supported EC private key');
} }
x = u8.slice(ci + 1, ci + 1 + len); x = u8.slice(ci + 1, ci + 1 + len);
return { return {
kty: jwk.kty, kty: jwk.kty,
crv: jwk.crv, crv: jwk.crv,
d: Enc.bufToUrlBase64(d), d: Enc.bufToUrlBase64(d),
//, dh: Enc.bufToHex(d) //, dh: Enc.bufToHex(d)
x: Enc.bufToUrlBase64(x), x: Enc.bufToUrlBase64(x),
//, xh: Enc.bufToHex(x) //, xh: Enc.bufToHex(x)
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) {
var index = 24 + OBJ_ID_EC.length / 2;
var len = 32;
if ('P-384' === jwk.crv) {
index = 24 + OBJ_ID_EC_384.length / 2 + 2;
len = 48;
}
//console.log(index, u8.slice(index));
if (0x04 !== u8[index]) {
//console.log(jwk);
throw new Error('privkey not found');
}
var d = u8.slice(index + 2, index + 2 + len);
var ci = index + 2 + len + 5;
var xi = ci + 1;
var x = u8.slice(xi, xi + len);
var yi = xi + len;
var y;
if (0x04 === u8[ci]) {
y = u8.slice(yi, yi + len);
} else if (0x02 !== u8[ci]) {
throw new Error('invalid compression bit (expected 0x04 or 0x02)');
}
return {
kty: jwk.kty,
crv: jwk.crv,
d: Enc.bufToUrlBase64(d),
//, dh: Enc.bufToHex(d)
x: Enc.bufToUrlBase64(x),
//, xh: Enc.bufToHex(x)
y: Enc.bufToUrlBase64(y)
//, yh: Enc.bufToHex(y)
}; };
};
x509.parsePkcs8 = function parseEcPkcs8(u8, jwk) { x509.parseSpki = function parsePem(u8, jwk) {
var index = 24 + OBJ_ID_EC.length / 2; var ci = 16 + OBJ_ID_EC.length / 2;
var len = 32; var len = 32;
if ('P-384' === jwk.crv) {
index = 24 + OBJ_ID_EC_384.length / 2 + 2;
len = 48;
}
//console.log(index, u8.slice(index)); if ('P-384' === jwk.crv) {
if (0x04 !== u8[index]) { ci = 16 + OBJ_ID_EC_384.length / 2;
//console.log(jwk); len = 48;
throw new Error('privkey not found'); }
}
var d = u8.slice(index + 2, index + 2 + len);
var ci = index + 2 + len + 5;
var xi = ci + 1;
var x = u8.slice(xi, xi + len);
var yi = xi + len;
var y;
if (0x04 === u8[ci]) {
y = u8.slice(yi, yi + len);
} else if (0x02 !== u8[ci]) {
throw new Error('invalid compression bit (expected 0x04 or 0x02)');
}
return { var c = u8[ci];
kty: jwk.kty, var xi = ci + 1;
crv: jwk.crv, var x = u8.slice(xi, xi + len);
d: Enc.bufToUrlBase64(d), var yi = xi + len;
//, dh: Enc.bufToHex(d) var y;
x: Enc.bufToUrlBase64(x), if (0x04 === c) {
//, xh: Enc.bufToHex(x) y = u8.slice(yi, yi + len);
y: Enc.bufToUrlBase64(y) } else if (0x02 !== c) {
//, yh: Enc.bufToHex(y) throw new Error('not a supported EC private key');
}; }
return {
kty: jwk.kty,
crv: jwk.crv,
x: Enc.bufToUrlBase64(x),
//, xh: Enc.bufToHex(x)
y: Enc.bufToUrlBase64(y)
//, yh: Enc.bufToHex(y)
}; };
};
x509.parsePkix = x509.parseSpki;
x509.parseSpki = function parsePem(u8, jwk) { x509.packSec1 = function(jwk) {
var ci = 16 + OBJ_ID_EC.length / 2; var d = Enc.base64ToHex(jwk.d);
var len = 32; var x = Enc.base64ToHex(jwk.x);
var y = Enc.base64ToHex(jwk.y);
if ('P-384' === jwk.crv) { var objId = 'P-256' === jwk.crv ? OBJ_ID_EC : OBJ_ID_EC_384;
ci = 16 + OBJ_ID_EC_384.length / 2; return Enc.hexToBuf(
len = 48; Asn1(
} '30',
UInt('01'),
var c = u8[ci]; Asn1('04', d),
var xi = ci + 1; Asn1('A0', objId),
var x = u8.slice(xi, xi + len); Asn1('A1', BitStr('04' + x + y))
var yi = xi + len; )
var y; );
if (0x04 === c) { };
y = u8.slice(yi, yi + len); /**
} else if (0x02 !== c) { * take a private jwk and creates a der from it
throw new Error('not a supported EC private key'); * @param {*} jwk
} */
x509.packPkcs8 = function(jwk) {
return { if ('RSA' === jwk.kty) {
kty: jwk.kty,
crv: jwk.crv,
x: Enc.bufToUrlBase64(x),
//, xh: Enc.bufToHex(x)
y: Enc.bufToUrlBase64(y)
//, yh: Enc.bufToHex(y)
};
};
x509.parsePkix = x509.parseSpki;
x509.packSec1 = function(jwk) {
var d = Enc.base64ToHex(jwk.d);
var x = Enc.base64ToHex(jwk.x);
var y = Enc.base64ToHex(jwk.y);
var objId = 'P-256' === jwk.crv ? OBJ_ID_EC : OBJ_ID_EC_384;
return Enc.hexToBuf(
ASN1(
'30',
ASN1.UInt('01'),
ASN1('04', d),
ASN1('A0', objId),
ASN1('A1', ASN1.BitStr('04' + x + y))
)
);
};
/**
* take a private jwk and creates a der from it
* @param {*} jwk
*/
x509.packPkcs8 = function(jwk) {
if ('RSA' === jwk.kty) {
if (!jwk.d) {
// Public RSA
return Enc.hexToBuf(
ASN1(
'30',
ASN1(
'30',
ASN1('06', '2a864886f70d010101'),
ASN1('05')
),
ASN1.BitStr(
ASN1(
'30',
ASN1.UInt(Enc.base64ToHex(jwk.n)),
ASN1.UInt(Enc.base64ToHex(jwk.e))
)
)
)
);
}
// Private RSA
return Enc.hexToBuf(
ASN1(
'30',
ASN1.UInt('00'),
ASN1('30', ASN1('06', '2a864886f70d010101'), ASN1('05')),
ASN1(
'04',
ASN1(
'30',
ASN1.UInt('00'),
ASN1.UInt(Enc.base64ToHex(jwk.n)),
ASN1.UInt(Enc.base64ToHex(jwk.e)),
ASN1.UInt(Enc.base64ToHex(jwk.d)),
ASN1.UInt(Enc.base64ToHex(jwk.p)),
ASN1.UInt(Enc.base64ToHex(jwk.q)),
ASN1.UInt(Enc.base64ToHex(jwk.dp)),
ASN1.UInt(Enc.base64ToHex(jwk.dq)),
ASN1.UInt(Enc.base64ToHex(jwk.qi))
)
)
)
);
}
var d = Enc.base64ToHex(jwk.d);
var x = Enc.base64ToHex(jwk.x);
var y = Enc.base64ToHex(jwk.y);
var objId = 'P-256' === jwk.crv ? OBJ_ID_EC : OBJ_ID_EC_384;
return Enc.hexToBuf(
ASN1(
'30',
ASN1.UInt('00'),
ASN1('30', OBJ_ID_EC_PUB, objId),
ASN1(
'04',
ASN1(
'30',
ASN1.UInt('01'),
ASN1('04', d),
ASN1('A1', ASN1.BitStr('04' + x + y))
)
)
)
);
};
x509.packSpki = function(jwk) {
if (/EC/i.test(jwk.kty)) {
return x509.packSpkiEc(jwk);
}
return x509.packSpkiRsa(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 +189,159 @@
// 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) {
var x = Enc.base64ToHex(jwk.x); var d = Enc.base64ToHex(jwk.d);
var y = Enc.base64ToHex(jwk.y); var x = Enc.base64ToHex(jwk.x);
var objId = 'P-256' === jwk.crv ? OBJ_ID_EC : OBJ_ID_EC_384; var y = Enc.base64ToHex(jwk.y);
var objId = 'P-256' === jwk.crv ? OBJ_ID_EC : OBJ_ID_EC_384;
return Enc.hexToBuf(
Asn1(
'30',
UInt('00'),
Asn1('30', OBJ_ID_EC_PUB, objId),
Asn1(
'04',
Asn1(
'30',
UInt('01'),
Asn1('04', d),
Asn1('A1', BitStr('04' + x + y))
)
)
)
);
};
x509.packSpki = function(jwk) {
if (/EC/i.test(jwk.kty)) {
return x509.packSpkiEc(jwk);
}
return x509.packSpkiRsa(jwk);
};
x509.packSpkiRsa = function(jwk) {
if (!jwk.d) {
// Public RSA
return Enc.hexToBuf( return Enc.hexToBuf(
ASN1( Asn1(
'30', '30',
ASN1('30', OBJ_ID_EC_PUB, objId), Asn1('30', Asn1('06', '2a864886f70d010101'), Asn1('05')),
ASN1.BitStr('04' + x + y) BitStr(
Asn1(
'30',
UInt(Enc.base64ToHex(jwk.n)),
UInt(Enc.base64ToHex(jwk.e))
)
)
) )
); );
}; }
x509.packPkix = x509.packSpki;
})('undefined' !== typeof module ? module.exports : window); // Private RSA
return Enc.hexToBuf(
Asn1(
'30',
UInt('00'),
Asn1('30', Asn1('06', '2a864886f70d010101'), Asn1('05')),
Asn1(
'04',
Asn1(
'30',
UInt('00'),
UInt(Enc.base64ToHex(jwk.n)),
UInt(Enc.base64ToHex(jwk.e)),
UInt(Enc.base64ToHex(jwk.d)),
UInt(Enc.base64ToHex(jwk.p)),
UInt(Enc.base64ToHex(jwk.q)),
UInt(Enc.base64ToHex(jwk.dp)),
UInt(Enc.base64ToHex(jwk.dq)),
UInt(Enc.base64ToHex(jwk.qi))
)
)
)
);
};
x509.packSpkiEc = function(jwk) {
var x = Enc.base64ToHex(jwk.x);
var y = Enc.base64ToHex(jwk.y);
var objId = 'P-256' === jwk.crv ? OBJ_ID_EC : OBJ_ID_EC_384;
return Enc.hexToBuf(
Asn1('30', Asn1('30', OBJ_ID_EC_PUB, objId), BitStr('04' + x + y))
);
};
x509.packPkix = x509.packSpki;
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);
}