mirror of
				https://github.com/therootcompany/acme.js.git
				synced 2024-11-16 17:29:00 +00:00 
			
		
		
		
	WIP acme accounts
This commit is contained in:
		
							parent
							
								
									959d2ff009
								
							
						
					
					
						commit
						3156229e2c
					
				
							
								
								
									
										9
									
								
								app.js
									
									
									
									
									
								
							
							
						
						
									
										9
									
								
								app.js
									
									
									
									
									
								
							| @ -49,10 +49,19 @@ function run() { | ||||
|       $('.js-jwk').hidden = false; | ||||
|       $$('input').map(function ($el) { $el.disabled = false; }); | ||||
|       $$('button').map(function ($el) { $el.disabled = false; }); | ||||
|       $('.js-toc-jwk').hidden = false; | ||||
|     }); | ||||
|   }); | ||||
| 
 | ||||
|   $('form.js-acme-account').addEventListener('submit', function (ev) { | ||||
|     ev.preventDefault(); | ||||
|     ev.stopPropagation(); | ||||
|     $('.js-loading').hidden = false; | ||||
|     ACME.accounts.create | ||||
|   }); | ||||
| 
 | ||||
|   $('.js-generate').hidden = false; | ||||
|   $('.js-create-account').hidden = false; | ||||
| } | ||||
| 
 | ||||
| window.addEventListener('load', run); | ||||
|  | ||||
							
								
								
									
										23
									
								
								index.html
									
									
									
									
									
								
							
							
						
						
									
										23
									
								
								index.html
									
									
									
									
									
								
							| @ -43,10 +43,29 @@ | ||||
|         <label for="-modlen5">4096</label> | ||||
|       </div> | ||||
|       <button class="js-generate" hidden>Generate</button> | ||||
|     <form> | ||||
|     </form> | ||||
| 
 | ||||
|     <h2>ACME Account</h2> | ||||
|     <form class="js-acme-account"> | ||||
|       <label for="-acmeEmail">Email:</label> | ||||
|       <input class="js-email" type="email" id="-acmeEmail"> | ||||
|       <button class="js-create-account" hidden>Create Account</button> | ||||
|     </form> | ||||
| 
 | ||||
|     <div class="js-loading" hidden>Loading</div> | ||||
|     <pre><code class="js-jwk"> </code></pre> | ||||
| 
 | ||||
|     <details class="js-toc-jwk" hidden> | ||||
|       <summary>JWK Keypair</summary> | ||||
|       <pre><code class="js-jwk"> </code></pre> | ||||
|     </details> | ||||
|     <details class="js-toc-acme-account-request" hidden> | ||||
|       <summary>ACME Account Request</summary> | ||||
|       <pre><code class="js-acme-account-request"> </code></pre> | ||||
|     </details> | ||||
|     <details class="js-toc-acme-account-response" hidden> | ||||
|       <summary>ACME Account Response</summary> | ||||
|       <pre><code class="js-acme-account-response"> </code></pre> | ||||
|     </details> | ||||
| 
 | ||||
|     <script src="./lib/ecdsa.js"></script> | ||||
|     <script src="./lib/rsa.js"></script> | ||||
|  | ||||
							
								
								
									
										144
									
								
								lib/acme.js
									
									
									
									
									
								
							
							
						
						
									
										144
									
								
								lib/acme.js
									
									
									
									
									
								
							| @ -7,6 +7,7 @@ | ||||
| /* globals Promise */ | ||||
| 
 | ||||
| var ACME = exports.ACME = {}; | ||||
| var Keypairs = exports.Keypairs || {}; | ||||
| var Enc = exports.Enc || {}; | ||||
| var Crypto = exports.Crypto || {}; | ||||
| 
 | ||||
| @ -120,79 +121,94 @@ ACME._registerAccount = function (me, options) { | ||||
|           return; | ||||
|         } | ||||
| 
 | ||||
|         var jwk = me.RSA.exportPublicJwk(options.accountKeypair); | ||||
|         var contact; | ||||
|         if (options.contact) { | ||||
|           contact = options.contact.slice(0); | ||||
|         } else if (options.email) { | ||||
|           contact = [ 'mailto:' + options.email ]; | ||||
|         var jwk = options.accountKeypair.privateKeyJwk; | ||||
|         var p; | ||||
|         if (jwk) { | ||||
|           p = Promise.resolve({ private: jwk, public: Keypairs.neuter(jwk) }); | ||||
|         } else { | ||||
|           p = Keypairs.import({ pem: options.accountKeypair.privateKeyPem }); | ||||
|         } | ||||
|         var body = { | ||||
|           termsOfServiceAgreed: tosUrl === me._tos | ||||
|         , onlyReturnExisting: false | ||||
|         , contact: contact | ||||
|         }; | ||||
|         if (options.externalAccount) { | ||||
|           // TODO is this really done by HMAC or is it arbitrary?
 | ||||
|           body.externalAccountBinding = me.RSA.signJws( | ||||
|             options.externalAccount.secret | ||||
|         return p.then(function (pair) { | ||||
|           if (pair.public.kid) { | ||||
|             pair = JSON.parse(JSON.stringify(pair)); | ||||
|             delete pair.public.kid; | ||||
|             delete pair.private.kid; | ||||
|           } | ||||
|           return pair; | ||||
|         }).then(function (pair) { | ||||
|           var contact; | ||||
|           if (options.contact) { | ||||
|             contact = options.contact.slice(0); | ||||
|           } else if (options.email) { | ||||
|             contact = [ 'mailto:' + options.email ]; | ||||
|           } | ||||
|           var body = { | ||||
|             termsOfServiceAgreed: tosUrl === me._tos | ||||
|           , onlyReturnExisting: false | ||||
|           , contact: contact | ||||
|           }; | ||||
|           if (options.externalAccount) { | ||||
|             body.externalAccountBinding = me.RSA.signJws( | ||||
|               // TODO is HMAC the standard, or is this arbitrary?
 | ||||
|               options.externalAccount.secret | ||||
|             , undefined | ||||
|             , { alg: options.externalAccount.alg || "HS256" | ||||
|               , kid: options.externalAccount.id | ||||
|               , url: me._directoryUrls.newAccount | ||||
|               } | ||||
|             , Buffer.from(JSON.stringify(pair.public)) | ||||
|             ); | ||||
|           } | ||||
|           var payload = JSON.stringify(body); | ||||
|           var jws = Keypairs.signJws( | ||||
|             options.accountKeypair | ||||
|           , undefined | ||||
|           , { alg: "HS256" | ||||
|             , kid: options.externalAccount.id | ||||
|           , { nonce: me._nonce | ||||
|             , alg: (me._alg || 'RS256') | ||||
|             , url: me._directoryUrls.newAccount | ||||
|             , jwk: pair.public | ||||
|             } | ||||
|           , Buffer.from(JSON.stringify(jwk)) | ||||
|           , Buffer.from(payload) | ||||
|           ); | ||||
|         } | ||||
|         var payload = JSON.stringify(body); | ||||
|         var jws = me.RSA.signJws( | ||||
|           options.accountKeypair | ||||
|         , undefined | ||||
|         , { nonce: me._nonce | ||||
|           , alg: (me._alg || 'RS256') | ||||
| 
 | ||||
|           delete jws.header; | ||||
|           if (me.debug) { console.debug('[acme-v2] accounts.create JSON body:'); } | ||||
|           if (me.debug) { console.debug(jws); } | ||||
|           me._nonce = null; | ||||
|           return me._request({ | ||||
|             method: 'POST' | ||||
|           , url: me._directoryUrls.newAccount | ||||
|           , jwk: jwk | ||||
|           } | ||||
|         , Buffer.from(payload) | ||||
|         ); | ||||
|           , headers: { 'Content-Type': 'application/jose+json' } | ||||
|           , json: jws | ||||
|           }).then(function (resp) { | ||||
|             var account = resp.body; | ||||
| 
 | ||||
|         delete jws.header; | ||||
|         if (me.debug) { console.debug('[acme-v2] accounts.create JSON body:'); } | ||||
|         if (me.debug) { console.debug(jws); } | ||||
|         me._nonce = null; | ||||
|         return me._request({ | ||||
|           method: 'POST' | ||||
|         , url: me._directoryUrls.newAccount | ||||
|         , headers: { 'Content-Type': 'application/jose+json' } | ||||
|         , json: jws | ||||
|         }).then(function (resp) { | ||||
|           var account = resp.body; | ||||
|             if (2 !== Math.floor(resp.statusCode / 100)) { | ||||
|               throw new Error('account error: ' + JSON.stringify(body)); | ||||
|             } | ||||
| 
 | ||||
|           if (2 !== Math.floor(resp.statusCode / 100)) { | ||||
|             throw new Error('account error: ' + JSON.stringify(body)); | ||||
|           } | ||||
|             me._nonce = resp.toJSON().headers['replay-nonce']; | ||||
|             var location = resp.toJSON().headers.location; | ||||
|             // the account id url
 | ||||
|             me._kid = location; | ||||
|             if (me.debug) { console.debug('[DEBUG] new account location:'); } | ||||
|             if (me.debug) { console.debug(location); } | ||||
|             if (me.debug) { console.debug(resp.toJSON()); } | ||||
| 
 | ||||
|           me._nonce = resp.toJSON().headers['replay-nonce']; | ||||
|           var location = resp.toJSON().headers.location; | ||||
|           // the account id url
 | ||||
|           me._kid = location; | ||||
|           if (me.debug) { console.debug('[DEBUG] new account location:'); } | ||||
|           if (me.debug) { console.debug(location); } | ||||
|           if (me.debug) { console.debug(resp.toJSON()); } | ||||
| 
 | ||||
|           /* | ||||
|           { | ||||
|             contact: ["mailto:jon@example.com"], | ||||
|             orders: "https://some-url", | ||||
|             status: 'valid' | ||||
|           } | ||||
|           */ | ||||
|           if (!account) { account = { _emptyResponse: true, key: {} }; } | ||||
|           // https://git.coolaj86.com/coolaj86/acme-v2.js/issues/8
 | ||||
|           if (!account.key) { account.key = {}; } | ||||
|           account.key.kid = me._kid; | ||||
|           return account; | ||||
|         }).then(resolve, reject); | ||||
|             /* | ||||
|             { | ||||
|               contact: ["mailto:jon@example.com"], | ||||
|               orders: "https://some-url", | ||||
|               status: 'valid' | ||||
|             } | ||||
|             */ | ||||
|             if (!account) { account = { _emptyResponse: true, key: {} }; } | ||||
|             // https://git.coolaj86.com/coolaj86/acme-v2.js/issues/8
 | ||||
|             if (!account.key) { account.key = {}; } | ||||
|             account.key.kid = me._kid; | ||||
|             return account; | ||||
|           }).then(resolve, reject); | ||||
|         }); | ||||
|       } | ||||
| 
 | ||||
|       if (me.debug) { console.debug('[acme-v2] agreeToTerms'); } | ||||
|  | ||||
							
								
								
									
										127
									
								
								lib/asn1-packer.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										127
									
								
								lib/asn1-packer.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,127 @@ | ||||
| ;(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)); | ||||
							
								
								
									
										161
									
								
								lib/asn1-parser.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										161
									
								
								lib/asn1-parser.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,161 @@ | ||||
| // 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 { der: 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)); | ||||
							
								
								
									
										175
									
								
								lib/keypairs.js
									
									
									
									
									
								
							
							
						
						
									
										175
									
								
								lib/keypairs.js
									
									
									
									
									
								
							| @ -5,6 +5,7 @@ | ||||
| var Keypairs = exports.Keypairs = {}; | ||||
| var Rasha = exports.Rasha || require('rasha'); | ||||
| var Eckles = exports.Eckles || require('eckles'); | ||||
| var Enc = exports.Enc || {}; | ||||
| 
 | ||||
| Keypairs._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."; | ||||
| @ -76,6 +77,163 @@ Keypairs.publish = function (opts) { | ||||
|   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'"); | ||||
|       } | ||||
|       return ('RSA' === opts.jwk.kty) ? "RS256" : "ES256"; | ||||
|     } | ||||
| 
 | ||||
|     function sign(pem) { | ||||
|       var header = opts.header; | ||||
|       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 Let's Encrypt explicitly doesn't use a kid
 | ||||
|         if (!protect.kid && false !== 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.byteLength) | ||||
|       ) { | ||||
|         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 nodeAlg = "SHA" + (((protect||header).alg||'').replace(/^[^\d]+/, '')||'256'); | ||||
|       var protected64 = Enc.strToUrlBase64(protectedHeader); | ||||
|       var payload64 = Enc.bufToUrlBase64(payload); | ||||
|       var binsig = require('crypto') | ||||
|         .createSign(nodeAlg) | ||||
|         .update(protect ? (protected64 + "." + payload64) : payload64) | ||||
|         .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 = convertIfEcdsa(binsig); | ||||
|       } | ||||
| 
 | ||||
|       var sig = binsig.toString('base64') | ||||
|         .replace(/\+/g, '-') | ||||
|         .replace(/\//g, '_') | ||||
|         .replace(/=/g, '') | ||||
|       ; | ||||
| 
 | ||||
|       return { | ||||
|         header: header | ||||
|       , protected: protected64 || undefined | ||||
|       , payload: payload64 | ||||
|       , signature: sig | ||||
|       }; | ||||
|     } | ||||
| 
 | ||||
|     function convertIfEcdsa(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 Enc.hexToBuf(r + s); | ||||
|     } | ||||
| 
 | ||||
|     if (opts.pem && opts.jwk) { | ||||
|       return sign(opts.pem); | ||||
|     } else { | ||||
|       return Keypairs.export({ jwk: opts.jwk }).then(sign); | ||||
|     } | ||||
|   }); | ||||
| }; | ||||
| 
 | ||||
| function setTime(time) { | ||||
|   if ('number' === typeof time) { return time; } | ||||
| 
 | ||||
| @ -106,4 +264,21 @@ function setTime(time) { | ||||
|   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; | ||||
| }; | ||||
| 
 | ||||
| }('undefined' !== typeof module ? module.exports : window)); | ||||
|  | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user