|
|
@ -6,6 +6,55 @@ var crypto = require('crypto'); |
|
|
|
// prime256v1 (ANSI X9.62 named elliptic curve)
|
|
|
|
var OBJ_ID_EC = '06 08 2A8648CE3D030107'.replace(/\s+/g, '').toLowerCase(); |
|
|
|
|
|
|
|
function ASN1() { |
|
|
|
var args = Array.prototype.slice.call(arguments); |
|
|
|
var typ = args.shift(); |
|
|
|
var str = args.join('').replace(/\s+/g, ''); |
|
|
|
var len = (str.length/2); |
|
|
|
var len2 = len; |
|
|
|
var lenlen = 0; |
|
|
|
var hex = typ; |
|
|
|
var hlen = ''; |
|
|
|
// high-order bit means multiple bytes
|
|
|
|
if (len2 !== Math.round(len2)) { |
|
|
|
throw new Error("invalid hex"); |
|
|
|
} |
|
|
|
if (len2 > 127) { |
|
|
|
lenlen += 1; |
|
|
|
while (len2 > 255) { |
|
|
|
lenlen += 1; |
|
|
|
len2 = len2 >> 8; |
|
|
|
//console.warn("LEN2", len2);
|
|
|
|
} |
|
|
|
} |
|
|
|
if (lenlen) { |
|
|
|
hlen = numToHex(0x80 + lenlen); |
|
|
|
} |
|
|
|
/* |
|
|
|
console.warn( |
|
|
|
'typ:', typ |
|
|
|
, 'lenlen:', hlen |
|
|
|
, 'len:', len, numToHex(len) |
|
|
|
); |
|
|
|
console.warn('str:', str); |
|
|
|
*/ |
|
|
|
return hex + hlen + numToHex(len) + str; |
|
|
|
} |
|
|
|
ASN1.UInt = function UINT() { |
|
|
|
var str = Array.prototype.slice.call(arguments).join(''); |
|
|
|
var first = parseInt(str.slice(0, 2), 16); |
|
|
|
// high-order bit means signed, negative
|
|
|
|
// we want positive, so we pad with a leading '00'
|
|
|
|
if (0x80 & first) { str = '00' + str; } |
|
|
|
return ASN1('02', str); |
|
|
|
}; |
|
|
|
|
|
|
|
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); |
|
|
|
}; |
|
|
|
|
|
|
|
function fromBase64(b64) { |
|
|
|
var buf; |
|
|
|
var ab; |
|
|
@ -139,29 +188,32 @@ function toBase64(der) { |
|
|
|
return btoa(chs.join('')); |
|
|
|
} |
|
|
|
|
|
|
|
// these are static ASN.1 segments
|
|
|
|
// The head specifies that there will be 3 segments and a content length
|
|
|
|
// (those segments will be content, signature header, and signature)
|
|
|
|
var csrHead = '30 82 {0seq0len}'.replace(/\s+/g, ''); |
|
|
|
// The tail specifies the ES256 signature header (and is followed by the signature
|
|
|
|
var csrEcFoot = |
|
|
|
( '30 0A' |
|
|
|
function csrEcSig(r, s) { |
|
|
|
return [ |
|
|
|
ASN1('30' |
|
|
|
// 1.2.840.10045.4.3.2 ecdsaWithSHA256
|
|
|
|
// (ANSI X9.62 ECDSA algorithm with SHA256)
|
|
|
|
+ '06 08 2A 86 48 CE 3D 04 03 02' |
|
|
|
+ '03 {len} 00' // bit stream (why??)
|
|
|
|
+ '30 {rslen}' // sequence
|
|
|
|
+ '02 {rlen} {r}' // integer r
|
|
|
|
+ '02 {slen} {s}' // integer s
|
|
|
|
).replace(/\s+/g, ''); |
|
|
|
var csrDomains = '82 {dlen} {domain.tld}'; // 2+n bytes (type 82?)
|
|
|
|
, ASN1('06', '2A 86 48 CE 3D 04 03 02') |
|
|
|
) |
|
|
|
, ASN1.BitStr( |
|
|
|
ASN1('30' |
|
|
|
, ASN1.UInt(toHex(r)) |
|
|
|
, ASN1.UInt(toHex(s)) |
|
|
|
) |
|
|
|
) |
|
|
|
].join(''); |
|
|
|
} |
|
|
|
|
|
|
|
function strToHex(str) { |
|
|
|
return str.split('').map(function (ch) { |
|
|
|
var h = ch.charCodeAt(0).toString(16); |
|
|
|
if (2 === h.length) { |
|
|
|
return h; |
|
|
|
} |
|
|
|
var escstr = encodeURIComponent(str); |
|
|
|
// replaces any uri escape sequence, such as %0A,
|
|
|
|
// with binary escape, such as 0x0A
|
|
|
|
var binstr = escstr.replace(/%([0-9A-F]{2})/g, function(match, p1) { |
|
|
|
return String.fromCharCode('0x' + p1); |
|
|
|
}); |
|
|
|
return binstr.split('').map(function (b) { |
|
|
|
var h = b.charCodeAt(0).toString(16); |
|
|
|
if (2 === h.length) { return h; } |
|
|
|
return '0' + h; |
|
|
|
}).join(''); |
|
|
|
} |
|
|
@ -190,11 +242,6 @@ function fromHex(hex) { |
|
|
|
} |
|
|
|
|
|
|
|
function createCsrBodyEc(domains, xy) { |
|
|
|
var altnames = domains.map(function (d) { |
|
|
|
return csrDomains.replace(/{dlen}/, numToHex(d.length)).replace(/{domain\.tld}/, strToHex(d)); |
|
|
|
}).join('').replace(/\s+/g, ''); |
|
|
|
var sublen = domains[0].length; |
|
|
|
var sanlen = (altnames.length/2); |
|
|
|
var publen = xy.x.byteLength; |
|
|
|
var compression = '04'; |
|
|
|
var hxy = ''; |
|
|
@ -214,70 +261,55 @@ function createCsrBodyEc(domains, xy) { |
|
|
|
hxy += toHex(xy.y); |
|
|
|
} |
|
|
|
|
|
|
|
var body = [ '30 81 {+85+n}' // 4 bytes, sequence
|
|
|
|
.replace(/{[^}]+}/, numToHex( |
|
|
|
3 |
|
|
|
+ 13 + sublen |
|
|
|
+ 27 + publen // Length for EC-related P-256 stuff
|
|
|
|
+ 30 + sanlen |
|
|
|
)) |
|
|
|
var version = ASN1.UInt('00'); |
|
|
|
var subject = ASN1('30' |
|
|
|
, ASN1('31' |
|
|
|
, ASN1('30' |
|
|
|
// object id (commonName)
|
|
|
|
, ASN1('06', '55 04 03') |
|
|
|
, ASN1('0C', strToHex(domains[0]))))); |
|
|
|
var pubkey = ASN1('30' |
|
|
|
, ASN1('30' |
|
|
|
// 1.2.840.10045.2.1 ecPublicKey
|
|
|
|
// (ANSI X9.62 public key type)
|
|
|
|
, ASN1('06', '2A 86 48 CE 3D 02 01') |
|
|
|
// 1.2.840.10045.3.1.7 prime256v1
|
|
|
|
// (ANSI X9.62 named elliptic curve)
|
|
|
|
, ASN1('06', '2A 86 48 CE 3D 03 01 07') |
|
|
|
) |
|
|
|
, ASN1.BitStr(compression + hxy)); |
|
|
|
var altnames = ASN1('A0' |
|
|
|
, ASN1('30' |
|
|
|
// (extensionRequest (PKCS #9 via CRMF))
|
|
|
|
, ASN1('06', '2A 86 48 86 F7 0D 01 09 0E') |
|
|
|
, ASN1('31' |
|
|
|
, ASN1('30' |
|
|
|
, ASN1('30' |
|
|
|
// (subjectAltName (X.509 extension))
|
|
|
|
, ASN1('06', '55 1D 11') |
|
|
|
, ASN1('04' |
|
|
|
, ASN1('30', domains.map(function (d) { |
|
|
|
return ASN1('82', strToHex(d)); |
|
|
|
}).join('')))))))); |
|
|
|
var body = ASN1('30' |
|
|
|
|
|
|
|
// #0 Total 3
|
|
|
|
, '02 01 00' // 3 bytes, int 0
|
|
|
|
, version |
|
|
|
|
|
|
|
// Subject
|
|
|
|
// #1 Total 2+11+n
|
|
|
|
, '30 {3.2.0seqlen}' // 2 bytes, sequence
|
|
|
|
.replace(/{[^}]+}/, numToHex(2+2+5+2+sublen)) |
|
|
|
, '31 {4.3.0setlen}' // 2 bytes, set
|
|
|
|
.replace(/{[^}]+}/, numToHex(2+5+2+sublen)) |
|
|
|
, '30 {5.4.0seqlen}' // 2 bytes, sequence
|
|
|
|
.replace(/{[^}]+}/, numToHex(5+2+sublen)) |
|
|
|
, '06 03 55 04 03' // 5 bytes, object id (commonName)
|
|
|
|
, '0C {dlen} {domain.tld}' // 2+n bytes, utf8string
|
|
|
|
.replace(/{dlen}/, numToHex(sublen)) |
|
|
|
.replace(/{domain\.tld}/, strToHex(domains[0])) |
|
|
|
, subject |
|
|
|
|
|
|
|
// P-256 Public Key
|
|
|
|
// #2 Total 2+25+xy
|
|
|
|
, '30 {+25+xy}' // 2 bytes, sequence
|
|
|
|
.replace(/{[^}]+}/, numToHex(2+9+10+3+1+publen)) |
|
|
|
, '30 13' // 2 bytes, sequence
|
|
|
|
// 1.2.840.10045.2.1 ecPublicKey
|
|
|
|
// (ANSI X9.62 public key type)
|
|
|
|
, '06 07 2A 86 48 CE 3D 02 01' // 9 bytes, object id
|
|
|
|
// 1.2.840.10045.3.1.7 prime256v1
|
|
|
|
// (ANSI X9.62 named elliptic curve)
|
|
|
|
, '06 08 2A 86 48 CE 3D 03 01 07' // 10 bytes, object id
|
|
|
|
, '03 {xylen} 00 {xy}' // 3+1+n bytes
|
|
|
|
.replace(/{xylen}/, numToHex(publen+2)) |
|
|
|
.replace(/{xy}/, compression + hxy) |
|
|
|
, pubkey |
|
|
|
|
|
|
|
// Altnames
|
|
|
|
// #3 Total 2+28+n
|
|
|
|
, 'A0 {+28}' // 2 bytes, ?? [4B]
|
|
|
|
.replace(/{[^}]+}/, numToHex(2+11+2+2+2+5+2+2+sanlen)) |
|
|
|
, '30 {+26}' // 2 bytes, sequence
|
|
|
|
.replace(/{[^}]+}/, numToHex(11+2+2+2+5+2+2+sanlen)) |
|
|
|
// (extensionRequest (PKCS #9 via CRMF))
|
|
|
|
, '06 09 2A 86 48 86 F7 0D 01 09 0E' // 11 bytes, object id
|
|
|
|
, '31 {+13}' // 2 bytes, set
|
|
|
|
.replace(/{[^}]+}/, numToHex(2+2+5+2+2+sanlen)) |
|
|
|
, '30 {+11}' // 2 bytes, sequence
|
|
|
|
.replace(/{[^}]+}/, numToHex(2+5+2+2+sanlen)) |
|
|
|
, '30 {+9}' // 2 bytes, sequence
|
|
|
|
.replace(/{[^}]+}/, numToHex(5+2+2+sanlen)) |
|
|
|
// (subjectAltName (X.509 extension))
|
|
|
|
, '06 03 55 1D 11' // 5 bytes, object id
|
|
|
|
, '04 {+2}' // 2 bytes, octet string
|
|
|
|
.replace(/{[^}]+}/, numToHex(2+sanlen)) |
|
|
|
, '30 {+n}' // 2 bytes, sequence
|
|
|
|
.replace(/{[^}]+}/, numToHex(sanlen)) |
|
|
|
, '{altnames}' // n (elements of sequence)
|
|
|
|
.replace(/{altnames}/, altnames) |
|
|
|
]; |
|
|
|
body = body.join('').replace(/\s+/g, ''); |
|
|
|
return fromHex(body); |
|
|
|
, altnames |
|
|
|
); |
|
|
|
|
|
|
|
return body; |
|
|
|
} |
|
|
|
|
|
|
|
// https://gist.github.com/codermapuche/da4f96cdb6d5ff53b7ebc156ec46a10a
|
|
|
@ -320,7 +352,7 @@ function createEcCsr(domains, keypem, ecpub) { |
|
|
|
// TODO get pub from priv
|
|
|
|
|
|
|
|
var csrBody = createCsrBodyEc(domains, ecpub); |
|
|
|
var sig = signEc(keypem, csrBody); |
|
|
|
var sig = signEc(keypem, fromHex(csrBody)); |
|
|
|
var rLen = sig.r.byteLength; |
|
|
|
var rc = ''; |
|
|
|
var sLen = sig.s.byteLength; |
|
|
@ -329,18 +361,8 @@ function createEcCsr(domains, keypem, ecpub) { |
|
|
|
if (0x80 & new Uint8Array(sig.r)[0]) { rc = '00'; rLen += 1; } |
|
|
|
if (0x80 & new Uint8Array(sig.s)[0]) { sc = '00'; sLen += 1; } |
|
|
|
|
|
|
|
var csrSig = csrEcFoot |
|
|
|
.replace(/{len}/, numToHex(1 + 2 + 2 + 2 + rLen + sLen)) |
|
|
|
.replace(/{rslen}/, numToHex(2 + 2 + rLen + sLen)) |
|
|
|
.replace(/{rlen}/, numToHex(rLen)) |
|
|
|
.replace(/{r}/, rc + toHex(sig.r)) |
|
|
|
.replace(/{slen}/, numToHex(sLen)) |
|
|
|
.replace(/{s}/, sc + toHex(sig.s)) |
|
|
|
; |
|
|
|
|
|
|
|
// Note: If we supported P-521 a number of the lengths would change
|
|
|
|
// by one byte and that would be... annoying to update
|
|
|
|
var len = csrBody.byteLength + (csrSig.length/2); |
|
|
|
var csrSig = csrEcSig(sig.r, sig.s); |
|
|
|
|
|
|
|
/* |
|
|
|
console.log('sig:', sig.raw.byteLength, toHex(sig.raw)); |
|
|
|
console.log('r:', sig.r.byteLength, toHex(sig.r)); |
|
|
@ -348,23 +370,7 @@ function createEcCsr(domains, keypem, ecpub) { |
|
|
|
console.log('csr sig:', csrSig.length / 2, csrSig); |
|
|
|
console.log('csrBodyLen + csrSigLen', numToHex(len)); |
|
|
|
*/ |
|
|
|
var head = csrHead.replace(/{[^}]+}/, numToHex(len)); |
|
|
|
var ab = new Uint8Array(new ArrayBuffer((head.length/2) + len)); |
|
|
|
var i = 0; |
|
|
|
fromHex(head).forEach(function (b) { |
|
|
|
ab[i] = b; |
|
|
|
i += 1; |
|
|
|
}); |
|
|
|
csrBody.forEach(function (b) { |
|
|
|
ab[i] = b; |
|
|
|
i += 1; |
|
|
|
}); |
|
|
|
fromHex(csrSig).forEach(function (b) { |
|
|
|
ab[i] = b; |
|
|
|
i += 1; |
|
|
|
}); |
|
|
|
|
|
|
|
return ab; |
|
|
|
return fromHex(ASN1('30', csrBody, csrSig)); |
|
|
|
} |
|
|
|
|
|
|
|
function createEcCsrPem(domains, keypem) { |
|
|
|