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:
@@ -4,7 +4,7 @@
|
||||
* Handles message signing and verification for federation protocol.
|
||||
*/
|
||||
|
||||
import { Injectable, Logger } from "@nestjs/common";
|
||||
import { Injectable, Logger, Inject } from "@nestjs/common";
|
||||
import { createSign, createVerify } from "crypto";
|
||||
import { FederationService } from "./federation.service";
|
||||
import type {
|
||||
@@ -12,14 +12,19 @@ import type {
|
||||
SignatureValidationResult,
|
||||
ConnectionRequest,
|
||||
} from "./types/connection.types";
|
||||
import type Redis from "ioredis";
|
||||
|
||||
@Injectable()
|
||||
export class SignatureService {
|
||||
private readonly logger = new Logger(SignatureService.name);
|
||||
private readonly TIMESTAMP_TOLERANCE_MS = 5 * 60 * 1000; // 5 minutes
|
||||
private readonly TIMESTAMP_TOLERANCE_MS = 60 * 1000; // 60 seconds
|
||||
private readonly CLOCK_SKEW_TOLERANCE_MS = 60 * 1000; // 1 minute for future timestamps
|
||||
private readonly NONCE_TTL_SECONDS = 60; // Nonce TTL matches tolerance window
|
||||
|
||||
constructor(private readonly federationService: FederationService) {}
|
||||
constructor(
|
||||
private readonly federationService: FederationService,
|
||||
@Inject("REDIS_CLIENT") private readonly redis: Redis
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Sign a message with a private key
|
||||
@@ -153,7 +158,7 @@ export class SignatureService {
|
||||
/**
|
||||
* Verify a connection request signature
|
||||
*/
|
||||
verifyConnectionRequest(request: ConnectionRequest): SignatureValidationResult {
|
||||
async verifyConnectionRequest(request: ConnectionRequest): Promise<SignatureValidationResult> {
|
||||
// Extract signature and create message for verification
|
||||
const { signature, ...message } = request;
|
||||
|
||||
@@ -165,14 +170,30 @@ export class SignatureService {
|
||||
};
|
||||
}
|
||||
|
||||
// Check for replay attack (nonce already used)
|
||||
const nonceKey = `nonce:${signature}`;
|
||||
const nonceExists = await this.redis.get(nonceKey);
|
||||
|
||||
if (nonceExists) {
|
||||
this.logger.warn("Replay attack detected: signature already used");
|
||||
return {
|
||||
valid: false,
|
||||
error: "Request rejected: potential replay attack detected",
|
||||
};
|
||||
}
|
||||
|
||||
// Verify signature using the public key from the request
|
||||
const result = this.verify(message, signature, request.publicKey);
|
||||
|
||||
if (!result.valid) {
|
||||
const errorMsg = result.error ?? "Unknown error";
|
||||
this.logger.warn(`Connection request signature verification failed: ${errorMsg}`);
|
||||
return result;
|
||||
}
|
||||
|
||||
// Store nonce to prevent replay attacks
|
||||
await this.redis.setex(nonceKey, this.NONCE_TTL_SECONDS, "1");
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user