Skip to content

Added presentation message to sample #93

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Jan 31, 2025
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions sample/README.md
Original file line number Diff line number Diff line change
@@ -31,7 +31,7 @@ The *Setup Service* sets up the Crescent parameters. These parameters, identifie

To obtain a JWT, Alice visits the Issuer welcome page using a browser with the *Browser Extension* installed and the *Client Helper* running. She logs in using her username and password, and clicks "Issue" to get issued a JWT. The browser extension reads the JWT from the HTML page and sends it to the Client Helper which 1) retrieves the corresponding Crescent parameters from the Setup Service, and 2) runs the `prove` library function preparing the JWT for later showing. The proving parameters are stored in the Client Helper and associated with the JWT. A mDL can be loaded directly into the Browser Extension, in absence of a sample issuance workflow; the same preparation steps are performed by the Client Helper.

Later, Alice visits a *Verifier* page. Her browser extension detects a meta tag indicating a Crescent proof request requesting a specific disclosure UID (see below). She opens the extensions and selects the credential to use (matching the requesting type (JWT or mDL) and disclosure capabilities). The Client then generates a showing by calling the `show` library function and sends it to the Verifier endpoint specified in a meta tag. Upon reception, the Verifier downloads the validation parameters from the Setup Service (the first time it sees a presentation for the schema UID) and, for JWTs, the Issuer's public key (the first time it sees credential from this Issuer), and calls the `verify` library function. Upon successful proof validation, Alice is granted access.
Later, Alice visits a *Verifier* page. Her browser extension detects a meta tag indicating a Crescent proof request requesting a specific disclosure UID (see below), and specifying a random session ID value (the challenge) to be signed by the client as the presentation message, to prevent replay attacks. She opens the extensions and selects the credential to use (matching the requesting type (JWT or mDL) and disclosure capabilities). The Client then generates a showing by calling the `show` library function and sends it to the Verifier endpoint specified in a meta tag. Upon reception, the Verifier downloads the validation parameters from the Setup Service (the first time it sees a presentation for the schema UID) and, for JWTs, the Issuer's public key (the first time it sees credential from this Issuer), and calls the `verify` library function. Upon successful proof validation, Alice is granted access.

# Sample details

@@ -109,10 +109,10 @@ sequenceDiagram
participant V as Verifier
participant I as Issuer
B->>V: visit login page
V->>E: read {disclosure_UID, verify_URL} from <meta> tag
V->>E: read {disclosure_UID, verify_URL, challenge} from <meta> tag
E->>E: filter JWT that support disclosure_uid
B->>E: user selects a JWT to present
E->>C: fetch show proof from /show?cred_uid=<cred_UID>&disc_uid=<disclosure_uid>
E->>C: fetch show proof from /show?cred_uid=<cred_UID>&disc_uid=<disclosure_uid>&pm=<challenge>
C->>C: generate Crescent proof
C->>E: return proof
E->>V: post {proof, schema_uid, issuer_UID, disclosure_uid} to verify_URL
9 changes: 5 additions & 4 deletions sample/client/src/components/card.ts
Original file line number Diff line number Diff line change
@@ -18,7 +18,7 @@ export class CardElement extends LitElement {
private _ready = false
private readonly _status: Card_status = 'PENDING'
private readonly _progress = 0
public _disclosureParams: { verifierUrl: string, disclosureValue: string, disclosureUid: string } | null = null
public _disclosureParams: { verifierUrl: string, disclosureValue: string, disclosureUid: string, disclosureChallenge: string } | null = null

@property({ type: Object })
private _credential: CredentialWithCard | null = null
@@ -227,15 +227,15 @@ export class CardElement extends LitElement {
(getElementById<HTMLDivElement>('errorMessage')).innerText = message
}

discloseRequest (verifierUrl: string, disclosureValue: string, disclosureUid: string): void {
discloseRequest (verifierUrl: string, disclosureValue: string, disclosureUid: string, disclosureChallenge: string): void {
assert(this.shadowRoot)
const disclosePropertyLabel = this.shadowRoot.querySelector<HTMLParagraphElement>('#disclosePropertyLabel')
assert(disclosePropertyLabel)
const discloseVerifierLabel = this.shadowRoot.querySelector<HTMLParagraphElement>('#discloseVerifierLabel')
assert(discloseVerifierLabel)
disclosePropertyLabel.innerText = `${disclosureValue}`
discloseVerifierLabel.innerText = `to ${verifierUrl.replace(/:\d+.+$/g, '')}?`
this._disclosureParams = { verifierUrl, disclosureValue, disclosureUid }
this._disclosureParams = { verifierUrl, disclosureValue, disclosureUid, disclosureChallenge }
}

get progress (): { show: () => void, hide: () => void, value: number, label: string } {
@@ -302,7 +302,8 @@ export class CardElement extends LitElement {
assert(this._credential)
assert(this._disclosureParams?.verifierUrl)
assert(this._disclosureParams.disclosureUid)
this._credential.disclose(this._disclosureParams.verifierUrl, this._disclosureParams.disclosureUid)
assert(this._disclosureParams.disclosureChallenge)
this._credential.disclose(this._disclosureParams.verifierUrl, this._disclosureParams.disclosureUid, this._disclosureParams.disclosureChallenge)
}

get credential (): CredentialWithCard {
7 changes: 4 additions & 3 deletions sample/client/src/content.ts
Original file line number Diff line number Diff line change
@@ -21,11 +21,12 @@ async function scanForCredential (): Promise<void> {
}
}

function queryDisclosureRequest (): { url: string, uid: string } | null {
function queryDisclosureRequest (): { url: string, uid: string, challenge: string } | null {
const verifyUrl = document.querySelector('meta[crescent_verify_url]')?.getAttribute('crescent_verify_url') ?? ''
const disclosureUid = document.querySelector('meta[crescent_disclosure_uid]')?.getAttribute('crescent_disclosure_uid') ?? ''
if (verifyUrl.length > 0 && disclosureUid.length > 0) {
return { url: verifyUrl, uid: disclosureUid }
const challenge = document.querySelector('meta[crescent_challenge]')?.getAttribute('crescent_challenge') ?? ''
if (verifyUrl.length > 0 && disclosureUid.length > 0 && challenge.length > 0) {
return { url: verifyUrl, uid: disclosureUid, challenge: challenge }
}
return null
}
8 changes: 4 additions & 4 deletions sample/client/src/cred.ts
Original file line number Diff line number Diff line change
@@ -211,20 +211,20 @@ export class CredentialWithCard extends Credential {
this._onStatusChangeCallback?.(status)
}

public discloserRequest (url: string, disclosureUid: string): void {
public discloserRequest (url: string, disclosureUid: string, challenge: string): void {
if (this.status !== 'PREPARED') {
return
}
const disclosureProperty = this.getDisclosureProperty(disclosureUid)
if (disclosureProperty === null) {
return
}
this.element.discloseRequest(url, disclosureProperty, disclosureUid)
this.element.discloseRequest(url, disclosureProperty, disclosureUid, challenge)
this.status = 'DISCLOSABLE'
}

public disclose (url: string, disclosureUid: string): void {
void verifier.disclose(this, url, disclosureUid)
public disclose (url: string, disclosureUid: string, challenge: string): void {
void verifier.disclose(this, url, disclosureUid, challenge)
this.status = 'PREPARED'
window.close()
}
4 changes: 2 additions & 2 deletions sample/client/src/popup.ts
Original file line number Diff line number Diff line change
@@ -143,11 +143,11 @@ async function init (): Promise<void> {
}

async function scanForDisclosureRequest (): Promise<void> {
const disclosureRequest = await messageToActiveTab<{ url: string, uid: string } | null>(MSG_POPUP_CONTENT_SCAN_DISCLOSURE)
const disclosureRequest = await messageToActiveTab<{ url: string, uid: string, challenge: string } | null>(MSG_POPUP_CONTENT_SCAN_DISCLOSURE)
console.debug('disclosureRequest', disclosureRequest)
if (disclosureRequest != null) {
CredentialWithCard.creds.forEach((cred) => {
cred.discloserRequest(disclosureRequest.url, disclosureRequest.uid)
cred.discloserRequest(disclosureRequest.url, disclosureRequest.uid, disclosureRequest.challenge)
})
}
}
25 changes: 0 additions & 25 deletions sample/client/src/utils.ts
Original file line number Diff line number Diff line change
@@ -90,31 +90,6 @@ export function base64Decode (base64: string): Uint8Array {
}
}

// eslint-disable-next-line @typescript-eslint/naming-convention, @typescript-eslint/max-params
function _postToURL (tabId: number, url: string, issuer_url: string, schema_uid: string, proof: string): void {
const formHtml = `
<form id="postForm" action="${url}" method="POST" style="display: none;">
<input type="hidden" name="issuer_url" value="${issuer_url}">
<input type="hidden" name="schema_uid" value="${schema_uid}">
<input type="hidden" name="proof" value="${proof}">
</form>
<script>
document.getElementById('postForm').submit();
</script>
`

void chrome.scripting.executeScript({
target: { tabId },
func: (formHtml) => {
console.log('Injecting form:', formHtml)
const formContainer = document.createElement('div')
formContainer.innerHTML = formHtml
document.body.appendChild(formContainer)
},
args: [formHtml]
})
}

export function guid (): string {
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (char) {
const random = (Math.random() * 16) | 0
13 changes: 7 additions & 6 deletions sample/client/src/verifier.ts
Original file line number Diff line number Diff line change
@@ -17,20 +17,20 @@ export interface ClientHelperShowResponse {

export type ShowProof = string

export async function show (cred: Credential, disclosureUid: string): Promise<RESULT<ShowProof, Error>> {
const response = await fetchText(`${config.clientHelperUrl}/show`, { cred_uid: cred.id, disc_uid: disclosureUid }, 'GET')
export async function show (cred: Credential, disclosureUid: string, challenge: string): Promise<RESULT<ShowProof, Error>> {
const response = await fetchText(`${config.clientHelperUrl}/show`, { cred_uid: cred.id, disc_uid: disclosureUid, challenge: challenge }, 'GET')
if (!response.ok) {
console.error('Failed to show:', response.error)
return response
}
return response
}

async function handleDisclose (id: string, destinationUrl: string, disclosureUid: string): Promise<void> {
async function handleDisclose (id: string, destinationUrl: string, disclosureUid: string, challenge: string): Promise<void> {
const cred = Credential.get(id)
assert(cred)

const showProof = await show(cred, disclosureUid)
const showProof = await show(cred, disclosureUid, challenge)
if (!showProof.ok) {
console.error('Failed to show proof:', showProof.error)
return
@@ -41,14 +41,15 @@ async function handleDisclose (id: string, destinationUrl: string, disclosureUid
disclosure_uid: disclosureUid,
issuer_url: cred.data.issuer.url,
schema_uid: cred.data.token.schema,
session_id: challenge,
proof: showProof.value
}

void messageToActiveTab(MSG_BACKGROUND_CONTENT_SEND_PROOF, params)
}

export async function disclose (cred: Credential, verifierUrl: string, disclosureUid: string): Promise<void> {
void sendMessage('background', MSG_POPUP_BACKGROUND_DISCLOSE, cred.id, verifierUrl, disclosureUid)
export async function disclose (cred: Credential, verifierUrl: string, disclosureUid: string, challenge: string): Promise<void> {
void sendMessage('background', MSG_POPUP_BACKGROUND_DISCLOSE, cred.id, verifierUrl, disclosureUid, challenge)
}

// if this is running the the extension background service worker, then listen for messages
14 changes: 8 additions & 6 deletions sample/client_helper/src/main.rs
Original file line number Diff line number Diff line change
@@ -247,11 +247,13 @@ async fn get_show_data(cred_uid: String, state: &State<SharedState>) -> Result<J
}
}

#[get("/show?<cred_uid>&<disc_uid>")]
async fn show<'a>(cred_uid: String, disc_uid: String, state: &State<SharedState>) -> Result<String, String> {
println!("*** /show called with credential UID {} and disc_uid {}", cred_uid, disc_uid);
#[get("/show?<cred_uid>&<disc_uid>&<challenge>")]
async fn show<'a>(cred_uid: String, disc_uid: String, challenge: String, state: &State<SharedState>) -> Result<String, String> {
println!("*** /show called with credential UID {}, disc_uid {}, and challenge {}", cred_uid, disc_uid, challenge);
let tasks = state.inner().0.lock().await;

// Parse the challenge as a byte array for the presentation message
let pm = challenge.as_bytes();

match tasks.get(&cred_uid) {
Some(Some(show_data)) => {

@@ -273,10 +275,10 @@ async fn show<'a>(cred_uid: String, disc_uid: String, state: &State<SharedState>
let show_proof =
if &client_state.credtype == "mdl" {
let age = disc_uid_to_age(&disc_uid).map_err(|_| "Disclosure UID does not have associated age parameter".to_string())?;
create_show_proof_mdl(&mut client_state, &range_pk, None, &io_locations, age)
create_show_proof_mdl(&mut client_state, &range_pk, Some(pm), &io_locations, age)
}
else {
create_show_proof(&mut client_state, &range_pk, None, &io_locations)
create_show_proof(&mut client_state, &range_pk, Some(pm), &io_locations)
};

// Return the show proof as a base64-url encoded string
Empty file modified sample/issuer/setup_issuer.sh
100644 → 100755
Empty file.
39 changes: 28 additions & 11 deletions sample/verifier/src/main.rs
Original file line number Diff line number Diff line change
@@ -12,7 +12,7 @@ use rocket::response::status::Custom;
use rocket::State;
use rocket::fs::{FileServer, NamedFile};
use rocket::http::Status;
use std::collections::HashMap;
use std::collections::{HashMap, HashSet};
use serde_json::Value;
use jsonwebkey::JsonWebKey;
use std::path::Path;
@@ -46,6 +46,10 @@ struct VerifierConfig {
site2_verifier_name: String,
site2_verifier_domain: String,

// holds active session IDs (in a real system, these would be removed
// after a timeout period)
active_session_ids: Mutex<HashSet<String>>,

// holds validation state
validation_results: Mutex<HashMap<String, ValidationResult>>,
}
@@ -56,7 +60,8 @@ struct ProofInfo {
proof: String,
schema_uid: String,
issuer_url: String,
disclosure_uid: String,
disclosure_uid: String,
session_id: String,
}

// helper function to provide the base context for the login page
@@ -66,11 +71,15 @@ fn base_context(verifier_config: &State<VerifierConfig>) -> HashMap<String, Stri
let site2_verifier_name_str = verifier_config.site2_verifier_name.clone();
let site2_verify_url_str = verifier_config.site2_verify_url.clone();

let session_id = Uuid::new_v4().to_string();
verifier_config.active_session_ids.lock().unwrap().insert(session_id.clone());

let mut context = HashMap::new();
context.insert("site1_verifier_name".to_string(), site1_verifier_name_str);
context.insert("site1_verify_url".to_string(), site1_verify_url_str);
context.insert("site2_verifier_name".to_string(), site2_verifier_name_str);
context.insert("site2_verify_url".to_string(), site2_verify_url_str);
context.insert("session_id".to_string(), session_id);

context
}
@@ -217,11 +226,18 @@ macro_rules! error_template {
#[post("/verify", format = "json", data = "<proof_info>")]
async fn verify(proof_info: Json<ProofInfo>, verifier_config: &State<VerifierConfig>) -> Result<Custom<Redirect>, Template> {
println!("*** /verify called");
println!("Session ID: {}", proof_info.session_id);
println!("Schema UID: {}", proof_info.schema_uid);
println!("Issuer URL: {}", proof_info.issuer_url);
println!("Disclosure UID: {}", proof_info.disclosure_uid);
println!("Proof: {}", proof_info.proof);

// check if session_id is present in active_session_ids
if !verifier_config.active_session_ids.lock().unwrap().contains(&proof_info.session_id) {
let msg = format!("Unknown session ID ({})", proof_info.session_id);
error_template!(msg, verifier_config);
}

// verify if the schema_uid is one of our supported SCHEMA_UIDS
if !SCHEMA_UIDS.contains(&proof_info.schema_uid.as_str()) {
let msg = format!("Unsupported schema UID ({})", proof_info.schema_uid);
@@ -238,7 +254,10 @@ async fn verify(proof_info: Json<ProofInfo>, verifier_config: &State<VerifierCon
Ok(cred_type) => cred_type,
Err(_) => error_template!("Credential type not found", verifier_config),
};


// Parse the challenge session ID as a byte array for the presentation message
let pm = proof_info.session_id.as_bytes();

// Define base folder path and credential-specific folder path
let base_folder = format!("{}/{}", CRESCENT_DATA_BASE_PATH, proof_info.schema_uid);
let shared_folder = format!("{}/{}", base_folder, CRESCENT_SHARED_DATA_SUFFIX);
@@ -275,30 +294,27 @@ async fn verify(proof_info: Json<ProofInfo>, verifier_config: &State<VerifierCon
let is_valid;
let disclosed_info;
if cred_type == "jwt" {
let (valid, info) = verify_show(&vp, &show_proof, None);
let (valid, info) = verify_show(&vp, &show_proof, Some(pm));
is_valid = valid;
disclosed_info = Some(info);
} else {
let age = disc_uid_to_age(&proof_info.disclosure_uid).unwrap(); // disclosure UID validated, so unwrap should be safe
let (valid, info) = verify_show_mdl(&vp, &show_proof, None, age);
let (valid, info) = verify_show_mdl(&vp, &show_proof, Some(pm), age);
is_valid = valid;
disclosed_info = Some(info);
}

if is_valid {
// Generate a unique session_id
let session_id = Uuid::new_v4().to_string();

// Store the validation result in the hashmap
let validation_result = ValidationResult {
disclosed_info: disclosed_info.clone(),
};
verifier_config.validation_results.lock().unwrap().insert(session_id.clone(), validation_result);
verifier_config.validation_results.lock().unwrap().insert(proof_info.session_id.clone(), validation_result);

// Redirect to the resource page or signup2 page with the session_id as a query parameter
let redirect_url = match cred_type {
"jwt" => uri!(resource_page(session_id = session_id.clone())).to_string(),
"mdl" => uri!(signup2_page(session_id = session_id.clone())).to_string(),
"jwt" => uri!(resource_page(session_id = proof_info.session_id.clone())).to_string(),
"mdl" => uri!(signup2_page(session_id = proof_info.session_id.clone())).to_string(),
_ => error_template!("Unsupported credential type", verifier_config),
};

@@ -341,6 +357,7 @@ fn rocket() -> _ {
site2_verifier_name,
site2_verifier_domain,
site2_verify_url,
active_session_ids: Mutex::new(HashSet::new()),
validation_results: Mutex::new(HashMap::new()),
};

1 change: 1 addition & 0 deletions sample/verifier/templates/login.html.tera
Original file line number Diff line number Diff line change
@@ -4,6 +4,7 @@
<meta charset="UTF-8">
<meta crescent_verify_url="{{ site1_verify_url | safe }}">
<meta crescent_disclosure_uid="crescent://email_domain">
<meta crescent_challenge="{{ session_id }}">
<title>Login - {{ site1_verifier_name }}</title>
<link rel="stylesheet" href="css/site1_style.css">
<link rel="icon" href="img/site1-favicon.ico" type="image/x-icon">
1 change: 1 addition & 0 deletions sample/verifier/templates/signup1.html.tera
Original file line number Diff line number Diff line change
@@ -4,6 +4,7 @@
<meta charset="UTF-8">
<meta crescent_verify_url="{{ site2_verify_url | safe }}">
<meta crescent_disclosure_uid="crescent://over_18">
<meta crescent_challenge="{{ session_id }}">
<title>{{ site2_verifier_name }} Sign Up</title>
<link rel="stylesheet" href="css/site2_style.css">
<link rel="icon" href="img/site2-favicon.ico" type="image/x-icon">