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.
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.
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 theclaim
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 theclaim
section of the credential. It is quite similar to thefieldNames
section, except no error will be thrown if the fields are missing. -
type
maps directly to thetype
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 thehttp://schema.org
link so that theemail
key can be mapped to thehttp://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.
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" }
})
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 .