1. Introduction
This document provides information to the developers who want to integrate the Nixar api to their development environment using the C Interface wrappers. The C Interface wrappers are written for Java, Swift, Python.
2. Who Should Read This Document
If you study on SSID and Distributed ID blockchains, have interest in creating blockchain agnostic SSID solutions, you already know about SSID and you are interested in developing business applications using the nixar-core library, this document is for you.
3. Who Should NOT Read This Document
Well it wouldn’t be so polite to ask people not to read this document; however, if you have one of the following aims: i) you seek for theoretical information about the SSID, ii) you want to learn more about blockchain, ii) you need to elaborate and research on the business use cases of SSID, then this document is NOT for you.
4. A tiny intro to SSID
The most fundamental use case of the SSID is given below. The main actors of the use case are Issuer, * Holder/Prover* and Verifier.
In most plain words, an Issuer creates credentials (i.e. identity information for an entity) for the Holder who in return saves these credentials in her wallet.
In the next step the Holder who holds her credentials becomes a , and proves (as a Prover) these credentials to a Verifier (e.g. a Service Provider)
While in the above scenarios the roles Holder and Prover are used interchangeably, in fact an entity in the SSID context might have multiple roles simultaneously (e.g. an issuer might become a prover in another business scenario).
We will use the term Prover for both Holder and Prover roles in the rest of the document# |
An entity in SSID is context is represented with an Agent, which is mostly a piece of software that can act on behalf of the entity. For example, Lucy 's digital agent, on her favorite android device, might create proof from her digital wallet and send it to her schools agent (verifier) in order to submit her homework to the homework submission system.
5. Prerequisites
5.1. Downloading the binaries
The nixar-core library can be built for all popular platforms. The list of the existing precompiled binaries and where to find them is given below
Platform/Resource | Link |
---|---|
Api Docs (Kickstart (this document) and c interfaces) |
|
OsX |
|
IOS XCFramework |
|
IOS Swift Wrapper |
|
Linux |
|
Android |
|
Win64 |
https://proje.bag.org.tr/nixar_shared/nixar_api_win_x86_64 * |
The binaries need to be discoverable to your development environment in order for your application to work with the nixar-core library. For example on OsX , the binaries libnixar_core.dylib, libcrypto.1.1.dylib, libssl.1.1.dylib, libzmq.5.dylib. Please not that you can also use compatible versions of the dependent libraries (i.e. OpenSSL, LibZmq and LibSodium) that preinstalled on your environment. See following sections for more details.
5.2. Android: Notes for users
5.2.1. How to include in Android Projects
Please clone or download the java library and install using maven and JDK8.
> mvn -DskipTests=true clean install
Then to include the library in your gradle project, please don’t forget to add mavenLocal()
to your build.gradle
file.
allprojects {
repositories {
mavenLocal()
//others...
google()
jcenter()
}
}
There are two places that mavenLocal() can be written,
one of them is buildscript and the other is allprojects . Please
ignore the one for buildscript and write mavenLocal to allprojects
as shown above.
|
5.2.2. How To Include Native Libs
In order to include libnixar_core.so
in your project
clone or download this repository. Copy files to your Apk’s source path
as follows:
app/
build
build.gradle
proguard-rules.pro
src/
main/
jniLibs/
java/
...
armv64-v8a/
libc++_shared.so
libjnidispatch.so
libnixar_core.so
armeabi-v7a/
libc++_shared.so
libjnidispatch.so
libnixar_core.so
5.2.3. Enabling nixar-core logs
By default nixar-core will be able to log INFO
, WARN
and ERROR
.
In order to enable DEBUG
and VERBOSE
please set this property via
adb
as below.
#For verbose logging
adb shell setprop log.tag.libnixar-core VERBOSE
#For verbose logging
adb shell setprop log.tag.libnixar-core DEBUG
5.2.4. Choosing Nixar-Home Directory
In the most recent android versions, writing to certain locations has been highly restricted. Therefore, developers might get file permission errors.
By default, Nixar will try to write its content to EXTERNAL_STORAGE
. But
it is a bit confusing and complicated to allow the same access on all devices.
Due to this fact, the most recent version of Nixar includes a new api that allows the end users to set the Nixar home directory.
The sample code snippet below shows how to use that:
NixarAgentFactory.initLogging(Level.TRACE);
//set nixar home path manually to data dir.
NixarAgentFactory.setNixarHomePath(getApplicationContext().getDataDir().getAbsolutePath());
NixarAgent nixarAgent = NixarAgent.createAgent(...
Please note that this call must take place before any agent manipulation.
5.2.6. Enable Cleartext traffix to http
networks
-
In order to connect to bcovrin network and download the genesis file automatically from http://test.bcovrin.vonx.io/genesis, you need to enable cleartext traffic:
AndroidManifest.xml<application ... android:usesCleartextTraffic="true" ...>
Otherwise you can manually download the genesis file and feed to the app.
5.3. Building from source (Optional)
The nixar-core library needs Rust, libzmq, OpenSSL and LibSodium if you want to build from source.
5.3.1. Rust and Cargo
Please install Rust [Rust](https://www.rust-lang.org/tools/install) and [Cargo](https://doc.rust-lang.org/cargo/getting-started/installation.html) in order to be able to build the codes from the source. You need to access the repository addresses of the nixar-core source code as well as its dependent
5.3.2. OpenSSL
The library needs [OpenSSL](https://www.openssl.org/) V1.1 which is already included in the repository (libssl.1.1.dylib
and libcrypto.1.1.dylib). Feel free to use it or install it your system via brew
5.3.3. ZeroMQ
[ZeroMQ](https://zeromq.org/) is used for communicating with the Hyperledger Indy networks. We have included a precompiled dylib (libzmq.5.dylib), but you can also obtain it from outside
5.3.4. Libsodium
Please follow Libsodium installation instructions are provided [here](https://doc.libsodium.org/installation). Nixar-core is compatible with [libsodium 1.0.18 -RELEASE](https://github.com/jedisct1/libsodium/tree/1.0.18-RELEASE) or newer.
6. Library Samples
In this chapter we provide samples for the agent creation,
connection manipulation and generic anoncreds
use cases (e.g create agent
, issue
, store
and verify
)
You can see the full source codes for the wrappers and example codes from the below links: |
Language | Address |
---|---|
Rust |
TBD |
Java |
|
Python |
|
Swift |
6.1. Create Agent
The main actor of the nixar-core library is an agent object. In order to start using the library you need to create an agent object.
AgentInitParams agentInitParams = new AgentInitParams("java_issuer55", "ENDORSER", "000000000000000000000TUBISSUER10", genesisPath);
try {
issuer = NixarAgent.createAgent(agentInitParams, () -> "123456".toCharArray());
LOGGER.info("Issuer agent created");
} catch (AgentNotRegisteredException ex) {
String did = ex.did;
String verkey = ex.verkey;
registerAgentToLedger(agentInitParams, did, verkey);
issuer = NixarAgent.openAgent(agentInitParams.getAlias(), agentInitParams.getGenesisPath(), () -> "123456".toCharArray());
LOGGER.info("Issuer agent created");
}
issuer = NixarApi('genesis-local.txn')
try:
issuer.create_agent('python_issuer', 'ENDORSER', '12345', '001090101901001100000TUBISSUER90')
except NixarError as err:
logging.warning(err)
# register agent to ledger
if 'AgentNotRegistered' == err.code:
registration_info = json.loads(err.message)
registration_info['alias'] = 'python_issuer'
registration_info['role'] = 'ENDORSER'
logging.warning(json.dumps(registration_info))
trustee.register_agent_to_ledger(json.dumps(registration_info))
# issuer.open_agent('python_issuer', '12345')
logging.info('Issuer agent created')
In the above code, we first create and AgentInitParams
object/json string. We have to provide an agent_name
that
will be the name of our agent. We shall then refer to our agent (e.g. open it) with the same name. A role
is
optional for an agent. Mostly mobile device users (i.e. Prover) will not need a role; because one wouldn’t need
a role
in order to perform READ
operations on the ledger.
However, if you are an issuer your role has to at least be an ENDORSER
in order to add schemas and credential
definitions to the ledger as well as revoking credentials and so on. In other words, to perform WRITE
operations on
the ledger you need to have your NYM
registered to the ledger and you need
a role. In that case another agent with e TRUSTEE
or ENDORSER
role must have added your agent to the ledger.
Otherwise, you can skip the role and set it to None/null
.
6.2. Manipulating Connections Between Agents
In order for agents to communicate via a did channel, their connection information must previously have been established. This scenario is called creating a connection between agents. The steps are given below.
In below samples we assume that we want to connect two agents, namely Agent1 and Agent2
6.2.1. Agent 2 Create Connection Invitation
Assuming that your agent is agent1
and you want to connecto to agent2
, you need an invitation that has been created by agent2
. For that agent2
should create an invitation.
There are two types of invitations, i) public invitation, ii) private invitation. In the first case, the invitation contains the information about the did of the agent. In the second case, the invitation contains additinally the endpoint (mostly HTTP) of the agent so that the other agent can discover its location and connects to it. In the case of a public invitation, the connecting agent needs to resolve the endpoint and connection keys from the ledger.
Below is the sample code for creating a connection invitation or getting the public invitation. The owner of the agent is supposed to share this information with the clients, via means like publishing on the web, QR codes and so on.
ConnectionTypes.ConnectionInvitation publicInvitation = issuer.getPublicInvitation();
inv_seed = "00000000000000000000000000AGENTA"
conn_inv = issuer.connection_create_local_invitation("agent A", "http://localhost:8080", inv_seed)
logging.info("Agent A create connection invitation, conn_inv" +
json.dumps(conn_inv))
6.2.2. Agent-1 Create Connection Request
Having received the invitation from agent2, our agent (agent1) needs to process this invitation and create a connection request from it. As stated previously, if the invitation is public, the endpoint and recipient keys of the agent2 are resolved from the ledger by nixar-core automatically.
//If the seed is provided, it has to be 16 bytes, ASCII characters. Please don't use accented non ascii chars
String seed = "000000000000000000MYCONNECTION01";
final ConnectionTypes.EncryptedConnectionRequest encryptedConnectionRequest = prover.createConnectionRequest(publicInvitation, seed);
# Agent B receive invitation and create connection request
req_seed = "00000000000000000000000000AGENTB"
conn_req = prover.connection_create_request(conn_inv, req_seed)
logging.info("Agent B create connection request, conn_req" +
json.dumps(conn_req))
When an invitation is processed for creating a connection request,
a seed
value my optionally be provided. While we discourage its usage due to
privacy, when you provide it, the pairwise did for this connection will
be generated from this value.
The resulting request_message
is the encrypted connection request message created by agent1. The encryption is performed using did-comm and only agent2 can open the message.
6.2.3. Agent-2 Accept Connection Request
Agent 2 receives the encrypted connection request via means decided by the business layer (e.g. HTTP, e-mail, AS4 …). Here we assume that agent2 has received the message; subsequently processes it, accepts the connection request and creates an encrypted connection response.
final ConnectionTypes.EncryptedConnectionResponse encryptedConnectionResponse = issuer.acceptConnectionRequest(encryptedConnectionRequest, proverAlias);
# Agent A receive connection request and create connection response
conn_res = issuer.connection_accept_request(conn_req, None)
logging.info("Agent A create connection response, conn_res" +
json.dumps(conn_res))
The second parameter is an alias that agent2 can optionally set to the underlying connection object.
The response_message created by agent2 is also encrypted and can only be opened by agent1.
6.2.4. Agent-1 Accept Connection Response
As the last step of connection creation, agent1 needs to process the response_message created by agent2 in the previous step. Below is the code.
prover.acceptConnectionResponse(encryptedConnectionResponse);
# Agent B receive connection response and finalize connection creation
prover.connection_accept_response(conn_res)
logging.info("Connection created between Agent A and Agent B")
At the end of this sequence of messages agent1
and agent2
have a common pairwise did and are ready to communicate further via this connection using DidComm protocol
6.2.5. Encrypting - Decrypting Messages Between Agents
After the creation of connection between two agents, the rest of the communication for sensitive messages needs to be performed via a secure channel based on DidCOMM. A sample code for encrypting and decrypting a credential is given below:
//send the signature and the data to issuer (via didcomm)
Map<String, String> packedSignature = new HashMap<>();
packedSignature.put("signedData", Base64.getEncoder().encodeToString(signedData));
packedSignature.put("signature", base64EncodedSignature);
String messageString = JsonHelper.toJson(packedSignature);
encryptedMessage = prover.encryptMessage(issuerConnection.getMyDid(), issuerConnection.getTheirDid(),
NixarNativeInterface.NixarMessageType.MessageTypeSimple, messageString);
packed_signature = {}
packed_signature['signedData'] = base64.b64encode(signed_data).decode('utf-8')
packed_signature['signature'] = base64_encoded_signature
logging.info("Packed signature {}".format(packed_signature))
encrypted_message = prover.connection_encrypt(from_did,
their_did,
NixarMessageType.MESSAGE_TYPE_SIMPLE,
packed_signature)
logging.info("Prover Agent create message of signature info, encrypted_message: {}".format(encrypted_message))
//send the signature and the data to issuer (via didcomm)
var packedSignature : [String: String] = [:]
packedSignature["signedData"] = Data(signedData).base64EncodedString()
packedSignature["signature"] = base64EncodedSignature;
let messageString = try JSONHelper.encode(packedSignature);
let encryptedMessage = try agentb.encryptMessage(fromDid: connection.myDid, toDid: connection.theirDid,
messageType: SwiftNixarMessageType.MessageTypeSimple, content: messageString);
6.3. Sign and Verify
Nixar API provides means to sign and verify data between agents using the did keypairs of the previously created connections.
In below example, the prover signs some data using the did
of a previously created connection.
-
Prover sign data:
JavaConnectionTypes.Connection issuerConnection = findConnection(prover, publicInvitation.getLabel()); final byte[] signedData = "Data To Be Signed".getBytes(StandardCharsets.UTF_8); final String base64EncodedSignature = prover.signWithDid(issuerConnection.getMyDid(), signedData);
Python# Sign and verify with did # Prover get connections agent_b_conns = prover.connection_get_connections() from_did = agent_b_conns[0]["my_did"] their_did = agent_b_conns[0]["their_did"] data = "Data To Be Signed" signed_data = bytes(data, 'utf-8') logging.info("Data To Be Signed: {}".format(base64.b64encode(signed_data))); base64_encoded_signature = prover.sign_with_did(from_did, signed_data) logging.info("Signature: {}".format(base64_encoded_signature));
Swiftlet signedData = "Some Data To Sign".data(using: .utf8)!.bytes print("Data To Sign \(signedData)") let base64EncodedSignature = try agentb.signWithDid(did: connection.myDid, data: signedData) print("Signature \(base64EncodedSignature)")
-
The prover sends this data to the other agent (e.g. issuer) via did-comm.
Java//send the signature and the data to issuer (via didcomm) Map<String, String> packedSignature = new HashMap<>(); packedSignature.put("signedData", Base64.getEncoder().encodeToString(signedData)); packedSignature.put("signature", base64EncodedSignature); String messageString = JsonHelper.toJson(packedSignature); encryptedMessage = prover.encryptMessage(issuerConnection.getMyDid(), issuerConnection.getTheirDid(), NixarNativeInterface.NixarMessageType.MessageTypeSimple, messageString);
Pythonpacked_signature = {} packed_signature['signedData'] = base64.b64encode(signed_data).decode('utf-8') packed_signature['signature'] = base64_encoded_signature logging.info("Packed signature {}".format(packed_signature)) encrypted_message = prover.connection_encrypt(from_did, their_did, NixarMessageType.MESSAGE_TYPE_SIMPLE, packed_signature) logging.info("Prover Agent create message of signature info, encrypted_message: {}".format(encrypted_message))
Swift//send the signature and the data to issuer (via didcomm) var packedSignature : [String: String] = [:] packedSignature["signedData"] = Data(signedData).base64EncodedString() packedSignature["signature"] = base64EncodedSignature; let messageString = try JSONHelper.encode(packedSignature); let encryptedMessage = try agentb.encryptMessage(fromDid: connection.myDid, toDid: connection.theirDid, messageType: SwiftNixarMessageType.MessageTypeSimple, content: messageString);
-
After receiving, the decrypts the message
Javafinal String messageJson = JsonHelper.toJson(encryptedMessage); final MessageTypes.NixarDecryptedMessage decryptMessage = issuer.decryptMessage(messageJson); Map<String, String> packedSignature = JsonHelper.readValue(decryptMessage.getContent(), HashMap.class); String sSignedData = packedSignature.get("signedData"); String base64EncodedSignature = packedSignature.get("signature"); byte[] signedData = Base64.getDecoder().decode(sSignedData); byte[] signature = Base64.getDecoder().decode(base64EncodedSignature);
Pythondecrypted_message = issuer.connection_decrypt(encrypted_message) logging.info("Issuer Agent decrypt message, decrypted_message: {}".format(decrypted_message)) unpacked_signature = json.loads(decrypted_message['content']) logging.info("Unpacked signature info {}".format(unpacked_signature))
Swiftlet decryptedMessage = try agenta.decryptMessage(message: try JSONHelper.encode(encryptedMessage)); let pPackedSignature = try JSONHelper.decode([String: String].self, from: decryptedMessage.content.data(using: .utf8)!); guard let pSignedDataBase64 = pPackedSignature["signedData"] else {return} guard let pSignatureBase64 = pPackedSignature["signature"] else {return} guard let pSignedData = Data(base64Encoded: pSignedDataBase64)?.bytes else {return} guard let pSignature = Data(base64Encoded: pSignatureBase64)?.bytes else {return}
-
Finally issuer verifies the signature of the sender.
Java//1. verify with their did from connection. final boolean b1 = issuer.verifyWithTheirDid(decryptMessage.getSenderDid(), signedData, signature); System.out.println("Verification Result " + b1); //2. OR verify with their PUBLIC Key final ConnectionTypes.DidDoc oTheirDidAsDidDoc = issuer.getTheirDidAsDidDoc(decryptMessage.getSenderDid()); final boolean b2 = issuer.verifyWithDidPublicKey(oTheirDidAsDidDoc.getPublicKeys().get(0).getBase58EncodedPublicKey(), signedData, signature); System.out.println("Verification Result " + b2);
Python# 1. verify with their did from connection. is_verified = issuer.verify_signature_with_their_did(decrypted_message['senderDid'], unpacked_signature['signedData'], unpacked_signature['signature']) logging.info("Verification Result with their did {}".format(is_verified)) # 2. OR verify with their PUBLIC Key did_doc = issuer.get_their_did_as_did_doc(decrypted_message['senderDid']) logging.info(did_doc) logging.info(did_doc['publicKeys'][0]) is_verified = prover.verify_signature_with_did_public_key(did_doc['publicKeys'][0]['value'], unpacked_signature['signedData'], unpacked_signature['signature']) logging.info("Verification Result {}".format(is_verified))
Swift//1. verify with their did from connection. let b1 = try agenta.verifyWithTheirDid(theirDid:decryptedMessage.senderDid, data: pSignedData, signature: pSignature); print("Verification Result \(b1)"); //2. OR verify with their PUBLIC Key let oTheirDidAsDidDoc = try agenta.getTheirDidAsDidDoc(theirDid: decryptedMessage.senderDid); let b2 = try agenta.verifyWithDidPublicKey(didPublicKeyBase58: (oTheirDidAsDidDoc.publicKeys?[0].base58EncodedPublicKey)!, data: pSignedData, signature: pSignature); print("Verification Result \(b2)");
As you can see there are two ways of verifying a signature:
-
Using the did of the other side
-
Using the public key of the did of the other side
-
If the connection is in your wallet, using the fist method is easier. But if the signature will be validated later in other places as well, you can export the public key and save it somewhere to verify the signature explicitly.
6.4. Issue-Store-Verify-Revoke Credentials
After communication creation between different agents, the famous trinity of issue-prove-verify can be achieved. This is the basic and most generic scenario of SSID.
Please refer to [Create Agent](#Create%20Agent) section for agent creation and [Agent Connection](#connect-to-another-agent-agent1-agent2) for connection between agents.
Please also note that the message flow between the issuer
, prover
and verifier
needs to be encrypted using DidComm.
Although the examples below do not contain an encryption example, we will remind about this frequently and give an ecryption-decryption example in the end of the document.
6.4.1. Issuer Creates Schema and Credential Definition
A prerequisite of creating credentials for another entity is to create a schema and a credential definition for the target credential.
A schema defines which attributes will exist in a credential. Its basically a list of attributes with additional schema name and ID.
A credential definition cryptologically defines how an issuer will issue a credential based on the schema.
The issuer creates the schema and the credential definition and writes them to the ledger.
final String schemaName = "javaTestSchema1139";
String schemaId;
try {
SchemaTypes.Schema schema = issuer.getSchema("TbAqayWyFFK6VN7eebBTf1:2:" + schemaName + ":2.0");
schemaId = schema.getId();
} catch (Exception ex) {
schemaId = issuer.createSchema(schemaName, new HashSet<>(Arrays.asList("name", "surname", "age", "gender")), "2.0");
}
LOGGER.info("SchemaId: " + schemaId);
String credDefId;
try {
final List<CredDefTypes.CredentialDefinition> credentialDefinitions = issuer.getCredentialDefinitions(schemaId);
if (credentialDefinitions.size() > 0) {
credDefId = credentialDefinitions.get(credentialDefinitions.size() - 1).getId();
} else {
throw new IllegalStateException("No cred def");
}
} catch (Exception ex) {
credDefId = issuer.createCredentialDefinition(schemaId, true, NixarNativeInterface.CredDefIssuanceType.IssuanceByDefault, 20);
}
LOGGER.info("CredDefId: " + credDefId);
schema_version = '2.0'
schema_id = issuer.issuer_create_schema(schema_name, attribute_set, schema_version)
logging.info('Schema created, schema_id: ' + schema_id)
# if it is not revocable, CredDefIssuanceType.ISSUANCE_ON_DEMAND, max_cred_num will be ignored in nixar
cred_def_id = issuer.issuer_create_credential_definition(schema_id, is_revocable,
CredDefIssuanceType.ISSUANCE_ON_DEMAND, max_cred_num)
logging.info('Credential definition created, cred_def_id: ' + cred_def_id)
6.4.2. Issuer Creates Credential Offer
The second step of credential issuance is to create an offer for a prover. This use case can be triggered by a client (i.e. a prover requests a credential from an issuer) and issuer creates an offer as below.
Random random = new Random();
byte bytes[] = new byte[10];
random.nextBytes(bytes);
BigInteger bIssuerNonce = new BigInteger(1, bytes);
String sIssuerNonce = bIssuerNonce.toString(10);
CredentialTypes.CredentialOffer credOffer = issuer.createCredentialOffer(credDefId, sIssuerNonce);
LOGGER.info("Credential offer created, credOffer: " + JsonHelper.toJson(credOffer));
issuer_nonce = str(random.getrandbits(80))
cred_offer = issuer.issuer_create_credential_offer(cred_def_id, issuer_nonce)
logging.info('Credential offer created, cred_offer: ' + str(cred_offer))
Plase don’t forget to ensure message security as described in [Encrypting - Decrypting Messages Between Agents](#encrypting---decrypting-messages-between-agents)
6.4.3. Prover Creates Credential Request
Having received an offer from the issuer, the prover
needs to create a credential_request
from the offer.
The sample code is below.
CredentialTypes.CredentialRequest credReq = prover.createCredentialRequest(credOffer);
LOGGER.info("Credential request created, credReq: " + JsonHelper.toJson(credReq));
cred_req = prover.prover_create_credential_request(cred_offer)
logging.info('Credential request created, cred_req: ' + str(cred_req))
Prover send credential the credential request and the issuer nonce which is used while creating credential offer to issuer to create credential.
Plase don’t forget to ensure message security as described in [Encrypting - Decrypting Messages Between Agents](#encrypting---decrypting-messages-between-agents)
6.4.4. Issuer Creates Credential
The prover
creates and sends a credential request to the issuer in the previous step.
In this step, the issuer
will process the credential request and create a credential for prover
Map<String, CredentialTypes.AttributeValues> credValues = new HashMap<>();
credValues.put("name", new CredentialTypes.AttributeValues("Mehmet"));
credValues.put("surname", new CredentialTypes.AttributeValues("Öztürk"));
credValues.put("age", new CredentialTypes.AttributeValues("30"));
credValues.put("gender", new CredentialTypes.AttributeValues("male"));
String credValues1 = JsonHelper.toJson(credValues);
CredentialTypes.CredentialInfo issuedCredInfo = issuer.createCredential(credReq, credValues1, sIssuerNonce);
LOGGER.info("Credential created, cred: " + JsonHelper.toJson(issuedCredInfo.getCredential()));
issued_cred_info = issuer.issuer_create_credential(
cred_req, json.dumps(cred_values), issuer_nonce)
cred = issued_cred_info['credential']
cred_rev_id = issued_cred_info['credRevId']
logging.info('Credential created, cred: ' + json.dumps(cred))
Plase don’t forget to ensure message security as described in [Encrypting - Decrypting Messages Between Agents](#encrypting---decrypting-messages-between-agents)
6.4.5. Prover Stores Credential
Having received the new credentials from the issuer
, the prover
agent needs to verify and save those credentials to its wallet.
This is achieved as below:
boolean storeCred = prover.storeCredential(issuedCredInfo.getCredRevId() + "", issuedCredInfo.getCredential());
LOGGER.info("Store credential created, store_cred: " + storeCred);
store_cred = prover.prover_store_credential(None, cred)
logging.info('Store credential created, store_cred: ' + str(store_cred))
Plase don’t forget to ensure message security as described in [Encrypting - Decrypting Messages Between Agents](#encrypting---decrypting-messages-between-agents)
6.4.6. Verifier Creates Presentation Request (Proof Request)
Verifier is supposed to create a presentation request (i.e. e proof request) in order to provide service to an entity (i.e. a prover). A presentation request is a json object that contains information and metadata about what data the verifier needs for this specific verification scenario.
Verifier specify schema_id
, cred_def_id
and rev_reg_id
as restriction for requested attribute.
The following examples show that attribute name is name
and its schema id equals to schema-x
and credential definition id equals to cred-def-x
.
"attr_referent1": {
"name": "name",
"revealed": true,
"restrictions": [
{
"schema_id": "schema-x",
"cred_def_id": "cred-def-x",
}
]
},
"attr_referent1": {
"name": "name",
"revealed": true,
"restrictions": {
"$and":[
{"schema_id": "schema-x"},
{"cred_def_id": "cred-def-x"}
]
}
},
Verifier also specify restriction with or
, in
, like
and not
operation. Restriction Example 3 and Restriction Example 4 show or
operation usage. Restriction Example 5 shows in
operation usage. You can find more complex restriction in Restriction Example 6. You can also see the complete presentation request here.
"attr_referent1": {
"name": "name",
"revealed": true,
"restrictions": {
"$or":[
{"schema_id": "schema-x"},
{"cred_def_id": "cred-def-x"}
]
}
},
"attr_referent1": {
"name": "name",
"revealed": true,
"restrictions": [
{"schema_id": "schema-x"},
{"cred_def_id": "cred-def-x"}
]
},
"attr_referent1": {
"name": "name",
"restrictions":{
"schema_id": {
"$in": [
"schema-x",
"schema-x1"
]
}
}
},
"attr_referent1": {
"name": "name",
"restrictions":{
"$and": [
{
"cred_def_id": {
"$in": [
"rightside1",
"rightside2"
]
}
},
{
"schema_id": {
"$like": "rightside"
}
},
{
"leftside": "rightside"
},
{
"cred_def_id": {
"$neq": "rightside"
}
},
{
"$not": {
"$or": [
{
"schema_id": "someright1"
},
{
"rev_reg_id": "someright2"
}
]
}
}
]
}
},
Below, you can see a sample presentation request for creating a presentation.
PresentationTypes.PresentationRequest presReq = new PresentationTypes.PresentationRequest();
presReq.setName("IdentityPR");
presReq.setVersion("2.0");
presReq.setNonce("88510039");// + (int) (Math.random() * 100000000));
//requested_attributes
Map<String, PresentationTypes.AttributeInfo> reqAttrs = new HashMap<>();
PresentationTypes.AttributeInfo attributeInfo1 = new PresentationTypes.AttributeInfo();
attributeInfo1.setName("name");
reqAttrs.put("attribute1_referent", attributeInfo1);
PresentationTypes.AttributeInfo attributeInfo2 = new PresentationTypes.AttributeInfo();
attributeInfo2.setName("surname");
reqAttrs.put("attribute2_referent", attributeInfo2);
PresentationTypes.AttributeInfo attributeInfo3 = new PresentationTypes.AttributeInfo();
attributeInfo3.setName("gender");
reqAttrs.put("attribute3_referent", attributeInfo3);
PresentationTypes.AttributeInfo attributeInfo4 = new PresentationTypes.AttributeInfo();
attributeInfo4.setName("hobbies");
reqAttrs.put("attribute4_referent", attributeInfo4);
presReq.setRequestedAttributes(reqAttrs);
// requested_predicates
Map<String, PresentationTypes.PredicateInfo> reqPreds = new HashMap<>();
PresentationTypes.PredicateInfo predicateInfo = new PresentationTypes.PredicateInfo();
predicateInfo.setName("age");
predicateInfo.setP_type(PresentationTypes.PredicateTypes.GT);
predicateInfo.setP_value(10);
reqPreds.put("predicate1_referent", predicateInfo);
presReq.setRequestedPredicates(reqPreds);
# Default restriction operator or, the following restriction equal that
# "restrictions": { "$or":
# [
# {"schema_id": schema_id},
# {"cred_def_id": cred_def_id}
# ]}
pres_req = {
"name": "IdentityPR",
"version": "2.0",
"nonce": "92210735",
"requestedAttributes": {
"name_attribute": {
"name": "name",
"restrictions": {"$and": [
{"schema_id": schema_id},
{"cred_def_id": cred_def_id}
]}
},
"attribute2_referent": {
"name": "surname"
},
"attribute3_referent": {
"name": "gender"
},
"self_attested_referent": {
"name": "hobies"
}
},
"requestedPredicates": {
"predicate1_referent": {
"name": "age",
"p_type": ">",
"p_value": 10
}
}
}
The json contains what data the prover needs to prove: (name, surname, gender), optional/additional data (that is not issued but provided by the prover) (hobbies), and predicates (age > 10).
The verifier
will send this request to the prover
.
Plase don’t forget to ensure message security as described in [Encrypting - Decrypting Messages Between Agents](#encrypting---decrypting-messages-between-agents)
6.4.7. Prover Creates Presentation (Proof)
The prover
receives the presentation_request
from the verifier
and creates a presentation (a proof) from it using its existing credentials.
// Prover create presentation
String selfAttestedValues = "{\"attribute4_referent\": \"swim\"}";
PresentationTypes.Presentation pres = prover.createPresentation(presReq, selfAttestedValues);
LOGGER.info("Prover created presentation, pres: " + JsonHelper.toJson(pres));
# Prover create presentation
self_attested_values = {}
self_attested_values["self_attested_referent"] = "swim"
pres = prover.prover_create_presentation(pres_req, self_attested_values)
logging.info('Prover created presentation, pres: ' + json.dumps(pres))
6.4.8. Verifier Verifies Presentation (Proof)
Having received the presentation
from the prover
, the verifier
verifies the presentation
and then decides to give the service.
// verifier verify presentation
boolean vResult = verifier.verifyPresentation(presReq, pres);
LOGGER.info("Verifier verify presentation, result: " + vResult);
assert vResult;
# verifier verify presentation
v_result = verifier.verifier_verify_presentation(pres_req, pres)
logging.info('verfier verify presentation, result: ' + str(v_result))
Plase don’t forget to ensure message security as described in [Encrypting - Decrypting Messages Between Agents](#encrypting---decrypting-messages-between-agents)
6.4.9. Issuer Revoke Credential
The issuer
can revoke a credential that it has issued to a prover
before.
// issuer revoke credential
issuer.revokeCredential("" + issuedCredInfo.getCredRevId(), credDefId);
LOGGER.info("Issuer revoke credential: OK");
# issuer revoke credential
issuer.issuer_revoke_credential(cred_rev_id, cred_def_id)
logging.info('Issuer revoked credential')
6.5. Tail Sharing
If the issuer has created a revokable credential definition, the tail information has to be published so that the provers can access it. The issuer can share the tail information whatever way it wants, that is not in our scope.
Here we provide samples on how to get the tails info from the issuer and how to store this information on the prover.
6.5.1. Issuer Read Tail
final String tail = issuer.getTail("TbAqayWyFFK6VN7eebBTf1:4:TbAqayWyFFK6VN7eebBTf1:3:CL:302423:bilgem:CL_ACCUM:bilgem");
LOGGER.debug("TAIL");
LOGGER.debug(tail);
tail_name_of_rev_reg = cred['rev_reg_id']
tail = issuer.issuer_get_tail(tail_name_of_rev_reg)
6.5.2. Prover Store Tail
prover.storeTail("TmAqayWyFFK6VN7eebBTf1:4:TbAqayWyFFK6VN7eebBTf1:3:CL:302423:bilgem:CL_ACCUM:bilgem",
tail);
# store tail into prover, note: in this example issuer and prover use same location to store tail we have to change name of tail to see system is work
prover.prover_store_tail('P_' + tail_name_of_rev_reg, tail)
return issued_cred_info['credRevId']
return None
def create_proof_and_verify(pres_req):
# Prover create presentation
self_attested_values = {}
self_attested_values["self_attested_referent"] = "swim"
pres = prover.prover_create_presentation(pres_req, self_attested_values)
logging.info('Prover created presentation, pres: ' + json.dumps(pres))
# verifier verify presentation
v_result = verifier.verifier_verify_presentation(pres_req, pres)
logging.info('verfier verify presentation, result: ' + str(v_result))
return v_result
def revoke_cred(cred_rev_id, cred_def_id):
# issuer revoke credential
issuer.issuer_revoke_credential(cred_rev_id, cred_def_id)
logging.info('Issuer revoked credential')
def demo_with_revocable():
schema_name = 'python_schema_revocable.v.0.1'
schema_name = 'python_schema_non_revocable.v.0.1'
attribute_set = ['name', 'surname', 'age', 'gender']
schema_id = create_schema_if_not_exist(schema_name, attribute_set)
cred_def_id = create_credential_definition(schema_id, True)
cred_values = {
"name": {
"raw": "Metin",
"encoded": "332414609774"
},
"surname": {
"raw": "Öztürk",
"encoded": "521660491122"
},
"age": {
"raw": "30",
"encoded": "30"
},
"gender": {
"raw": "male",
"encoded": "1835101285"
}
}
cred_rev_id = create_credential(cred_def_id, cred_values)
cred_def_id = create_credential_definition(schema_id, True)
# create presentation reauest
pres_req = {
"name": "IdentityPR",
"version": "2.0",
"nonce": "92210735",
"requestedAttributes": {
"name_attribute": {
"name": "name",
"restrictions": {"$or": [
{"schema_id": schema_id},
{"cred_def_id": cred_def_id}
]}
},
"attribute2_referent": {
"name": "surname"
},
"attribute3_referent": {
"name": "gender"
},
"self_attested_referent": {
"name": "hobies"
}
},
"requestedPredicates": {
"predicate1_referent": {
"name": "age",
"p_type": ">",
"p_value": 10
}
}
}
v_result = create_proof_and_verify(pres_req)
assert v_result == True
# issuer revoke credential
revoke_cred(cred_rev_id, cred_def_id)
v_result = create_proof_and_verify(pres_req)
assert v_result == False
def demo_with_non_revocable():
schema_name = 'python_schema_non_revocable.v.0.1'
attribute_set = ['name', 'surname', 'age', 'gender']
schema_id = create_schema_if_not_exist(schema_name, attribute_set)
cred_def_id = create_credential_definition(schema_id, False)
cred_values = {
"name": {
"raw": "Metin",
"encoded": "332414609774"
},
"surname": {
"raw": "Öztürk",
"encoded": "521660491122"
},
"age": {
"raw": "30",
"encoded": "30"
},
"gender": {
"raw": "male",
"encoded": "1835101285"
}
}
cred_rev_id = create_credential(cred_def_id, cred_values)
# Default restriction operator or, the following restriction equal that
# "restrictions": { "$or":
# [
# {"schema_id": schema_id},
# {"cred_def_id": cred_def_id}
# ]}
pres_req = {
"name": "IdentityPR",
"version": "2.0",
"nonce": "92210735",
"requestedAttributes": {
"name_attribute": {
"name": "name",
"restrictions": {"$and": [
{"schema_id": schema_id},
{"cred_def_id": cred_def_id}
]}
},
"attribute2_referent": {
"name": "surname"
},
"attribute3_referent": {
"name": "gender"
},
"self_attested_referent": {
"name": "hobies"
}
},
"requestedPredicates": {
"predicate1_referent": {
"name": "age",
"p_type": ">",
"p_value": 10
}
}
}
v_result = create_proof_and_verify(pres_req)
assert v_result == True
def demo_did_comm():
# Agent A create connection invitation
inv_seed = "00000000000000000000000000AGENTA"
conn_inv = issuer.connection_create_local_invitation("agent A", "http://localhost:8080", inv_seed)
logging.info("Agent A create connection invitation, conn_inv" +
json.dumps(conn_inv))
# Agent B receive invitation and create connection request
req_seed = "00000000000000000000000000AGENTB"
conn_req = prover.connection_create_request(conn_inv, req_seed)
logging.info("Agent B create connection request, conn_req" +
json.dumps(conn_req))
# Agent A receive connection request and create connection response
conn_res = issuer.connection_accept_request(conn_req, None)
logging.info("Agent A create connection response, conn_res" +
json.dumps(conn_res))
# Agent B receive connection response and finalize connection creation
prover.connection_accept_response(conn_res)
logging.info("Connection created between Agent A and Agent B")
# Agent A Connections
agent_a_conns = issuer.connection_get_connections()
logging.info("Agent A Connections" + json.dumps(agent_a_conns))
assert len(agent_a_conns) == 1
# Agent B Connections
agent_b_conns = prover.connection_get_connections()
logging.info("Agent B Connections" + json.dumps(agent_b_conns))
assert len(agent_b_conns) == 1
# Agent A Create Simpele Message
from_did = agent_a_conns[0]["my_did"]
their_did = agent_a_conns[0]["their_did"]
message = "hello agent A"
encrypted_message = issuer.connection_encrypt(
from_did, their_did, NixarMessageType.MESSAGE_TYPE_SIMPLE, message)
logging.info("Agent A create message, encrypted_message:" +
json.dumps(encrypted_message))
# Agent B receive message and decrypt
decrypted_message = prover.connection_decrypt(encrypted_message)
logging.info("Agent B decrypt message, decrypted_message:" +
json.dumps(decrypted_message))
# Sign and verify with did
# Prover get connections
agent_b_conns = prover.connection_get_connections()
from_did = agent_b_conns[0]["my_did"]
their_did = agent_b_conns[0]["their_did"]
data = "Data To Be Signed"
signed_data = bytes(data, 'utf-8')
logging.info("Data To Be Signed: {}".format(base64.b64encode(signed_data)));
base64_encoded_signature = prover.sign_with_did(from_did, signed_data)
logging.info("Signature: {}".format(base64_encoded_signature));
packed_signature = {}
packed_signature['signedData'] = base64.b64encode(signed_data).decode('utf-8')
packed_signature['signature'] = base64_encoded_signature
logging.info("Packed signature {}".format(packed_signature))
encrypted_message = prover.connection_encrypt(from_did,
their_did,
NixarMessageType.MESSAGE_TYPE_SIMPLE,
packed_signature)
logging.info("Prover Agent create message of signature info, encrypted_message: {}".format(encrypted_message))
decrypted_message = issuer.connection_decrypt(encrypted_message)
logging.info("Issuer Agent decrypt message, decrypted_message: {}".format(decrypted_message))
unpacked_signature = json.loads(decrypted_message['content'])
logging.info("Unpacked signature info {}".format(unpacked_signature))
# 1. verify with their did from connection.
is_verified = issuer.verify_signature_with_their_did(decrypted_message['senderDid'],
unpacked_signature['signedData'],
unpacked_signature['signature'])
logging.info("Verification Result with their did {}".format(is_verified))
# 2. OR verify with their PUBLIC Key
did_doc = issuer.get_their_did_as_did_doc(decrypted_message['senderDid'])
logging.info(did_doc)
logging.info(did_doc['publicKeys'][0])
is_verified = prover.verify_signature_with_did_public_key(did_doc['publicKeys'][0]['value'],
unpacked_signature['signedData'],
unpacked_signature['signature'])
logging.info("Verification Result {}".format(is_verified))
def read_store_tail():
issuer.issuer_get_tail()
demo_did_comm()
demo_with_non_revocable()
demo_with_revocable()