chore: initial commit

This commit is contained in:
Filip Skokan
2018-11-03 12:30:32 +01:00
commit b8709756f0
29 changed files with 1268 additions and 0 deletions
+41
View File
@@ -0,0 +1,41 @@
# Logs
logs
*.log
npm-debug.log*
# Runtime data
pids
*.pid
*.seed
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
.nyc_output
# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# node-waf configuration
.lock-wscript
# Compiled binary addons (http://nodejs.org/api/addons.html)
build/Release
# Dependency directory and lockfiles
node_modules
yarn.lock
npm-shrinkwrap.json
package-lock.json
# Optional npm cache directory
.npm
# Optional REPL history
.node_repl_history
# TODOs
TODO*.md
.nvmrc
+21
View File
@@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright (c) 2018 Filip Skokan
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+43
View File
@@ -0,0 +1,43 @@
# WIP
```
$ nvm use stable
$ node removeme/some.js
```
# ultimate-jose
[![build][travis-image]][travis-url] [![codecov][codecov-image]][codecov-url]
TODO:
**Table of Contents**
TODO:
## Implemented specs & features
TODO:
Missing a feature? - If it wasn't already discussed before, [ask for it][suggest-feature].
Found a bug? - [report it][bug].
<h2>Support</h2>
[<img src="https://c5.patreon.com/external/logo/become_a_patron_button@2x.png" width="160" align="right">][support-patreon]
If you or your business use ultimate-jose, please consider becoming a [Patron][support-patreon] so I can continue maintaining it and adding new features carefree. You may also donate one-time via [PayPal][support-paypal].
[<img src="https://cdn.jsdelivr.net/gh/gregoiresgt/payment-icons@183140a5ff8f39b5a19d59ebeb2c77f03c3a24d3/Assets/Payment/PayPal/Paypal@2x.png" width="100" align="right">][support-paypal]
## Usage
TODO:
[travis-image]: https://api.travis-ci.com/panva/ultimate-jose.svg?branch=master
[travis-url]: https://travis-ci.com/panva/ultimate-jose
[codecov-image]: https://img.shields.io/codecov/c/github/panva/ultimate-jose/master.svg
[codecov-url]: https://codecov.io/gh/panva/ultimate-jose
[suggest-feature]: https://github.com/panva/ultimate-jose/issues/new?template=feature-request.md
[bug]: https://github.com/panva/ultimate-jose/issues/new?template=bug-report.md
[support-patreon]: https://www.patreon.com/panva
[support-paypal]: https://www.paypal.me/panva
+19
View File
@@ -0,0 +1,19 @@
const CODES = {
JWSMissingAlg: 'ERR_JWS_ALG_MISSING',
JWSInvalidHeader: 'ERR_JWS_INVALID_HEADER',
JWSNoRecipients: 'ERR_JWS_NO_RECIPIENTS'
}
class JoseError extends Error {
constructor (message) {
super(message)
this.name = this.constructor.name
this.code = CODES[this.constructor.name]
Error.captureStackTrace(this, this.constructor)
}
}
module.exports.JWSMissingAlg = class JWSMissingAlg extends JoseError {}
module.exports.JWSInvalidHeader = class JWSInvalidHeader extends JoseError {}
module.exports.JWSNoRecipients = class JWSNoRecipients extends JoseError {}
module.exports.JWSVerificationFailed = class JWSVerificationFailed extends JoseError {}
+36
View File
@@ -0,0 +1,36 @@
const fromBase64 = base64 => base64.replace(/=/g, '').replace(/\+/g, '-').replace(/\//g, '_')
const toBase64 = base64url => base64url.replace(/-/g, '+').replace(/_/g, '/')
const encode = (input, encoding = 'utf8') => {
if (Buffer.isBuffer(input)) {
return fromBase64(input.toString('base64'))
}
return fromBase64(Buffer.from(input, encoding).toString('base64'))
}
const decode = (input, encoding = 'utf8') => {
const buf = Buffer.from(toBase64(input), 'base64')
return encoding ? buf.toString(encoding) : buf
}
const decodeToBuffer = input => Buffer.from(toBase64(input), 'base64')
const b64uJSON = {
encode: input => encode(JSON.stringify(input)),
decode: input => JSON.parse(decode(input))
}
b64uJSON.decode.try = (input) => {
try {
return b64uJSON.decode(input)
} catch (err) {
return decode(input)
}
}
module.exports.decodeToBuffer = decodeToBuffer
module.exports.decode = decode
module.exports.encode = encode
module.exports.JSON = b64uJSON
+165
View File
@@ -0,0 +1,165 @@
// TODO: include license from https://github.com/Brightspace/node-ecdsa-sig-formatter
const MAX_OCTET = 0x80
const CLASS_UNIVERSAL = 0
const PRIMITIVE_BIT = 0x20
const TAG_SEQ = 0x10
const TAG_INT = 0x02
const ENCODED_TAG_SEQ = (TAG_SEQ | PRIMITIVE_BIT) | (CLASS_UNIVERSAL << 6)
const ENCODED_TAG_INT = TAG_INT | (CLASS_UNIVERSAL << 6)
const getParamSize = keySize => ((keySize / 8) | 0) + (keySize % 8 === 0 ? 0 : 1)
const paramBytesForAlg = {
ES256: getParamSize(256),
ES384: getParamSize(384)
// ES512: getParamSize(521)
}
const countPadding = (buf, start, stop) => {
let padding = 0
while (start + padding < stop && buf[start + padding] === 0) {
++padding
}
const needsSign = buf[start + padding] >= MAX_OCTET
if (needsSign) {
--padding
}
return padding
}
module.exports.derToJose = (signature, alg) => {
const paramBytes = paramBytesForAlg[alg]
// the DER encoded param should at most be the param size, plus a padding
// zero, since due to being a signed integer
const maxEncodedParamLength = paramBytes + 1
const inputLength = signature.length
let offset = 0
if (signature[offset++] !== ENCODED_TAG_SEQ) {
throw new Error('Could not find expected "seq"')
}
let seqLength = signature[offset++]
if (seqLength === (MAX_OCTET | 1)) {
seqLength = signature[offset++]
}
if (inputLength - offset < seqLength) {
throw new Error('"seq" specified length of "' + seqLength + '", only "' + (inputLength - offset) + '" remaining')
}
if (signature[offset++] !== ENCODED_TAG_INT) {
throw new Error('Could not find expected "int" for "r"')
}
const rLength = signature[offset++]
if (inputLength - offset - 2 < rLength) {
throw new Error('"r" specified length of "' + rLength + '", only "' + (inputLength - offset - 2) + '" available')
}
if (maxEncodedParamLength < rLength) {
throw new Error('"r" specified length of "' + rLength + '", max of "' + maxEncodedParamLength + '" is acceptable')
}
const rOffset = offset
offset += rLength
if (signature[offset++] !== ENCODED_TAG_INT) {
throw new Error('Could not find expected "int" for "s"')
}
const sLength = signature[offset++]
if (inputLength - offset !== sLength) {
throw new Error('"s" specified length of "' + sLength + '", expected "' + (inputLength - offset) + '"')
}
if (maxEncodedParamLength < sLength) {
throw new Error('"s" specified length of "' + sLength + '", max of "' + maxEncodedParamLength + '" is acceptable')
}
const sOffset = offset
offset += sLength
if (offset !== inputLength) {
throw new Error('Expected to consume entire buffer, but "' + (inputLength - offset) + '" bytes remain')
}
const rPadding = paramBytes - rLength
const sPadding = paramBytes - sLength
let dst = Buffer.allocUnsafe(rPadding + rLength + sPadding + sLength)
for (offset = 0; offset < rPadding; ++offset) {
dst[offset] = 0
}
signature.copy(dst, offset, rOffset + Math.max(-rPadding, 0), rOffset + rLength)
offset = paramBytes
for (const o = offset; offset < o + sPadding; ++offset) {
dst[offset] = 0
}
signature.copy(dst, offset, sOffset + Math.max(-sPadding, 0), sOffset + sLength)
return dst
}
module.exports.joseToDer = (signature, alg) => {
const paramBytes = paramBytesForAlg[alg]
const signatureBytes = signature.length
if (signatureBytes !== paramBytes * 2) {
throw new TypeError('"' + alg + '" signatures must be "' + paramBytes * 2 + '" bytes, saw "' + signatureBytes + '"')
}
const rPadding = countPadding(signature, 0, paramBytes)
const sPadding = countPadding(signature, paramBytes, signature.length)
const rLength = paramBytes - rPadding
const sLength = paramBytes - sPadding
const rsBytes = 1 + 1 + rLength + 1 + 1 + sLength
const shortLength = rsBytes < MAX_OCTET
let dst = Buffer.allocUnsafe((shortLength ? 2 : 3) + rsBytes)
let offset = 0
dst[offset++] = ENCODED_TAG_SEQ
if (shortLength) {
// Bit 8 has value "0"
// bits 7-1 give the length.
dst[offset++] = rsBytes
} else {
// Bit 8 of first octet has value "1"
// bits 7-1 give the number of additional length octets.
dst[offset++] = MAX_OCTET | 1 // eslint-disable-line no-tabs
// length, base 256
dst[offset++] = rsBytes & 0xff
}
dst[offset++] = ENCODED_TAG_INT
dst[offset++] = rLength
if (rPadding < 0) {
dst[offset++] = 0
offset += signature.copy(dst, offset, 0, paramBytes)
} else {
offset += signature.copy(dst, offset, rPadding, paramBytes)
}
dst[offset++] = ENCODED_TAG_INT
dst[offset++] = sLength
if (sPadding < 0) {
dst[offset++] = 0
signature.copy(dst, offset, paramBytes)
} else {
signature.copy(dst, offset, paramBytes + sPadding)
}
return dst
}
+12
View File
@@ -0,0 +1,12 @@
const { generateKeyPairSync, createSecretKey } = require('crypto')
// TODO: reach out to @tniessen to request constructors being exposed for this exact purpose
// (verifying inputs are already KeyObjects)
// TODO: what's the least blocking way to get to the constructors
const { publicKey, privateKey } = generateKeyPairSync('ec', { namedCurve: 'P-256' })
const PrivateKeyObject = privateKey.constructor
const PublicKeyObject = publicKey.constructor
const SecretKeyObject = createSecretKey(Buffer.allocUnsafe(1)).constructor
module.exports = { PrivateKeyObject, PublicKeyObject, SecretKeyObject }
+26
View File
@@ -0,0 +1,26 @@
const utils = {
rsa: require('rasha'),
ec: require('eckles')
}
const { PrivateKeyObject, PublicKeyObject } = require('./key_objects')
const SUPPORTED = new Set(Object.keys(utils))
module.exports.pemToJwk = (keyObject) => {
if (!(keyObject instanceof PrivateKeyObject) && !(keyObject instanceof PublicKeyObject)) {
throw new TypeError('invalid keyObject type')
}
const type = keyObject.type === 'private' ? 'pkcs8' : 'spki'
const format = 'pem'
return utils[keyObject.asymmetricKeyType].importSync({ pem: keyObject.export({ type, format }) })
}
module.exports.jwkToPem = (jwk) => {
if (!SUPPORTED.has(jwk.kty)) {
throw new TypeError('unsupported kty')
}
return utils[jwk.kty.toLowerCase()].exportSync({ jwk })
}
+7
View File
@@ -0,0 +1,7 @@
module.exports = {
JWT: require('./jwt'),
JWE: require('./jwe'),
JWS: require('./jws'),
JWK: require('./jwk'),
JWKS: require('./jwks')
}
+1
View File
@@ -0,0 +1 @@
module.exports.TODO = 'TODO'
+89
View File
@@ -0,0 +1,89 @@
const { pemToJwk } = require('../help/key_utils')
const thumbprint = require('./thumbprint')
const RSA_PUBLIC = ['e', 'n']
const RSA_PRIVATE = [...RSA_PUBLIC, 'd', 'p', 'q', 'dp', 'dq', 'qi']
const EC_PUBLIC = ['crv', 'x', 'y']
const EC_PRIVATE = [...EC_PUBLIC, 'd']
const props = {
EC: {
public: EC_PUBLIC,
private: EC_PRIVATE
},
RSA: {
public: RSA_PUBLIC,
private: RSA_PRIVATE
}
}
const USES = new Set(['sig', 'alg'])
function defineLazyComponents (obj) {
Object.defineProperties(obj, props[obj.keyObject.asymmetricKeyType.toUpperCase()][obj.keyObject.type].reduce((acc, component) => {
acc[component] = {
get () {
const jwk = pemToJwk(this.keyObject)
Object.defineProperties(
this,
Object.entries(jwk)
.filter(([key]) => props[this.kty][this.keyObject.type].includes(key))
.reduce((acc, [key, value]) => {
acc[key] = { value, enumerable: true, configurable: false }
return acc
}, {})
)
return this[component]
},
enumerable: true,
configurable: true
}
return acc
}, {}))
}
class Key {
constructor (keyObject, { alg, use, kid } = {}) {
if (use !== undefined) {
if (typeof use !== 'string' || !USES.has(use)) {
throw new TypeError('`use` must be either "sig" or "enc" string when provided')
}
}
if (use !== undefined) {
if (typeof use !== 'string') {
throw new TypeError('`alg` must be a string when provided')
}
}
if (kid !== undefined) {
if (typeof kid !== 'string') {
throw new TypeError('`kid` must be a string when provided')
}
}
Object.defineProperties(this, {
keyObject: { value: keyObject },
kty: { value: keyObject.asymmetricKeyType.toUpperCase(), enumerable: true },
private: { value: keyObject.type === 'private', enumerable: keyObject.type === 'private' },
public: { value: keyObject.type === 'public', enumerable: keyObject.type === 'public' },
alg: { value: alg, enumerable: alg !== undefined },
use: { value: use, enumerable: use !== undefined },
kid: {
enumerable: true,
...(kid ? { value: kid } : {
get () {
Object.defineProperty(this, 'kid', { value: thumbprint.kid(this), configurable: false })
return this.kid
},
configurable: true
})
}
})
defineLazyComponents(this)
}
}
module.exports = Key
+125
View File
@@ -0,0 +1,125 @@
const { createSign, createVerify } = require('crypto')
const { derToJose, joseToDer } = require('../help/ecdsa_signatures')
const Key = require('./base')
const SIG_ALGS = new Set([
'ES256',
'ES384'
// 'ES512'
])
const WRAP_ALGS = new Set([
'ECDH-ES',
'ECDH-ES+A128KW',
'ECDH-ES+A192KW',
'ECDH-ES+A256KW'
])
class ECKey extends Key {
constructor (...args) {
super(...args)
Object.defineProperties(this, {
length: {
get () {
Object.defineProperty(this, 'length', {
value: parseInt(this.crv.substring(2), 10),
configurable: false
})
return this.length
},
configurable: true
}
})
}
algorithms (operation) {
// TODO: operation empty, return all supported or throw
switch (operation) {
case 'sign':
if (this.public || this.use === 'enc') {
return new Set([])
}
if (this.alg) {
return new Set(SIG_ALGS.has(this.alg) ? [this.alg] : [])
}
return new Set([`ES${this.length}`])
case 'verify':
if (this.use === 'enc') {
return new Set([])
}
if (this.alg) {
return new Set(SIG_ALGS.has(this.alg) ? [this.alg] : [])
}
return new Set([`ES${this.length}`])
case 'unwrapKey':
if (this.public || this.use === 'sig') {
return new Set([])
}
if (this.alg) {
return new Set(WRAP_ALGS.has(this.alg) ? [this.alg] : [])
}
return new Set(WRAP_ALGS)
case 'wrapKey':
if (this.use === 'sig') {
return new Set([])
}
if (this.alg) {
return new Set(WRAP_ALGS.has(this.alg) ? [this.alg] : [])
}
return new Set(WRAP_ALGS)
default:
return new Set([])
}
}
// TODO: move to jwa
signAlg (alg) {
switch (alg) {
case 'ES256':
return 'RSA-SHA256'
case 'ES384':
return 'RSA-SHA384'
// case 'ES512':
// return 'RSA-SHA512'
default:
throw new Error('invalid alg')
}
}
sign (alg, payload) {
if (!this.algorithms('sign').has(alg)) {
throw new Error('the key does not support this signing algorithm')
}
const sign = createSign(this.signAlg(alg))
sign.update(payload)
return derToJose(sign.sign(this.keyObject), alg)
}
verify (alg, payload, signature) {
if (!this.algorithms('verify').has(alg)) {
throw new Error('the key does not support this verify algorithm')
}
const verify = createVerify(this.signAlg(alg))
verify.update(payload)
try {
return verify.verify(this.keyObject, joseToDer(signature, alg))
} catch (err) {
return false
}
}
}
module.exports = ECKey
+59
View File
@@ -0,0 +1,59 @@
const { createPublicKey, createPrivateKey } = require('crypto')
const Key = require('./base')
const RSAKey = require('./rsa')
const ECKey = require('./ec')
const { PrivateKeyObject, PublicKeyObject } = require('../help/key_objects')
const { jwkToPem } = require('../help/key_utils')
function importKey (key, opts) {
let privateKey, publicKey
if (key instanceof Key) {
if (key.private) {
privateKey = key.keyObject
} else if (key.public) {
publicKey = key.keyObject
} else { // secret
// TODO: symmetrical keys
// secret = key
}
} else if (key instanceof PrivateKeyObject) {
privateKey = key
} else if (key instanceof PublicKeyObject) {
publicKey = key
} else if (key && key.kty) { // assume JWK formatted <Object>
let parsedJWK
try {
parsedJWK = jwkToPem(key)
} catch (err) {}
if (parsedJWK && key.d) {
privateKey = createPrivateKey(parsedJWK)
} else if (parsedJWK) {
publicKey = createPublicKey(parsedJWK)
}
} else { // <Object> | <string> | <Buffer> passed to crypto.createPrivateKey or crypto.createPublicKey
try {
privateKey = createPrivateKey(key)
} catch (err) {}
try {
publicKey = createPublicKey(key)
} catch (err) {}
}
if (!privateKey && !publicKey) {
throw new Error('import failed')
}
const keyObject = privateKey || publicKey
switch (keyObject.asymmetricKeyType) {
case 'rsa':
return new RSAKey(keyObject, opts)
case 'ec':
return new ECKey(keyObject, opts)
default:
}
}
module.exports = importKey
+1
View File
@@ -0,0 +1 @@
module.exports.importKey = require('./import')
+135
View File
@@ -0,0 +1,135 @@
const { createSign, createVerify, constants } = require('crypto')
const Key = require('./base')
const SIG_ALGS = new Set([
'RS256',
'RS384',
'RS512',
'PS256',
'PS384',
'PS512'
])
const WRAP_ALGS = new Set([
'RSA-OAEP',
'RSA1_5'
])
class RSAKey extends Key {
constructor (...args) {
super(...args)
Object.defineProperties(this, {
length: {
get () {
Object.defineProperty(this, 'length', {
value: Buffer.byteLength(this.n, 'base64') * 8,
configurable: false
})
return this.length
},
configurable: true
}
})
}
algorithms (operation) {
// TODO: operation empty, return all supported or throw
switch (operation) {
case 'sign':
if (this.public || this.use === 'enc') {
return new Set([])
}
if (this.alg) {
return new Set(SIG_ALGS.has(this.alg) ? [this.alg] : [])
}
return new Set(SIG_ALGS)
case 'verify':
if (this.use === 'enc') {
return new Set([])
}
if (this.alg) {
return new Set(SIG_ALGS.has(this.alg) ? [this.alg] : [])
}
return new Set(SIG_ALGS)
case 'unwrapKey':
if (this.public || this.use === 'sig') {
return new Set([])
}
if (this.alg) {
return new Set(WRAP_ALGS.has(this.alg) ? [this.alg] : [])
}
return new Set(WRAP_ALGS)
case 'wrapKey':
if (this.use === 'sig') {
return new Set([])
}
if (this.alg) {
return new Set(WRAP_ALGS.has(this.alg) ? [this.alg] : [])
}
return new Set(WRAP_ALGS)
default:
return new Set([])
}
}
// TODO: move to jwa
signAlg (alg) {
switch (alg) {
case 'RS256':
case 'PS256':
return 'RSA-SHA256'
case 'RS384':
case 'PS384':
return 'RSA-SHA384'
case 'RS512':
case 'PS512':
return 'RSA-SHA512'
default:
throw new Error('invalid alg')
}
}
signPadding (alg) {
if (alg.startsWith('PS')) {
return {
padding: constants.RSA_PKCS1_PSS_PADDING,
saltLength: constants.RSA_PSS_SALTLEN_DIGEST
}
}
return undefined
}
sign (alg, payload) {
if (!this.algorithms('sign').has(alg)) {
throw new Error('the key does not support this signing algorithm')
}
const sign = createSign(this.signAlg(alg))
sign.update(payload)
return sign.sign({ key: this.keyObject, ...this.signPadding(alg) })
}
verify (alg, payload, signature) {
if (!this.algorithms('verify').has(alg)) {
throw new Error('the key does not support this verify algorithm')
}
const verify = createVerify(this.signAlg(alg))
verify.update(payload)
return verify.verify({ key: this.keyObject, ...this.signPadding(alg) }, signature)
}
}
module.exports = RSAKey
+21
View File
@@ -0,0 +1,21 @@
const { createHash } = require('crypto')
const base64url = require('../help/base64url')
module.exports.kid = (key) => {
let json
switch (key.kty) {
case 'RSA':
json = JSON.stringify({ e: key.e, kty: 'RSA', n: key.n })
break
case 'EC':
json = JSON.stringify({ crv: key.crv, kty: 'EC', x: key.x, y: key.y })
break
case 'oct':
json = JSON.stringify({ k: key.k, kty: 'oct' })
break
}
return base64url.encode(createHash('sha256').update(json).digest())
}
+1
View File
@@ -0,0 +1 @@
module.exports.TODO = 'TODO'
+12
View File
@@ -0,0 +1,12 @@
const Sign = require('./sign')
const verify = require('./verify')
module.exports.Sign = Sign
module.exports.sign = (payload, key, protectedHeader, unprotectedHeader, serialization) => {
const jws = new Sign(payload, protectedHeader, unprotectedHeader)
jws.recipient(key)
const result = jws.sign(serialization)
return result
}
module.exports.verify = verify
+61
View File
@@ -0,0 +1,61 @@
const base64url = require('../help/base64url')
const { JWSMissingAlg, JWSInvalidHeader, JWSNoRecipients } = require('../errors')
class Recipients extends Set {
process (payload, sharedProtectedHeader, sharedUnprotectedHeader) {
if (!this.size) {
throw new JWSNoRecipients('JWS needs to have recipients added first')
}
return Array.from(this)
.map((recipient) => {
return recipient.process(payload, sharedProtectedHeader, sharedUnprotectedHeader)
})
}
}
class Recipient {
constructor (key, protectedHeader, unprotectedHeader) {
this.key = key
if (typeof protectedHeader !== 'undefined') {
this.protectedHeader = protectedHeader
}
if (typeof unprotectedHeader !== 'undefined') {
this.unprotectedHeader = unprotectedHeader
}
}
process (payload, sharedProtectedHeader, sharedUnprotectedHeader) {
const joseHeader = {
protected: {
...sharedProtectedHeader,
...this.protectedHeader
},
unprotected: {
...sharedUnprotectedHeader,
...this.unprotectedHeader
}
}
const unprotectedKeys = new Set(Object.keys(joseHeader.unprotected))
if (new Set(Object.keys(joseHeader.protected).filter(x => unprotectedKeys.has(x))).size) {
throw new JWSInvalidHeader('JWS Protected and JWS Unprotected Header Parameter names must be disjoint')
}
const { alg } = { ...joseHeader.protected, ...joseHeader.unprotected }
if (!alg) {
throw new JWSMissingAlg('every JOSE header must contain an "alg" parameter')
}
if (Object.keys(joseHeader.unprotected).length) {
this.header = joseHeader.unprotected
}
this.protected = base64url.JSON.encode(joseHeader.protected)
this.signature = base64url.encode(this.key.sign(alg, `${this.protected}.${payload}`))
}
}
module.exports = { Recipients, Recipient }
+61
View File
@@ -0,0 +1,61 @@
function compactSerializer (payload, [{ signature, protected: prot }]) {
return `${prot}.${payload}.${signature}`
}
compactSerializer.validate = (jws, recipients) => {
if (recipients.length !== 1 || jws.sharedUnprotectedHeader || recipients.some(r => r.unprotectedHeader)) {
throw new TypeError("JWS Compact Serialization doesn't support multiple recipients or JWS unprotected headers")
}
}
function generalSerializer (payload, recipients) {
return {
payload,
signatures: recipients.map(({ header, signature, protected: prot }) => {
return {
signature,
protected: prot,
...header ? { header } : undefined
}
})
}
}
generalSerializer.validate = () => {}
function flattenedSerializer (payload, [{ header, signature, protected: prot }]) {
return {
payload,
signature,
protected: prot,
...header ? { header } : undefined
}
}
flattenedSerializer.validate = (jws, { length }) => {
if (length !== 1) {
throw new TypeError("Flattened JWS JSON Serialization doesn't support multiple recipients")
}
}
function detect (input) {
if (typeof input === 'string') {
return 'compact'
}
if ('signature' in input && 'payload' in input && 'protected' in input) {
return 'flattened'
}
if ('signatures' in input && 'payload' in input && Array.isArray(input.signatures)) {
if (input.signatures.every(s => 'signature' in s && 'protected' in s)) {
return 'general'
}
}
throw new Error('invalid serialization')
}
module.exports = {
compact: compactSerializer,
flattened: flattenedSerializer,
general: generalSerializer,
detect
}
+93
View File
@@ -0,0 +1,93 @@
const base64url = require('../help/base64url')
const serializers = require('./serializers')
const { Recipients, Recipient } = require('./recipients')
const Key = require('../jwk/base')
const map = new WeakMap()
function get (instance, property) {
return map.get(instance)[property]
}
class Sign {
constructor (payload, sharedProtectedHeader, sharedUnprotectedHeader) {
if (!Buffer.isBuffer(payload) && !(typeof payload === 'string')) {
payload = JSON.stringify(payload)
}
Object.freeze(sharedProtectedHeader)
Object.freeze(sharedUnprotectedHeader)
map.set(this, {
payload: base64url.encode(payload),
recipients: new Recipients()
})
if (typeof sharedProtectedHeader !== 'undefined') {
map.get(this).sharedProtectedHeader = sharedProtectedHeader
}
if (typeof sharedUnprotectedHeader !== 'undefined') {
map.get(this).sharedUnprotectedHeader = sharedUnprotectedHeader
}
}
/*
* @private
*/
get payload () {
return get(this, 'payload')
}
/*
* @private
*/
get sharedProtectedHeader () {
return get(this, 'sharedProtectedHeader')
}
/*
* @private
*/
get sharedUnprotectedHeader () {
return get(this, 'sharedUnprotectedHeader')
}
/*
* @public
*/
recipient (key, protectedHeader, unprotectedHeader) {
if (!(key instanceof Key)) {
throw new TypeError('key must be an instance of a key instantiated by JWK.importKey')
}
const recipient = new Recipient(key, protectedHeader, unprotectedHeader)
get(this, 'recipients').add(recipient)
return this
}
/*
* @public
*/
sign (serialization = 'compact') {
if (!serializers[serialization]) {
throw new TypeError('invalid serialization')
}
serializers[serialization].validate(this, Array.from(get(this, 'recipients')))
get(this, 'recipients').process(this.payload, this.sharedProtectedHeader, this.sharedUnprotectedHeader)
return serializers[serialization](this.payload, Array.from(get(this, 'recipients')))
}
/*
* @public
*/
static sign (payload, key, protectedHeader, unprotectedHeader, serialization) {
const jws = new this(payload, protectedHeader, unprotectedHeader)
jws.recipient(key)
const result = jws.sign(serialization)
return result
}
}
module.exports = Sign
+52
View File
@@ -0,0 +1,52 @@
const base64url = require('../help/base64url')
const serializers = require('./serializers')
const { JWSVerificationFailed } = require('../errors')
const SINGLE_RECIPIENT = new Set(['compact', 'flattened'])
/*
* @public
*/
// TODO: option to return everything not just the payload
// TODO: add kid magic
module.exports = (jws, key) => {
const serialization = serializers.detect(jws)
let prot
let payload
let signature
if (SINGLE_RECIPIENT.has(serialization)) {
if (serialization === 'compact') { // compact serialization format
([prot, payload, signature] = jws.split('.'))
} else { // flattened serialization format
({ protected: prot, payload, signature } = jws)
}
// TODO: alg may also be unprotected but must it must be provided disjoint
const { alg } = base64url.JSON.decode(prot)
if (!key.verify(alg, [prot, payload].join('.'), base64url.decodeToBuffer(signature))) {
throw new JWSVerificationFailed('verification failed')
}
return base64url.JSON.decode.try(payload)
}
// general serialization format
({ payload } = jws)
for ({ protected: prot, signature } of jws.signatures) {
// TODO: alg may also be unprotected but must it must be provided disjoint
const { alg } = base64url.JSON.decode(prot)
if (
!key.algorithms('verify').has(alg) ||
!key.verify(alg, [prot, payload].join('.'), base64url.decodeToBuffer(signature))
) {
continue
}
return base64url.JSON.decode.try(payload)
}
throw new JWSVerificationFailed('verification failed')
}
+1
View File
@@ -0,0 +1 @@
module.exports.TODO = 'TODO'
+61
View File
@@ -0,0 +1,61 @@
{
"name": "ultimate-jose",
"version": "0.0.1",
"description": "JSON Web Everything (JWA, JWS, JWE, JWK, JWKS, JWT) for Node.js with minimal dependencies",
"keywords": [
"jose",
"jsonwebtoken",
"jwa",
"jwe",
"jwk",
"jws",
"jwt",
"rfc7515",
"rfc7516",
"rfc7517",
"rfc7518",
"rfc7519",
"rfc7797"
],
"homepage": "https://github.com/panva/ultimate-jose",
"repository": "panva/ultimate-jose",
"license": "MIT",
"author": "Filip Skokan <[email protected]>",
"files": [
"lib"
],
"main": "lib/index.js",
"scripts": {
"lint": "standard",
"lint-fix": "standard --fix",
"test": "ava"
},
"husky": {
"hooks": {
"commit-msg": "commitlint -E HUSKY_GIT_PARAMS"
}
},
"dependencies": {
"eckles": "^1.3.2",
"rasha": "^1.1.0"
},
"devDependencies": {
"@commitlint/cli": "^7.3.2",
"@commitlint/config-conventional": "^7.3.1",
"ava": "^1.1.0",
"husky": "^1.3.1",
"standard": "^12.0.1"
},
"engines": {
"node": ">=11.7.0"
},
"ava": {
"babel": false,
"compileEnhancements": false
},
"commitlint": {
"extends": [
"@commitlint/config-conventional"
]
}
}
+5
View File
@@ -0,0 +1,5 @@
-----BEGIN PRIVATE KEY-----
MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQg0M8fij/yTb4tf/Zk
0qBQFeUrLBPnDFOhBO2g6DjzEzShRANCAAQrveSafGzJbSqtsQ4HF2F0lxjtPbKG
vE0k4vzi9OWhWO7P7o7dJCuN0PBK69tPS1ZR4pJRz+U6/7nQ8IMVr8jV
-----END PRIVATE KEY-----
+4
View File
@@ -0,0 +1,4 @@
-----BEGIN PUBLIC KEY-----
MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEK73kmnxsyW0qrbEOBxdhdJcY7T2y
hrxNJOL84vTloVjuz+6O3SQrjdDwSuvbT0tWUeKSUc/lOv+50PCDFa/I1Q==
-----END PUBLIC KEY-----
+52
View File
@@ -0,0 +1,52 @@
-----BEGIN PRIVATE KEY-----
MIIJQgIBADANBgkqhkiG9w0BAQEFAASCCSwwggkoAgEAAoICAQDClca18VOCEzsu
hfS1l/wpJcRpXPAL4ZIqEPjVkbViH0riy+8AArwucikxwcdtB22RE1VbXGvQDrwr
7U7QBJyTqBacOCJW9h69mnHX/1/6J11slkaiFDfwCfNVqD0bszU1nIVCJHI4UWJd
a6fsowlcAsr+UvEkr/lMJMsOlwEtXmlHDmE25V6NZfBaxjj641lgR87xIa30anQe
FqOyFH6f41Ci7nB0s17Sw22HKstNBTJm4QnJREtlkVRKZKcvPPN8LKdNw4eCnObA
6PInjZTKJQ8C6OXb1QKdv+MQ+wdF0DDP+i52aFDuhVQ6voLpWU4eL+CafScSiKxF
ChddardkYTb5aouASJzBm4b9ZCb2IYB5npQhpFFxGUWZETqCxV25vHJKJf4Y6s2j
jmb6gSyk2aQ5WlYWAlgMPshSnDikp1rmf29Ld9H4eOP72uNmOpbQ4i5HnKvQcbEh
P33ucv8owi6Efsc9EB6t33NwRdCKJZpG2remDeJKLu17pq7S1CEmqpkY1e26Wrys
Evgd+1f9SoLqL28DpZgYn4XLPNHNx6JMS0cQeHJmPvCuTUhEU9TZG4JrxO6iUfbo
uN2JLgJbIyHiGj9ey1DEfsUCcoTffFYFNAoc36D32bG+9LL2N8KB039TMSmUgvR5
QUCLNgWLqmJFnHUjUkAP0tk6D0TjBwIDAQABAoICAEflfWmGhHuATZKwDi32vUYa
3Uqs+0j4jMAcS0TmNKXPuCS6g1F2sT9cERqAZgZEA1jiUtyR9ogGmcKFSWSU/6GT
Lt+D4jRNdihKY5wSkMzmNJ/Fms2SOOTpWE6i/gk5GwT3E0nQG5/xei3YdFB8IR4t
LgHwdYc/uhZHVfvLEbVIuA94IbVd5KTMMJXpve39iO2JOs5fyl2P492mMc+/wNWk
8QtynHySNgwcB2E1D68ip2/Z4HrMUCt0GjfuntNWawTLnvmdsHkzLhPDPo4cwwg5
zyUdFqk2OGQ/HVAN4K8L8K033GkFvDmVa/zUnZxI0hNXT17ltUUk2Lods2ph3uaC
8rfBzmoALl5DsAiGD5bJIvYRYJpTW91HeIiSq+9ocbx6ydA4TZGvFliFKHTNEozR
55xLHh8nBfGVDgd2DnmS3ue3m7/MaZYDWrPqKo5Qa+MLaLx2Jrz6oMJN4a/Fm4V3
Sp7VpO4JZF/tITw0xCsCzBQcacgERcQwB5vSydjpgfxzI7ul2hxKFTCu4pq33gYJ
19ekYhbze/z8DOhnRHbZtUVvz9ZYFL1d9oZtxbLFPgYFart3zQ3dSMBB+P1kaeOI
3R97QIpTQ7YzVB9ljG0qpiKWKGfwTxgsIQQ4aV4dwJeolBzpd2JSpp3/W1wEXnPs
WyiKvVXmN8hCi7A/KUwpAoIBAQD6yeGZ+S1w/S+tMobnHXBe2Yhsj15qCpXykLj3
DXhHvFD9Ni209ah8ecJYhhR++20N01Stu8tGSa3rCcM7b+obXV/0iClTrLb5MFvm
VLfePrwZAINMUYqk62vlR1gas8XSlg/jJj8KX9QKdEzwjOQZtbauBa9j3SYrRkuK
LFIVrvooYWlD78miefRwvOCHOxbndCPM6AumDCJ2mRu5PUK5jbGBZZeot/ayecMl
j/Q3Fprzst2NSWbGcKrSZiBRzW7aPAyZPckTk3d6USHYctpOlKtxBHrP6ID1LsbJ
dURLOzZnRNsh77Gha/lgi/09y2rCx3yWogt+dveVtQCaqBtNAoIBAQDGoOjI/Vst
YRBcRnEzkksm/kZkJ4FVQSFAFMtY6kc4/u31byRRAI15zERLzhSwUb0KUsylpFfP
geyVW6odF68atOv0Nz5NnYHkrW1Dnrwddb2+aIQ/vfGXIgi3UhFFxPEsTH6gmaXs
Q0LmwX1HfuAGANgZ22lhQW191dqnbcZC6emOG+pgO3e2CRImaNoHpbZxy9lsLqha
hOJLdCZfZ4bK7RXK3yJFLo2r3UhgJCQk5XOPdfsEsZLYPRP+9YTGxj7li89jremI
x0ae9VrkI7BhV98n24C1u1BS4YALuEK/eVBr5zA/pH8B35sJkp1YGWUITYbE9NJK
Jw2fAbWM/wWjAoIBAFUycCnWtNFFsgSsvZeWMK6ISAhZTEHKBta4w51qPgpgt0LO
PQjUy8A9vQBFzIC2+X1Q5dt/iLcAIM1QK7rtciKJvidcem2qpx5Qh2fgL/aYaV41
BmXO7cLJF4Z0jkHEANhhklQgkhn8JBTJHbo6ouL5EG5eEA9k+aqJ3u+5zsj/AIzn
StOBioZf1+LUx5IpSMaodeEOk6Z9SNfxe7GaViijMkI7OSJgHLbVFq6UEOsGCu3v
PcTZwsh3ThDcEO65U4dgDwegLccwuc4b8duOaQt5BiUG2JH48pZYpb+azvCf1QOQ
KMGGpFmqTLW32Pg9Oq+hBXpG4pC76bA6JVJk+aECggEBAIDQfedLF8R4PGn7bRKq
/34MTY5KaH/nuy6Zzb6eQQ5flecj6wN1PksNBAW9YoFuoRB+3iVWTXRPqCS8y7gO
UgsZ5iP9Z7RiP4kUR9YgrZLVdvZzRcEo5/LBqbFp0bXToSy/dyekPYzZmhuYxCyH
xbVe+sbaGp9uFRoUhLcsY/WlSUhWJku+hUZS36h1O2DWQVnPf/O+1oPNxgx1CSW6
OBqGYymsR8diB5K78WIYTjB3ddhGBULn/nmqMruYckGO899eKmNUuC1yPwR7zjRC
SPL4T8rDeddh4Re/EpGHMBKJFHTF++5qOcv3Pc6ZfN1TqsgVrx8wPujEJvtF2n3U
Lc0CggEAYYPmdBLB4HtSKFkqDD9JYZezYpG4jFFAlFRVlQD3KiH1i+3vC5/nsl3/
/Nujxj9faL6Pf6PZ7rXBRGSzVaCh3uP70My/sFNNvzc1Wi25usZ5HVgyB9TUIGA0
AQtLAY+sOK5c9DmvXTDVZ3CXYWg0mmSv1a7EJRlowFvyEm63c1xaiamo4NmrzzGJ
YraSXx7mrESPLZRk7m2qSlH0G1MCqEUGT9Ll1ag/zApd+G8vfJFyT4edw8UCXXja
FlPzloYP5V2uqEYniTNtZhQxLuh+G+mudeN0E9IMbk8VRUkI9BIHxi6FMNfMRGs6
wmBYi5IzgDHVJ+m4U5XGDlDttyVPog==
-----END PRIVATE KEY-----
+14
View File
@@ -0,0 +1,14 @@
-----BEGIN PUBLIC KEY-----
MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAwpXGtfFTghM7LoX0tZf8
KSXEaVzwC+GSKhD41ZG1Yh9K4svvAAK8LnIpMcHHbQdtkRNVW1xr0A68K+1O0ASc
k6gWnDgiVvYevZpx1/9f+iddbJZGohQ38AnzVag9G7M1NZyFQiRyOFFiXWun7KMJ
XALK/lLxJK/5TCTLDpcBLV5pRw5hNuVejWXwWsY4+uNZYEfO8SGt9Gp0HhajshR+
n+NQou5wdLNe0sNthyrLTQUyZuEJyURLZZFUSmSnLzzzfCynTcOHgpzmwOjyJ42U
yiUPAujl29UCnb/jEPsHRdAwz/oudmhQ7oVUOr6C6VlOHi/gmn0nEoisRQoXXWq3
ZGE2+WqLgEicwZuG/WQm9iGAeZ6UIaRRcRlFmRE6gsVdubxySiX+GOrNo45m+oEs
pNmkOVpWFgJYDD7IUpw4pKda5n9vS3fR+Hjj+9rjZjqW0OIuR5yr0HGxIT997nL/
KMIuhH7HPRAerd9zcEXQiiWaRtq3pg3iSi7te6au0tQhJqqZGNXtulq8rBL4HftX
/UqC6i9vA6WYGJ+FyzzRzceiTEtHEHhyZj7wrk1IRFPU2RuCa8TuolH26LjdiS4C
WyMh4ho/XstQxH7FAnKE33xWBTQKHN+g99mxvvSy9jfCgdN/UzEplIL0eUFAizYF
i6piRZx1I1JAD9LZOg9E4wcCAwEAAQ==
-----END PUBLIC KEY-----
+50
View File
@@ -0,0 +1,50 @@
/* eslint-disable */
const { strict: assert } = require('assert')
const { readFileSync } = require('fs')
const { JWS, JWK: { importKey } } = require('..')
const ecPrivate = importKey(readFileSync('./removeme/ec.key'))
const rsaPrivate = importKey(readFileSync('./removeme/rsa.key'))
const ecPub = importKey(readFileSync('./removeme/ec.pem'))
const rsaPub = importKey(readFileSync('./removeme/rsa.pem'))
const payload = { sub: '1234567890', name: 'John Doe', iat: 1516239022 }
// compact ec
;(() => {
const signed = JWS.sign(payload, ecPrivate, { alg: 'ES256' }, undefined)
console.log('compact ES256 signed', signed, '\n')
assert(JWS.verify(signed, ecPrivate), 'compact JWS verification with ecPrivate failed')
assert(JWS.verify(signed, ecPub), 'compact JWS verification with ecPub failed')
})();
// compact rsa
(() => {
const signed = JWS.sign(payload, rsaPrivate, { alg: 'RS256' }, undefined)
console.log('compact RS256 signed', signed, '\n')
assert(JWS.verify(signed, rsaPrivate), 'compact JWS verification with rsaPrivate failed')
assert(JWS.verify(signed, rsaPub), 'compact JWS verification with rsaPub failed')
})();
// flattened
(() => {
const jws = new JWS.Sign({ sub: 'Filip', name: 'Skokan' }, { typ: 'JWT' })
jws.recipient(ecPrivate, { alg: 'ES256' })
const signed = jws.sign('flattened')
console.log('flattened ES256 signed', signed, '\n')
assert(JWS.verify(signed, ecPrivate), 'flattened JWS verification with ecPrivate failed')
assert(JWS.verify(signed, ecPub), 'flattened JWS verification with ecPub failed')
})();
// general
(() => {
const jws = new JWS.Sign({ sub: 'Filip', name: 'Skokan' }, { typ: 'JWT' })
jws.recipient(ecPrivate, { alg: 'ES256' })
jws.recipient(rsaPrivate, { alg: 'RS512' })
const signed = jws.sign('general')
console.log('general ES256 and RS512 signed', signed, '\n')
assert(JWS.verify(signed, ecPrivate), 'general JWS verification with ecPrivate failed')
assert(JWS.verify(signed, ecPub), 'general JWS verification with ecPub failed')
assert(JWS.verify(signed, rsaPrivate), 'general JWS verification with rsaPrivate failed')
assert(JWS.verify(signed, rsaPub), 'general JWS verification with rsaPub failed')
})();