diff --git a/README.md b/README.md index e1c2864..c681459 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,261 @@ -# root-keypairs.js +# @root/keypairs -Lightweight, Zero-Dependency RSA and EC/ECDSA crypto for Node.js and Browsers \ No newline at end of file +Lightweight JavaScript RSA and ECDSA utils that work on Windows, Mac, and Linux +using modern node.js APIs (no need for C compiler). + +A thin wrapper around [Eckles.js (ECDSA)](https://git.coolaj86.com/coolaj86/eckles.js/) +and [Rasha.js (RSA)](https://git.coolaj86.com/coolaj86/rasha.js/). + +# Features + +- [x] Generate keypairs + - [x] RSA + - [x] ECDSA (P-256, P-384) +- [x] PEM-to-JWK (and SSH-to-JWK) +- [x] JWK-to-PEM (and JWK-to-SSH) +- [x] Create JWTs (and sign JWS) +- [x] SHA256 JWK Thumbprints +- [ ] JWK fetching. See [Keyfetch.js](https://npmjs.com/packages/keyfetch/) + - [ ] OIDC + - [ ] Auth0 +- [ ] CLI + - See [keypairs-cli](https://npmjs.com/packages/keypairs-cli/) + + + +# Progress + +This is fully functional, but the re-usable code from ACME.js hasn't been fully teased out for the v2.0 release. + +(SSH conversions have not yet made it to 2.0) + +# Usage + +A brief introduction to the APIs: + +```js +// generate a new keypair as jwk +// (defaults to EC P-256 when no options are specified) +Keypairs.generate().then(function(pair) { + console.log(pair.private); + console.log(pair.public); +}); +``` + +```js +// JWK to PEM +// (supports various 'format' and 'encoding' options) +return Keypairs.export({ jwk: pair.private, format: 'pkcs8' }).then(function( + pem +) { + console.log(pem); +}); +``` + +```js +// PEM to JWK +return Keypairs.import({ pem: pem }).then(function(jwk) { + console.log(jwk); +}); +``` + +```js +// Thumbprint a JWK (SHA256) +return Keypairs.thumbprint({ jwk: jwk }).then(function(thumb) { + console.log(thumb); +}); +``` + +```js +// Sign a JWT (aka compact JWS) +return Keypairs.signJwt({ + jwk: pair.private +, iss: 'https://example.com' +, exp: '1h' + // optional claims +, claims: { + , sub: 'jon.doe@gmail.com' + } +}); +``` + +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. + +## API Overview + +- generate (JWK) +- parse (PEM) +- parseOrGenerate (PEM to JWK) +- import (PEM-to-JWK) +- export (JWK-to-PEM, private or public) +- publish (Private JWK to Public JWK) +- thumbprint (JWK SHA256) +- signJwt +- signJws + +#### Keypairs.generate(options) + +Generates a public/private pair of JWKs as `{ private, public }` + +Option examples: + +- RSA `{ kty: 'RSA', modulusLength: 2048 }` +- ECDSA `{ kty: 'ECDSA', namedCurve: 'P-256' }` + +When no options are supplied EC P-256 (also known as `prime256v1` and `secp256r1`) is used by default. + +#### Keypairs.parse(options) + +Parses either a JWK (encoded as JSON) or an x509 (encdode as PEM) and gives +back the JWK representation. + +Option Examples: + +- JWK { key: '{ "kty":"EC", ... }' } +- PEM { key: '-----BEGIN PRIVATE KEY-----\n...' } +- Public Key Only { key: '-----BEGIN PRIVATE KEY-----\n...', public: true } +- Must Have Private Key { key: '-----BEGIN PUBLIC KEY-----\n...', private: true } + +Example: + +```js +Keypairs.parse({ key: '...' }).catch(function(e) { + // could not be parsed or was a public key + console.warn(e); + return Keypairs.generate(); +}); +``` + +#### Keypairs.parseOrGenerate({ key, throw, [generate opts]... }) + +Parses the key. Logs a warning on failure, marches on. +(a shortcut for the above, with `private: true`) + +Option Examples: + +- parse key if exist, otherwise generate `{ key: process.env["PRIVATE_KEY"] }` +- generated key curve `{ key: null, namedCurve: 'P-256' }` +- generated key modulus `{ key: null, modulusLength: 2048 }` + +Example: + +```js +Keypairs.parseOrGenerate({ key: process.env['PRIVATE_KEY'] }).then(function( + pair +) { + console.log(pair.public); +}); +``` + +Great for when you have a set of shared keys for development and randomly +generated keys in + +#### Keypairs.import({ pem: '...' } + +Takes a PEM in pretty much any format (PKCS1, SEC1, PKCS8, SPKI) and returns a JWK. + +#### Keypairs.export(options) + +Exports a JWK as a PEM. + +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.publish({ jwk: jwk, exp: '3d', use: 'sig' }) + +Promises a public key that adheres to the OIDC and Auth0 spec (plus expiry), suitable to be published to a JWKs URL: + +``` +{ "kty": "EC" +, "crv": "P-256" +, "x": "..." +, "y": "..." +, "kid": "..." +, "use": "sig" +, "exp": 1552074208 +} +``` + +In particular this adds "use" and "exp". + +#### 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 + // required claims +, iss: 'https://example.com' +, exp: '15m' + // all optional claims +, claims: { + } +} +``` + +Exp may be human readable duration (i.e. 1h, 15m, 30s) or a datetime in seconds. + +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 for the following: + +- generate(options) +- import({ pem: '---BEGIN...' }) +- export({ jwk: { kty: 'EC', ... }) +- thumbprint({ jwk: jwk }) + +If you want to know the algorithm-specific options that are available for those +you'll want to take a look at the corresponding documentation: + +- 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/) diff --git a/keypairs.js b/keypairs.js new file mode 100644 index 0000000..7ee27e1 --- /dev/null +++ b/keypairs.js @@ -0,0 +1,3 @@ +'use strict'; + +module.exports = require('@root/acme/keypairs'); diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..bff564e --- /dev/null +++ b/package-lock.json @@ -0,0 +1,30 @@ +{ + "name": "@root/keypairs", + "version": "1.0.0-wip.0", + "lockfileVersion": 1, + "requires": true, + "dependencies": { + "@root/acme": { + "version": "3.0.0-wip.0", + "resolved": "https://registry.npmjs.org/@root/acme/-/acme-3.0.0-wip.0.tgz", + "integrity": "sha512-IwnG3ZFt1fl81O1M+FFV91b5Kpw7GYAD1jXwvOWnq9KF50AVO6+L7MUQIAFCK1q/u/weC73DCFrw/6kFN+Vi9A==", + "requires": { + "@root/csr": "^1.0.0-wip.0", + "@root/encoding": "^1.0.1" + } + }, + "@root/csr": { + "version": "1.0.0-wip.0", + "resolved": "https://registry.npmjs.org/@root/csr/-/csr-1.0.0-wip.0.tgz", + "integrity": "sha512-ZrZeGgf/hvfIyMDAZXfD45rYriaZF6LJu7+l0ioPPKgLWVEUAUBkV53z7JbzlcPvXXr6/ZjECzWQ7MYQfMBUAg==", + "requires": { + "@root/acme": "^3.0.0-wip.0" + } + }, + "@root/encoding": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@root/encoding/-/encoding-1.0.1.tgz", + "integrity": "sha512-OaEub02ufoU038gy6bsNHQOjIn8nUjGiLcaRmJ40IUykneJkIW5fxDqKxQx48cszuNflYldsJLPPXCrGfHs8yQ==" + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..6608ffd --- /dev/null +++ b/package.json @@ -0,0 +1,33 @@ +{ + "name": "@root/keypairs", + "version": "1.0.0-wip.0", + "description": "Lightweight, Zero-Dependency RSA and EC/ECDSA crypto for Node.js and Browsers", + "main": "keypairs.js", + "scripts": { + "test": "node tests" + }, + "files": [ + "*.js", + "lib", + "dist" + ], + "repository": { + "type": "git", + "url": "https://git.rootprojects.org/root/csr.js.git" + }, + "keywords": [ + "ASN.1", + "DER", + "PEM", + "x509", + "RSA", + "EC", + "ECDSA", + "asn1" + ], + "author": "AJ ONeal (https://coolaj86.com/)", + "license": "MPL-2.0", + "dependencies": { + "@root/acme": "^3.0.0-wip.0" + } +}