feat(#284): Reduce timestamp validation window to 60s with replay attack prevention

Security improvements:
- Reduce timestamp tolerance from 5 minutes to 60 seconds
- Add nonce-based replay attack prevention using Redis
- Store signature nonce with 60s TTL matching tolerance window
- Reject replayed messages with same signature

Changes:
- Update SignatureService.TIMESTAMP_TOLERANCE_MS to 60s
- Add Redis client injection to SignatureService
- Make verifyConnectionRequest async for nonce checking
- Create RedisProvider for shared Redis client
- Update ConnectionService to await signature verification
- Add comprehensive test coverage for replay prevention

Part of M7.1 Remediation Sprint P1 security fixes.

Fixes #284

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-02-03 21:43:01 -06:00
parent 61e2bf7063
commit 3bba2f1c33
38 changed files with 888 additions and 23 deletions

View File

@@ -102,7 +102,7 @@ describe("ConnectionService", () => {
provide: SignatureService,
useValue: {
signMessage: vi.fn().mockResolvedValue("mock-signature"),
verifyConnectionRequest: vi.fn().mockReturnValue({ valid: true }),
verifyConnectionRequest: vi.fn().mockResolvedValue({ valid: true }),
},
},
{
@@ -441,7 +441,7 @@ describe("ConnectionService", () => {
});
it("should create pending connection for valid request", async () => {
vi.spyOn(signatureService, "verifyConnectionRequest").mockReturnValue({ valid: true });
vi.spyOn(signatureService, "verifyConnectionRequest").mockResolvedValue({ valid: true });
vi.spyOn(prismaService.federationConnection, "create").mockResolvedValue(mockConnection);
const result = await service.handleIncomingConnectionRequest(mockWorkspaceId, mockRequest);
@@ -451,7 +451,7 @@ describe("ConnectionService", () => {
});
it("should reject request with invalid signature", async () => {
vi.spyOn(signatureService, "verifyConnectionRequest").mockReturnValue({
vi.spyOn(signatureService, "verifyConnectionRequest").mockResolvedValue({
valid: false,
error: "Invalid signature",
});
@@ -462,7 +462,7 @@ describe("ConnectionService", () => {
});
it("should log incoming connection attempt", async () => {
vi.spyOn(signatureService, "verifyConnectionRequest").mockReturnValue({ valid: true });
vi.spyOn(signatureService, "verifyConnectionRequest").mockResolvedValue({ valid: true });
vi.spyOn(prismaService.federationConnection, "create").mockResolvedValue(mockConnection);
const auditSpy = vi.spyOn(auditService, "logIncomingConnectionAttempt");
@@ -477,7 +477,7 @@ describe("ConnectionService", () => {
});
it("should log connection created on success", async () => {
vi.spyOn(signatureService, "verifyConnectionRequest").mockReturnValue({ valid: true });
vi.spyOn(signatureService, "verifyConnectionRequest").mockResolvedValue({ valid: true });
vi.spyOn(prismaService.federationConnection, "create").mockResolvedValue(mockConnection);
const auditSpy = vi.spyOn(auditService, "logIncomingConnectionCreated");
@@ -492,7 +492,7 @@ describe("ConnectionService", () => {
});
it("should log connection rejected on invalid signature", async () => {
vi.spyOn(signatureService, "verifyConnectionRequest").mockReturnValue({
vi.spyOn(signatureService, "verifyConnectionRequest").mockResolvedValue({
valid: false,
error: "Invalid signature",
});