Skip to content

transmute-industries/verifiable-credentials

Repository files navigation

@transmute/verifiable-credentials

CI Branches Functions Lines Statements Jest coverage

🚧 Experimental implementation of Verifiable Credentials Data Model v2.0 🔥

Usage

Requires node 18 or higher.

nvm use 18
npm i @transmute/verifiable-credentials@latest --save
import * as transmute from "@transmute/verifiable-credentials";

Generating Keys

const privateKey = await transmute.key.generate({
  alg,
  type: "application/jwk+json",
});
// console.log(new TextDecoder().decode(privateKey))
// {
//   "kid": "xSgm4GQOT_ZyYFApew0GnRvPWt70omVJV9XVB5tsmN8",
//   "alg": "ES256",
//   "kty": "EC",
//   "crv": "P-256",
//   "x": "XRkZngz2KSCrLdXKGCRNyDzBgsovioZIqMWnF42nmdg",
//   "y": "H2t6Xxdg8p8Cqn2-hsuWnXYj0192He4zTZghAxNXllo",
//   ...
// }
const publicKey = await transmute.key.publicFromPrivate({
  type: "application/jwk+json",
  content: privateKey,
});
// console.log(new TextDecoder().decode(publicKey))
// {
//   "kid": "xSgm4GQOT_ZyYFApew0GnRvPWt70omVJV9XVB5tsmN8",
//   "alg": "ES256",
//   "kty": "EC",
//   "crv": "P-256",
//   "x": "XRkZngz2KSCrLdXKGCRNyDzBgsovioZIqMWnF42nmdg",
//   "y": "H2t6Xxdg8p8Cqn2-hsuWnXYj0192He4zTZghAxNXllo",
// }

Issuing Credentials

const alg = `ES256`;
const statusListSize = 131072;
const revocationIndex = 94567;
const suspensionIndex = 23452;

const issuer = `did:example:123`;
const baseURL = `https://vendor.example/api`;
const issued = await transmute
  .issuer({
    alg,
    type: "application/vc+ld+json+jwt",
    signer: {
      sign: async (bytes: Uint8Array) => {
        const jws = await new jose.CompactSign(bytes)
          .setProtectedHeader({ kid: `${issuer}#key-42`, alg })
          .sign(
            await transmute.key.importKeyLike({
              type: "application/jwk+json",
              content: privateKey,
            })
          );
        return transmute.text.encoder.encode(jws);
      },
    },
  })
  .issue({
    claimset: transmute.text.encoder.encode(`
"@context":
  - https://www.w3.org/ns/credentials/v2
  - https://www.w3.org/ns/credentials/examples/v2

id: ${baseURL}/credentials/3732
type:
  - VerifiableCredential
  - ExampleDegreeCredential
issuer:
  id: ${issuer}
  name: "Example University"
validFrom: ${moment().toISOString()}
credentialSchema:
  id: ${baseURL}/schemas/product-passport
  type: JsonSchema
credentialStatus:
  - id: ${baseURL}/credentials/status/3#${revocationIndex}
    type: BitstringStatusListEntry
    statusPurpose: revocation
    statusListIndex: "${revocationIndex}"
    statusListCredential: "${baseURL}/credentials/status/3"
  - id: ${baseURL}/credentials/status/4#${suspensionIndex}
    type: BitstringStatusListEntry
    statusPurpose: suspension
    statusListIndex: "${suspensionIndex}"
    statusListCredential: "${baseURL}/credentials/status/4"
credentialSubject:
  id: did:example:ebfeb1f712ebc6f1c276e12ec21
  degree:
    type: ExampleBachelorDegree
    subtype: Bachelor of Science and Arts
`),
  });
// console.log(new TextDecoder().decode(issued))
// eyJraWQiOiJkaWQ6ZXhhbXBsZToxMjMja2V5LTQyIiwiYWxnIjoiRVMyNTYifQ.eyJAY29udGV4dCI6WyJodHRwczovL3d3dy53My5vcmcvbnMvY3JlZGVudGlhbHMvdjIiLCJodHRwczovL3ZlbmRvci5leGFtcGxlL2FwaS9jb250ZXh0L3YyIl0sImlkIjoiaHR0cHM6Ly92ZW5kb3IuZXhhbXBsZS9hcGkvY3JlZGVudGlhbHMvMzczMiIsInR5cGUiOlsiVmVyaWZpYWJsZUNyZWRlbnRpYWwiLCJFeGFtcGxlRGVncmVlQ3JlZGVudGlhbCJdLCJpc3N1ZXIiOnsiaWQiOiJkaWQ6ZXhhbXBsZToxMjMiLCJuYW1lIjoiRXhhbXBsZSBVbml2ZXJzaXR5In0sInZhbGlkRnJvbSI6IjIwMjQtMDQtMjRUMjI6MjM6MDIuODU2WiIsImNyZWRlbnRpYWxTY2hlbWEiOnsiaWQiOiJodHRwczovL3ZlbmRvci5leGFtcGxlL2FwaS9zY2hlbWFzL3Byb2R1Y3QtcGFzc3BvcnQiLCJ0eXBlIjoiSnNvblNjaGVtYSJ9LCJjcmVkZW50aWFsU3RhdHVzIjpbeyJpZCI6Imh0dHBzOi8vdmVuZG9yLmV4YW1wbGUvYXBpL2NyZWRlbnRpYWxzL3N0YXR1cy8zIzk0NTY3IiwidHlwZSI6IkJpdHN0cmluZ1N0YXR1c0xpc3RFbnRyeSIsInN0YXR1c1B1cnBvc2UiOiJyZXZvY2F0aW9uIiwic3RhdHVzTGlzdEluZGV4IjoiOTQ1NjciLCJzdGF0dXNMaXN0Q3JlZGVudGlhbCI6Imh0dHBzOi8vdmVuZG9yLmV4YW1wbGUvYXBpL2NyZWRlbnRpYWxzL3N0YXR1cy8zIn0seyJpZCI6Imh0dHBzOi8vdmVuZG9yLmV4YW1wbGUvYXBpL2NyZWRlbnRpYWxzL3N0YXR1cy80IzIzNDUyIiwidHlwZSI6IkJpdHN0cmluZ1N0YXR1c0xpc3RFbnRyeSIsInN0YXR1c1B1cnBvc2UiOiJzdXNwZW5zaW9uIiwic3RhdHVzTGlzdEluZGV4IjoiMjM0NTIiLCJzdGF0dXNMaXN0Q3JlZGVudGlhbCI6Imh0dHBzOi8vdmVuZG9yLmV4YW1wbGUvYXBpL2NyZWRlbnRpYWxzL3N0YXR1cy80In1dLCJjcmVkZW50aWFsU3ViamVjdCI6eyJpZCI6ImRpZDpleGFtcGxlOmViZmViMWY3MTJlYmM2ZjFjMjc2ZTEyZWMyMSIsImRlZ3JlZSI6eyJ0eXBlIjoiRXhhbXBsZUJhY2hlbG9yRGVncmVlIiwic3VidHlwZSI6IkJhY2hlbG9yIG9mIFNjaWVuY2UgYW5kIEFydHMifX19.xHjfiUwx61qmoVMGLrHT8FI-ZYUHXQy4B6oF0Cb5EOTYYPXdwjW9sa1l5aa008xvsFvrcNats9TywmN2nNKz6A

Validating Credentials

const validated = await transmute
  .validator({
    resolver: {
      resolve: async ({ id, type, content }) => {
        // Resolve external resources according to verifier policy
        // In this case, we return inline exampes...
        if (id === `${baseURL}/schemas/product-passport`) {
          return {
            type: `application/schema+json`,
            content: transmute.text.encoder.encode(`
{
  "$id": "${baseURL}/schemas/product-passport",
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "title": "Example JSON Schema",
  "description": "This is a test schema",
  "type": "object",
  "properties": {
    "credentialSubject": {
      "type": "object",
      "properties": {
        "id": {
          "type": "string"
        }
      }
    }
  }
}
              `),
          };
        }
        if (id === `${baseURL}/credentials/status/3`) {
          return {
            type: `application/vc+ld+json+jwt`,
            content: await transmute
              .issuer({
                alg: "ES384",
                type: "application/vc+ld+json+jwt",
                signer: {
                  sign: async (bytes: Uint8Array) => {
                    const jws = await new jose.CompactSign(bytes)
                      .setProtectedHeader({ kid: `${issuer}#key-42`, alg })
                      .sign(
                        await transmute.key.importKeyLike({
                          type: "application/jwk+json",
                          content: privateKey,
                        })
                      );
                    return transmute.text.encoder.encode(jws);
                  },
                },
              })
              .issue({
                claimset: transmute.text.encoder.encode(
                  `
"@context":
  - https://www.w3.org/ns/credentials/v2
id: ${baseURL}/status/3#list
type:
  - VerifiableCredential
  - BitstringStatusListCredential
issuer:
  id: ${issuer}
validFrom: ${moment().toISOString()}
credentialSubject:
  id: ${baseURL}/status/3#list#list
  type: BitstringStatusList
  statusPurpose: revocation
  encodedList: ${await transmute.status
    .bs(statusListSize)
    .set(revocationIndex, false)
    .encode()}
`.trim()
                ),
              }),
          };
        }
        if (id === `${baseURL}/credentials/status/4`) {
          return {
            type: `application/vc+ld+json+jwt`,
            content: await transmute
              .issuer({
                alg: "ES384",
                type: "application/vc+ld+json+jwt",
                signer: {
                  sign: async (bytes: Uint8Array) => {
                    const jws = await new jose.CompactSign(bytes)
                      .setProtectedHeader({ kid: `${issuer}#key-42`, alg })
                      .sign(
                        await transmute.key.importKeyLike({
                          type: "application/jwk+json",
                          content: privateKey,
                        })
                      );
                    return transmute.text.encoder.encode(jws);
                  },
                },
              })
              .issue({
                claimset: transmute.text.encoder.encode(
                  `
"@context":
  - https://www.w3.org/ns/credentials/v2
id: ${baseURL}/status/4#list
type:
  - VerifiableCredential
  - BitstringStatusListCredential
issuer:
  id: ${issuer}
validFrom: ${moment().toISOString()}
credentialSubject:
  id: ${baseURL}/status/4#list#list
  type: BitstringStatusList
  statusPurpose: suspension
  encodedList: ${await transmute.status
    .bs(statusListSize)
    .set(suspensionIndex, false)
    .encode()}
`.trim()
                ),
              }),
          };
        }
        if (content != undefined && type === `application/vc+ld+json+jwt`) {
          const { kid } = jose.decodeProtectedHeader(
            transmute.text.decoder.decode(content)
          );
          // lookup public key by kid on a trusted resolver
          if (kid === `did:example:123#key-42`) {
            return {
              type: "application/jwk+json",
              content: publicKey,
            };
          }
        }
        throw new Error("Resolver option not supported.");
      },
    },
  })
  .validate({
    type: "application/vc+ld+json+jwt",
    content: issued,
  });

// expect(validated.valid).toBe(true)
// expect(validated.schema[`${baseURL}/schemas/product-passport`].valid).toBe(true)
// expect(validated.status[`${baseURL}/credentials/status/3#${revocationIndex}`].valid).toBe(false)
// expect(validated.status[`${baseURL}/credentials/status/4#${suspensionIndex}`].valid).toBe(false)

Issuing Presentations

const presentation = await transmute
  .holder({
    alg,
    type: "application/vp+ld+json+jwt",
  })
  .issue({
    signer: {
      sign: async (bytes: Uint8Array) => {
        const jws = await new jose.CompactSign(bytes)
          .setProtectedHeader({ kid: `${issuer}#key-42`, alg })
          .sign(
            await transmute.key.importKeyLike({
              type: "application/jwk+json",
              content: privateKey,
            })
          );
        return transmute.text.encoder.encode(jws);
      },
    },
    presentation: {
      "@context": ["https://www.w3.org/ns/credentials/v2"],
      type: ["VerifiablePresentation"],
      holder: `${baseURL}/holders/565049`,
      // this part is built from disclosures without key binding below.
      // "verifiableCredential": [{
      //   "@context": "https://www.w3.org/ns/credentials/v2",
      //   "id": "data:application/vc+ld+json+sd-jwt;QzVjV...RMjU",
      //   "type": "EnvelopedVerifiableCredential"
      // }]
    },
    disclosures: [
      {
        type: `application/vc+ld+json+jwt`,
        credential: issued,
      },
    ],
  });

Validating Presentations

const validation = await transmute
  .validator({
    resolver: {
      resolve: async ({ type, content }) => {
        // Resolve external resources according to verifier policy
        // In this case, we return inline exampes...
        if (content != undefined && type === `application/vp+ld+json+jwt`) {
          const { kid } = jose.decodeProtectedHeader(
            transmute.text.decoder.decode(content)
          );
          // lookup public key on a resolver
          if (kid === `did:example:123#key-42`) {
            return {
              type: "application/jwk+json",
              content: publicKey,
            };
          }
        }
        throw new Error("Resolver option not supported.");
      },
    },
  })
  .validate<transmute.TraceablePresentationValidationResult>({
    type: `application/vp+ld+json+jwt`,
    content: presentation,
  });
// {
//   "valid": true,
//   "content": {
//     "@context": [
//       "https://www.w3.org/ns/credentials/v2"
//     ],
//     "type": [
//       "VerifiablePresentation"
//     ],
//     "holder": "https://vendor.example/api/holders/565049",
//     "verifiableCredential": [
//       {
//         "@context": "https://www.w3.org/ns/credentials/v2",
//         "id": "data:application/vc+ld+json+jwt;eyJraWQiOiJkaWQ6ZX...

Develop

npm i
npm t
npm run lint
npm run build