Introduction
The goal of these exercises is to explore the functionalities of Verifiable Credentials and Zero Knowledge Proofs in the scope of a mock authentication process
Verifiable Credentials
This guide will demonstrate how Verifiable Credentials can be used in a standalone manner, and specifically, how a Verifiable Credential can be created, presented and verified. We will not use any blockchain to store information, and will pass the VC between components directly as files.
In order to execute this project, several steps need to be completed. The first is to install the digitalbazaar
NodeJS components.
npm install @digitalbazaar/vc
npm install @digitalbazaar/did-method-key
npm install @digitalbazaar/did-method-web
npm install @digitalbazaar/ecdsa-multikey
npm install @digitalbazaar/ecdsa-rdfc-2019-cryptosuite
npm install @digitalbazaar/data-integrity
npm install @digitalbazaar/security-document-loader
npm install @digitalbazaar/data-integrity-context
Our scenario considers that there is an issuer
, a holder
and a verifier
. The issuer creates a Verifiable Credential
, which is provided to the holder
. The verifier
can process the Verifiable Credential
provided by the holder
to assert identity attributes. In a real world situation, the Verifiable Credential
would be in a wallet, backed by some Ledger. In our scnenario, we will skip that part and make the Verifiable Credential
directly available to the verifier
.
To start the process, you will need a Credential. We will issue academic diplomas, which obey a known schema. For an example of a VC related to a diploma, check: https://w3c-ccg.github.io/vc-ed-models/ and https://credreg.net/ctdl/terms.
A base credential would be:
{
"@context": [
"https://www.w3.org/2018/credentials/v1",
"https://www.w3.org/2018/credentials/examples/v1"
],
"id": "urn:uuid:a63a60be-f4af-491c-87fc-2c8fd3007a58",
"type": [
"VerifiableCredential",
"UniversityDegreeCredential"
],
"issuer": {
"type": [
"Profile"
],
"id": "did:key:z6MktiSzqF9kqwdU8VkdBKx56EYzXfpgnNPUAGznpicNiWfn",
"name": "Universidade de Aveiro"
},
"issuanceDate": "",
"expirationDate": "",
"credentialSubject": {
"id": "did:student:ebfeb1f712ebc6f1c276e12ec21",
"degree": {
"name": "Mestrado em Cibersegurança"
}
}
}
Save the content to credential.json
. You can customize the content to add your identity, name, etc… Take in consideration that you must follow the schema.
You need to set the issuanceDate
and the expirationDate
.
The next step is to have an issuer able to sign the credential. It must know the format of the objects that it is signing, and it must have a signing key. A full basic issuer would be:
import * as DidKey from '@digitalbazaar/did-method-key';
import * as DidWeb from '@digitalbazaar/did-method-web';
import * as EcdsaMultikey from '@digitalbazaar/ecdsa-multikey';
import {
cryptosuite as ecdsaRdfc2019Cryptosuite
} from '@digitalbazaar/ecdsa-rdfc-2019-cryptosuite';
import * as vc from '@digitalbazaar/vc';
import {CachedResolver} from '@digitalbazaar/did-io';
import {DataIntegrityProof} from '@digitalbazaar/data-integrity';
import {securityLoader} from '@digitalbazaar/security-document-loader';
import {contexts as diContexts} from '@digitalbazaar/data-integrity-context';
import fs from 'fs';
// setup documentLoader with security contexts
const loader = securityLoader();
loader.addDocuments({documents: diContexts});
//Load the JSON-LD context for the credential
loader.addStatic(
"https://www.w3.org/ns/odrl.jsonld",
await fetch("https://www.w3.org/ns/odrl.jsonld").then(res => res.json())
);
loader.addStatic(
"https://www.w3.org/2018/credentials/examples/v1",
await fetch("https://www.w3.org/2018/credentials/examples/v1").then(res => res.json())
)
const resolver = new CachedResolver();
const didKeyDriverMultikey = DidKey.driver();
const didWebDriver = DidWeb.driver();
didKeyDriverMultikey.use({
multibaseMultikeyHeader: 'zDna',
fromMultibase: EcdsaMultikey.from
});
didWebDriver.use({
multibaseMultikeyHeader: 'zDna',
fromMultibase: EcdsaMultikey.from
});
// P-384
didWebDriver.use({
multibaseMultikeyHeader: 'z82L',
fromMultibase: EcdsaMultikey.from
});
resolver.use(didKeyDriverMultikey);
resolver.use(didWebDriver);
loader.setDidResolver(resolver);
const documentLoader = loader.build();
async function main({credential, documentLoader}) {
// generate example keypair for VC signer
const vcEcdsaKeyPair = await EcdsaMultikey.generate({curve: 'P-256'});
const {
didDocument: vcDidDocument/*, keyPairs, methodFor*/
} = await didKeyDriverMultikey.fromKeyPair({
verificationKeyPair: vcEcdsaKeyPair
});
vcEcdsaKeyPair.id = vcDidDocument.assertionMethod[0];
vcEcdsaKeyPair.controller = vcDidDocument.id;
// ensure issuer matches key controller
credential.issuer = vcEcdsaKeyPair.controller;
// setup ecdsa-rdfc-2019 signing suite
const vcSigningSuite = new DataIntegrityProof({
signer: vcEcdsaKeyPair.signer(),
// date: '2023-01-01T01:01:01Z',
cryptosuite: ecdsaRdfc2019Cryptosuite
});
// sign credential
const verifiableCredential = await vc.issue({
credential,
suite: vcSigningSuite,
documentLoader
});
console.log('SIGNED CREDENTIAL:');
console.log(verifiableCredential);
fs.writeFileSync('vc.json', JSON.stringify(verifiableCredential, null, 2));
}
console.log("Loading credential.json");
var credential = fs.readFileSync('credential.json', 'utf8');
credential = JSON.parse(credential);
//Adjust the expiration date to one year from now
credential["issuanceDate"] = new Date().toISOString();
credential["expirationDate"] = new Date(new Date().setFullYear(new Date().getFullYear() + 1)).toISOString();
if (credential) {
console.log("Signing Credential: "+credential);
await main({credential, documentLoader});
}else{
console.log("Credential not found");
}
Save the previous code as issuer.js
and update the date
of the signing suite.
As you can notice, the issuer
knowns which documents it will sign, as the document loader will restrict the schema of the documents to a restricted set.
This makes sense, because and issuer will only sign documents of its authority, and doesn’t constitute a general entity signing any type of credential.
You can run it with:
node issuer.js
The result is a verifiable credential which is written to the console. Save it to a file named vc.json
.
Notice that there is an added element to the credential, named proof
, with is the signature created by the issuer
.
"proof": {
"type": "DataIntegrityProof",
"created": "2025-02-28T14:38:32Z",
"verificationMethod": "did:key:zDnaeRXcigSKT4nx4mRaGLTaRY7aj7vbCH7DmWCdS4m9575KF#zDnaeRXcigSKT4nx4mRaGLTaRY7aj7vbCH7DmWCdS4m9575KF",
"cryptosuite": "ecdsa-rdfc-2019",
"proofPurpose": "assertionMethod",
"proofValue": "zRyXm45NRGeAWyxQj1YNbBYhZNgPpXkcUEaYraGxJrdjhBAjKQXwcGoXLSQnMD5yXrXVmd2KccYka14aXnBAn1aw"
}
You can create a holder
by using the preamble of the issuer
and adding a new main
method.
// TODO: Add Initial Imports like in issuer.
//Create a VC PRESENTATION by the HOLDER to a VERIFIER
async function main({verifiableCredential, documentLoader}) {
// setup example for did:web
const VP_DID = 'did:web:example.org:issuer:123';
const VP_DID_URL = 'https://example.org/issuer/123'; // The target DID URL
// generate example keypair for VP signer (The HOLDER)
const vpEcdsaKeyPair = await EcdsaMultikey.generate({curve: 'P-384'});
fs.writeFileSync('vpEcdsaKeyPair.json', JSON.stringify(await vpEcdsaKeyPair.export({publicKey: true})));
const {
didDocument: vpDidDocument, methodFor: vpMethodFor
} = await didWebDriver.fromKeyPair({
url: VP_DID_URL,
verificationKeyPair: vpEcdsaKeyPair
});
const didWebKey = vpMethodFor({purpose: 'authentication'});
vpEcdsaKeyPair.id = didWebKey.id;
vpEcdsaKeyPair.controller = vpDidDocument.id;
const vpSigningSuite = new DataIntegrityProof({
signer: vpEcdsaKeyPair.signer(),
cryptosuite: ecdsaRdfc2019Cryptosuite
});
loader.addStatic(VP_DID, vpDidDocument);
//loader.addStatic(VP_DID_DOC_URL, vpDidDocument);
// using a static verification method result
// In the real world, this would be fetched from the DID Document
const vpDidVm = structuredClone(vpDidDocument.verificationMethod[0]);
vpDidVm['@context'] = 'https://w3id.org/security/multikey/v1';
loader.addStatic(vpDidVm.id, vpDidVm);
// presentation holder
const holder = 'did:web:ua.pt:holder:student:456';
// presentation challenge - required for authentication proof purpose
const challenge = 'abc123';
// presentation domain - optional in this use case
const domain = 'https://example.com/';
const presentation = await vc.createPresentation({
verifiableCredential,
holder
});
console.log('PRESENTATION:');
console.log(presentation);
// sign presentation
// note this adds the proof to the input presentation
const vp = await vc.signPresentation({
presentation,
suite: vpSigningSuite,
challenge,
domain,
documentLoader
});
console.log('SIGNED PRESENTATION:');
console.log(vp);
}
console.log("Loading vc.json");
var credential = JSON.parse(fs.readFileSync('vc.json', 'utf8'));
if (credential) {
console.log("Creating Signed Presentation");
await main({credential, documentLoader});
}else{
console.log("VC not found");
}
This result should be a Verifiable Presentation of the Credential, that can be verified by the verifier
. Write the result to vp.json
.
You can check that an additional file is created (vpEcdsaKeyPair.json
). The purpose of the file is to pass the Public Key of the Holder to the
Verifier. The loader.addStatic(VP_DID, vpDidDocument);
and others related create a local handler to authenticate the host. Otherwise, a request
would be made to https://example.org/issuer/123/did.json
as an attempt to fetch the public key.
The code for the verifier
is also similar:
// TODO: Add additional imports
//Verifies a VC PRESENTATION
async function main({verifyablePresentation, documentLoader, challenge}) {
// setup example for did:web
const VP_DID = 'did:web:example.org:issuer:123';
const VP_DID_URL = 'https://example.org/issuer/123'; // The target DID URL
const vpEcdsaKeyPair = await EcdsaMultikey.from(JSON.parse(fs.readFileSync('vpEcdsaKeyPair.json', 'utf8')));
const {
didDocument: vpDidDocument, methodFor: vpMethodFor
} = await didWebDriver.fromKeyPair({
url: VP_DID_URL,
verificationKeyPair: vpEcdsaKeyPair
});
const didWebKey = vpMethodFor({purpose: 'authentication'});
vpEcdsaKeyPair.id = didWebKey.id;
vpEcdsaKeyPair.controller = vpDidDocument.id;
const vpSigningSuite = new DataIntegrityProof({
cryptosuite: ecdsaRdfc2019Cryptosuite
});
// setup VP ecdsa-rdfc-2019 verifying suite
const vpVerifyingSuite = new DataIntegrityProof({
cryptosuite: ecdsaRdfc2019Cryptosuite
});
loader.addStatic(VP_DID, vpDidDocument);
//loader.addStatic(VP_DID_DOC_URL, vpDidDocument);
// using a static verification method result
// In the real world, this would be fetched from the DID Document
const vpDidVm = structuredClone(vpDidDocument.verificationMethod[0]);
vpDidVm['@context'] = 'https://w3id.org/security/multikey/v1';
loader.addStatic(vpDidVm.id, vpDidVm);
// verify signed presentation
const verifyPresentationResult = await vc.verify({
presentation: verifyablePresentation,
challenge,
suite: vpVerifyingSuite,
documentLoader
});
console.log('VERIFY PRESENTATION RESULT:');
console.log(verifyPresentationResult);
if(verifyPresentationResult.error) {
console.log('VP ERROR STACK:',
verifyPresentationResult.error.errors[0].stack);
}
}
console.log("Loading vp.json");
var verifyablePresentation = JSON.parse(fs.readFileSync('vp.json', 'utf8'));
if (verifyablePresentation) {
console.log("Verifying Verifiable Presentation");
var challenge = "abc123";
await main({verifyablePresentation, documentLoader, challenge});
}else{
console.log("VP not found");
}
The result should be a validated Credential.
The holder
could also disclose only a set of credentials, but this would require additional steps to ensure that the mechanism can operate.
Please check the example at: https://github.com/digitalbazaar/vc?tab=readme-ov-file#deriving-a-selective-disclosure-verifiable-credential
Setup
This lab requires that you obtain and install noknow
python package available at: https://github.com/jpbarraca/noknow-python
It will include both the noknow
system, developed by https://github.com/GoodiesHQ and a client and server developed for this lab.
Obtain the repository, install the requirements and install the package, with the following commands:
sudo apt install python3-virtualenv
git clone https://github.com/jpbarraca/noknow-python.git
cd noknow-python
virtualenv venv
bash
source venv/bin/activate
pip install -r requirements
python3 setup.py install
cd examples
Zero Knowledge Proofs
zero-knowledge proof or zero-knowledge protocol is a method by which one party (the prover) can prove to another party (the verifier) that a given statement is true, while avoiding conveying to the verifier any information beyond the mere fact of the statement’s truth. The intuition underlying zero-knowledge proofs is that it is trivial to prove the possession of certain information by simply revealing it
- the challenge is to prove this possession without revealing the information, or any aspect of it whatsoever.
In light of the fact that one should be able to generate a proof of some statement only when in possession of certain secret information connected to the statement, the verifier, even after having become convinced of the statement’s truth, should nonetheless remain unable to prove the statement to third parties. 1
An important protocol in the scope of identities is the Schnorr’s identification protocol, which consists of a public-key based challenge-response identification protocol. In the scope of ZKP, the protocol can operate both in interactive and non-interactive modes, making it very versatile.
The Verifier
In our simplified scenario we will use a Flask webserver that acts as a verifier. Therefore, it will validate the identity of
provers (users) that wish to access the resources.
For practical purposes, we will also support a register
endpoint which allows users to register their identity.
This code is available in the server.py
file below. It is a simple Flask webserver with the following properties:
- Supports
register
andlogin
methods. - The
register
method allows clients to register their keys, and corresponds to an initial provisioning. - During the
register
phase, the client will set a password which can be later used for authentication. While the password is used in this phase, the key is not disclosed to the server. - The
login
method has two actions: sending a token, which acts as a challenge. - The client can take the token and transform it using the password. It proves to know the password without disclosing it.
- The server also signs their tokens to prevent forgery, and the signing key is unique per client
from flask import Flask
from flask import request
from noknow.core import ZK, ZKSignature, ZKParameters, ZKData, ZKProof
import os
import json
app = Flask(__name__)
clients = {}
@app.route("/")
def index():
return "Howdy"
@app.route("/register", methods=["POST"])
def register():
clientid = request.json.get('clientid', None)
sig = request.json.get('sig', None)
if sig is None or clientid is None:
return "Invalid request"
if clientid in clients:
return "Client already registered"
client_signature = ZKSignature.from_json(sig)
client_zk = ZK(client_signature.params)
clients[clientid] = {'zk': client_zk, 'sig': client_signature}
return "Client registered"
@app.route("/login", methods=["POST"])
def login():
clientid = request.json.get('clientid', None)
proof = request.json.get('proof', None)
# First request, send token to client for proof
if clientid in clients and proof is None:
# Generate a server password and ZK object for the server
server_password = os.urandom(32)
# Set up server component, generating a new ZK object
server_zk = ZK.new(curve_name="secp384r1", hash_alg="sha3_512")
# store the server zk and password for later use
clients[clientid]['server_zk'] = server_zk
clients[clientid]['server_password'] = server_password
# get the client zk
client_zk = clients[clientid]['zk']
# Create a signed token and send to the client
token = server_zk.sign(server_password, client_zk.token())
return token.to_json()
if clientid in clients and proof is not None:
# Get the token from the client
zkproof = ZKData.from_json(proof)
token = ZKData.from_json(zkproof.data)
# Get the client zk and server password
server_zk = clients[clientid]['server_zk']
server_signature = server_zk.create_signature(clients[clientid]['server_password'])
client_zk = clients[clientid]['zk']
# check if the server signature is valid
if not server_zk.verify(token, server_signature):
print("Invalid server auth: ")
return "Authentication failure"
else:
# Verify the proof from the client
# uses the client proof and signature to verify the token
result = client_zk.verify(zkproof, clients[clientid]['sig'], data=token)
return "Authentication success" if result else "Authentication failure"
# Invalid request
return "Invalid request"
if __name__ == "__main__":
app.run(debug=True)
The Prover
The prover corresponds to the client, and will execute two actions: register a key and then create a proof that asserts the identity. The proof is only valid for a single client, at a single server. The proof doesn’t include the key but the creation of the proof requires knowing the key. The server can verify that the proof is correct even without knowing the key.
The code for this client is present in the client.py
directory and below.
The client first registers itself into the server, storing its signature in the server. The signature depends on the password
and on a private key which is kept.
Then the client will contact the server and request a token
. This token can be used to build a proof
together with the password
, and is sent to the server, which replies with the result.
The client will ask for a password
twice, and you can provide a different password
to see what happens (it will fail to prove the identity as expected).
from getpass import getpass
import requests
from noknow.core import ZK, ZKSignature, ZKParameters, ZKData, ZKProof
import uuid
print("Registering client")
client_zk = ZK.new(curve_name="secp256k1", hash_alg="sha3_256")
clientid = uuid.uuid4().hex
password = getpass("Enter Password: ")
# Create signature and send to server
signature = client_zk.create_signature(password)
print("Client: signature ", signature.to_json())
r = requests.post("http://localhost:5000/register", json={"clientid": clientid, "sig": signature.to_json()})
print(r.status_code)
input("Press Enter to continue...")
print("Logging in")
password = getpass("Enter Password: ")
print("Getting token...")
r = requests.post("http://localhost:5000/login", json={"clientid": clientid})
print("Token: ", r.json())
token = r.text
proof = client_zk.sign(password, token).to_json()
print("Proof: ", proof)
print("Sending proof...")
r = requests.post("http://localhost:5000/login", json={"clientid": clientid, "proof": proof})
print(r.text)
TASKS:
- Obtain the code install the dependencies
- Execute the server in one terminal and the client on another terminal
- Provide the same, or different passwords
- Analyze the messages exchanged
References
- https://github.com/jpbarraca/noknow-python
- Ioannis Chatzigiannakis, Apostolos Pyrgelis, Paul G. Spirakis, Yannis C. Stamatiou, Elliptic Curve Based Zero Knowledge Proofs and Their Applicability on Resource Constrained Devices, https://arxiv.org/pdf/1107.1626