Skip to content

Commit

Permalink
Add second consent message language and make message more descriptive
Browse files Browse the repository at this point in the history
This PR improves the consent messages to be available in 2 languages.
The message is improved to be more human readable and is now formatted
as markdown.

Additionally, the ICRC21 types were updated to reflect the recent
spec changes.
  • Loading branch information
Frederik Rothenberger committed Nov 23, 2023
1 parent 05b1f99 commit 2026a4f
Show file tree
Hide file tree
Showing 5 changed files with 170 additions and 82 deletions.
107 changes: 107 additions & 0 deletions demos/vc_issuer/src/consent_message.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
//! This module contains the various consent messages that is displayed to the user when they are asked to consent to the issuance of a credential.
use crate::SupportedCredentialType;
use std::fmt::{Display, Formatter};
use vc_util::issuer_api::{
Icrc21ConsentInfo, Icrc21ConsentMessageResponse, Icrc21ConsentPreferences,
};
use SupportedCredentialType::{UniversityDegreeCredential, VerifiedEmployee};
use SupportedLanguage::{English, German};

const EMPLOYMENT_VC_DESCRIPTION_ENG: &str = r###"# DFINITY Foundation Employment Credential
Credential that states that the holder is employed by the DFINITY Foundation at the time of issuance."###;
const DEGREE_VC_DESCRIPTION_ENG: &str = r###"# Bachelor of Engineering, DFINITY College of Engineering
Credential that states that the holder has a degree in engineering from the DFINITY College of Engineering."###;
const VALIDITY_INFO_ENG: &str = "The credential is valid for 15 minutes.";
const ANONYMITY_DISCLAIMER_ENG: &str = "This credential does **not** contain any additional personal information. It is issued to an ephemeral identity that is created for the sole purpose of issuing this credential.";

const EMPLOYMENT_VC_DESCRIPTION_DE: &str = r###"# Beschäftigungsausweis DFINITY Stiftung
Ausweis, der bestätigt, dass der Besitzer oder die Besitzerin zum Zeitpunkt der Austellung bei der DFINITY Stiftung beschäftigt ist."###;
const DEGREE_VC_DESCRIPTION_DE: &str = r###"# Bachelor of Engineering, DFINITY Hochschule für Ingenieurwissenschaften
Ausweis, der bestätigt, dass der Besitzer oder die Besitzerin einen Bachelorabschluss in einer Ingenieurwissenschaft der DFINITY Hochschule für Ingenieurwissenschaften besitzt."###;

const VALIDITY_INFO_DE: &str = "Dieser Ausweis ist gültig für 15 Minuten.";
const ANONYMITY_DISCLAIMER_DE: &str =
"Dieser Ausweis enthält **keine** zusätzlichen persönlichen Daten. Er wird auf eine kurzlebige Identität lautend ausgestellt, die für den alleinigen Verwendungszweck der Austellung dieses Ausweises erzeugt wird.";

pub enum SupportedLanguage {
English,
German,
}

impl From<Icrc21ConsentPreferences> for SupportedLanguage {
fn from(value: Icrc21ConsentPreferences) -> Self {
match &value.language.to_lowercase()[..2] {
"de" => German,
_ => English, // english is also the fallback
}
}
}

impl Display for SupportedLanguage {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match self {
English => write!(f, "en"),
German => write!(f, "de"),
}
}
}

pub fn get_vc_consent_message(
credential_type: &SupportedCredentialType,
language: &SupportedLanguage,
) -> Icrc21ConsentMessageResponse {
let message = match (credential_type, language) {
(VerifiedEmployee(_), English) => employment_consent_msg_eng(),
(VerifiedEmployee(_), German) => employment_consent_msg_de(),
(UniversityDegreeCredential(_), English) => degree_consent_msg_eng(),
(UniversityDegreeCredential(_), German) => degree_consent_msg_de(),
};
Icrc21ConsentMessageResponse::Ok(Icrc21ConsentInfo {
consent_message: message,
language: format!("{}", language),
})
}

fn employment_consent_msg_eng() -> String {
format_message(
EMPLOYMENT_VC_DESCRIPTION_ENG,
VALIDITY_INFO_ENG,
ANONYMITY_DISCLAIMER_ENG,
)
}

fn employment_consent_msg_de() -> String {
format_message(
EMPLOYMENT_VC_DESCRIPTION_DE,
VALIDITY_INFO_DE,
ANONYMITY_DISCLAIMER_DE,
)
}

fn degree_consent_msg_eng() -> String {
format_message(
DEGREE_VC_DESCRIPTION_ENG,
VALIDITY_INFO_ENG,
ANONYMITY_DISCLAIMER_ENG,
)
}

fn degree_consent_msg_de() -> String {
format_message(
DEGREE_VC_DESCRIPTION_DE,
VALIDITY_INFO_DE,
ANONYMITY_DISCLAIMER_DE,
)
}

fn format_message(description: &str, validity_info: &str, anonymity_disclaimer: &str) -> String {
format!(
"{} {}\n\n{}",
description, validity_info, anonymity_disclaimer
)
}
61 changes: 25 additions & 36 deletions demos/vc_issuer/src/main.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
use crate::consent_message::{get_vc_consent_message, SupportedLanguage};
use candid::{candid_method, CandidType, Deserialize, Principal};
use canister_sig_util::signature_map::{SignatureMap, LABEL_SIG};
use canister_sig_util::{extract_raw_root_pk_from_der, CanisterSigPublicKey, IC_ROOT_PK_DER};
Expand All @@ -16,9 +17,9 @@ use serde_json::json;
use sha2::{Digest, Sha256};
use std::borrow::Cow;
use std::cell::RefCell;
use std::collections::{HashMap, HashSet};
use std::collections::HashSet;
use vc_util::issuer_api::{
ArgumentValue, CredentialSpec, GetCredentialRequest, GetCredentialResponse, Icrc21ConsentInfo,
ArgumentValue, CredentialSpec, GetCredentialRequest, GetCredentialResponse,
Icrc21ConsentMessageResponse, Icrc21Error, Icrc21ErrorInfo, Icrc21VcConsentMessageRequest,
IssueCredentialError, IssuedCredentialData, PrepareCredentialRequest,
PrepareCredentialResponse, PreparedCredentialData, SignedIdAlias,
Expand All @@ -29,6 +30,8 @@ use vc_util::{
};
use SupportedCredentialType::{UniversityDegreeCredential, VerifiedEmployee};

mod consent_message;

/// We use restricted memory in order to ensure the separation between non-managed config memory (first page)
/// and the managed memory for potential other data of the canister.
type Memory = RestrictedMemory<DefaultMemoryImpl>;
Expand Down Expand Up @@ -171,11 +174,6 @@ async fn prepare_credential(req: PrepareCredentialRequest) -> PrepareCredentialR
Ok(alias_tuple) => alias_tuple,
Err(err) => return PrepareCredentialResponse::Err(err),
};
if let Err(err) = verify_credential_spec(&req.credential_spec) {
return PrepareCredentialResponse::Err(IssueCredentialError::UnsupportedCredentialSpec(
err,
));
}
let credential_type = match verify_credential_spec(&req.credential_spec) {
Ok(credential_type) => credential_type,
Err(err) => {
Expand Down Expand Up @@ -262,16 +260,18 @@ fn get_credential(req: GetCredentialRequest) -> GetCredentialResponse {
#[update]
#[candid_method]
async fn vc_consent_message(req: Icrc21VcConsentMessageRequest) -> Icrc21ConsentMessageResponse {
if let Err(err) = verify_credential_spec(&req.credential_spec) {
return Icrc21ConsentMessageResponse::Err(Icrc21Error::NotSupported(Icrc21ErrorInfo {
description: err,
error_code: 0,
}));
}
Icrc21ConsentMessageResponse::Ok(Icrc21ConsentInfo {
consent_message: get_vc_consent_message(&req),
language: "en-US".to_string(),
})
let credential_type = match verify_credential_spec(&req.credential_spec) {
Ok(credential_type) => credential_type,
Err(err) => {
return Icrc21ConsentMessageResponse::Err(Icrc21Error::UnsupportedCanisterCall(
Icrc21ErrorInfo {
error_code: 0,
description: err,
},
));
}
};
get_vc_consent_message(&credential_type, &SupportedLanguage::from(req.preferences))
}

fn verify_credential_spec(spec: &CredentialSpec) -> Result<SupportedCredentialType, String> {
Expand Down Expand Up @@ -340,23 +340,6 @@ fn verify_single_argument(
Ok(())
}

fn get_vc_consent_message(req: &Icrc21VcConsentMessageRequest) -> String {
format!(
"Issue credential '{}' with arguments:{}",
req.credential_spec.credential_name,
arguments_as_string(&req.credential_spec.arguments)
)
}
fn arguments_as_string(maybe_args: &Option<HashMap<String, ArgumentValue>>) -> String {
let mut arg_str = String::new();
let empty_args = HashMap::new();
let args = maybe_args.as_ref().unwrap_or(&empty_args);
for (key, value) in args {
arg_str.push_str(&format!("\n\t{}: {}", key, value));
}
arg_str
}

#[update]
#[candid_method]
fn add_employee(employee_id: Principal) -> String {
Expand Down Expand Up @@ -482,13 +465,19 @@ fn prepare_credential_payload(
EMPLOYEES.with_borrow(|employees| {
verify_authorized_principal(credential_type, alias_tuple, employees)
})?;
Ok(dfinity_employment_credential(alias_tuple.id_alias, employer_name.as_str()))
Ok(dfinity_employment_credential(
alias_tuple.id_alias,
employer_name.as_str(),
))
}
UniversityDegreeCredential(institution_name) => {
GRADUATES.with_borrow(|graduates| {
verify_authorized_principal(credential_type, alias_tuple, graduates)
})?;
Ok(bachelor_degree_credential(alias_tuple.id_alias, institution_name.as_str()))
Ok(bachelor_degree_credential(
alias_tuple.id_alias,
institution_name.as_str(),
))
}
}
}
Expand Down
69 changes: 34 additions & 35 deletions demos/vc_issuer/tests/issue_credential.rs
Original file line number Diff line number Diff line change
Expand Up @@ -163,36 +163,41 @@ mod api {
/// Verifies that the consent message can be requested.
#[test]
fn should_return_vc_consent_message() {
let test_cases = [
("en-US", "en", "# DFINITY Foundation Employment Credential"),
("de-DE", "de", "# Beschäftigungsausweis DFINITY Stiftung"),
("ja_JP", "en", "# DFINITY Foundation Employment Credential"), // test fallback language
];
let env = env();
let canister_id = install_canister(&env, VC_ISSUER_WASM.clone());

let mut args = HashMap::new();
args.insert(
"employerName".to_string(),
ArgumentValue::String("DFINITY Foundation".to_string()),
);
let consent_message_request = Icrc21VcConsentMessageRequest {
credential_spec: CredentialSpec {
credential_name: "VerifiedEmployee".to_string(),
arguments: Some(args),
},
preferences: Icrc21ConsentPreferences {
language: "en-US".to_string(),
},
};
for (requested_language, actual_language, consent_message_snippet) in test_cases {
let mut args = HashMap::new();
args.insert(
"employerName".to_string(),
ArgumentValue::String("DFINITY Foundation".to_string()),
);
let consent_message_request = Icrc21VcConsentMessageRequest {
credential_spec: CredentialSpec {
credential_name: "VerifiedEmployee".to_string(),
arguments: Some(args),
},
preferences: Icrc21ConsentPreferences {
language: requested_language.to_string(),
},
};

let response =
api::vc_consent_message(&env, canister_id, principal_1(), &consent_message_request)
.expect("API call failed")
.expect("Got 'None' from vc_consent_message");
assert_matches!(response, Icrc21ConsentMessageResponse::Ok(_));
if let Icrc21ConsentMessageResponse::Ok(info) = response {
assert_eq!(info.language, "en-US");
assert!(info
.consent_message
.contains("Issue credential 'VerifiedEmployee'"));
assert!(info.consent_message.contains("employerName"));
assert!(info.consent_message.contains("DFINITY Foundation"));
let response =
api::vc_consent_message(&env, canister_id, principal_1(), &consent_message_request)
.expect("API call failed")
.expect("Got 'None' from vc_consent_message");
assert_matches!(response, Icrc21ConsentMessageResponse::Ok(_));
if let Icrc21ConsentMessageResponse::Ok(info) = response {
assert_eq!(info.language, actual_language);
assert!(info
.consent_message
.starts_with(consent_message_snippet));
}
}
}

Expand All @@ -217,7 +222,7 @@ fn should_fail_vc_consent_message_if_not_supported() {
.expect("Got 'None' from vc_consent_message");
assert_matches!(
response,
Icrc21ConsentMessageResponse::Err(Icrc21Error::NotSupported(_))
Icrc21ConsentMessageResponse::Err(Icrc21Error::UnsupportedCanisterCall(_))
);
}

Expand All @@ -240,10 +245,7 @@ fn should_fail_vc_consent_message_if_missing_arguments() {
api::vc_consent_message(&env, canister_id, principal_1(), &consent_message_request)
.expect("API call failed")
.expect("Got 'None' from vc_consent_message");
assert_matches!(
response,
Icrc21ConsentMessageResponse::Err(Icrc21Error::NotSupported(_))
);
assert_matches!(response,Icrc21ConsentMessageResponse::Err(Icrc21Error::UnsupportedCanisterCall(_)));
}

#[test]
Expand All @@ -268,10 +270,7 @@ fn should_fail_vc_consent_message_if_missing_required_argument() {
api::vc_consent_message(&env, canister_id, principal_1(), &consent_message_request)
.expect("API call failed")
.expect("Got 'None' from vc_consent_message");
assert_matches!(
response,
Icrc21ConsentMessageResponse::Err(Icrc21Error::NotSupported(_))
);
assert_matches!(response,Icrc21ConsentMessageResponse::Err(Icrc21Error::UnsupportedCanisterCall(_)));
}

fn employee_credential_spec() -> CredentialSpec {
Expand Down
4 changes: 2 additions & 2 deletions demos/vc_issuer/vc_issuer.did
Original file line number Diff line number Diff line change
Expand Up @@ -57,8 +57,8 @@ type GetCredentialRequest = record {
prepared_context : opt vec nat8;
};
type GetCredentialResponse = variant {
ok : IssuedCredentialData;
err : IssueCredentialError;
Ok : IssuedCredentialData;
Err : IssueCredentialError;
};

type SignedIdAlias = record {
Expand Down
11 changes: 2 additions & 9 deletions src/vc_util/src/issuer_api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -121,13 +121,8 @@ pub struct Icrc21ErrorInfo {

#[derive(Clone, Debug, CandidType, Deserialize, Eq, PartialEq)]
pub enum Icrc21Error {
#[serde(rename = "forbidden")]
Forbidden(Icrc21ErrorInfo),
#[serde(rename = "malformed_call")]
MalformedCall(Icrc21ErrorInfo),
#[serde(rename = "not_supported")]
NotSupported(Icrc21ErrorInfo),
#[serde(rename = "generic_error")]
UnsupportedCanisterCall(Icrc21ErrorInfo),
ConsentMessageUnavailable(Icrc21ErrorInfo),
GenericError(Icrc21ErrorInfo),
}

Expand All @@ -139,9 +134,7 @@ pub struct Icrc21ConsentInfo {

#[derive(Clone, Debug, CandidType, Deserialize, Eq, PartialEq)]
pub enum Icrc21ConsentMessageResponse {
#[serde(rename = "ok")]
Ok(Icrc21ConsentInfo),
#[serde(rename = "err")]
Err(Icrc21Error),
}

Expand Down

0 comments on commit 2026a4f

Please sign in to comment.