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
3 changes: 2 additions & 1 deletion src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,8 @@ const defaultConfig: IoDevServerConfig = {
noSignatureFieldsCount: 0,
response: {
getFciResponseCode: 200,
documentExpirationDurationSeconds: 5 * 60 // make it 30 to allow testing expired documents without waiting too much
documentExpirationDurationSeconds: 5 * 60, // make it 30 to allow testing expired documents without waiting too much
nonceDurationSeconds: 5 * 60 // 5 minutes as production environment
}
},
withCTA: false,
Expand Down
37 changes: 37 additions & 0 deletions src/features/fci/qtspNonceStore.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { randomUUID } from "crypto";
import { ioDevServerConfig } from "../../config";

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

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

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

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

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

export const validateQtspNonce = (nonce: string): boolean => {
cleanupExpiredQtspNonces();
const expiration = qtspNonceExpirations.get(nonce);
return expiration !== undefined;
};

export const getQtspNonceExpirations = () => qtspNonceExpirations;
2 changes: 2 additions & 0 deletions src/features/messages/types/messagesConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@ export const MessagesConfig = t.intersection([
response: t.type({
// 200 success with payload
getFciResponseCode: HttpResponseCode,
// qtsp nonce duration in seconds
nonceDurationSeconds: t.number,
documentExpirationDurationSeconds: t.number
})
}),
Expand Down
69 changes: 66 additions & 3 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,16 +87,59 @@ describe("io-sign API", () => {
});
});
describe("POST create signature", () => {
const SHOULD_RETURN_400 = "should return 400";

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_400, async () => {
const response = await request
.post(addFciPrefix(`/signatures`))
.send(createSignatureBody);
expect(response.status).toBe(200);
expect(response.status).toBe(400);
});
});
describe("when the signer request a signature with an expired nonce", () => {
it(SHOULD_RETURN_400, 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(400);
});
});
describe("when the signer request signature detail with a not valid body", () => {
it("should return 400", async () => {
it(SHOULD_RETURN_400, async () => {
const response = await request.post(addFciPrefix(`/signatures`));
expect(response.status).toBe(400);
});
Expand Down
24 changes: 22 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,
validateQtspNonce
} from "../../../features/fci/qtspNonceStore";

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

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

addHandler(
Expand All @@ -165,9 +169,25 @@ 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 => validateQtspNonce(nonce))
)
),
O.fold(
() => res.sendStatus(400),
_ => res.status(200).json(mockSignatureDetailView)
nonceValidationResult => {
if (nonceValidationResult) {
return res.status(200).json(mockSignatureDetailView);
}
return res.status(400).json({
detail:
"An error occurred while validating the request body | undefined",
status: 400,
title: "Invalid request"
});
}
)
);
});
Expand Down
Loading