Skip to content

Commit

Permalink
Add presentation for SD-JWTs (openwallet-foundation#1057)
Browse files Browse the repository at this point in the history
* feat: (WIP) presentation boilerplate

Signed-off-by: Micah Peltier <micah6_8@yahoo.com>

* refactor: split cred processor protocol

Signed-off-by: Daniel Bluhm <dbluhm@pm.me>

* feat: (WIP) sd jwt presentation

Signed-off-by: Micah Peltier <micah6_8@yahoo.com>

* feat: return unpacked sd jwt payload

Signed-off-by: Daniel Bluhm <dbluhm@pm.me>

* style: fix linting issues

Signed-off-by: Micah Peltier <micah6_8@yahoo.com>

* fix: issuance, test credo sd-jwt pres

Signed-off-by: Daniel Bluhm <dbluhm@pm.me>

* feat: adjust tests for new cred processor expextations

Signed-off-by: Micah Peltier <micah6_8@yahoo.com>

* fix: credo jwt-vc presentations

Signed-off-by: Daniel Bluhm <dbluhm@pm.me>

* fix: interop fixes for credo

Signed-off-by: Micah Peltier <micah6_8@yahoo.com>

* feat: update demo for sd-jwt pres

Signed-off-by: Micah Peltier <micah6_8@yahoo.com>

* feat: minor tweaks to make the demo happy

Signed-off-by: Micah Peltier <micah6_8@yahoo.com>

* feat: touch up the demo frontend

Signed-off-by: Daniel Bluhm <dbluhm@pm.me>

---------

Signed-off-by: Micah Peltier <micah6_8@yahoo.com>
Signed-off-by: Daniel Bluhm <dbluhm@pm.me>
Co-authored-by: Daniel Bluhm <dbluhm@pm.me>
  • Loading branch information
mepeltier and dbluhm authored Sep 27, 2024
1 parent a9928ce commit 9910c7a
Show file tree
Hide file tree
Showing 21 changed files with 821 additions and 238 deletions.
175 changes: 163 additions & 12 deletions oid4vc/demo/frontend/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@ async function issue_jwt_credential(req, res) {
headers: commonHeaders,
body: JSON.stringify({
cryptographic_binding_methods_supported: ["did"],
cryptographic_suites_supported: ["EdDSA"],
cryptographic_suites_supported: ["ES256"],
display: [
{
name: "University Credential",
Expand Down Expand Up @@ -432,9 +432,9 @@ async function issue_sdjwt_credential(req, res) {
}


// Begin Presentation Flow
async function create_presentation(presentationId, req, res) {

// Begin JWT VC JSON Presentation Flow
async function create_jwt_vc_presentation(req, res) {
const presentationId = req.params.id;
const commonHeaders = {
accept: "application/json",
"Content-Type": "application/json",
Expand Down Expand Up @@ -544,10 +544,146 @@ async function create_presentation(presentationId, req, res) {
body: JSON.stringify({
"pres_def_id": presentationDefinitionData.pres_def_id,
"vp_formats": {
"jwt_vc_json": { "alg": [ "ES256", "EdDSA" ] },
"jwt_vp_json": { "alg": [ "ES256", "EdDSA" ] },
"jwt_vc": { "alg": [ "ES256", "EdDSA" ] },
"jwt_vp": { "alg": [ "ES256", "EdDSA" ] }
"jwt_vp": { "alg": [ "ES256", "EdDSA" ] },
"jwt_vc_json": { "alg": [ "ES256", "EdDSA" ] },
"jwt_vp_json": { "alg": [ "ES256", "EdDSA" ] }
},
}),
};
events.emit(`presentation-${presentationId}`, {type: "message", message: `Generating Presentation Request.`});
events.emit(`presentation-${presentationId}`, {type: "message", message: `Posting Presentation Request to: ${presentationRequestUrl}`});
events.emit(`presentation-${presentationId}`, {type: "debug-message", message: "Request options", data: presentationRequestOptions});
const presentationRequestData = await fetchApiData(
presentationRequestUrl,
presentationRequestOptions
);
events.emit(`presentation-${presentationId}`, {type: "message", message: `Generated Presentation Request.`});
events.emit(`presentation-${presentationId}`, {type: "message", message: `Presentation Request URI: ${presentationRequestData?.request_uri}`});
events.emit(`presentation-${presentationId}`, {type: "debug-message", message: "Response data", data: presentationRequestData});

// Grab the relevant data and store it for later reference while waiting for the webhooks from ACA-Py
let code = presentationRequestData.request_uri;
presentationCache.set(presentationDefinitionData.pres_def_id, { presentationDefinitionData, presentationRequestData, presentationId: presentationId });
logger.trace(JSON.stringify(presentationRequestData, null, 2));

// Generate a QRCode and return it to the browser (HTMX replaces a div with our current response)
var qrcode = new QRCode({
content: code,
padding: 4,
width: 256,
height: 256,
color: "#000000",
background: "#ffffff",
ecl: "M",
});
qrcode = qrcode.svg()
qrcode = qrcode.substring(qrcode.indexOf('?>')+2,qrcode.length)
res.setHeader('Content-Type', 'text/html; charset=utf-8');
res.send(qrcode);

// Polling for the credential is an option at this stage, but we opt to just listen for the appropriate webhook instead
}

// Begin SD-JWT Presentation Flow
async function create_sd_jwt_presentation(req, res) {
const presentationId = req.params.id;
const commonHeaders = {
accept: "application/json",
"Content-Type": "application/json",
};
if (API_KEY) {
commonHeaders["X-API-KEY"] = API_KEY;
}
axios.defaults.withCredentials = true;
axios.defaults.headers.common["Access-Control-Allow-Origin"] = API_BASE_URL;
axios.defaults.headers.common["X-API-KEY"] = API_KEY;

const fetchApiData = async (url, options) => {
const response = await fetch(url, options);
return await response.json();
};


// Create Presentation Definition
events.emit(`presentation-${presentationId}`, {type: "message", message: "Creating Presentation Definition."});
const presentationDefinition = {"pres_def": {
"id": uuidv4(),
"purpose": "Present basic profile info",
"input_descriptors": [
{
"format": {
"vc+sd-jwt": {}
},
"id": "ID Card",
"name": "Profile",
"purpose": "Present basic profile info",
"constraints": {
"limit_disclosure": "required",
"fields": [
{
"path": [
"$.vct"
],
"filter": {
"type": "string"
}
},
{
"path": [
"$.family_name"
]
},
{
"path": [
"$.given_name"
]
}
]
}
}
]
}};

const presentationDefinitionUrl = `${API_BASE_URL}/oid4vp/presentation-definition`;
const presentationDefinitionOptions = {
method: "POST",
headers: commonHeaders,
body: JSON.stringify(presentationDefinition),
};
logger.warn(presentationDefinitionUrl);
events.emit(`presentation-${presentationId}`, {type: "message", message: `Posting Presentation Definition to: ${presentationDefinitionUrl}`});
events.emit(`presentation-${presentationId}`, {type: "debug-message", message: "Request options", data: presentationDefinitionOptions});
const presentationDefinitionData = await fetchApiData(
presentationDefinitionUrl,
presentationDefinitionOptions
);
logger.info("Created presentation?");
logger.trace(JSON.stringify(presentationDefinitionData));
logger.trace(presentationDefinitionData.pres_def_id);
events.emit(`presentation-${presentationId}`, {type: "message", message: `Created Presentation Definition`});
events.emit(`presentation-${presentationId}`, {type: "message", message: `Presentation Definition ID: ${presentationDefinitionData.pres_def_id}`});
events.emit(`presentation-${presentationId}`, {type: "debug-message", message: "Response data", data: presentationDefinitionData});


// Create Presentation Request
const presentationRequestUrl = `${API_BASE_URL}/oid4vp/request`;
const presentationRequestOptions = {
method: "POST",
headers: commonHeaders,
body: JSON.stringify({
"pres_def_id": presentationDefinitionData.pres_def_id,
"vp_formats": {
"vc+sd-jwt": {
"sd-jwt_alg_values": [
"ES256",
"ES384"
],
"kb-jwt_alg_values": [
"ES256",
"ES384"
]
}
},
}),
};
Expand Down Expand Up @@ -642,9 +778,9 @@ function handleEvents(event_type, req, res) {
if (state == "request-retrieved")
res.write(`event: status\ndata: <div style="text-align: center;">QRCode Scanned, awaiting presentation...</div>\n\n`);
if (state == "presentation-invalid")
res.write(`event: status\ndata: <div style="text-align: center;">PRESENTATION INVALID</div>\n\n`);
res.write(`event: status\ndata: <div style="text-align: center;">Presentaion verification failed</div>\n\n`);
if (state == "presentation-valid")
res.write(`event: status\ndata: <div style="text-align: center;">Presentation Valid</div>\n\n`);
res.write(`event: status\ndata: <div style="text-align: center;">Presentation Verified!</div>\n\n`);
}

// Handle OID4VCI webhooks
Expand Down Expand Up @@ -707,7 +843,7 @@ app.post("/issue", (req, res, next) => {
//events.on(`${event_type}-${req.params.id}`, (data) => {
console.log(req.body);
switch(req.body["credential-type"]) {
case "oid4vc":
case "jwt":
issue_jwt_credential(req, res).catch(next);
break;
case "sdjwt":
Expand All @@ -723,14 +859,29 @@ app.post("/issue", (req, res, next) => {
handleEvents("issuance", req, res);
});

app.get("/present/select/:id", (req, res) => {
console.log(req.query);
res.render(`present/${req.query["credential-type"]}`, {"page": "register", "presentationId": req.params.id});
});

// Render Presentation Exchange form
app.get("/present", (req, res) => {
res.render("presentation", {"page": "present", "presentationId": uuidv4()});
});

app.post("/present/create/:id", (req, res, next) => {
app.get("/present/create/:id", (req, res, next) => {
// Begin Presentation Exchange flow
create_presentation(req.params.id, req, res).catch(next);

switch(req.query["credential-type"]) {
case "jwt":
create_jwt_vc_presentation(req, res).catch(next);
break;
case "sdjwt":
create_sd_jwt_presentation(req, res).catch(next);
break;
default:
res.status(400).send("");
}
});

// Event Stream for Presentation page
Expand Down
117 changes: 61 additions & 56 deletions oid4vc/demo/frontend/templates/index.ejs
Original file line number Diff line number Diff line change
@@ -1,68 +1,73 @@
<html>
<head>
<title>OpenID4VC</title>
<link rel="stylesheet" href="https://www.w3schools.com/w3css/4/w3.css">
<script src="https://unpkg.com/htmx.org@2.0.1"></script>
<script src="https://unpkg.com/htmx-ext-sse@2.2.1/sse.js"></script>
</head>
<body>
<!-- Sidebar/menu -->
<nav class="w3-sidebar w3-green w3-collapse w3-top w3-large w3-padding" style="z-index:3;width:300px;font-weight:bold;" id="mySidebar"><br>
<a href="javascript:void(0)" onclick="w3_close()" class="w3-button w3-hide-large w3-display-topleft" style="width:100%;font-size:22px">Close Menu</a>
<div class="w3-container">
<h3 class="w3-padding-64"><b>ACA-Py<br />OpenID4VC<br />Plugin</b></h3>
</div>
<div class="w3-bar-block" id="navigation">
<%- include('navigation.ejs') %>
</div>
</nav>
<head>
<title>OpenID4VC Demo</title>
<link rel="stylesheet" href="https://www.w3schools.com/w3css/4/w3.css">
<script src="https://unpkg.com/htmx.org@2.0.1"></script>
<script src="https://unpkg.com/htmx-ext-sse@2.2.1/sse.js"></script>
</head>
<body>
<!-- Sidebar/menu -->
<nav class="w3-sidebar w3-green w3-collapse w3-top w3-large w3-padding" style="z-index:3;width:300px;font-weight:bold;" id="mySidebar"><br>
<a href="javascript:void(0)" onclick="w3_close()" class="w3-button w3-hide-large w3-display-topleft" style="width:100%;font-size:22px">Close Menu</a>
<div class="w3-container">
<h3 class="w3-padding-64"><b>ACA-Py<br />OpenID4VC<br />Plugin</b></h3>
</div>
<div class="w3-bar-block" id="navigation">
<%- include('navigation.ejs') %>
</div>
</nav>

<!-- Top menu on small screens -->
<header class="w3-container w3-top w3-hide-large w3-green w3-xlarge w3-padding">
<a href="javascript:void(0)" class="w3-button w3-green w3-margin-right" onclick="w3_open()">☰</a>
<span>OpenID4VC</span>
</header>
<!-- Top menu on small screens -->
<header class="w3-container w3-top w3-hide-large w3-green w3-xlarge w3-padding">
<a href="javascript:void(0)" class="w3-button w3-green w3-margin-right" onclick="w3_open()">☰</a>
<span>OpenID4VC Demo</span>
</header>

<!-- Overlay effect when opening sidebar on small screens -->
<div class="w3-overlay w3-hide-large" onclick="w3_close()" style="cursor:pointer" title="close side menu" id="myOverlay"></div>
<main style="display: flex; flex-direction: column; height: 100%;">
<!-- !PAGE CONTENT! -->
<div class="w3-main" style="margin-left:340px;margin-right:40px; flex-grow: 1; flex-shrink: 0; flex-basis: auto;">
<!-- Overlay effect when opening sidebar on small screens -->
<div class="w3-overlay w3-hide-large" onclick="w3_close()" style="cursor:pointer" title="close side menu" id="myOverlay"></div>
<main style="display: flex; flex-direction: column; height: 100%;">
<!-- !PAGE CONTENT! -->
<div class="w3-main" style="margin-left:340px;margin-right:40px; flex-grow: 1; flex-shrink: 0; flex-basis: auto;">

<!-- Header -->
<div class="w3-container" style="margin-top:80px" id="content-header">
<h1 class="w3-jumbo"><b>Admin Credential Demo</b></h1>
<h1 class="w3-xxxlarge w3-text-green"><b>Demo</b></h1>
<hr style="width:50px;border:5px solid green" class="w3-round">
</div>
<!-- Header -->
<div class="w3-container" style="margin-top:80px" id="content-header">
<h1 class="w3-jumbo"><b>OpenID4VC Demo</b></h1>
<hr style="width:50px;border:5px solid green" class="w3-round">
</div>


<!-- Designers -->
<div class="w3-container" id="content" style="margin-top:75px">
<p>Welcome to the demo interface for the ACA-Py OpenID4VC Plugin. This demo demonstrates both OpenID4VCI (OpenID for Verifiable Credential Issuance) and OpenID4VP (OpenID for Verifiable Presentations).</p>
</div>
<!-- Designers -->
<div class="w3-container" id="content" style="margin-top:75px">
<p>
Welcome to the demo interface for the ACA-Py OpenID4VC Plugin. This demo
demonstrates both OpenID4VCI (OpenID for Verifiable Credential Issuance)
and OpenID4VP (OpenID for Verifiable Presentations).

Click on the nav menu to issue or present a credential.
</p>
</div>

<!-- End page content -->
</div>

<footer style="flex-shrink: 0;">
<!-- W3.CSS Container -->
<div class="w3-light-grey w3-container w3-padding-32" style="margin-top:75px;padding-right:58px"><p class="w3-right">Powered by <a href="https://www.w3schools.com/w3css/default.asp" title="W3.CSS" target="_blank" class="w3-hover-opacity">w3.css</a></p></div>
<!-- End page content -->
</div>

<script>
// Script to open and close sidebar
function w3_open() {
document.getElementById("mySidebar").style.display = "block";
document.getElementById("myOverlay").style.display = "block";
}
<footer style="flex-shrink: 0;">
<!-- W3.CSS Container -->
<div class="w3-light-grey w3-container w3-padding-32" style="margin-top:75px;padding-right:58px"><p class="w3-right">Powered by <a href="https://www.w3schools.com/w3css/default.asp" title="W3.CSS" target="_blank" class="w3-hover-opacity">w3.css</a></p></div>

function w3_close() {
document.getElementById("mySidebar").style.display = "none";
document.getElementById("myOverlay").style.display = "none";
}
</script>
</footer>
</main>
</body>
<script>
// Script to open and close sidebar
function w3_open() {
document.getElementById("mySidebar").style.display = "block";
document.getElementById("myOverlay").style.display = "block";
}
function w3_close() {
document.getElementById("mySidebar").style.display = "none";
document.getElementById("myOverlay").style.display = "none";
}
</script>
</footer>
</main>
</body>
</html>
Loading

0 comments on commit 9910c7a

Please sign in to comment.