Skip to content
Merged
Show file tree
Hide file tree
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
65 changes: 33 additions & 32 deletions index.html
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
SPDX-License-Identifier: BSD-3-Clause
-->


<html>
<head>
<meta http-equiv="content-security-policy" content="frame-src 'none'">
Expand Down Expand Up @@ -135,6 +136,30 @@
// FIXME: dereference template from `renderMethod.id`
const template = `
<div>
<h1 id="credentialSubject-name"></h1>
<p>Issued by: <span id="issuer-name"></span></p>
<div>
<label for="content">Selectively Disclosed Credential:</label>
<div><textarea id="content"></textarea></div>
</div>
<div>
The outer/Wallet HTML uses
<abbr title="Content-Security-Policy">CSP</abbr> set in its
<code>&lt;meta&gt;</code> tag to prevent the use of an
<code>src</code> attribute being used (i.e., "frame-src 'none'").
Consequently, clicking the following link will result in an error
(i.e., it won't hit the server at all):
<ul>
<li><a href="https://example.com/link-blocked">
Remote Link (should be blocked)</a>
</li>
<li>
<a href="${window.location.href}/?exfiltration=attempt">
Local Link (should be blocked)
</a>
</li>
</ul>
</div>
<script>
console.log('running template render script');

Expand Down Expand Up @@ -164,42 +189,18 @@
window.renderMethodReady()
}, ${useButton ? 2000 : 0});
<\/script>
<h1 id="credentialSubject-name"></h1>
<p>Issued by: <span id="issuer-name"></span></p>
<div>
<label for="content">Selectively Disclosed Credential:</label>
<div><textarea id="content"></textarea></div>
</div>
<div>
The outer/Wallet HTML uses
<abbr title="Content-Security-Policy">CSP</abbr> set in its
<code>&lt;meta&gt;</code> tag to prevent the use of an
<code>src</code> attribute being used (i.e., "frame-src 'none'").
Consequently, clicking the following link will result in an error
(i.e., it won't hit the server at all):
<ul>
<li><a href="https://example.com/link-blocked">
Remote Link (should be blocked)</a>
</li>
<li>
<a href="${window.location.href}/?exfiltration=attempt">
Local Link (should be blocked)
</a>
</li>
</ul>
</div>
</div>`;

const {iframe} = await render({
const {iframe, ready} = await render({
credential,
renderProperty: renderMethod.renderProperty,
template,
renderMethodReady() {
console.log('rendering ready');
const loading = document.querySelector('#loading');
loading.style.display = 'none';
iframe.style.display = 'block';
}
template
});
ready.then(() => {
console.log('rendering ready');
const loading = document.querySelector('#loading');
loading.style.display = 'none';
iframe.style.display = 'block';
});
const {style} = renderMethod.outputPreference;
for(const key in style) {
Expand Down
159 changes: 53 additions & 106 deletions render.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,22 @@
import {selectJsonLd} from './select.js';

// `template` is fully resolved / dereferenced
// `renderMethodReady` is a listener that will be called when template is ready
export async function render({
credential, renderProperty, template, renderMethodReady
} = {}) {
export async function render({credential, renderProperty, template} = {}) {
// filter credential (selective disclosure)
credential = selectJsonLd({
document: credential,
pointers: renderProperty ?? ['/']
});

// a promise that resolves when the rendering is ready (or rejects if it
// fails); can be used to show the display or an error instead
let resolveRender;
let rejectRender;
const readyPromise = new Promise((resolve, reject) => {
resolveRender = resolve;
rejectRender = reject;
});

// create iframe for sandboxed rendering
const iframe = document.createElement('iframe');
// permissions TBD
Expand All @@ -24,120 +30,61 @@ export async function render({
// browsers will use the meta tag in the iframe HTML which does not appear
// to be changeable by JavaScript once set (prevents change of the policy
// by a template even if `iframe.csp` does not work)
iframe.setAttribute('csp', `connect-src 'none'`);
iframe.setAttribute('csp', `default-src 'none' data: 'unsafe-inline'`);
iframe.onload = () => {
// create a MessageChannel; transfer one port to the iframe
const channel = new MessageChannel();
// start message queue so messages won't be lost while iframe loads
channel.port1.start();
// do we need any feedback from the iframe? perhaps when the template
// is ready:
channel.port1.onmessage = event => {
const {jsonrpc, method, params} = event.data;
if(!(jsonrpc === '2.0' &&
typeof method === 'string' &&
Array.isArray(params))) {
throw new Error('Unknown message format.');
}
if(method === 'renderMethodReady') {
renderMethodReady?.();
return;
// handle `ready` message
channel.port1.onmessage = function ready(event) {
if(event.data === 'ready') {
resolveRender();
} else {
rejectRender(new Error(event.data?.error?.message));
}
throw new Error(`Unknown RPC method "${method}".`);
channel.port1.onmessage = undefined;
};

// message name "start" TBD; send `port2` to iframe for comms
// send "start" message; send `port2` to iframe for return communication
iframe.contentWindow.postMessage('start', '*', [channel.port2]);

// tell the iframe to render the template
channel.port1.postMessage({
// message format TBD
jsonrpc: '2.0',
id: crypto.randomUUID(),
method: 'render',
// 'template' is fully resolved
params: [{credential, template}]
});
};

// start up the iframe
iframe.srcdoc = SRCDOC;
return {iframe};
}
iframe.srcdoc =
`<html>
<head>
<meta
http-equiv="content-security-policy"
content="default-src 'none' data: 'unsafe-inline'">
<script
name="credential"
type="application/vc">${JSON.stringify(credential, null, 2)}</script>
<script>
// add promise that will resolve to the communication port from
// the parent window
const portPromise = new Promise(resolve => {
window.addEventListener('message', function start(event) {
if(event.data === 'start' && event.ports?.[0]) {
window.removeEventListener('message', start);
resolve(event.ports[0]);
}
});
});

const SRCDOC = `
<html>
<head>
<meta http-equiv="content-security-policy" content="connect-src 'none'">
<script>
// bootstrap renderer
window.addEventListener('message', event => {
const {data: message, ports} = event;
const port = ports?.[0];
if(!(message === 'start' && port)) {
// ignore unknown message
return;
};

// we might want to attach a function to the window for the
// template to call when it's "ready" that will send a message to
// the parent so the parent can decide to show the iframe or not
if(!window.renderMethodReady) {
window.renderMethodReady = function() {
port.postMessage({
jsonrpc: '2.0',
// use a different method name for the other end?
method: 'renderMethodReady',
params: []
});
};
}
// attach a function to the window for the template to call when
// it's "ready" (or that an error occurred) that will send a message
// to the parent so the parent can decide whether to show the iframe
window.renderMethodReady = function(err) {
portPromise.then(port => port.postMessage(
!err ? 'ready' : {error: {message: err.message}}));
};
</script>
</head>

// start message queue for channel port
port.start();
// handle messages from parent
port.onmessage = event => {
const {jsonrpc, method, params} = event.data;
if(!(jsonrpc === '2.0' &&
typeof method === 'string' &&
Array.isArray(params))) {
throw new Error('Unknown message format.');
}
const [options] = params;
if(method === 'render') {
render(options);
return;
}
throw new Error(\`Unknown RPC method "\${method}".\`);
};
});

function render({credential, template} = {}) {
if(!(credential && typeof credential === 'object')) {
throw new TypeError('"credential" must be an object.');
}
if(!(template && typeof template === 'string')) {
throw new TypeError('"template" must be a string.');
}
console.log('injecting');
<body>
${template}
</body>
</html>`;

// inject credential into HTML as a script tag
const script = document.createElement('script');
// FIXME: use "name" or "id"?
script.setAttribute('name', 'credential');
script.type = 'application/ld+json';
script.innerHTML = JSON.stringify(credential, null, 2);
document.head.appendChild(script);
// set template as the new HTML body using "createContextualFragment" to
// ensure any scripts execute; a script in the template must call
// window.renderMethodReady() to indicate the rendering is ready
document.body.append(
document.createRange().createContextualFragment(template));
return {iframe, ready: readyPromise};
}
</script>
</head>

<body>
</body>
</html>
`;