v1.0.1: export and update docs
This commit is contained in:
		
							parent
							
								
									1a0e92eb52
								
							
						
					
					
						commit
						738be9b656
					
				
							
								
								
									
										114
									
								
								README.md
									
									
									
									
									
								
							
							
						
						
									
										114
									
								
								README.md
									
									
									
									
									
								
							@ -30,11 +30,19 @@ and [Rasha.js (RSA)](https://git.coolaj86.com/coolaj86/rasha.js/).
 | 
				
			|||||||
A brief (albeit somewhat nonsensical) introduction to the APIs:
 | 
					A brief (albeit somewhat nonsensical) introduction to the APIs:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
```
 | 
					```
 | 
				
			||||||
Keypairs.generate().then(function (jwk) {
 | 
					Keypairs.generate().then(function (pair) {
 | 
				
			||||||
  return Keypairs.export({ jwk: jwk }).then(function (pem) {
 | 
					  return Keypairs.export({ jwk: pair.private }).then(function (pem) {
 | 
				
			||||||
    return Keypairs.import({ pem: pem }).then(function (jwk) {
 | 
					    return Keypairs.import({ pem: pem }).then(function (jwk) {
 | 
				
			||||||
      return Keypairs.thumbprint({ jwk: jwk }).then(function (thumb) {
 | 
					      return Keypairs.thumbprint({ jwk: jwk }).then(function (thumb) {
 | 
				
			||||||
        console.log(thumb);
 | 
					        console.log(thumb);
 | 
				
			||||||
 | 
					        return Keypairs.signJwt({
 | 
				
			||||||
 | 
					          jwk: keypair.private
 | 
				
			||||||
 | 
					        , claims: {
 | 
				
			||||||
 | 
					            iss: 'https://example.com'
 | 
				
			||||||
 | 
					          , sub: 'jon.doe@gmail.com'
 | 
				
			||||||
 | 
					          , exp: Math.round(Date.now()/1000) + (3 * 24 * 60 * 60)
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
      });
 | 
					      });
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
  });
 | 
					  });
 | 
				
			||||||
@ -44,36 +52,94 @@ Keypairs.generate().then(function (jwk) {
 | 
				
			|||||||
By default ECDSA keys will be used since they've had native support in node
 | 
					By default ECDSA keys will be used since they've had native support in node
 | 
				
			||||||
_much_ longer than RSA has, and they're smaller, and faster to generate.
 | 
					_much_ longer than RSA has, and they're smaller, and faster to generate.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
## API
 | 
					## API Overview
 | 
				
			||||||
 | 
					
 | 
				
			||||||
Each of these return a Promise.
 | 
					#### Keypairs.generate(options)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
* `Keypairs.generate(options)`
 | 
					Generates a public/private pair of JWKs as `{ private, public }`
 | 
				
			||||||
  * options example `{ kty: 'RSA', modulusLength: 2048 }`
 | 
					 | 
				
			||||||
  * options example `{ kty: 'ECDSA', namedCurve: 'P-256' }`
 | 
					 | 
				
			||||||
* `Keypairs.import(options)`
 | 
					 | 
				
			||||||
  * options example `{ pem: '...' }`
 | 
					 | 
				
			||||||
* `Keypairs.export(options)`
 | 
					 | 
				
			||||||
  * options example `{ jwk: jwk }`
 | 
					 | 
				
			||||||
  * options example `{ jwk: jwk, public: true }`
 | 
					 | 
				
			||||||
* `Keypairs.thumbprint({ jwk: jwk })`
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
<!--
 | 
					Option examples:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
* `Keypairs.jws.sign(options)`
 | 
					  * RSA `{ kty: 'RSA', modulusLength: 2048 }`
 | 
				
			||||||
  * options example `{ keypair, header, protected, payload }`
 | 
					  * ECDSA `{ kty: 'ECDSA', namedCurve: 'P-256' }`
 | 
				
			||||||
* `Keypairs.csr.generate(options)`
 | 
					 | 
				
			||||||
  * options example `{ keypair, [ 'example.com' ] }`
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
-->
 | 
					When no options are supplied EC P-256 (also known as `prime256v1` and `secp256r1`) is used by default.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# Full Documentation
 | 
					#### Keypairs.import({ pem: '...' }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
Keypairs.js provides a 1-to-1 mapping to the Rasha.js and Eckles.js APIs.
 | 
					Takes a PEM in pretty much any format (PKCS1, SEC1, PKCS8, SPKI) and returns a JWK.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
The full RSA documentation is at [Rasha.js](https://git.coolaj86.com/coolaj86/rasha.js/)
 | 
					#### Keypairs.export(options)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
The full ECDSA documentation is at [Eckles.js](https://git.coolaj86.com/coolaj86/eckles.js/)
 | 
					Exports a JWK as a PEM.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
Any option you pass to Keypairs will be passed directly to the corresponding API
 | 
					Exports PEM in PKCS8 (private) or SPKI (public) by default.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Options
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					```js
 | 
				
			||||||
 | 
					{ jwk: jwk
 | 
				
			||||||
 | 
					, public: true
 | 
				
			||||||
 | 
					, encoding: 'pem' // or 'der'
 | 
				
			||||||
 | 
					, format: 'pkcs8' // or 'ssh', 'pkcs1', 'sec1', 'spki'
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					```
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#### Keypairs.thumbprint({ jwk: jwk })
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Promises a JWK-spec thumbprint: URL Base64-encoded sha256
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#### Keypairs.signJwt({ jwk, header, claims })
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Returns a JWT (otherwise known as a protected JWS in "compressed" format).
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					```js
 | 
				
			||||||
 | 
					{ jwk: jwk
 | 
				
			||||||
 | 
					, claims: {
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					```
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Header defaults:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					```js
 | 
				
			||||||
 | 
					{ kid: thumbprint
 | 
				
			||||||
 | 
					, alg: 'xS256'
 | 
				
			||||||
 | 
					, typ: 'JWT'
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					```
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Payload notes:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					* `iat: now` is added by default (set `false` to disable)
 | 
				
			||||||
 | 
					* `exp` must be set (set `false` to disable)
 | 
				
			||||||
 | 
					* `iss` should be the base URL for JWK lookup (i.e. via OIDC, Auth0)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Notes:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					`header` is actually the JWS `protected` value, as all JWTs use protected headers (yay!)
 | 
				
			||||||
 | 
					and `claims` are really the JWS `payload`.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#### Keypairs.signJws({ jwk, header, protected, payload })
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					This is provided for APIs like ACME (Let's Encrypt) that use uncompressed JWS (instead of JWT, which is compressed).
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Options:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					* `header` not what you think. Leave undefined unless you need this for the spec you're following.
 | 
				
			||||||
 | 
					* `protected` is the typical JWT-style header
 | 
				
			||||||
 | 
					  * `kid` and `alg` will be added by default (these are almost always required), set `false` explicitly to disable
 | 
				
			||||||
 | 
					* `payload` can be JSON, a string, or even a buffer (which gets URL Base64 encoded)
 | 
				
			||||||
 | 
					  * you must set this to something, even if it's an empty string, object, or Buffer
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# Additional Documentation
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Keypairs.js provides a 1-to-1 mapping to the Rasha.js and Eckles.js APIs,
 | 
				
			||||||
 | 
					but it also includes the additional convenience methods `signJwt` and `signJws`.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					That is to say that any option you pass to Keypairs will be passed directly to the corresponding API
 | 
				
			||||||
of either Rasha or Eckles.
 | 
					of either Rasha or Eckles.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					* See ECDSA documentation at [Eckles.js](https://git.coolaj86.com/coolaj86/eckles.js/)
 | 
				
			||||||
 | 
					* See RSA documentation at [Rasha.js](https://git.coolaj86.com/coolaj86/rasha.js/)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										33
									
								
								example.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										33
									
								
								example.js
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,33 @@
 | 
				
			|||||||
 | 
					'use strict';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					var Keypairs = require('./keypairs.js');
 | 
				
			||||||
 | 
					var Keyfetch = require('keyfetch');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Keypairs.generate().then(function (keypair) {
 | 
				
			||||||
 | 
					  return Keypairs.thumbprint({ jwk: keypair.public }).then(function (thumb) {
 | 
				
			||||||
 | 
					    var iss = 'https://coolaj86.com/';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // shim so that no http request is necessary
 | 
				
			||||||
 | 
					    keypair.private.kid = thumb;
 | 
				
			||||||
 | 
					    Keyfetch._setCache(iss, { thumbprint: thumb, jwk: keypair.private });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return Keypairs.signJwt({
 | 
				
			||||||
 | 
					      jwk: keypair.private
 | 
				
			||||||
 | 
					    , claims: {
 | 
				
			||||||
 | 
					        iss: iss
 | 
				
			||||||
 | 
					      , sub: 'coolaj86@gmail.com'
 | 
				
			||||||
 | 
					      , exp: Math.round(Date.now()/1000) + (3 * 24 * 60 * 60)
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					}).then(function (jwt) {
 | 
				
			||||||
 | 
					  console.log(jwt);
 | 
				
			||||||
 | 
					  return Keyfetch.verify({ jwt: jwt }).then(function (ok) {
 | 
				
			||||||
 | 
					    if (!ok) {
 | 
				
			||||||
 | 
					      throw new Error("SANITY: did not verify (should have failed)");
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    console.log("Verified token");
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					}).catch(function (err) {
 | 
				
			||||||
 | 
					  console.error(err);
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
							
								
								
									
										120
									
								
								keypairs.js
									
									
									
									
									
								
							
							
						
						
									
										120
									
								
								keypairs.js
									
									
									
									
									
								
							@ -2,7 +2,8 @@
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
var Eckles = require('eckles');
 | 
					var Eckles = require('eckles');
 | 
				
			||||||
var Rasha = require('rasha');
 | 
					var Rasha = require('rasha');
 | 
				
			||||||
var Keypairs = {};
 | 
					var Enc = {};
 | 
				
			||||||
 | 
					var Keypairs = module.exports;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/*global Promise*/
 | 
					/*global Promise*/
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -40,3 +41,120 @@ Keypairs.thumbprint = function (opts) {
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
  });
 | 
					  });
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// 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 (false === claims.iat) {
 | 
				
			||||||
 | 
					      claims.iat = undefined;
 | 
				
			||||||
 | 
					    } else if (!claims.iat) {
 | 
				
			||||||
 | 
					      claims.iat = Math.round(Date.now()/1000);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    if (false === claims.exp) {
 | 
				
			||||||
 | 
					      claims.exp = undefined;
 | 
				
			||||||
 | 
					    } else if (!claims.exp) {
 | 
				
			||||||
 | 
					      throw new Error("opts.claims.exp should be the expiration date (as seconds since the Unix epoch) or false");
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    if (false === claims.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.typ) ? "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);
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      // Convert payload to Buffer
 | 
				
			||||||
 | 
					      if ('string' !== typeof payload && !Buffer.isBuffer(payload)) {
 | 
				
			||||||
 | 
					        if (!payload) {
 | 
				
			||||||
 | 
					          throw new Error("opts.payload should be JSON, string, or Buffer (it may be empty, but that must be explicit)");
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        payload = JSON.stringify(payload);
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      if ('string' === typeof payload) {
 | 
				
			||||||
 | 
					        payload = Buffer.from(payload, 'binary');
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      // node specifies RSA-SHAxxx even whet it's actually ecdsa (it's all encoded x509 shasums anyway)
 | 
				
			||||||
 | 
					      var nodeAlg = "RSA-SHA" + (((protect||header).alg||'').replace(/^[^\d]+/, '')||'256');
 | 
				
			||||||
 | 
					      var protected64 = Enc.strToUrlBase64(protectedHeader);
 | 
				
			||||||
 | 
					      var payload64 = Enc.bufToUrlBase64(payload);
 | 
				
			||||||
 | 
					      var sig = require('crypto')
 | 
				
			||||||
 | 
					        .createSign(nodeAlg)
 | 
				
			||||||
 | 
					        .update(protect ? (protected64 + "." + payload64) : payload64)
 | 
				
			||||||
 | 
					        .sign(pem, 'base64')
 | 
				
			||||||
 | 
					        .replace(/\+/g, '-')
 | 
				
			||||||
 | 
					        .replace(/\//g, '_')
 | 
				
			||||||
 | 
					        .replace(/=/g, '')
 | 
				
			||||||
 | 
					      ;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      return {
 | 
				
			||||||
 | 
					        header: header
 | 
				
			||||||
 | 
					      , protected: protected64 || undefined
 | 
				
			||||||
 | 
					      , payload: payload64
 | 
				
			||||||
 | 
					      , signature: sig
 | 
				
			||||||
 | 
					      };
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (opts.pem && opts.jwk) {
 | 
				
			||||||
 | 
					      return sign(opts.pem);
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
 | 
					      return Keypairs.export({ jwk: opts.jwk }).then(sign);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Enc.strToUrlBase64 = function (str) {
 | 
				
			||||||
 | 
					  // node automatically can tell the difference
 | 
				
			||||||
 | 
					  // between uc2 (utf-8) strings and binary strings
 | 
				
			||||||
 | 
					  // so we don't have to re-encode the strings
 | 
				
			||||||
 | 
					  return Buffer.from(str).toString('base64')
 | 
				
			||||||
 | 
					    .replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '');
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					Enc.bufToUrlBase64 = function (buf) {
 | 
				
			||||||
 | 
					  // allow for Uint8Array as a Buffer
 | 
				
			||||||
 | 
					  return Buffer.from(buf).toString('base64')
 | 
				
			||||||
 | 
					    .replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '');
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										2
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										2
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							@ -1,6 +1,6 @@
 | 
				
			|||||||
{
 | 
					{
 | 
				
			||||||
  "name": "keypairs",
 | 
					  "name": "keypairs",
 | 
				
			||||||
  "version": "1.0.0",
 | 
					  "version": "1.0.1",
 | 
				
			||||||
  "lockfileVersion": 1,
 | 
					  "lockfileVersion": 1,
 | 
				
			||||||
  "requires": true,
 | 
					  "requires": true,
 | 
				
			||||||
  "dependencies": {
 | 
					  "dependencies": {
 | 
				
			||||||
 | 
				
			|||||||
@ -1,6 +1,6 @@
 | 
				
			|||||||
{
 | 
					{
 | 
				
			||||||
  "name": "keypairs",
 | 
					  "name": "keypairs",
 | 
				
			||||||
  "version": "1.0.0",
 | 
					  "version": "1.0.1",
 | 
				
			||||||
  "description": "Lightweight RSA/ECDSA keypair generation and JWK <-> PEM",
 | 
					  "description": "Lightweight RSA/ECDSA keypair generation and JWK <-> PEM",
 | 
				
			||||||
  "main": "keypairs.js",
 | 
					  "main": "keypairs.js",
 | 
				
			||||||
  "files": [],
 | 
					  "files": [],
 | 
				
			||||||
 | 
				
			|||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user