Verify vaccination data using Zero Knowledge Proofs with ASP.NET Core and MATTR

This article shows how Zero Knowledge Proofs ZKP verifiable credentials can be used to verify a persons vaccination data implemented in ASP.NET Core and MATTR. The ZKP BBS+ verifiable credentials are issued and stored on a digital wallet using a Self-Issued Identity Provider (SIOP) and Open ID Connect. The data can then be used to verify if the holder has the required credentials, but only the required data is used and returned to the verification application. The holder of the data who owns the wallet can consent or not consent to allow the verification application to see and use the vaccination data. Auth0 is used to implement the identity provider.

Code https://github.com/swiss-ssi-group/MattrZeroKnowledgeProofsAspNetCore

Blogs in the series

What are Zero Knowledge Proof enabled credentials

Zero Knowledge Proof enabled credentials allows you to selectively disclose claims from a verifiable credential without disclosing all of the information of the verifiable credential. It also makes it possible to verify data without having to share the sensitive data required to verify something. In this post, we will just selectively request part of the data from a verifiable credential. This would make it possible to implement business flows without having to share or copy the sensitive data.

Setup ZKP Vaccination Data Issue and Verify

The demo application implements a covid vaccination data process. A number of components, applications are required to implement these flows. The idea is that when a person is vaccinated, the authority responsible for this could add the vaccination data to the persons identity. In this demo, that would be added to the Auth0 service which can be accessed in the id_token claims. The vaccination data organization can use the credentials issuer application to create a DID credential issuer using ZKP verifiable credentials. The end user can use a digital wallet to add his or his credentials using the SIOP flow which gets the claims from the IDP and adds the data to the digital wallet. The verification application would have to create a presentation template defining the claims which are required to use to verify. Once created, a new verification request can be created and used to verify the vaccination data from the user. The user would scan the presented QR Code from the verifier application and display a verify in the digital wallet. Once consented, the data is returned to the verifier application using an API. The data can be processed and the UI is updated with a verified result or not. The blockchain ledger is abstracted away and used indirectly through MATTR which has APIs for the Self sovereign identity specifications.

Issuing Zero Knowledge Proof enabled credentials

The VaccineCredentialsIssuer ASP.NET Core application is used to create the credential issuer and present this as a QR Code for the user to add vaccination Zero Knowledge Proofs verifiable credentials. The flow implemented is very similar to the flow used in the previous blog Create an OIDC credential Issuer with MATTR and ASP.NET Core . A DID is created to use a BLS key type which supports BBS+ signatures for issuing ZKP-enabled credentials.

public class MattrOptions
{
	/// <summary>
	/// The supported key types for the DIDs are ed25519 and bls12381g2. 
	/// If the keyType is omitted, the default key type that will be used is ed25519.
	/// 
	/// If the keyType in options is set to bls12381g2 a DID will be created with 
	/// a BLS key type which supports BBS+ signatures for issuing ZKP-enabled credentials.
	/// </summary>
	public string keyType { get; set; } = "bls12381g2";
}

The DID is used to create a credential issuer for the ZKP credentials. The CreateMattrCredentialIssuer method takes the DID created with the bls12381g2 key and creates the OIDC credential issuer.

private async Task<V1_CreateOidcIssuerResponse> CreateMattrCredentialIssuer(HttpClient client, V1_CreateDidResponse did)
{
	// create vc, post to credentials api
	// https://learn.mattr.global/tutorials/issue/oidc-bridge/setup-issuer

	var createCredentialsUrl = $"https://{_mattrConfiguration.TenantSubdomain}/ext/oidc/v1/issuers";

	var payload = new MattrOpenApiClient.V1_CreateOidcIssuerRequest
	{
		Credential = new Credential
		{
			IssuerDid = did.Did,
			Name = "VaccinationCertificate7",
			Context = new List<Uri> {
				new Uri( "https://schema.org"), 
				new Uri( "https://www.w3.org/2018/credentials/v1")
			},
			Type = new List<string> { "VerifiableCredential" }
		},
		ClaimMappings = new List<ClaimMappings>
		{
			new ClaimMappings{ JsonLdTerm="family_name", OidcClaim=$"https://{_mattrConfiguration.TenantSubdomain}/family_name"},
			new ClaimMappings{ JsonLdTerm="given_name", OidcClaim=$"https://{_mattrConfiguration.TenantSubdomain}/given_name"},
			new ClaimMappings{ JsonLdTerm="date_of_birth", OidcClaim=$"https://{_mattrConfiguration.TenantSubdomain}/date_of_birth"},
			new ClaimMappings{ JsonLdTerm="medicinal_product_code", OidcClaim=$"https://{_mattrConfiguration.TenantSubdomain}/medicinal_product_code"},
			new ClaimMappings{ JsonLdTerm="number_of_doses", OidcClaim=$"https://{_mattrConfiguration.TenantSubdomain}/number_of_doses"},
			new ClaimMappings{ JsonLdTerm="total_number_of_doses", OidcClaim=$"https://{_mattrConfiguration.TenantSubdomain}/total_number_of_doses"},
			new ClaimMappings{ JsonLdTerm="vaccination_date", OidcClaim=$"https://{_mattrConfiguration.TenantSubdomain}/vaccination_date"},
			new ClaimMappings{ JsonLdTerm="country_of_vaccination", OidcClaim=$"https://{_mattrConfiguration.TenantSubdomain}/country_of_vaccination"}
		},
		FederatedProvider = new FederatedProvider
		{
			ClientId = _configuration["Auth0Wallet:ClientId"],
			ClientSecret = _configuration["Auth0Wallet:ClientSecret"],
			Url = new Uri($"https://{_configuration["Auth0Wallet:Domain"]}"),
			Scope = new List<string> { "openid", "profile", "email" }
		}
	};

	var payloadJson = JsonConvert.SerializeObject(payload);

	var uri = new Uri(createCredentialsUrl);

	using (var content = new StringContentWithoutCharset(payloadJson, "application/json"))
	{
		var createOidcIssuerResponse = await client.PostAsync(uri, content);

		if (createOidcIssuerResponse.StatusCode == System.Net.HttpStatusCode.Created)
		{
			var v1CreateOidcIssuerResponse = JsonConvert.DeserializeObject<V1_CreateOidcIssuerResponse>(
					await createOidcIssuerResponse.Content.ReadAsStringAsync());

			return v1CreateOidcIssuerResponse;
		}

		var error = await createOidcIssuerResponse.Content.ReadAsStringAsync();
	}

	throw new Exception("whoops something went wrong");
}

The data from the MATTR response is used to create the callback for the credentials issuer. This is persisted to a database as this needs to be created only once and can be re-used.

 public async Task<string> CreateCredentialsAndCallback(string name)
        {
            // create a new one
            var vaccinationDataCredentials = await CreateMattrDidAndCredentialIssuer();
            vaccinationDataCredentials.Name = name;
            await _vaccineCredentialsIssuerCredentialsService.CreateVaccinationData(vaccinationDataCredentials);

            var callback = $"https://{_mattrConfiguration.TenantSubdomain}/ext/oidc/v1/issuers/{vaccinationDataCredentials.OidcIssuerId}/federated/callback";
            return callback;
        }

The data is displayed as a QR Code in a ASP.NET Core Razor page application. This can be scanned and the credentials will be added to your digital wallet, if the Open ID Connect server has the claims for the identity required by this issuer.

Auth0 is used to add the identity data for the claims. An Auth0 pipeline rule was created and used to add these claims to the id_tokens.

function (user, context, callback) {
    const namespace = 'https://damianbod-sandbox.vii.mattr.global/';

    context.idToken[namespace + 'date_of_birth'] = user.user_metadata.date_of_birth;
  
    context.idToken[namespace + 'family_name'] = user.user_metadata.family_name;
    context.idToken[namespace + 'given_name'] = user.user_metadata.given_name;
    context.idToken[namespace + 'medicinal_product_code'] = user.user_metadata.medicinal_product_code;
    context.idToken[namespace + 'number_of_doses'] = user.user_metadata.number_of_doses;
    context.idToken[namespace + 'total_number_of_doses'] = user.user_metadata.total_number_of_doses;
    context.idToken[namespace + 'vaccination_date'] = user.user_metadata.vaccination_date;
    context.idToken[namespace + 'country_of_vaccination'] = user.user_metadata.country_of_vaccination;
  
    callback(null, user, context);
}

The data needs to be added to each user in Auth0. If using this in a real application, a UI could be created and used to add the specific data for each user. The credential issuer is tightly coupled through the data with the IDP. So each credential issuer which creates verifiable credentials would require it’s own identity provider and full access to update the profiles. The IDP contains the business data required to issuer the credentials. Auth0 might not be a good choice for this, maybe something like IdentityServer or Openiddict would be a better choice because you could implement custom UIs with ASP.NET Core Identity and the complete UIs for the credential issuing flows.

When the credential issuer is scanned by the digital wallet, the user logs into the OIDC server and gets the data for the ZKP verifiable credentials. In a MATTR wallet, this is displayed with the Privacy enhancing credential information.

Verifying the credentials

Before the ZKP credentials can be verified, a presentation template is created to define the required credentials to verify. The DID ID from the credential issuer is used to find the DID in the ledger. The CreateMattrPresentationTemplate method creates the template using the QueryByFrame so that the exact claims can be defined. The context must use the https://w3c-ccg.github.io/ldp-bbs2020/context/v1 namespace to use the ZKP BBS+ credentials in MATTR. The type must be VerifiableCredential.

private async Task<V1_PresentationTemplateResponse> CreateMattrPresentationTemplate(
	HttpClient client, string didId)
{
	// create presentation, post to presentations templates api
	// https://learn.mattr.global/tutorials/verify/presentation-request-template
	// https://learn.mattr.global/tutorials/verify/presentation-request-template#create-a-privacy-preserving-presentation-request-template-for-zkp-enabled-credentials

	var createPresentationsTemplatesUrl = $"https://{_mattrConfiguration.TenantSubdomain}/v1/presentations/templates";

	var additionalPropertiesCredentialSubject = new Dictionary<string, object>();
	additionalPropertiesCredentialSubject.Add("credentialSubject", new VaccanationDataCredentialSubject
	{ 
		Explicit = true
	});

	var additionalPropertiesCredentialQuery = new Dictionary<string, object>();
	additionalPropertiesCredentialQuery.Add("required", true);

	var additionalPropertiesQuery = new Dictionary<string, object>();
	additionalPropertiesQuery.Add("type", "QueryByFrame");
	additionalPropertiesQuery.Add("credentialQuery", new List<CredentialQuery2> {
		new CredentialQuery2
		{
			Reason = "Please provide your vaccination data",
			TrustedIssuer = new List<TrustedIssuer>{
				new TrustedIssuer
				{
					Required = true,
					Issuer = didId // DID use to create the oidc
				}
			},
			Frame = new Frame
			{
				Context = new List<object>{
					"https://www.w3.org/2018/credentials/v1",
					"https://w3c-ccg.github.io/ldp-bbs2020/context/v1",
					"https://schema.org",
				},
				Type = "VerifiableCredential",
				AdditionalProperties = additionalPropertiesCredentialSubject

			},
			AdditionalProperties = additionalPropertiesCredentialQuery
		}
	});

	var payload = new MattrOpenApiClient.V1_CreatePresentationTemplate
	{
		Domain = _mattrConfiguration.TenantSubdomain,
		Name = "zkp-certificate-presentation-11",
		Query = new List<Query>
		{
			new Query
			{
				AdditionalProperties = additionalPropertiesQuery
			}
		}
	};

	var payloadJson = JsonConvert.SerializeObject(payload);

	var uri = new Uri(createPresentationsTemplatesUrl);

	using (var content = new StringContentWithoutCharset(payloadJson, "application/json"))
	{
		var presentationTemplateResponse = await client.PostAsync(uri, content);

		if (presentationTemplateResponse.StatusCode == System.Net.HttpStatusCode.Created)
		{

			var v1PresentationTemplateResponse = JsonConvert
					.DeserializeObject<MattrOpenApiClient.V1_PresentationTemplateResponse>(
					await presentationTemplateResponse.Content.ReadAsStringAsync());

			return v1PresentationTemplateResponse;
		}

		var error = await presentationTemplateResponse.Content.ReadAsStringAsync();

	}

	throw new Exception("whoops something went wrong");
}

The VaccanationDataCredentialSubject class defines the specific claims to use for the verification.

public class VaccanationDataCredentialSubject
{
	[Newtonsoft.Json.JsonProperty("@explicit", Required = Newtonsoft.Json.Required.Always)]
	public bool Explicit { get; set; }

	[Newtonsoft.Json.JsonProperty("family_name", Required = Newtonsoft.Json.Required.Always)]
	[System.ComponentModel.DataAnnotations.Required]
	public object FamilyName { get; set; } = new object();

	[Newtonsoft.Json.JsonProperty("given_name", Required = Newtonsoft.Json.Required.Always)]
	[System.ComponentModel.DataAnnotations.Required]
	public object GivenName { get; set; } = new object();

	[Newtonsoft.Json.JsonProperty("date_of_birth", Required = Newtonsoft.Json.Required.Always)]
	[System.ComponentModel.DataAnnotations.Required]
	public object DateOfBirth { get; set; } = new object();

	[Newtonsoft.Json.JsonProperty("medicinal_product_code", Required = Newtonsoft.Json.Required.Always)]
	[System.ComponentModel.DataAnnotations.Required]
	public object MedicinalProductCode { get; set; } = new object();
}

Verifying is very similar to the blog Present and Verify Verifiable Credentials in ASP.NET Core using Decentralized Identities and MATTR. A new DID of type ed25519 is used to invoke a verify request and also sign the request. The verifying flow in the application presents a QR Code using the redirectURL technic because the signed request is too long to present as a QR Code. This request returns a 302 with the full jws.

The application needs to be started using a public domain because the digital wallet will request back to the API with the data. I use ngrok to test locally. The verifier application can be started and the verify process is started by clicking the verify button which displays the QR Code to verify.

Start the application and start ngrok

ngrok http http://localhost:5000

The QR Code can be scanned to verify.

In the digital wallet, the verification request for the vaccination data can be viewed and if ok sent. The digital wallet displays the data which is disclosed and the data which is not. When the user clicks send, the data is validated and the API from the verifier application is called.

When the Verify application receives the callback from the digital wallet, the data is validated and the challenge ID is used to notify the user of a successful verification. The data is saved to a database and ASP.NET Core SignalR is used to update the UI. When the message from SignalR is sent, the user is redirected to the success page using the challenge ID and the data is displayed with the success image.

Notes

Now we have a full create, holder, verify process implemented for Zero Knowledge Proof verifiable credentials using covid vaccination data. OIDC is used to authenticate and create the claims used for the credentials. The OIDC connect server or identity provider is tightly coupled to the credential issuer because business uses the data from the id_token. When using SIOP, I would use ASP.NET Core Identity and either OpenIddict of Identityserver4 to implement this as part of the credential issuer. You need full control of the claims so using Auth0, Azure AD or Azure B2C would probably be a bad choice here. You could federate then to one of these from the credential issuer to use the profiles as required. Each vaccinated user would also require a user account. ZKP verifiable credentials makes it possible to support user privacy better and mix claims easier from different credentials. Another problem with this solution is the vendor lockdown. This is a problem with any self sovereign solution at the moment. Even though, the specifications are all standard, unless you want to implement this completely yourself, you would choose a vendor specific implementation which locks you down to a specific wallet or with specific features only. Interop does not seem to work at the moment. This is a problem with all security solutions at present not just SSI, all software producers, security services use security reasoning as an excuse to try to force you into lockdown into their specific product. You can see this with most of the existing OIDC solutions and services. Typical quotes for this are “You can use any OIDC client, but we recommend our OIDC client…” SSI will open possibilities for new security solutions and it will be very interesting to see how application security develops in the next five years.

Links

https://mattr.global/

https://learn.mattr.global/tutorials/verify/using-callback/callback-e-to-e

https://mattr.global/get-started/

https://learn.mattr.global/

https://keybase.io/

Generating a ZKP-enabled BBS+ credential using the MATTR Platform

https://learn.mattr.global/tutorials/dids/did-key

https://gunnarpeipman.com/httpclient-remove-charset/

https://auth0.com/

Where to begin with OIDC and SIOP

https://anonyome.com/2020/06/decentralized-identity-key-concepts-explained/

Verifiable-Credentials-Flavors-Explained

3 comments

  1. […] Verify vaccination data using Zero Knowledge Proofs with ASP.NET Core and MATTR (Damien Bowden) […]

  2. […] Verify vaccination data using Zero Knowledge Proofs with ASP.NET Core and MATTR – Damien Bowden […]

  3. […] Verify vaccination data using Zero Knowledge Proofs with ASP.NET Core and MATTR […]

Leave a comment

This site uses Akismet to reduce spam. Learn how your comment data is processed.