Sfoglia il codice sorgente

v1.0.1: export and update docs

tags/v1.0.1
AJ ONeal 6 mesi fa
parent
commit
738be9b656
5 ha cambiato i file con 244 aggiunte e 27 eliminazioni
  1. 90
    24
      README.md
  2. 33
    0
      example.js
  3. 119
    1
      keypairs.js
  4. 1
    1
      package-lock.json
  5. 1
    1
      package.json

+ 90
- 24
README.md Vedi File

@@ -30,11 +30,19 @@ and [Rasha.js (RSA)](https://git.coolaj86.com/coolaj86/rasha.js/).
30 30
 A brief (albeit somewhat nonsensical) introduction to the APIs:
31 31
 
32 32
 ```
33
-Keypairs.generate().then(function (jwk) {
34
-  return Keypairs.export({ jwk: jwk }).then(function (pem) {
33
+Keypairs.generate().then(function (pair) {
34
+  return Keypairs.export({ jwk: pair.private }).then(function (pem) {
35 35
     return Keypairs.import({ pem: pem }).then(function (jwk) {
36 36
       return Keypairs.thumbprint({ jwk: jwk }).then(function (thumb) {
37 37
         console.log(thumb);
38
+        return Keypairs.signJwt({
39
+          jwk: keypair.private
40
+        , claims: {
41
+            iss: 'https://example.com'
42
+          , sub: 'jon.doe@gmail.com'
43
+          , exp: Math.round(Date.now()/1000) + (3 * 24 * 60 * 60)
44
+          }
45
+        });
38 46
       });
39 47
     });
40 48
   });
@@ -44,36 +52,94 @@ Keypairs.generate().then(function (jwk) {
44 52
 By default ECDSA keys will be used since they've had native support in node
45 53
 _much_ longer than RSA has, and they're smaller, and faster to generate.
46 54
 
47
-## API
55
+## API Overview
48 56
 
49
-Each of these return a Promise.
57
+#### Keypairs.generate(options)
50 58
 
51
-* `Keypairs.generate(options)`
52
-  * options example `{ kty: 'RSA', modulusLength: 2048 }`
53
-  * options example `{ kty: 'ECDSA', namedCurve: 'P-256' }`
54
-* `Keypairs.import(options)`
55
-  * options example `{ pem: '...' }`
56
-* `Keypairs.export(options)`
57
-  * options example `{ jwk: jwk }`
58
-  * options example `{ jwk: jwk, public: true }`
59
-* `Keypairs.thumbprint({ jwk: jwk })`
59
+Generates a public/private pair of JWKs as `{ private, public }`
60 60
 
61
-<!--
61
+Option examples:
62 62
 
63
-* `Keypairs.jws.sign(options)`
64
-  * options example `{ keypair, header, protected, payload }`
65
-* `Keypairs.csr.generate(options)`
66
-  * options example `{ keypair, [ 'example.com' ] }`
63
+  * RSA `{ kty: 'RSA', modulusLength: 2048 }`
64
+  * ECDSA `{ kty: 'ECDSA', namedCurve: 'P-256' }`
67 65
 
68
--->
66
+When no options are supplied EC P-256 (also known as `prime256v1` and `secp256r1`) is used by default.
67
+
68
+#### Keypairs.import({ pem: '...' }
69
+
70
+Takes a PEM in pretty much any format (PKCS1, SEC1, PKCS8, SPKI) and returns a JWK.
71
+
72
+#### Keypairs.export(options)
73
+
74
+Exports a JWK as a PEM.
75
+
76
+Exports PEM in PKCS8 (private) or SPKI (public) by default.
69 77
 
70
-# Full Documentation
78
+Options
71 79
 
72
-Keypairs.js provides a 1-to-1 mapping to the Rasha.js and Eckles.js APIs.
80
+```js
81
+{ jwk: jwk
82
+, public: true
83
+, encoding: 'pem' // or 'der'
84
+, format: 'pkcs8' // or 'ssh', 'pkcs1', 'sec1', 'spki'
85
+}
86
+```
87
+
88
+#### Keypairs.thumbprint({ jwk: jwk })
73 89
 
74
-The full RSA documentation is at [Rasha.js](https://git.coolaj86.com/coolaj86/rasha.js/)
90
+Promises a JWK-spec thumbprint: URL Base64-encoded sha256
75 91
 
76
-The full ECDSA documentation is at [Eckles.js](https://git.coolaj86.com/coolaj86/eckles.js/)
92
+#### Keypairs.signJwt({ jwk, header, claims })
77 93
 
78
-Any option you pass to Keypairs will be passed directly to the corresponding API
94
+Returns a JWT (otherwise known as a protected JWS in "compressed" format).
95
+
96
+```js
97
+{ jwk: jwk
98
+, claims: {
99
+  }
100
+}
101
+```
102
+
103
+Header defaults:
104
+
105
+```js
106
+{ kid: thumbprint
107
+, alg: 'xS256'
108
+, typ: 'JWT'
109
+}
110
+```
111
+
112
+Payload notes:
113
+
114
+* `iat: now` is added by default (set `false` to disable)
115
+* `exp` must be set (set `false` to disable)
116
+* `iss` should be the base URL for JWK lookup (i.e. via OIDC, Auth0)
117
+
118
+Notes:
119
+
120
+`header` is actually the JWS `protected` value, as all JWTs use protected headers (yay!)
121
+and `claims` are really the JWS `payload`.
122
+
123
+#### Keypairs.signJws({ jwk, header, protected, payload })
124
+
125
+This is provided for APIs like ACME (Let's Encrypt) that use uncompressed JWS (instead of JWT, which is compressed).
126
+
127
+Options:
128
+
129
+* `header` not what you think. Leave undefined unless you need this for the spec you're following.
130
+* `protected` is the typical JWT-style header
131
+  * `kid` and `alg` will be added by default (these are almost always required), set `false` explicitly to disable
132
+* `payload` can be JSON, a string, or even a buffer (which gets URL Base64 encoded)
133
+  * you must set this to something, even if it's an empty string, object, or Buffer
134
+
135
+# Additional Documentation
136
+
137
+Keypairs.js provides a 1-to-1 mapping to the Rasha.js and Eckles.js APIs,
138
+but it also includes the additional convenience methods `signJwt` and `signJws`.
139
+
140
+That is to say that any option you pass to Keypairs will be passed directly to the corresponding API
79 141
 of either Rasha or Eckles.
142
+
143
+* See ECDSA documentation at [Eckles.js](https://git.coolaj86.com/coolaj86/eckles.js/)
144
+* See RSA documentation at [Rasha.js](https://git.coolaj86.com/coolaj86/rasha.js/)
145
+

+ 33
- 0
example.js Vedi File

@@ -0,0 +1,33 @@
1
+'use strict';
2
+
3
+var Keypairs = require('./keypairs.js');
4
+var Keyfetch = require('keyfetch');
5
+
6
+Keypairs.generate().then(function (keypair) {
7
+  return Keypairs.thumbprint({ jwk: keypair.public }).then(function (thumb) {
8
+    var iss = 'https://coolaj86.com/';
9
+
10
+    // shim so that no http request is necessary
11
+    keypair.private.kid = thumb;
12
+    Keyfetch._setCache(iss, { thumbprint: thumb, jwk: keypair.private });
13
+
14
+    return Keypairs.signJwt({
15
+      jwk: keypair.private
16
+    , claims: {
17
+        iss: iss
18
+      , sub: 'coolaj86@gmail.com'
19
+      , exp: Math.round(Date.now()/1000) + (3 * 24 * 60 * 60)
20
+      }
21
+    });
22
+  });
23
+}).then(function (jwt) {
24
+  console.log(jwt);
25
+  return Keyfetch.verify({ jwt: jwt }).then(function (ok) {
26
+    if (!ok) {
27
+      throw new Error("SANITY: did not verify (should have failed)");
28
+    }
29
+    console.log("Verified token");
30
+  });
31
+}).catch(function (err) {
32
+  console.error(err);
33
+});

+ 119
- 1
keypairs.js Vedi File

@@ -2,7 +2,8 @@
2 2
 
3 3
 var Eckles = require('eckles');
4 4
 var Rasha = require('rasha');
5
-var Keypairs = {};
5
+var Enc = {};
6
+var Keypairs = module.exports;
6 7
 
7 8
 /*global Promise*/
8 9
 
@@ -40,3 +41,120 @@ Keypairs.thumbprint = function (opts) {
40 41
     }
41 42
   });
42 43
 };
44
+
45
+// JWT a.k.a. JWS with Claims using Compact Serialization
46
+Keypairs.signJwt = function (opts) {
47
+  return Keypairs.thumbprint({ jwk: opts.jwk }).then(function (thumb) {
48
+    var header = opts.header || {};
49
+    var claims = JSON.parse(JSON.stringify(opts.claims || {}));
50
+    header.typ = 'JWT';
51
+    if (!header.kid) {
52
+      header.kid = thumb;
53
+    }
54
+    if (false === claims.iat) {
55
+      claims.iat = undefined;
56
+    } else if (!claims.iat) {
57
+      claims.iat = Math.round(Date.now()/1000);
58
+    }
59
+    if (false === claims.exp) {
60
+      claims.exp = undefined;
61
+    } else if (!claims.exp) {
62
+      throw new Error("opts.claims.exp should be the expiration date (as seconds since the Unix epoch) or false");
63
+    }
64
+    if (false === claims.iss) {
65
+      claims.iss = undefined;
66
+    } else if (!claims.iss) {
67
+      throw new Error("opts.claims.iss should be in the form of https://example.com/, a secure OIDC base url");
68
+    }
69
+
70
+    return Keypairs.signJws({
71
+      jwk: opts.jwk
72
+    , pem: opts.pem
73
+    , protected: header
74
+    , header: undefined
75
+    , payload: claims
76
+    }).then(function (jws) {
77
+      return [ jws.protected, jws.payload, jws.signature ].join('.');
78
+    });
79
+  });
80
+};
81
+
82
+Keypairs.signJws = function (opts) {
83
+  return Keypairs.thumbprint(opts).then(function (thumb) {
84
+
85
+    function alg() {
86
+      if (!opts.jwk) {
87
+        throw new Error("opts.jwk must exist and must declare 'typ'");
88
+      }
89
+      return ('RSA' === opts.jwk.typ) ? "RS256" : "ES256";
90
+    }
91
+
92
+    function sign(pem) {
93
+      var header = opts.header;
94
+      var protect = opts.protected;
95
+      var payload = opts.payload;
96
+
97
+      // Compute JWS signature
98
+      var protectedHeader = "";
99
+      // Because unprotected headers are allowed, regrettably...
100
+      // https://stackoverflow.com/a/46288694
101
+      if (false !== protect) {
102
+        if (!protect) { protect = {}; }
103
+        if (!protect.alg) { protect.alg = alg(); }
104
+        // There's a particular request where Let's Encrypt explicitly doesn't use a kid
105
+        if (!protect.kid && false !== protect.kid) { protect.kid = thumb; }
106
+        protectedHeader = JSON.stringify(protect);
107
+      }
108
+
109
+      // Convert payload to Buffer
110
+      if ('string' !== typeof payload && !Buffer.isBuffer(payload)) {
111
+        if (!payload) {
112
+          throw new Error("opts.payload should be JSON, string, or Buffer (it may be empty, but that must be explicit)");
113
+        }
114
+        payload = JSON.stringify(payload);
115
+      }
116
+      if ('string' === typeof payload) {
117
+        payload = Buffer.from(payload, 'binary');
118
+      }
119
+
120
+      // node specifies RSA-SHAxxx even whet it's actually ecdsa (it's all encoded x509 shasums anyway)
121
+      var nodeAlg = "RSA-SHA" + (((protect||header).alg||'').replace(/^[^\d]+/, '')||'256');
122
+      var protected64 = Enc.strToUrlBase64(protectedHeader);
123
+      var payload64 = Enc.bufToUrlBase64(payload);
124
+      var sig = require('crypto')
125
+        .createSign(nodeAlg)
126
+        .update(protect ? (protected64 + "." + payload64) : payload64)
127
+        .sign(pem, 'base64')
128
+        .replace(/\+/g, '-')
129
+        .replace(/\//g, '_')
130
+        .replace(/=/g, '')
131
+      ;
132
+
133
+      return {
134
+        header: header
135
+      , protected: protected64 || undefined
136
+      , payload: payload64
137
+      , signature: sig
138
+      };
139
+    }
140
+
141
+    if (opts.pem && opts.jwk) {
142
+      return sign(opts.pem);
143
+    } else {
144
+      return Keypairs.export({ jwk: opts.jwk }).then(sign);
145
+    }
146
+  });
147
+};
148
+
149
+Enc.strToUrlBase64 = function (str) {
150
+  // node automatically can tell the difference
151
+  // between uc2 (utf-8) strings and binary strings
152
+  // so we don't have to re-encode the strings
153
+  return Buffer.from(str).toString('base64')
154
+    .replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '');
155
+};
156
+Enc.bufToUrlBase64 = function (buf) {
157
+  // allow for Uint8Array as a Buffer
158
+  return Buffer.from(buf).toString('base64')
159
+    .replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '');
160
+};

+ 1
- 1
package-lock.json Vedi File

@@ -1,6 +1,6 @@
1 1
 {
2 2
   "name": "keypairs",
3
-  "version": "1.0.0",
3
+  "version": "1.0.1",
4 4
   "lockfileVersion": 1,
5 5
   "requires": true,
6 6
   "dependencies": {

+ 1
- 1
package.json Vedi File

@@ -1,6 +1,6 @@
1 1
 {
2 2
   "name": "keypairs",
3
-  "version": "1.0.0",
3
+  "version": "1.0.1",
4 4
   "description": "Lightweight RSA/ECDSA keypair generation and JWK <-> PEM",
5 5
   "main": "keypairs.js",
6 6
   "files": [],

Loading…
Annulla
Salva