@meeco/sd-jwt
TypeScript icon, indicating that this package has built-in type declarations

1.0.2 • Public • Published

npm npm

SD-JWT

This is an implementation of SD-JWT (I-D version 05) in typescript.

Functionalities

  • No cryptographic dependencies (BYOC):

    • [x] Hasher
    • [x] Signer
    • [x] Salt Generator
  • Issue SD-JWT:

    • [x] Support recursive disclosures (parent object and its keys)
    • [x] Support for nested objects
    • [x] Support for arrays
    • [x] Optional: public key binding (cnf)
    • [x] Optional: decoy digest
  • Verify SD-JWT:

    • [x] Support recursive disclosures (parent object and its keys)
    • [x] Support for nested objects
    • [x] Support for arrays
    • [x] Optional: public key binding (cnf) check against the key binding JWT if one was provided
    • [x] Optional: decoy digest
  • Additional:

    • [ ] Holder: separate function for a holder to be able to create a key binding JWT (6.2.2)
  • Tests:

    • [x] Issuer SD-JWT tests
    • [x] Verifier SD-JWT tests
    • [x] Tests that check compatibility with SD-JWT generated by external libs
    • [x] e2e test
  • Release:

    • [x] Create CommonJS and ESM builds
    • [x] Documentation
    • [x] Publish on npm

Disclosure Frame

To issue or pack claims into a valid SD-JWT we use Disclosure Frame to define which properties/values should be selectively diclosable.
It follows the following format:

type ArrayIndex = number;
type DisclosureFrame = {
  [key: string | ArrayIndex]: DisclosureFrame | unknown;
  _sd?: Array<string | ArrayIndex>;
  _decoyCount?: number;
};

_decoyCount is an optional property that defines the number of decoy digests to add.

Examples:

set property as selectively disclosable

const claims = {
  firstname: 'John',
  lastname: 'Doe'
}
const diclosureFrame = {
  _sd: ['firstname'] // set firstname as selectively discloseable
}

// result
const sdjwt = {
  _sd: ['LjgwZy8TNXmmPO9mNqVDtq3jiX5r3YS-P-qw2hBNYyU']
  lastname: 'Doe',
}

nested property

const claims = {
  address: {
    street: '123 Main St',
    suburb: 'Anytown',
    postcode: '1234'
  }
}

const disclosureFrame = {
  address: {
    // set address.street and address.suburb as selectively discloseable
    _sd: ['street', 'suburb'];
  }
}

// result
const sdjwt = {
  address: {
    _sd: [
      '02d7bUYevjfAzJ0Gr42ymHy66ezQVL7huNGBO68xSfs',
      'ai7P4vgPZ-Jk1QwL55BLQqtN2gwWy31-pi2VGWiIggs',
    ],
    postcode: '1234'
  }
}

Array item

const claims = {
  nicknames: ['Johnny', 'JD']
}

const disclosureFrame = {
  nicknames: {
    _sd: [0, 1] // index of items in 'nicknames' Array
  }
}

// result
const sdjwt = {
  nicknames: [
    { '...': 'yfhdm_aKTMgm666j79GoXr2mer2dBW0cFfap8iXnAzY' },
    { '...': 'EU0ORASnAlqNtRwttBXsGTISxQ6myFPMBHPE0Ds8aSE' }
  ]
}

Object in Arrays

const claims = {
  items: [
    {
      type: 'shirt',
      size: 'M'
    },
    'Towel',
    'Water Bottle'
  ]
}

const disclosureFrame = {
  items: {
    0: {
      _sd: ['size'] // `size` property of items[0]
    }
  }
}

// result
const sdjwt = {
  items: [
    {
      _sd: ['7aGqCE9HepzELBi59BvxxriDiV7uiB4yHTyN1im_m4M'],
      type: 'shirt'
    },
    'Towel',
    'Water Bottle'
  ]
}

Array in Arrays

const claims = {
  colors: [
    ['R','G','B'],
    ['C','Y','M','K']
  ]
}

const disclosureFrame = {
  colors: {
    0: {
      _sd: [0, 2] // `R` and `B` in colors[0]
    }
  }
}

// result
const sdjwt = {
  colors: [
    [
      { '...': '' },
      'G',
      { '...': '' }
    ],
    ['C','Y','M','K']
  ]
}

issueSDJWT Example

The issueSDJWT function takes a JWT header, payload, and disclosure frame and returns a compact SD-JWT combined with the disclosures.

As the library is unopinionated when it comes to how you want to deal with cryptographic functions, so it requires a signer and hasher function to be provided.

Optional Holder key material can be provided in cnf options as a JWK.
Optional custom generateSalt function can also be provided that will be used when creating the claims and decoy claims.

Basic Usage

Example Using jose lib for signer function & crypto for hasher;

import crypto from 'crypto'
import { SignJWT, importJWK } from 'jose';

const header = {
  alg: 'ES256',
  kid: 'issuer-key-id'
};

const payload = {
  iss: 'https://example.com/issuer',
  iat: 168300000,
  exp: 188300000,
  sub: 'subject-id',
  name: 'John Doe'
};

const disclosureFrame = {
  _sd: ['name']
};

const signer = async (header, payload) => {
  const issuerPrivateKey = await importJWK(ISSUER_KEYPAIR.PRIVATE_KEY_JWK, header.alg);
  // Only the signature value should be returned.
  return (await new SignJWT(payload).setProtectedHeader(header).sign(issuerPrivateKey)).split('.').pop();
};

const hasher = (data) => {
  const digest = crypto.createHash('sha256').update(data).digest();
  const hash = Buffer.from(digest).toString('base64url');
  return Promise.resolve(hash);
};

// Optional
const cnf = { jwk: holderKey }
const generateSalt = generateSaltFunction

const sdjwt = await issueSDJWT(header, payload, disclosureFrame, {
  hash: {
    alg: 'sha-256',
    callback: hasher,
  },
  signer,
  cnf,
  generateSalt
});

// Decoded sdjwt.payload
{
  iss: 'https://example.com/issuer',
  iat: 168300000,
  exp: 188300000,
  sub: 'subject-id',
  _sd: [
    'jlJfq0qqkvwwgPrHh6kfzO2p7hpDYX1Mve-62bHgpHE' // HASH Digest of disclosure
  ]
}

// Disclosure
"WyJ2NEVHUzhKRzlTdW9TUjVGIiwibmFtZSIsIkpvaG4gRG9lIl0" // base64url encode of ["v4EGS8JG9SuoSR5F","name","John Doe"]

verifySDJWT Example

The verifySDJWT function takes a Compact combined SD-JWT (include optional disclosures & KB-JWT)
Required: a verifier function that can verify the JWT signature
Required: a getHasher function that returns a Hashed depending on the _sd_alg in the SD-JWT payload
Optional: A Keybinding Verifier function that can verify the embedded holder key
Returns SD-JWT with all the disclosed claims.

Basic Usage

Example Using jose lib for verifier Uses crypto for Hasher;

import { importJWK, jwtVerify } from 'jose';

const verifier = async (jwt) => {
  const key = await getIssuerKey(); // Get SD-JWT issuer public key
  return jwtVerify(jwt, key);
};

const keyBindingVerifier = (kbjwt, holderJWK) => {
  // check against kb-jwt.aud && kb-jwt.nonce
  const { header } = decodeJWT(kbjwt);
  const holderKey = await importJWK(holderJWK, header.alg);
  const verifiedKbJWT = await jwtVerify(kbjwt, holderKey);
  return !!verifiedKbJWT;
}

const getHasher = (hashAlg) => {
  let hasher;
  // Default Hasher = Hasher for SHA-256
  if (!hashAlg || hashAlg.toLowerCase() === 'sha-256') {
    hasher = (data) => {
      const digest = crypto.createHash('sha256').update(data).digest();
      return base64encode(digest);
    };
  }
  return Promise.resolve(hasher);
};

const opts = {
  kb: {
    verifier: keyBindingVerifier
  }
}
try {
  const sdJWTwithDisclosedClaims = await verifySDJWT(compactSDJWT, verifier, getHasher, opts);
} catch (e) {
  console.log('Could not verify SD-JWT', e);
}

unpackSDJWT Example

The unpackSDJWT function takes a SD-JWT payload with _sd digests, array of disclosures and returns the disclosed claims
Required: a sd-jwt payload with _sd digests
Required: an array of Disclosure objects
Required: a getHasher function that returns a Hashed depending on the _sd_alg in the SD-JWT payload

Basic Usage

import crypto from 'crypto';

const getHasher = (hashAlg) => {
  let hasher;
  // Default Hasher = Hasher for SHA-256
  if (!hashAlg || hashAlg.toLowerCase() === 'sha-256') {
    hasher = (data) => {
      const digest = crypto.createHash('sha256').update(data).digest();
      return base64encode(digest);
    };
  }
  return Promise.resolve(hasher);
};

const disclosures = [{
  disclosure: 'disclosure_array_as_string', // [<salt>, <key>, <value>]
  key: 'key_of_disclosed_claim',
  value: 'value_of_disclosed_claim'
}]

const sdjwt = {
  _sd: [
    'SD_DIGEST_1',
    'SD_DIGEST_2',
  ]
}

const result = await unpackSDJWT(sdjwt, disclosures, getHasher);

packSDJWT Examples

The packSDJWT function takes a claims object and disclosure frame and returns packed claims with selective disclosures encrypted.
Required: a claims object
Required: a disclosureFrame object
Required: a getHasher function that returns a Hashed depending on the _sd_alg in the SD-JWT payload
Optional: options with custom generateSalt function used in creating claims and decoy claims \

Returns SD-JWT and an array of disclosures.

Basic Usage

import { packSDJWT } from 'sd-jwt';

const claims = {
  name: 'Jane',
  ssn: '123-45-6789'
};

const disclosureFrame = {
  _sd: ['ssn']
};

const hasher = hasherFunction

const options = {
  generateSalt: generateSaltFunction
}

const {claims: packed, disclosures} = await packSDJWT(claims, disclosureFrame, hasher, options);

This will selectively disclose ssn and return the packed claims and disclosures array.

Disclosing Multiple Claims

To selectively disclose multiple claims:

const claims = {
  name: 'Jane Doe',
  ssn: '123-45-6789',
  id: '1234'
};

const disclosureFrame = {
  _sd: ['ssn', 'id']
};

const {claims: packed, disclosures} = await packSDJWT(claims, disclosureFrame, hasher);

// Results
packed =  {
  "name": "Jane Doe",
  "_sd": [
      "DZkUdg_W43hB25uuSxEyt2ialCeDbweHVXcRrhQHbLY",
      "85kfxIj8lWd5WODcupbDiYEw6upYWoD1GI048JUVAHw"
  ]
}

disclosures = [
  "WyJzNnZtNTJzWjN3Y1NXNUEzIiwic3NuIiwiMTIzLTQ1LTY3ODkiXQ",
  "WyJxZEt6MURIVDRlOHBpWlZ5IiwiaWQiLCIxMjM0Il0"
]

Selective Disclosable items in array

const claims = {
  items: ['a', 'b', 'c']
};

const disclosureFrame = {
  items: { _sd: [1] } // item at index 1
}

const {claims: packedClaims, disclosures} = await packSDJWT(claims, disclosureFrame, hasher);

// Results
packedClaims = {
  items: [
    'a',
    {
      "...": "b64encodedhash"
    },
    'c'
  ]
}

disclosures = [
  "WyJzYWx0IiwgMV0=" // b64 encoded [salt, 'b']
]

Selectively Disclosable Map

createSDMap

createSDMap returns
sdMap: an object representation of the SD claims in an sd-jwt
DisclosureMap: a map of hash values to get the disclosure and its parent disclosures if the SD claim was recursively packed

  const { sdMap, disclosureMap } = await createSDMap(sdjwt, hasher);

  // sdMap
  {
    nationalities: [
      {
        '...': {}, // item array value (if any recursive sd is present)
        _sd: 'pFndjkZ_VCzmyTa6UjlZo3dh-ko8aIKQc9DlGzhaVYo'
      },
      { '...': {}, _sd: '7Cf6JkPudry3lcbwHgeZ8khAv1U1OSlerP0VkBJrWZ0' }
    ],
    updated_at: { _sd: 'CrQe7S5kqBAHt-nMYXgc6bdt2SH5aTY1sU_M-PgkjPI' },
    email: { _sd: 'JzYjH4svliH0R3PyEMfeZu6Jt69u5qehZo7F7EPYlSE' },
  }

  // disclosureMap
  {
    'JzYjH4svliH0R3PyEMfeZu6Jt69u5qehZo7F7EPYlSE': {
      disclosure: 'WyI2SWo3dE0tYTVpVlBHYm9TNXRtdlZBIiwgImVtYWlsIiwgImpvaG5kb2VAZXhhbXBsZS5jb20iXQ',
      value: 'johndoe@example.com',
      parentDisclosures: []
    },
    'CrQe7S5kqBAHt-nMYXgc6bdt2SH5aTY1sU_M-PgkjPI': {
      disclosure: 'WyJHMDJOU3JRZmpGWFE3SW8wOXN5YWpBIiwgInVwZGF0ZWRfYXQiLCAxNTcwMDAwMDAwXQ',
      value: 1570000000,
      parentDisclosures: []
    },
    'pFndjkZ_VCzmyTa6UjlZo3dh-ko8aIKQc9DlGzhaVYo': {
      disclosure: 'WyJsa2x4RjVqTVlsR1RQVW92TU5JdkNBIiwgIlVTIl0',
      value: 'US',
      parentDisclosures: []
    },
    '7Cf6JkPudry3lcbwHgeZ8khAv1U1OSlerP0VkBJrWZ0': {
      disclosure: 'WyJuUHVvUW5rUkZxM0JJZUFtN0FuWEZBIiwgIkRFIl0',
      value: 'DE',
      parentDisclosures: []
    }
  }

Development

Installation

git clone https://github.com/Meeco/sd-jwt
npm install

npm run dev:setup

Test

Runs against examples in test/examples directory

Examples are generated using sd-jwt-generate

npm run test

Readme

Keywords

Package Sidebar

Install

npm i @meeco/sd-jwt

Weekly Downloads

170

Version

1.0.2

License

none

Unpacked Size

108 kB

Total Files

45

Last publish

Collaborators

  • janvereecken-meeco
  • dmunneke-meeco
  • linasisganaitis-meeco
  • meeco-ci