Skip to content
Open
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
3 changes: 2 additions & 1 deletion src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,8 @@ const defaultConfig: IoDevServerConfig = {
canceledCount: 0,
noSignatureFieldsCount: 0,
response: {
getFciResponseCode: 200
getFciResponseCode: 200,
nonceDuration: 300 // 5 minutes as production environment
Copy link
Contributor

Choose a reason for hiding this comment

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

In my opinion, it's a bit complicated to calculate the duration using this method (even though there's a comment). Why didn't you just set the duration in seconds?

}
},
withCTA: false,
Expand Down
56 changes: 56 additions & 0 deletions src/features/fci/qtspNonceStore.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import { randomUUID } from "crypto";
import { ioDevServerConfig } from "../../config";

export const QTSP_NONCE_EXPIRING_MS =
ioDevServerConfig.messages.fci.response.nonceDuration * 1000;

const qtspNonceExpirations = new Map<string, Date>();

const cleanupExpiredQtspNonces = (referenceDate: Date) => {
qtspNonceExpirations.forEach((expiresAt, nonce) => {
if (expiresAt <= referenceDate) {
qtspNonceExpirations.delete(nonce);
}
});
};

const generateQtspNonce = () =>
`devnonce-${randomUUID({ disableEntropyCache: true })}`;

export const generateAndStoreQtspNonce = (now = new Date()) => {
cleanupExpiredQtspNonces(now);
const nonce = generateQtspNonce();
qtspNonceExpirations.set(
nonce,
new Date(now.getTime() + QTSP_NONCE_EXPIRING_MS)
);
return nonce;
};

export type QtspNonceValidationResult = "valid" | "expired" | "missing";

export const getQtspNonceValidationResult = (
nonce: string,
now = new Date()
): QtspNonceValidationResult => {
const expiration = qtspNonceExpirations.get(nonce);

if (expiration === undefined) {
cleanupExpiredQtspNonces(now);
return "missing";
}

if (expiration <= now) {
qtspNonceExpirations.delete(nonce);
cleanupExpiredQtspNonces(now);
return "expired";
}

cleanupExpiredQtspNonces(now);
return "valid";
};

export const isStoredQtspNonceValid = (nonce: string, now = new Date()) =>
getQtspNonceValidationResult(nonce, now) === "valid";

export const getQtspNonceExpirations = () => qtspNonceExpirations;
4 changes: 3 additions & 1 deletion src/features/messages/types/messagesConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,9 @@ export const MessagesConfig = t.intersection([
noSignatureFieldsCount: t.number,
response: t.type({
// 200 success with payload
getFciResponseCode: HttpResponseCode
getFciResponseCode: HttpResponseCode,
// qtsp nonce duration in seconds
nonceDuration: t.number
})
}),
// if true, messages (all available) with nested CTA will be included
Expand Down
65 changes: 63 additions & 2 deletions src/routers/features/fci/__tests__/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { SIGNATURE_REQUEST_ID } from "../../../../payloads/features/fci/signatur
import app from "../../../../server";
import { addFciPrefix } from "../index";
import { EnvironmentEnum } from "../../../../../generated/definitions/fci/Environment";
import { getQtspNonceExpirations } from "../../../../features/fci/qtspNonceStore";

const request = supertest(app);

Expand Down Expand Up @@ -47,11 +48,30 @@ describe("io-sign API", () => {
});
});
describe("GET qtsp clauses", () => {
beforeEach(() => {
getQtspNonceExpirations().clear();
});

describe("when the signer request qtsp clauses", () => {
it("should return 200 and the clauses list", async () => {
const response = await request.get(addFciPrefix(`/qtsp/clauses`));
expect(response.status).toBe(200);
expect(response.body).toHaveProperty("clauses");
expect(response.body.nonce).toMatch(/^devnonce-/);
expect(getQtspNonceExpirations().has(response.body.nonce)).toBe(true);
});

it("should store the nonce with an expiration date", async () => {
const response = await request.get(addFciPrefix(`/qtsp/clauses`));
const nonceExpiration = getQtspNonceExpirations().get(
response.body.nonce
);

if (nonceExpiration === undefined) {
throw new Error("missing nonce expiration");
}

expect(nonceExpiration.getTime()).toBeGreaterThan(Date.now());
});
});
});
Expand All @@ -67,12 +87,53 @@ describe("io-sign API", () => {
});
});
describe("POST create signature", () => {
beforeEach(() => {
getQtspNonceExpirations().clear();
});

describe("when the signer request a signature with a valid body", () => {
it("should return 201", async () => {
it("should return 200", async () => {
const qtspClausesResponse = await request.get(
addFciPrefix(`/qtsp/clauses`)
);
const response = await request.post(addFciPrefix(`/signatures`)).send({
...createSignatureBody,
qtsp_clauses: {
...createSignatureBody.qtsp_clauses,
nonce: qtspClausesResponse.body.nonce
}
});
expect(response.status).toBe(200);
});
});
describe("when the signer request a signature with an invalid nonce", () => {
it("should return 500", async () => {
const response = await request
.post(addFciPrefix(`/signatures`))
.send(createSignatureBody);
expect(response.status).toBe(200);
expect(response.status).toBe(500);
});
});
describe("when the signer request a signature with an expired nonce", () => {
it("should return 500", async () => {
const qtspClausesResponse = await request.get(
addFciPrefix(`/qtsp/clauses`)
);
const expiredNonce = qtspClausesResponse.body.nonce;
getQtspNonceExpirations().set(
expiredNonce,
new Date(Date.now() - 1000)
);

const response = await request.post(addFciPrefix(`/signatures`)).send({
...createSignatureBody,
qtsp_clauses: {
...createSignatureBody.qtsp_clauses,
nonce: expiredNonce
}
});

expect(response.status).toBe(500);
});
});
describe("when the signer request signature detail with a not valid body", () => {
Expand Down
19 changes: 17 additions & 2 deletions src/routers/features/fci/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,10 @@ import { SignatureRequestStatusEnum } from "../../../../generated/definitions/fc
import { EnvironmentEnum } from "../../../../generated/definitions/fci/Environment";
import { signatureRequestList } from "../../../payloads/features/fci/signature-requests";
import { getProblemJson } from "../../../payloads/error";
import {
generateAndStoreQtspNonce,
getQtspNonceValidationResult
} from "../../../features/fci/qtspNonceStore";

export const fciRouter = Router();
const configResponse = ioDevServerConfig.messages.fci.response;
Expand Down Expand Up @@ -128,7 +132,7 @@ addHandler(
);

addHandler(fciRouter, "get", addFciPrefix("/qtsp/clauses"), (_, res) => {
res.status(200).json(qtspClauses);
res.status(200).json({ ...qtspClauses, nonce: generateAndStoreQtspNonce() });
});

addHandler(
Expand All @@ -151,9 +155,20 @@ addHandler(fciRouter, "post", addFciPrefix("/signatures"), (req, res) => {
pipe(
O.fromNullable(req.body),
O.chain(cb => (isEqual(cb, {}) ? O.none : O.some(cb))),
O.chain(cb =>
pipe(
O.fromNullable(cb.qtsp_clauses?.nonce),
O.map(nonce => getQtspNonceValidationResult(nonce))
)
),
O.fold(
() => res.sendStatus(400),
_ => res.status(200).json(mockSignatureDetailView)
nonceValidationResult => {
if (nonceValidationResult === "valid") {
return res.status(200).json(mockSignatureDetailView);
}
return res.sendStatus(500);
Copy link
Contributor

Choose a reason for hiding this comment

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

In addition to the error code, it would be helpful to have the error message. What do you think about using the error that also occurs in the app?

}
)
);
});
Expand Down
Loading