Skip to content

Instantly share code, notes, and snippets.

@ilovelili
Created June 28, 2021 11:45
Show Gist options
  • Save ilovelili/be5e2e0f5565b0a92222c4b50adbde65 to your computer and use it in GitHub Desktop.
Save ilovelili/be5e2e0f5565b0a92222c4b50adbde65 to your computer and use it in GitHub Desktop.

Credential creation flows using the MONiD Library

Introduction

At MONiD we model verifiable credentials using W3C Verifiable credential specification, aside from some very recent spec modifications. Below you can see an example of a credential, serialized as JSON-LD, stating that the did did:monid:ffcc8f84fae1b6ad253561d7b78167a661d72f58e86e60dbd04cd9b81096cdbe is associated with the email address [email protected]. You might notice that the subject, issuer, and signer of the credential, are the same identity. This can be effectively referred to as a self signed, or self issued, credential.

{
  "@context": [
    "https://w3id.org/identity/v1",
    "https://identity.monid.io/terms",
    "https://w3id.org/security/v1",
    "https://w3id.org/credentials/v1",
    "http://schema.org"
  ],
  "id": "claimId:25453fa543da7",
  "name": "Email address",
  "issuer": "did:monid:ffcc8f84fae1b6ad253561d7b78167a661d72f58e86e60dbd04cd9b81096cdbe",
  "type": ["Credential", "ProofOfEmailCredential"],
  "claim": {
    "id": "did:monid:ffcc8f84fae1b6ad253561d7b78167a661d72f58e86e60dbd04cd9b81096cdbe",
    "email": "[email protected]"
  },
  "issued": "2018-08-14T15:09:26.709Z",
  "proof": {
    "type": "EcdsaKoblitzSignature2016",
    "created": "2018-08-14T15:09:26.710Z",
    "creator": "did:monid:ffcc8f84fae1b6ad253561d7b78167a661d72f58e86e60dbd04cd9b81096cdbe#keys-1",
    "nonce": "d62dab8e29e11",
    "signatureValue": "6UgXjR6668RWLw45PEOsoxytWbX00prza733mbNKO+NpI4QcqPUUbsVEzRQ10E6YF0OZv+d8pzng+djLfweA2w=="
  }
}

In order for a signed credential to be of any use or value, other independent entities must be able to verify the validity of the signature presented in the proof section. In order to do that, they first need to normalize the JSON-LD document. The following section goes into more details about the normalization process and it's importance.

Normalization

JSON-LD is only one way of serializing a linked data graph. Below a number of alternative serialization of the same credential are presented:

N-QUADS:

<claimId:25453fa543da7> <http://schema.org/name> "Email address" .
<claimId:25453fa543da7> <http://schema.org/proof> _:b0 .
<claimId:25453fa543da7> <http://www.w3.org/1999/02/22-rdf-syntax-ns#type> <https://identity.monid.io/terms/ProofOfEmailCredential> .
<claimId:25453fa543da7> <http://www.w3.org/1999/02/22-rdf-syntax-ns#type> <https://w3id.org/credentials#Credential> .
<claimId:25453fa543da7> <https://w3id.org/credentials#claim> <did:monid:ffcc8f84fae1b6ad253561d7b78167a661d72f58e86e60dbd04cd9b81096cdbe> .
<claimId:25453fa543da7> <https://w3id.org/credentials#issued> "2018-08-14T15:09:26.709Z"^^<http://www.w3.org/2001/XMLSchema#dateTime> .
<claimId:25453fa543da7> <https://w3id.org/credentials#issuer> <did:monid:ffcc8f84fae1b6ad253561d7b78167a661d72f58e86e60dbd04cd9b81096cdbe> .

<did:monid:ffcc8f84fae1b6ad253561d7b78167a661d72f58e86e60dbd04cd9b81096cdbe> <http://schema.org/email> "[email protected]" .

_:b0 <http://purl.org/dc/terms/created> "2018-08-14T15:09:26.710Z"^^<http://www.w3.org/2001/XMLSchema#dateTime> .
_:b0 <http://schema.org/creator> "did:monid:ffcc8f84fae1b6ad253561d7b78167a661d72f58e86e60dbd04cd9b81096cdbe#keys-1" .
_:b0 <http://www.w3.org/1999/02/22-rdf-syntax-ns#type> <https://w3id.org/security#EcdsaKoblitzSignature2016> .
_:b0 <https://w3id.org/security#nonce> "d62dab8e29e11" .
_:b0 <https://w3id.org/security#signatureValue> "6UgXjR6668RWLw45PEOsoxytWbX00prza733mbNKO+NpI4QcqPUUbsVEzRQ10E6YF0OZv+d8pzng+djLfweA2w==" .

Turtle:

@prefix schema: <http://schema.org/> .
@prefix xsd: <http://www.w3.org/2001/XMLSchema#> .
@prefix dc: <http://purl.org/dc/terms/> .
@prefix ns0: <https://w3id.org/security#> .
@prefix ns1: <https://w3id.org/credentials#> .

<claimId:25453fa543da7>
  a <https://w3id.org/credentials#Credential>, <https://identity.monid.io/terms/ProofOfEmailCredential> ;
  schema:name "Email address"^^xsd:string ;
  schema:proof [
    a <https://w3id.org/security#EcdsaKoblitzSignature2016> ;
    dc:created "2018-08-14T15:09:26.710Z"^^xsd:dateTime ;
    schema:creator "did:monid:ffcc8f84fae1b6ad253561d7b78167a661d72f58e86e60dbd04cd9b81096cdbe#keys-1"^^xsd:string ;
    ns0:nonce "d62dab8e29e11"^^xsd:string ;
    ns0:signatureValue "6UgXjR6668RWLw45PEOsoxytWbX00prza733mbNKO+NpI4QcqPUUbsVEzRQ10E6YF0OZv+d8pzng+djLfweA2w=="^^xsd:string
  ] ;
  ns1:claim <did:monid:ffcc8f84fae1b6ad253561d7b78167a661d72f58e86e60dbd04cd9b81096cdbe> ;
  ns1:issued "2018-08-14T15:09:26.709Z"^^xsd:dateTime ;
  ns1:issuer <did:monid:ffcc8f84fae1b6ad253561d7b78167a661d72f58e86e60dbd04cd9b81096cdbe> .

<did:monid:ffcc8f84fae1b6ad253561d7b78167a661d72f58e86e60dbd04cd9b81096cdbe> schema:email "[email protected]"^^xsd:string .

All three documents describe the same credential, and will result in the same signature. This is possible because we are not digesting and signing the serialized document directly, but rather it's canonical or normalized form. Generally speaking we are signing the described graph itself. An useful online tool for comparing the various serialization formats of a RDF graph is available here. We use the same javascript library for normalizing the credentials in the MONiD library.

After normalization, the same JSON-LD document looks as follows:

<claimId:25453fa543da7> <http://schema.org/name> "Email address" .
<claimId:25453fa543da7> <http://schema.org/proof> _:b0 .
<claimId:25453fa543da7> <http://www.w3.org/1999/02/22-rdf-syntax-ns#type> <https://identity.monid.io/terms/ProofOfEmailCredential> .
<claimId:25453fa543da7> <http://www.w3.org/1999/02/22-rdf-syntax-ns#type> <https://w3id.org/credentials#Credential> .
<claimId:25453fa543da7> <https://w3id.org/credentials#claim> <did:monid:ffcc8f84fae1b6ad253561d7b78167a661d72f58e86e60dbd04cd9b81096cdbe> .
<claimId:25453fa543da7> <https://w3id.org/credentials#issued> "2018-08-14T15:09:26.709Z"^^<http://www.w3.org/2001/XMLSchema#dateTime> .
<claimId:25453fa543da7> <https://w3id.org/credentials#issuer> <did:monid:ffcc8f84fae1b6ad253561d7b78167a661d72f58e86e60dbd04cd9b81096cdbe> .
<did:monid:ffcc8f84fae1b6ad253561d7b78167a661d72f58e86e60dbd04cd9b81096cdbe> <http://schema.org/email> "[email protected]" .
_:b0 <http://purl.org/dc/terms/created> "2018-08-14T15:09:26.710Z"^^<http://www.w3.org/2001/XMLSchema#dateTime> .
_:b0 <http://schema.org/creator> "did:monid:ffcc8f84fae1b6ad253561d7b78167a661d72f58e86e60dbd04cd9b81096cdbe#keys-1" .
_:b0 <http://www.w3.org/1999/02/22-rdf-syntax-ns#type> <https://w3id.org/security#EcdsaKoblitzSignature2016> .
_:b0 <https://w3id.org/security#nonce> "d62dab8e29e11" .
_:b0 <https://w3id.org/security#signatureValue> "6UgXjR6668RWLw45PEOsoxytWbX00prza733mbNKO+NpI4QcqPUUbsVEzRQ10E6YF0OZv+d8pzng+djLfweA2w==" .

It is worth noting that all JSON keys, e.g. type, email, nonce, have been expanded to their full form, e.g. <http://schema.org/name>, <http://schema.org/email>, and <https://w3id.org/security#nonce> respectively.

As part of the normalization process, all keys in the JSON-LD document need to be mapped to corresponding RDF predicates / subjects / objects. This is where the @context section comes in. It's purpose is to aid this mapping. In the presented case, all entries are links to external contexts that can be fetched to proceed with the mapping process.

For instance the first url, https://w3id.org/security/v1, once dereferenced will result in the following document:

{
  "@context": {
    "id": "@id",
    "type": "@type",

    "dc": "http://purl.org/dc/terms/",
    "sec": "https://w3id.org/security#",
    "xsd": "http://www.w3.org/2001/XMLSchema#",

    "EcdsaKoblitzSignature2016": "sec:EcdsaKoblitzSignature2016",
    "Ed25519Signature2018": "sec:Ed25519Signature2018",
    "EncryptedMessage": "sec:EncryptedMessage",
    "GraphSignature2012": "sec:GraphSignature2012",
    "LinkedDataSignature2015": "sec:LinkedDataSignature2015",
    "LinkedDataSignature2016": "sec:LinkedDataSignature2016",
    "CryptographicKey": "sec:Key",

    "authenticationTag": "sec:authenticationTag",
    "canonicalizationAlgorithm": "sec:canonicalizationAlgorithm",
    "cipherAlgorithm": "sec:cipherAlgorithm",
    "cipherData": "sec:cipherData",
    "cipherKey": "sec:cipherKey",
    "created": {"@id": "dc:created", "@type": "xsd:dateTime"},
    "creator": {"@id": "dc:creator", "@type": "@id"},
    "digestAlgorithm": "sec:digestAlgorithm",
    "digestValue": "sec:digestValue",
    "domain": "sec:domain",
    "encryptionKey": "sec:encryptionKey",
    "expiration": {"@id": "sec:expiration", "@type": "xsd:dateTime"},
    "expires": {"@id": "sec:expiration", "@type": "xsd:dateTime"},
    "initializationVector": "sec:initializationVector",
    "iterationCount": "sec:iterationCount",
    "nonce": "sec:nonce",
    "normalizationAlgorithm": "sec:normalizationAlgorithm",
    "owner": {"@id": "sec:owner", "@type": "@id"},
    "password": "sec:password",
    "privateKey": {"@id": "sec:privateKey", "@type": "@id"},
    "privateKeyPem": "sec:privateKeyPem",
    "publicKey": {"@id": "sec:publicKey", "@type": "@id"},
    "publicKeyBase58": "sec:publicKeyBase58",
    "publicKeyPem": "sec:publicKeyPem",
    "publicKeyService": {"@id": "sec:publicKeyService", "@type": "@id"},
    "revoked": {"@id": "sec:revoked", "@type": "xsd:dateTime"},
    "salt": "sec:salt",
    "signature": "sec:signature",
    "signatureAlgorithm": "sec:signingAlgorithm",
    "signatureValue": "sec:signatureValue"
  }
}

At this point the keys in the original JSON-LD document can be mapped to the corresponding entries in the fetched context. In case a mapping for a key has not been found in any of the fetched contexts, the normalization will continue, but the outcome will be incorrect.

We can verify this by taking the original credential, and removing two entries from the @context array, specifically https://w3id.org/identity/v1 and https://w3id.org/credentials/v1. The resulting credential looks as follows:

{
  "@context": [
    "https://identity.monid.io/terms",
    "https://w3id.org/security/v1",
    "http://schema.org"
  ],
  "id": "claimId:25453fa543da7",
  "name": "Email address",
  "issuer": "did:monid:ffcc8f84fae1b6ad253561d7b78167a661d72f58e86e60dbd04cd9b81096cdbe",
  "type": ["Credential", "ProofOfEmailCredential"],
  "claim": {
    "id": "did:monid:ffcc8f84fae1b6ad253561d7b78167a661d72f58e86e60dbd04cd9b81096cdbe",
    "email": "[email protected]"
  },
  "issued": "2018-08-14T15:09:26.709Z",
  "proof": {
    "type": "EcdsaKoblitzSignature2016",
    "created": "2018-08-14T15:09:26.710Z",
    "creator": "did:monid:ffcc8f84fae1b6ad253561d7b78167a661d72f58e86e60dbd04cd9b81096cdbe#keys-1",
    "nonce": "d62dab8e29e11",
    "signatureValue": "6UgXjR6668RWLw45PEOsoxytWbX00prza733mbNKO+NpI4QcqPUUbsVEzRQ10E6YF0OZv+d8pzng+djLfweA2w=="
  }
}

If we look at the normalized form, we can notice that it is different from the initial normalized form, namely the key issuer, amongst others, has been incorrectly expanded to http://schema.org/issuer instead of https://w3id.org/credentials#issued, because no matching mapping was found.

<claimId:25453fa543da7> <http://schema.org/claim> <did:monid:ffcc8f84fae1b6ad253561d7b78167a661d72f58e86e60dbd04cd9b81096cdbe> .
<claimId:25453fa543da7> <http://schema.org/issued> "2018-08-14T15:09:26.709Z" .
<claimId:25453fa543da7> <http://schema.org/issuer> "did:monid:ffcc8f84fae1b6ad253561d7b78167a661d72f58e86e60dbd04cd9b81096cdbe" .
<claimId:25453fa543da7> <http://schema.org/name> "Email address" .
<claimId:25453fa543da7> <http://schema.org/proof> _:c14n0 .
<claimId:25453fa543da7> <http://www.w3.org/1999/02/22-rdf-syntax-ns#type> <http://schema.org/Credential> .
<claimId:25453fa543da7> <http://www.w3.org/1999/02/22-rdf-syntax-ns#type> <https://identity.monid.io/terms/ProofOfEmailCredential> .
<did:monid:ffcc8f84fae1b6ad253561d7b78167a661d72f58e86e60dbd04cd9b81096cdbe> <http://schema.org/email> "[email protected]" .
_:c14n0 <http://purl.org/dc/terms/created> "2018-08-14T15:09:26.710Z"^^<http://www.w3.org/2001/XMLSchema#dateTime> .
_:c14n0 <http://schema.org/creator> "did:monid:ffcc8f84fae1b6ad253561d7b78167a661d72f58e86e60dbd04cd9b81096cdbe#keys-1" .
_:c14n0 <http://www.w3.org/1999/02/22-rdf-syntax-ns#type> <https://w3id.org/security#EcdsaKoblitzSignature2016> .
_:c14n0 <https://w3id.org/security#nonce> "d62dab8e29e11" .
_:c14n0 <https://w3id.org/security#signatureValue> "6UgXjR6668RWLw45PEOsoxytWbX00prza733mbNKO+NpI4QcqPUUbsVEzRQ10E6YF0OZv+d8pzng+djLfweA2w==" .

This being said, whenever we introduce a custom credential type, or a custom key in general, we need to provide the corresponding mapping through the @context field, otherwise normalization will not go as expected (in this case the keys have been incorrectly mapped to schema.org predicates as a fallback, but we cannot rely on that).

Since the normalized form is what is being digested and signed, the signature generated in the first case will be different from the signature generated in the second one.

Creating "standard" verifiable credentials using the MONiD library.

The following function can be used to create a new credential using the MONiD Library:

const identityWallet = await registry.authenticate(privateIdentityKey)

const signedEmailCredential = await identityWallet.create.signedCredential({
  metadata: claimsMetadata.emailAddress,
  claim: { email: "[email protected]" }
})

The first argument to the function is the metadata for the credential. The MONiD Library ships with a few default metadata definitions that can be found here. If we examine the metadata definition for the emailAddress credential, it looks as follows:

emailAddress: {
    "fieldNames": ["email"],
    "optionalFieldNames": [],
    "type": ["Credential", "ProofOfEmailCredential"],
    "name": "Email address",
    "context": [
      "https://w3id.org/identity/v1",
      "https://identity.monid.io/terms",
      "https://w3id.org/security/v1",
      "https://w3id.org/credentials/v1",
      "http://schema.org"
    ]
}

The purpose of each field is:

  • fieldNames defines what fields must be present in the claim section of the credential, this is used to ensure that the required data has been passed, as seen here. As one might observe, this check happens at run-time. In case some fields are missing, an error will be thrown. We will hopefully find a way in the future to leverage some of the TypeScript features to perhaps detect / enforce this during transpilation.

  • optionalFieldNames defines what fields can be present in the claim section of the credential. It is quite similar to the fieldNames section, except no error will be thrown if the fields are missing.

  • type maps directly to the type key in the final verifiable credential.

  • name is a human friendly name for the credential type that can be used for rendering / user facing purposes.

  • context field contains an array of links that will listed in the @context field of the final credential. In this case, we include the http://schema.org link so that the email key can be mapped to the http://schema.org/email term.

The second argument that needs to be passed to the function, is the claim data itself, in this case

{"email": "[email protected]"}

The email key must correspond to one of the keys listed in the fieldNames in the metadata section, otherwise a run time error will be thrown.

Creating custom verifiable credentials using the MONiD library.

Let's assume you need to create a custom credential that describes the subject's job title. Firstly, you need to define a corresponding metadata object, for instance:

{
    "fieldNames": ["jobTitle"],
    "optionalFieldNames": [],
    "type": ["Credential", "ProofOfJobTitle"],
    "name": "Job title",
    "context": [
      "https://w3id.org/identity/v1",
      "https://mywebsite.com/terms",
      "https://w3id.org/security/v1",
      "https://w3id.org/credentials/v1",
      "http://schema.org"
    ]
}

Besides defining the claim fields (in this case jobTitle), you will also need to provide a context link or definition that can be used to map the aforementioned field name to the corresponding RDF predicate. In this case the jobTitle key will be mapped to http://schema.org/jobTitle, a valid predicate. In addition, you will need to make sure that the credential type you defined (in this case ProofOfJobTitle) can also be mapped to a RDF subject, e.g. https://mywebsite.com/terms/ProofOfJobTitle.

This is the reason most credentials created by the MONiD Library contain the https://identity.monid.io/terms entry in the @context field, to dereferences custom credential types such as ProofOfEmailCredential or ProofOfMobilePhoneNumberCredential. As of now none of those terms are defined in any other contexts. Furthermore most types shown in the specification document (e.g. ProofOfAgeCredential) lack the mappings as well.

Once you defined the metadata, you can proceed with creating the credential. All in all, the process should look as follows:

const myCustomMetadata = {
    fieldNames: ["jobTitle"],
    optionalFieldNames: [],
    type: ["Credential", "ProofOfJobTitle"],
    name: "Job title",
    context: [
      "https://w3id.org/identity/v1",
      "https://identity.monid.io/terms",
      "https://w3id.org/security/v1",
      "https://w3id.org/credentials/v1",
      "http://schema.org"
    ]
}


const signedJobTitleCredential = await identityWallet.create.signedCredential({
  metadata: myCustomMetadata,
  claim: { jobTitle: "Software Developer" }
})

// In case you want to issue a credential to another identity:

const signedJobTitleCredential = await identityWallet.create.signedCredential({
  metadata: myCustomMetadata,
  claim: { 
    id: 'did:monid:subjectDID...', // if this is ommited, the signer's identity is used as the subject / receiver of the credential
    jobTitle: "Software Developer" 
    }
})

One does not have to make any modifications to the MONiD Library itself to issue new credential types, the only thing that one must do is provide the metadata definition. A good way to manage this would perhaps be to encapsulate the metadata definitions created for your use case in a separate npm package and simply import them into your project.

The final flow might look as follows:

import { MONiDLib } from 'MONiD-lib'
import myMetadata from 'credentialTypes/myCustomTypes'

...
...

const signedJobTitleCredential = await identityWallet.create.signedCredential({
  metadata: myMetadata.jobTitle,
  claim: { jobTitle: "Software Developer" }
})

Do I need to publish a new context if I define custom terms?

Actually, no! The following credential format is also fully valid:

 "@context": [
   {
     "ProofOfEmailCredential": "https://identity.monid.io/terms/ProofOfEmailCredential"
   }
    "https://w3id.org/identity/v1",
    "https://w3id.org/security/v1",
    "https://w3id.org/credentials/v1",
    "http://schema.org"
  ],
  "id": "claimId:25453fa543da7",
  "name": "Email address",
  "issuer": "did:monid:ffcc8f84fae1b6ad253561d7b78167a661d72f58e86e60dbd04cd9b81096cdbe",
  "type": ["Credential", "ProofOfEmailCredential"],
  "claim": {
    "id": "did:monid:ffcc8f84fae1b6ad253561d7b78167a661d72f58e86e60dbd04cd9b81096cdbe",
    "email": "[email protected]"
  },
  "issued": "2018-08-14T15:09:26.709Z",
  "proof": {
    "type": "EcdsaKoblitzSignature2016",
    "created": "2018-08-14T15:09:26.710Z",
    "creator": "did:monid:ffcc8f84fae1b6ad253561d7b78167a661d72f58e86e60dbd04cd9b81096cdbe#keys-1",
    "nonce": "d62dab8e29e11",
    "signatureValue": "6UgXjR6668RWLw45PEOsoxytWbX00prza733mbNKO+NpI4QcqPUUbsVEzRQ10E6YF0OZv+d8pzng+djLfweA2w=="
  }

The benefit of this approach is that the ProofOfEmailCredential key can be mapped to the corresponding term without making any external http calls.

It is also worth noting that the https://identity.monid.io/terms/ProofOfEmailCredential url does not have to be dereferenceable as far as the normalization and signature generation is concerned, and could be deployed at a later point in time.

We are currently actively considering employing this approach at MONiD as well, to not have to rely on the availability of external contexts / resources .

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment