Implement Compound Proof BBS+ verifiable credentials using ASP.NET Core and MATTR

This article shows how Zero Knowledge Proofs BBS+ verifiable credentials can be used to verify credential subject data from two separate verifiable credentials 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 OpenID Connect. A compound proof presentation template is created to verify the user data in a single verify.

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

Blogs in the series

What are ZKP BBS+ verifiable credentials

BBS+ verifiable credentials are built using JSON-LD and makes it possible to support selective disclosure of subject claims from a verifiable credential, compound proofs of different VCs, zero knowledge proofs where the subject claims do not need to be exposed to verify something, private holder binding and prevent tracking. The specification and implementation are still a work in progress.

Setup

The solution is setup to issue and verify the BBS+ verifiable credentials. The credential issuers are implemented in ASP.NET Core as well as the verifiable credential verifier. One credential issuer implements a BBS+ JSON-LD E-ID verifiable credential using SIOP together with Auth0 as the identity provider and the MATTR API which implements the access to the ledger and implements the logic for creating and verifying the verifiable credential and implementing the SSI specifications. The second credential issuer implements a county of residence BBS+ verifiable credential issuer like the first one. The ASP.NET Core verifier project uses a BBS+ verify presentation to verify that a user has the correct E-ID credentials and the county residence verifiable credentials in one request. This is presented as a compound proof using credential subject data from both verifiable credentials. The credentials are presented from the MATTR wallet to the ASP.NET Core verifier application.

The BBS+ compound proof is made up from the two verifiable credentials stored on the wallet. The holder of the wallet owns the credentials and can be trusted to a fairly high level because SIOP was used to add the credentials to the MATTR wallet which requires a user authentication on the wallet using OpenID Connect. If the host system has strong authentication, the user of the wallet is probably the same person for which the credentials where intended for and issued too. We only can prove that the verifiable credentials are valid, we cannot prove that the person sending the credentials is also the subject of the credentials or has the authorization to act on behalf of the credential subject. With SIOP, we know that the credentials were issued in a way which allows for strong authentication.

Implementing the Credential Issuers

The credentials are created using a credential issuer and can be added to the users wallet using SIOP. An ASP.NET Core application is used to implement the MATTR API client for creating and issuing the credentials. Auth0 is used for the OIDC server and the profiles used in the verifiable credentials are added here. The Auth0 server is part of the credential issuer service business. The application has two separate flows for administrators and users, or holders of the credentials and credential issuer administrators.

An administrator can signin to the credential issuer ASP.NET Core application using OIDC and can create new OIDC credential issuers using BBS+. Once created, the callback URL for the credential issuer needs to be added to the Auth0 client application as a redirect URL.

A user can login to the ASP.NET Core application and request the verifiable credentials only for themselves. This is not authenticated on the ASP.NET Core application, but on the wallet application using the SIOP flow. The application presents a QR Code which starts the flow. Once authenticated, the credentials are added to the digital wallet. Both the E-ID and the county of residence credentials are added and stored on the wallet.

Auth0 Auth pipeline rules

The credential subject claims added to the verifiable credential uses the profile data from the Auth0 identity provider. This data can be added using an Auth0 auth pipeline rule. Once defined, if the user has the profile data, the verifiable credentials can be created from the data.

function (user, context, callback) {
    const namespace = 'https://damianbod-sandbox.vii.mattr.global/';
    context.idToken[namespace + 'name'] = user.user_metadata.name;
    context.idToken[namespace + 'first_name'] = user.user_metadata.first_name;
    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 + 'birth_place'] = user.user_metadata.birth_place;
    context.idToken[namespace + 'gender'] = user.user_metadata.gender;
    context.idToken[namespace + 'height'] = user.user_metadata.height;
    context.idToken[namespace + 'nationality'] = user.user_metadata.nationality;
  
    context.idToken[namespace + 'address_country'] = user.user_metadata.address_country;
    context.idToken[namespace + 'address_locality'] = user.user_metadata.address_locality;
    context.idToken[namespace + 'address_region'] = user.user_metadata.address_region;
    context.idToken[namespace + 'street_address'] = user.user_metadata.street_address;
    context.idToken[namespace + 'postal_code'] = user.user_metadata.postal_code;
  
    callback(null, user, context);
}

Once issued, the verifiable credential is saved to the digital wallet like this:

{
  "type": [
    "VerifiableCredential",
    "VerifiableCredentialExtension"
  ],
  "issuer": {
    "id": "did:key:zUC7GiWMGY2pynrFG7TcstDiZeNKfpMPY8YT5z4xgd58wE927UxaJfaqFuXb9giCS1diTwLi8G18hRgZ928b4qd8nkPRdZCEaBGChGSjUzfFDm6Tyio1GN2npT9o7K5uu8mDs2g",
    "name": "damianbod-sandbox.vii.mattr.global"
  },
  "name": "EID",
  "issuanceDate": "2021-12-04T11:47:41.319Z",
  "credentialSubject": {
    "id": "did:key:z6MkmGHPWdKjLqiTydLHvRRdHPNDdUDKDudjiF87RNFjM2fb",
    "family_name": "Bob",
    "given_name": "Lammy",
    "date_of_birth": "1953-07-21",
    "birth_place": "Seattle",
    "height": "176cm",
    "nationality": "USA",
    "gender": "Male"
  },
  "@context": [
    "https://www.w3.org/2018/credentials/v1",
    "https://w3id.org/security/bbs/v1",
    {
      "@vocab": "https://w3id.org/security/undefinedTerm#"
    },
    "https://mattr.global/contexts/vc-extensions/v1",
    "https://schema.org",
    "https://w3id.org/vc-revocation-list-2020/v1"
  ],
  "credentialStatus": {
    "id": "https://damianbod-sandbox.vii.mattr.global/core/v1/revocation-lists/dd507c44-044c-433b-98ab-6fa9934d6b01#0",
    "type": "RevocationList2020Status",
    "revocationListIndex": "0",
    "revocationListCredential": "https://damianbod-sandbox.vii.mattr.global/core/v1/revocation-lists/dd507c44-044c-433b-98ab-6fa9934d6b01"
  },
  "proof": {
    "type": "BbsBlsSignature2020",
    "created": "2021-12-04T11:47:42Z",
    "proofPurpose": "assertionMethod",
    "proofValue": "qquknHC7zaklJd0/IbceP0qC9sGYfkwszlujrNQn+RFg1/lUbjCe85Qnwed7QBQkIGnYRHydZiD+8wJG8/R5i8YPJhWuneWNE151GbPTaMhGNZtM763yi2A11xYLmB86x0d1JLdHaO30NleacpTs9g==",
    "verificationMethod": "did:key:zUC7GiWMGY2pynrFG7TcstDiZeNKfpMPY8YT5z4xgd58wE927UxaJfaqFuXb9giCS1diTwLi8G18hRgZ928b4qd8nkPRdZCEaBGChGSjUzfFDm6Tyio1GN2npT9o7K5uu8mDs2g#zUC7GiWMGY2pynrFG7TcstDiZeNKfpMPY8YT5z4xgd58wE927UxaJfaqFuXb9giCS1diTwLi8G18hRgZ928b4qd8nkPRdZCEaBGChGSjUzfFDm6Tyio1GN2npT9o7K5uu8mDs2g"
  }
}

For more information on adding BBS+ verifiable credentials using MATTR, see the documentation, or a previous blog in this series.

Verifying the compound proof BBS+ verifiable credential

The verifier application needs to use both E-ID and county of residence verifiable credentials. This is done using a presentation template which is specific to the MATTR platform. Once created, a verify request is created using this template and presented to the user in the UI as a QR code. The holder of the wallet can scan this code and the verification begins. The wallet will use the verification request and try to find the credentials on the wallet which matches what was requested. If the wallet has the data from the correct issuers, the holder of the wallet consents, the data is sent to the verifier application using a new presentation verifiable credential using the credential subject data from both of the existing verifiable credentials stored on the wallet. The webhook or an API on the verifier application handles this and validates the request. If all is good, the data is persisted and the UI is updated using SignalR messaging.

Creating a verifier presentation template

Before verifier presentations can be sent a the digital wallet, a template needs to be created in the MATTR platform. The CreatePresentationTemplate Razor page is used to create a new template. The template requires the two DIDs used for issuing the credentials from the credential issuer applications.

public class CreatePresentationTemplateModel : PageModel
{
	private readonly MattrPresentationTemplateService _mattrVerifyService;
	public bool CreatingPresentationTemplate { get; set; } = true;
	public string TemplateId { get; set; }

	[BindProperty]
	public PresentationTemplate PresentationTemplate { get; set; }
	public CreatePresentationTemplateModel(MattrPresentationTemplateService mattrVerifyService)
	{
		_mattrVerifyService = mattrVerifyService;
	}
	public void OnGet()
	{
		PresentationTemplate = new PresentationTemplate();
	}

	public async Task<IActionResult> OnPostAsync()
	{
		if (!ModelState.IsValid)
		{
			return Page();
		}

		TemplateId = await _mattrVerifyService.CreatePresentationTemplateId(
			PresentationTemplate.DidEid, PresentationTemplate.DidCountyResidence);

		CreatingPresentationTemplate = false;
		return Page();
	}
}

public class PresentationTemplate
{
	[Required]
	public string DidEid { get; set; }

	[Required]
	public string DidCountyResidence { get; set; }
	
}

The MattrPresentationTemplateService class implements the logic required to create a new presentation template. The service gets a new access token for your MATTR tenant and creates a new template using the credential subjects required and the correct contexts. BBS+ and frames require specific contexts. The CredentialQuery2 has two separate Frame items, one for each verifiable credential created and stored on the digital wallet.

public class MattrPresentationTemplateService
{
	private readonly IHttpClientFactory _clientFactory;
	private readonly MattrTokenApiService _mattrTokenApiService;
	private readonly VerifyEidCountyResidenceDbService _verifyEidAndCountyResidenceDbService;
	private readonly MattrConfiguration _mattrConfiguration;

	public MattrPresentationTemplateService(IHttpClientFactory clientFactory,
		IOptions<MattrConfiguration> mattrConfiguration,
		MattrTokenApiService mattrTokenApiService,
		VerifyEidCountyResidenceDbService VerifyEidAndCountyResidenceDbService)
	{
		_clientFactory = clientFactory;
		_mattrTokenApiService = mattrTokenApiService;
		_verifyEidAndCountyResidenceDbService = VerifyEidAndCountyResidenceDbService;
		_mattrConfiguration = mattrConfiguration.Value;
	}

	public async Task<string> CreatePresentationTemplateId(string didEid, string didCountyResidence)
	{
		// create a new one
		var v1PresentationTemplateResponse = await CreateMattrPresentationTemplate(didEid, didCountyResidence);

		// save to db
		var template = new EidCountyResidenceDataPresentationTemplate
		{
			DidEid = didEid,
			DidCountyResidence = didCountyResidence,
			TemplateId = v1PresentationTemplateResponse.Id,
			MattrPresentationTemplateReponse = JsonConvert.SerializeObject(v1PresentationTemplateResponse)
		};
		await _verifyEidAndCountyResidenceDbService.CreateEidAndCountyResidenceDataTemplate(template);

		return v1PresentationTemplateResponse.Id;
	}

	private async Task<V1_PresentationTemplateResponse> CreateMattrPresentationTemplate(string didId, string didCountyResidence)
	{
		HttpClient client = _clientFactory.CreateClient();
		var accessToken = await _mattrTokenApiService.GetApiToken(client, "mattrAccessToken");

		client.DefaultRequestHeaders.Authorization =
			new AuthenticationHeaderValue("Bearer", accessToken);
		client.DefaultRequestHeaders.TryAddWithoutValidation("Content-Type", "application/json");

		var v1PresentationTemplateResponse = await CreateMattrPresentationTemplate(client, didId, didCountyResidence);
		return v1PresentationTemplateResponse;
	}

	private async Task<V1_PresentationTemplateResponse> CreateMattrPresentationTemplate(
		HttpClient client, string didEid, string didCountyResidence)
	{
		// 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 eidAdditionalPropertiesCredentialSubject = new Dictionary<string, object>();
		eidAdditionalPropertiesCredentialSubject.Add("credentialSubject", new EidDataCredentialSubject
		{
			Explicit = true
		});

		var countyResidenceAdditionalPropertiesCredentialSubject = new Dictionary<string, object>();
		countyResidenceAdditionalPropertiesCredentialSubject.Add("credentialSubject", new CountyResidenceDataCredentialSubject
		{
			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 E-ID",
				TrustedIssuer = new List<TrustedIssuer>{
					new TrustedIssuer
					{
						Required = true,
						Issuer = didEid // DID used to create the oidc
					}
				},
				Frame = new Frame
				{
					Context = new List<object>{
						"https://www.w3.org/2018/credentials/v1",
						"https://w3id.org/security/bbs/v1",
						"https://mattr.global/contexts/vc-extensions/v1",
						"https://schema.org",
						"https://w3id.org/vc-revocation-list-2020/v1"
					},
					Type = "VerifiableCredential",
					AdditionalProperties = eidAdditionalPropertiesCredentialSubject

				},
				AdditionalProperties = additionalPropertiesCredentialQuery
			},
			new CredentialQuery2
			{
				Reason = "Please provide your Residence data",
				TrustedIssuer = new List<TrustedIssuer>{
					new TrustedIssuer
					{
						Required = true,
						Issuer = didCountyResidence // DID used to create the oidc
					}
				},
				Frame = new Frame
				{
					Context = new List<object>{
						"https://www.w3.org/2018/credentials/v1",
						"https://w3id.org/security/bbs/v1",
						"https://mattr.global/contexts/vc-extensions/v1",
						"https://schema.org",
						"https://w3id.org/vc-revocation-list-2020/v1"
					},
					Type = "VerifiableCredential",
					AdditionalProperties = countyResidenceAdditionalPropertiesCredentialSubject

				},
				AdditionalProperties = additionalPropertiesCredentialQuery
			}
		});

		var payload = new MattrOpenApiClient.V1_CreatePresentationTemplate
		{
			Domain = _mattrConfiguration.TenantSubdomain,
			Name = "zkp-eid-county-residence-compound",
			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");
	}
}

public class EidDataCredentialSubject
{
	[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("birth_place", Required = Newtonsoft.Json.Required.Always)]
	[System.ComponentModel.DataAnnotations.Required]
	public object BirthPlace { get; set; } = new object();

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

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

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

public class CountyResidenceDataCredentialSubject
{
	[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("address_country", Required = Newtonsoft.Json.Required.Always)]
	[System.ComponentModel.DataAnnotations.Required]
	public object AddressCountry { get; set; } = new object();

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

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

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

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

When the presentation template is created, the following JSON payload in returned. This is what is used to create verifier presentation requests. The context must contain the value of the context value of the credentials on the wallet. You can also verify that the trusted issuer matches and that the two Frame objects are created correctly with the required values.

{
	"id": "f188df35-e76f-4794-8e64-eedbe0af2b19",
	"domain": "damianbod-sandbox.vii.mattr.global",
	"name": "zkp-eid-county-residence-compound",
	"query": [
		{
			"type": "QueryByFrame",
			"credentialQuery": [
				{
					"reason": "Please provide your E-ID",
					"frame": {
						"@context": [
							"https://www.w3.org/2018/credentials/v1",
							"https://w3id.org/security/bbs/v1",
							"https://mattr.global/contexts/vc-extensions/v1",
							"https://schema.org",
							"https://w3id.org/vc-revocation-list-2020/v1"
						],
						"type": "VerifiableCredential",
						"credentialSubject": {
							"@explicit": true,
							"family_name": {},
							"given_name": {},
							"date_of_birth": {},
							"birth_place": {},
							"height": {},
							"nationality": {},
							"gender": {}
						}
					},
					"trustedIssuer": [
						{
							"required": true,
							"issuer": "did:key:zUC7GiWMGY2pynrFG7TcstDiZeNKfpMPY8YT5z4xgd58wE927UxaJfaqFuXb9giCS1diTwLi8G18hRgZ928b4qd8nkPRdZCEaBGChGSjUzfFDm6Tyio1GN2npT9o7K5uu8mDs2g"
						}
					],
					"required": true
				},
				{
					"reason": "Please provide your Residence data",
					"frame": {
						"@context": [
							"https://www.w3.org/2018/credentials/v1",
							"https://w3id.org/security/bbs/v1",
							"https://mattr.global/contexts/vc-extensions/v1",
							"https://schema.org",
							"https://w3id.org/vc-revocation-list-2020/v1"
						],
						"type": "VerifiableCredential",
						"credentialSubject": {
							"@explicit": true,
							"family_name": {},
							"given_name": {},
							"date_of_birth": {},
							"address_country": {},
							"address_locality": {},
							"address_region": {},
							"street_address": {},
							"postal_code": {}
						}
					},
					"trustedIssuer": [
						{
							"required": true,
							"issuer": "did:key:zUC7G95fmyuYXNP2oqhhWkysmMPafU4dUWtqzXSsijsLCVauFDhAB7Dqbk2LCeo488j9iWGLXCL59ocYzhTmS3U7WNdukoJ2A8Z8AVCzeS5TySDJcYCjzuaPm7voPGPqtYa6eLV"
						}
					],
					"required": true
				}
			]
		}
	]
}

The presentation template is ready and can be used now. This is just a specific definition used by the MATTR platform. This is not saved to the ledger.

Creating a verifier request and present QR Code

Now that we have a presentation template, we initialize a verifier presentation request and present this as a QR Code for the holder of the digital wallet to scan. The CreateVerifyCallback method creates the verification and returns a signed token which is added to the QR Code to scan and the challengeId is encoded in base64 as we use this in the URL to request or handle the webhook callback.

public class CreateVerifierDisplayQrCodeModel : PageModel
{
	private readonly MattrCredentialVerifyCallbackService _mattrCredentialVerifyCallbackService;

	public bool CreatingVerifier { get; set; } = true;
	public string QrCodeUrl { get; set; }

	[BindProperty]
	public string ChallengeId { get; set; }

	[BindProperty]
	public string Base64ChallengeId { get; set; }

	[BindProperty]
	public CreateVerifierDisplayQrCodeCallbackUrl CallbackUrlDto { get; set; }
	public CreateVerifierDisplayQrCodeModel(MattrCredentialVerifyCallbackService mattrCredentialVerifyCallbackService)
	{
		_mattrCredentialVerifyCallbackService = mattrCredentialVerifyCallbackService;
	}
	public void OnGet()
	{
		CallbackUrlDto = new CreateVerifierDisplayQrCodeCallbackUrl();
		CallbackUrlDto.CallbackUrl = $"https://{HttpContext.Request.Host.Value}";
	}

	public async Task<IActionResult> OnPostAsync()
	{
		if (!ModelState.IsValid)
		{
			return Page();
		}

		var result = await _mattrCredentialVerifyCallbackService
			.CreateVerifyCallback(CallbackUrlDto.CallbackUrl);

		CreatingVerifier = false;

		var walletUrl = result.WalletUrl.Trim();
		ChallengeId = result.ChallengeId;
		var valueBytes = Encoding.UTF8.GetBytes(ChallengeId);
		Base64ChallengeId = Convert.ToBase64String(valueBytes);

		VerificationRedirectController.WalletUrls.Add(Base64ChallengeId, walletUrl);

		// https://learn.mattr.global/tutorials/verify/using-callback/callback-e-to-e#redirect-urls
		//var qrCodeUrl = $"didcomm://{walletUrl}";

		QrCodeUrl = $"didcomm://https://{HttpContext.Request.Host.Value}/VerificationRedirect/{Base64ChallengeId}";
		return Page();
	}
}

public class CreateVerifierDisplayQrCodeCallbackUrl
{
	[Required]
	public string CallbackUrl { get; set; }
}

The CreateVerifyCallback method uses the host as the base URL for the callback definition which is included in the verification. An access token is requested for the MATTR API, this is used for all the requests. The last issued template is used in the verification. A new DID is created or the existing DID for this verifier is used to attach the verify presentation on the ledger. The InvokePresentationRequest is used to initialize the verification presentation. This request uses the templateId, the callback URL and the DID. Part of the body payload of the response of the request is signed and this is returned to the Razor page to be displayed as part of the QR code. This signed token is longer and so a didcomm redirect is used in the QR Code and not the value directly in the Razor page..

/// <summary>
/// https://learn.mattr.global/tutorials/verify/using-callback/callback-e-to-e
/// </summary>
/// <param name="callbackBaseUrl"></param>
/// <returns></returns>
public async Task<(string WalletUrl, string ChallengeId)> CreateVerifyCallback(string callbackBaseUrl)
{
	callbackBaseUrl = callbackBaseUrl.Trim();
	if (!callbackBaseUrl.EndsWith('/'))
	{
		callbackBaseUrl = $"{callbackBaseUrl}/";
	}

	var callbackUrlFull = $"{callbackBaseUrl}{MATTR_CALLBACK_VERIFY_PATH}";
	var challenge = GetEncodedRandomString();

	HttpClient client = _clientFactory.CreateClient();
	var accessToken = await _mattrTokenApiService.GetApiToken(client, "mattrAccessToken");

	client.DefaultRequestHeaders.Authorization =
		new AuthenticationHeaderValue("Bearer", accessToken);
	client.DefaultRequestHeaders.TryAddWithoutValidation("Content-Type", "application/json");

	var template = await _VerifyEidAndCountyResidenceDbService.GetLastPresentationTemplate();

	var didToVerify = await _mattrCreateDidService.GetDidOrCreate("did_for_verify");
	// Request DID from ledger
	V1_GetDidResponse did = await RequestDID(didToVerify.Did, client);

	// Invoke the Presentation Request
	var invokePresentationResponse = await InvokePresentationRequest(
		client,
		didToVerify.Did,
		template.TemplateId,
		challenge,
		callbackUrlFull);

	// Sign and Encode the Presentation Request body
	var signAndEncodePresentationRequestBodyResponse = await SignAndEncodePresentationRequestBody(
		client, did, invokePresentationResponse);

	// fix strange DTO
	var jws = signAndEncodePresentationRequestBodyResponse.Replace("\"", "");

	// save to db 
	var vaccinationDataPresentationVerify = new EidCountyResidenceDataPresentationVerify
	{
		DidEid = template.DidEid,
		DidCountyResidence = template.DidCountyResidence,
		TemplateId = template.TemplateId,
		CallbackUrl = callbackUrlFull,
		Challenge = challenge,
		InvokePresentationResponse = JsonConvert.SerializeObject(invokePresentationResponse),
		Did = JsonConvert.SerializeObject(did),
		SignAndEncodePresentationRequestBody = jws
	};
	await _VerifyEidAndCountyResidenceDbService.CreateEidAndCountyResidenceDataPresentationVerify(vaccinationDataPresentationVerify);

	var walletUrl = $"https://{_mattrConfiguration.TenantSubdomain}/?request={jws}";

	return (walletUrl, challenge);
}

The QR Code is displayed in the UI.

Once the QR Code is created and scanned, the SignalR client starts listening for messages returned for the challengeId.

@section scripts {
    <script src="~/js/qrcode.min.js"></script>
    <script type="text/javascript">
    new QRCode(document.getElementById("qrCode"),
    {
        text: "@Html.Raw(Model.QrCodeUrl)",
        width: 300,
        height: 300,
        correctLevel: QRCode.CorrectLevel.L
    });

    $(document).ready(() => {

    });

    var connection = new signalR.HubConnectionBuilder().withUrl("/mattrVerifiedSuccessHub").build();

    connection.on("MattrCallbackSuccess", function (base64ChallengeId) {
        console.log("received verification:" + base64ChallengeId);
        window.location.href = "/VerifiedUser?base64ChallengeId=" + base64ChallengeId;
    });

    connection.start().then(function () {
        console.log(connection.connectionId);
        const base64ChallengeId = $("#Base64ChallengeId").val();
        console.warn("base64ChallengeId: " + base64ChallengeId);

        if (base64ChallengeId) {
            console.log(base64ChallengeId);
            // join message
            connection.invoke("AddChallenge", base64ChallengeId, connection.connectionId).catch(function (err) {
                return console.error(err.toString());
            });
        }
    }).catch(function (err) {
        return console.error(err.toString());
    });
    </script>
}

Validating the verification callback

After the holder of the digital wallet has given consent, the wallet sends the verifiable credential data back to the verifier application in a HTTP request. This is sent to a webhook or an API in the verifier application. This needs to be verified correctly. In this demo, only the challengeId is used to match the request, the payload is not validated which it should be. The callback handler stores the data to the database and sends a SignalR message to inform the waiting client that the verify has been completed successfully.

private readonly VerifyEidCountyResidenceDbService _verifyEidAndCountyResidenceDbService;

private readonly IHubContext<MattrVerifiedSuccessHub> _hubContext;

public VerificationController(VerifyEidCountyResidenceDbService verifyEidAndCountyResidenceDbService,
	IHubContext<MattrVerifiedSuccessHub> hubContext)
{
	_hubContext = hubContext;
	_verifyEidAndCountyResidenceDbService = verifyEidAndCountyResidenceDbService;
}

/// <summary>
///  {
///	 "presentationType": "QueryByFrame",
///	 "challengeId": "nGu/E6eQ8AraHzWyB/kluudUhraB8GybC3PNHyZI",
///	 "claims": {
///		"id": "did:key:z6MkmGHPWdKjLqiTydLHvRRdHPNDdUDKDudjiF87RNFjM2fb",
///		"http://schema.org/birth_place": "Seattle",
///		"http://schema.org/date_of_birth": "1953-07-21",
///		"http://schema.org/family_name": "Bob",
///		"http://schema.org/gender": "Male",
///		"http://schema.org/given_name": "Lammy",
///		"http://schema.org/height": "176cm",
///		"http://schema.org/nationality": "USA",
///		"http://schema.org/address_country": "Schweiz",
///		"http://schema.org/address_locality": "Thun",
///		"http://schema.org/address_region": "Bern",
///		"http://schema.org/postal_code": "3000",
///		"http://schema.org/street_address": "Thunerstrasse 14"
///	 },
///	 "verified": true,
///	 "holder": "did:key:z6MkmGHPWdKjLqiTydLHvRRdHPNDdUDKDudjiF87RNFjM2fb"
///  }
/// </summary>
/// <param name="body"></param>
/// <returns></returns>
[HttpPost]
[Route("[action]")]
public async Task<IActionResult> VerificationDataCallback()
{
	string content = await new System.IO.StreamReader(Request.Body).ReadToEndAsync();
	var body = JsonSerializer.Deserialize<VerifiedEidCountyResidenceData>(content);

	var valueBytes = Encoding.UTF8.GetBytes(body.ChallengeId);
	var base64ChallengeId = Convert.ToBase64String(valueBytes);

	string connectionId;
	var found = MattrVerifiedSuccessHub.Challenges
		.TryGetValue(base64ChallengeId, out connectionId);

	//test Signalr
	//await _hubContext.Clients.Client(connectionId).SendAsync("MattrCallbackSuccess", $"{base64ChallengeId}");
	//return Ok();

	var exists = await _verifyEidAndCountyResidenceDbService.ChallengeExists(body.ChallengeId);

	if (exists)
	{
		await _verifyEidAndCountyResidenceDbService.PersistVerification(body);

		if (found)
		{
			//$"/VerifiedUser?base64ChallengeId={base64ChallengeId}"
			await _hubContext.Clients
				.Client(connectionId)
				.SendAsync("MattrCallbackSuccess", $"{base64ChallengeId}");
		}

		return Ok();
	}

	return BadRequest("unknown verify request");
}

The VerifiedUser ASP.NET Core Razor page displays the data after a successful verification. This uses the challengeId to get the data from the database and display this in the UI for the next steps.

public class VerifiedUserModel : PageModel
{
	private readonly VerifyEidCountyResidenceDbService _verifyEidCountyResidenceDbService;

	public VerifiedUserModel(VerifyEidCountyResidenceDbService verifyEidCountyResidenceDbService)
	{
		_verifyEidCountyResidenceDbService = verifyEidCountyResidenceDbService;
	}

	public string Base64ChallengeId { get; set; }
	public EidCountyResidenceVerifiedClaimsDto VerifiedEidCountyResidenceDataClaims { get; private set; }

	public async Task OnGetAsync(string base64ChallengeId)
	{
		// user query param to get challenge id and display data
		if (base64ChallengeId != null)
		{
			var valueBytes = Convert.FromBase64String(base64ChallengeId);
			var challengeId = Encoding.UTF8.GetString(valueBytes);

			var verifiedDataUser = await _verifyEidCountyResidenceDbService.GetVerifiedUser(challengeId);
			VerifiedEidCountyResidenceDataClaims = new EidCountyResidenceVerifiedClaimsDto
			{
				// Common
				DateOfBirth = verifiedDataUser.DateOfBirth,
				FamilyName = verifiedDataUser.FamilyName,
				GivenName = verifiedDataUser.GivenName,

				// E-ID
				BirthPlace = verifiedDataUser.BirthPlace,
				Height = verifiedDataUser.Height,
				Nationality = verifiedDataUser.Nationality,
				Gender = verifiedDataUser.Gender,

				// County Residence
				AddressCountry = verifiedDataUser.AddressCountry,
				AddressLocality = verifiedDataUser.AddressLocality,
				AddressRegion = verifiedDataUser.AddressRegion,
				StreetAddress = verifiedDataUser.StreetAddress,
				PostalCode = verifiedDataUser.PostalCode
			};
		}
	}
}

The demo UI displays the data after a successful verification. The next steps of the verifier process can be implemented using these values. This would typically included creating an account and setting up an authentication which is not subject to phishing for high security or at least which has a second factor.

Notes

The MATTR BBS+ verifiable credentials look really good and supports selective disclosure and compound proofs. The implementation is still a WIP and MATTR are investing in this at present and will hopefully complete and improve all the BBS+ features. Until BBS+ is implemented by the majority of SSI platform providers and the specs are completed, I don’t not see how SSI can be adopted unless of course all converge on some other standard. This would help improve some of the interop problems between the vendors.

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

https://learn.mattr.global/api-reference/

https://w3c-ccg.github.io/ld-proofs/

Verifiable Credentials Data Model v1.1 (w3.org)

5 comments

  1. […] Implement Compound Proof BBS+ verifiable credentials using ASP.NET Core and MATTR [#.NET Core #ASP.NET Core #OAuth2 #Security #Self Sovereign Identity #BBS+ #Mattr #OIDC #OpenId connect #SSI] […]

  2. […] Implement Compound Proof BBS+ verifiable credentials using ASP.NET Core and MATTR (Damien Bowden) […]

  3. […] Implement Compound Proof BBS+ verifiable credentials using ASP.NET Core and MATTR (Damien Bowden) […]

  4. […] Position Sticky With CSS Grid (Chris Coyier) A Simple Kubernetes Admission Webhook (Clément Labbe) Implement Compound Proof BBS+ verifiable credentials using ASP.NET Core and MATTR (Damien Bowden) Case-sensitivity on AWS – redux (Julian M. Bucknall) How to connect Azure SQL […]

  5. […] Implement Compound Proof BBS+ verifiable credentials using ASP.NET Core and MATTR – Damien Bowden […]

Leave a comment

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