Implement Compound Proof BBS+ verifiable credentials using ASP.NET Core and MATT...
source link: https://damienbod.com/2021/12/13/implement-compound-proof-bbs-verifiable-credentials-using-asp-net-core-and-mattr/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
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"
: [
{
},
],
"credentialStatus"
: {
"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
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
>{
},
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
>{
},
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"
: [
],
"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"
: [
],
"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();
}
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);
//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>
/// </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);
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://learn.mattr.global/tutorials/verify/using-callback/callback-e-to-e
https://mattr.global/get-started/
Generating a ZKP-enabled BBS+ credential using the MATTR Platform
https://learn.mattr.global/tutorials/dids/did-key
https://gunnarpeipman.com/httpclient-remove-charset/
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/
4 comments
-
[…] 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] […]
-
[…] Implement Compound Proof BBS+ verifiable credentials using ASP.NET Core and MATTR (Damien Bowden) […]
-
[…] Implement Compound Proof BBS+ verifiable credentials using ASP.NET Core and MATTR (Damien Bowden) […]
-
[…] 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 […]
Leave a Reply Cancel reply
This site uses Akismet to reduce spam. Learn how your comment data is processed.
Recommend
About Joyk
Aggregate valuable and interesting links.
Joyk means Joy of geeK