Skip to content
Closed
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
45 changes: 45 additions & 0 deletions examples/verification-workers/src/html.ts
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,20 @@ button:focus {
button:active {
box-shadow: inset 0 1px 1px 1px rgba(0,0,0,.4);
}
input[type="url"] {
border: 1px solid #999;
border-radius: 6px;
box-sizing: border-box;
display: block;
font: inherit;
margin: 0.5rem 0;
max-width: 720px;
padding: 8px 10px;
width: 100%;
}
.validation-result {
margin-top: 0.75rem;
}

.question-list {
margin-bottom: 2rem;
Expand Down Expand Up @@ -273,6 +287,17 @@ footer {
</ul>
</p>

<h2>Validate your key directory</h2>
<p>
Paste the full HTTPS URL for a <code>/.well-known/http-message-signatures-directory</code> endpoint to check whether it returns a usable directory.
</p>
<form id="directory-validator">
<label for="directory-url">Directory URL</label>
<input id="directory-url" name="url" type="url" placeholder="https://example.com/.well-known/http-message-signatures-directory" required />
<button type="submit">Validate directory</button>
<p id="directory-validation-result" class="validation-result" aria-live="polite"></p>
</form>

<h2>It's hard to debug. How can this website help?</h2>
<p>
This website expose an endpoint dropping incoming request headers on <a>/debug</a>
Expand All @@ -289,6 +314,26 @@ footer {
To contribute to the standard discussion, the current draft is hosted on <a href="https://github.com/thibmeu/http-message-signatures-directory">thibmeu/http-message-signatures-directory</a>, and is being discussed on <a href="https://mailarchive.ietf.org/arch/browse/web-bot-auth/">web-bot-auth</a> IETF mailing list.
</p>
</section>
<script>
const form = document.getElementById("directory-validator");
const result = document.getElementById("directory-validation-result");

form.addEventListener("submit", async (event) => {
event.preventDefault();
const data = new FormData(form);
result.textContent = "Checking directory...";

try {
const response = await fetch("/v0/api/validate-directory?url=" + encodeURIComponent(data.get("url")));
const body = await response.json();
result.textContent = body.ok
? "Directory looks valid."
: "Directory check failed: " + body.errors.join("; ");
} catch (_e) {
result.textContent = "Directory check failed.";
}
});
</script>
</body>
</html>`;

Expand Down
101 changes: 101 additions & 0 deletions examples/verification-workers/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,103 @@ async function fetchDirectory(signatureAgent: string): Promise<Directory> {
return response.json();
}

function validateDirectory(directory: unknown): string[] {
Comment thread
AkshatM marked this conversation as resolved.
const errors: string[] = [];

if (directory === null || typeof directory !== "object") {
return ["Directory must be a JSON object"];
}

const value = directory as Partial<Directory>;

if (!Array.isArray(value.keys)) {
errors.push("Directory must include a keys array");
} else if (value.keys.length === 0) {
errors.push("Directory keys array must not be empty");
} else {
for (const [index, key] of value.keys.entries()) {
if (key === null || typeof key !== "object") {
errors.push(`keys[${index}] must be a JSON object`);
continue;
}
if (typeof key.kty !== "string") {
errors.push(`keys[${index}].kty must be a string`);
}
}
}

if (typeof value.purpose !== "string" || value.purpose.length === 0) {
errors.push("Directory must include a non-empty purpose string");
}

return errors;
}

async function validateDirectoryURL(request: Request): Promise<Response> {
const url = new URL(request.url);
const directoryURL = url.searchParams.get("url");

if (directoryURL === null || directoryURL.length === 0) {
return Response.json(
{ ok: false, errors: ["Missing url query parameter"] },
{ status: 400 }
);
}

let parsed: URL;
try {
parsed = new URL(directoryURL);
} catch (_e) {
return Response.json(
{ ok: false, errors: ["URL must be valid"] },
{ status: 400 }
);
}

if (parsed.protocol !== "https:") {
return Response.json(
{ ok: false, errors: ['Directory URL must use "https:"'] },
{ status: 400 }
);
}

if (parsed.pathname !== HTTP_MESSAGE_SIGNATURES_DIRECTORY) {
return Response.json(
{
ok: false,
errors: [
`Directory URL path must be ${HTTP_MESSAGE_SIGNATURES_DIRECTORY}`,
],
},
{ status: 400 }
);
}

const response = await fetch(parsed);
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is an arbitrary fetch, which creates a DoS vector

if (!response.ok) {
return Response.json(
{
ok: false,
errors: [`Directory returned HTTP ${response.status}`],
},
{ status: 502 }
);
}

let directory: unknown;
try {
directory = await response.json();
} catch (_e) {
return Response.json(
{ ok: false, errors: ["Directory response must be valid JSON"] },
{ status: 502 }
);
}

const errors = validateDirectory(directory);
return Response.json({ ok: errors.length === 0, errors });
}

async function getSigner(): Promise<Signer> {
return Ed25519Signer.fromJWK(jwk);
}
Expand Down Expand Up @@ -180,6 +277,10 @@ export default {
return new Response(status);
}

if (url.pathname.startsWith("/v0/api/validate-directory")) {
return validateDirectoryURL(request);
}

if (url.pathname.startsWith(HTTP_MESSAGE_SIGNATURES_DIRECTORY)) {
const directory = await getExampleDirectory();

Expand Down
48 changes: 48 additions & 0 deletions examples/verification-workers/test/index.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,3 +49,51 @@ describe("/debug endpoint", () => {
expect(await response.text()).toMatch(headersString);
});
});

describe("/v0/api/validate-directory endpoint", () => {
it("requires a url query parameter", async () => {
const response = await SELF.fetch(
new Request(`${sampleURL}/v0/api/validate-directory`)
);

expect(response.status).toEqual(400);
expect(await response.json()).toEqual({
ok: false,
errors: ["Missing url query parameter"],
});
});

it("requires an https well-known directory URL", async () => {
const response = await SELF.fetch(
new Request(
`${sampleURL}/v0/api/validate-directory?url=${encodeURIComponent(
"http://example.com/"
)}`
)
);

expect(response.status).toEqual(400);
expect(await response.json()).toEqual({
ok: false,
errors: ['Directory URL must use "https:"'],
});
});

it("requires the directory well-known path", async () => {
const response = await SELF.fetch(
new Request(
`${sampleURL}/v0/api/validate-directory?url=${encodeURIComponent(
"https://example.com/"
)}`
)
);

expect(response.status).toEqual(400);
expect(await response.json()).toEqual({
ok: false,
errors: [
"Directory URL path must be /.well-known/http-message-signatures-directory",
],
});
});
});
Loading