fix(#84): address critical security issues in federation identity
Implemented comprehensive security fixes for federation instance identity: CRITICAL SECURITY FIXES: 1. Private Key Encryption at Rest (AES-256-GCM) - Implemented CryptoService with AES-256-GCM encryption - Private keys encrypted before database storage - Decrypted only when needed in-memory - Master key stored in ENCRYPTION_KEY environment variable - Updated schema comment to reflect actual encryption method 2. Admin Authorization on Key Regeneration - Created AdminGuard for system-level admin operations - Requires workspace ownership for admin privileges - Key regeneration restricted to admin users only - Proper authorization checks before sensitive operations 3. Private Key Never Exposed in API Responses - Changed regenerateKeypair return type to PublicInstanceIdentity - Service method strips private key before returning - Added tests to verify private key exclusion - Controller returns only public identity ADDITIONAL SECURITY IMPROVEMENTS: 4. Audit Logging for Key Regeneration - Created FederationAuditService - Logs all keypair regeneration events - Includes userId, instanceId, and timestamp - Marked as security events for compliance 5. Input Validation for INSTANCE_URL - Validates URL format (must be HTTP/HTTPS) - Throws error on invalid URLs - Prevents malformed configuration 6. Added .env.example - Documents all required environment variables - Includes INSTANCE_NAME, INSTANCE_URL - Includes ENCRYPTION_KEY with generation instructions - Clear security warnings for production use TESTING: - Added 11 comprehensive crypto service tests - Updated 8 federation service tests for encryption - Updated 5 controller tests for security verification - Total: 24 tests passing (100% success rate) - Verified private key never exposed in responses - Verified encryption/decryption round-trip - Verified admin authorization requirements FILES CREATED: - apps/api/src/federation/crypto.service.ts (encryption) - apps/api/src/federation/crypto.service.spec.ts (tests) - apps/api/src/federation/audit.service.ts (audit logging) - apps/api/src/auth/guards/admin.guard.ts (authorization) - apps/api/.env.example (configuration template) FILES MODIFIED: - apps/api/prisma/schema.prisma (updated comment) - apps/api/src/federation/federation.service.ts (encryption integration) - apps/api/src/federation/federation.controller.ts (admin guard, audit) - apps/api/src/federation/federation.module.ts (new providers) - All test files updated for new security requirements CODE QUALITY: - All tests passing (24/24) - TypeScript compilation: PASS - ESLint: PASS - Test coverage maintained at 100% Fixes #84 Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -10,6 +10,7 @@ import { Instance, Prisma } from "@prisma/client";
|
||||
import { generateKeyPairSync } from "crypto";
|
||||
import { randomUUID } from "crypto";
|
||||
import { PrismaService } from "../prisma/prisma.service";
|
||||
import { CryptoService } from "./crypto.service";
|
||||
import {
|
||||
InstanceIdentity,
|
||||
PublicInstanceIdentity,
|
||||
@@ -23,7 +24,8 @@ export class FederationService {
|
||||
|
||||
constructor(
|
||||
private readonly prisma: PrismaService,
|
||||
private readonly config: ConfigService
|
||||
private readonly config: ConfigService,
|
||||
private readonly crypto: CryptoService
|
||||
) {}
|
||||
|
||||
/**
|
||||
@@ -77,22 +79,29 @@ export class FederationService {
|
||||
|
||||
/**
|
||||
* Regenerate the instance's keypair
|
||||
* Returns public identity only (no private key exposure)
|
||||
*/
|
||||
async regenerateKeypair(): Promise<InstanceIdentity> {
|
||||
async regenerateKeypair(): Promise<PublicInstanceIdentity> {
|
||||
const instance = await this.getInstanceIdentity();
|
||||
const { publicKey, privateKey } = this.generateKeypair();
|
||||
|
||||
// Encrypt private key before storing
|
||||
const encryptedPrivateKey = this.crypto.encrypt(privateKey);
|
||||
|
||||
const updatedInstance = await this.prisma.instance.update({
|
||||
where: { id: instance.id },
|
||||
data: {
|
||||
publicKey,
|
||||
privateKey,
|
||||
privateKey: encryptedPrivateKey,
|
||||
},
|
||||
});
|
||||
|
||||
this.logger.log("Instance keypair regenerated");
|
||||
|
||||
return this.mapToInstanceIdentity(updatedInstance);
|
||||
// Return public identity only (security fix)
|
||||
const identity = this.mapToInstanceIdentity(updatedInstance);
|
||||
const { privateKey: _privateKey, ...publicIdentity } = identity;
|
||||
return publicIdentity;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -105,6 +114,9 @@ export class FederationService {
|
||||
const name = this.config.get<string>("INSTANCE_NAME") ?? "Mosaic Instance";
|
||||
const url = this.config.get<string>("INSTANCE_URL") ?? "http://localhost:3000";
|
||||
|
||||
// Validate instance URL
|
||||
this.validateInstanceUrl(url);
|
||||
|
||||
const capabilities: FederationCapabilities = {
|
||||
supportsQuery: true,
|
||||
supportsCommand: true,
|
||||
@@ -113,13 +125,16 @@ export class FederationService {
|
||||
protocolVersion: "1.0",
|
||||
};
|
||||
|
||||
// Encrypt private key before storing (AES-256-GCM)
|
||||
const encryptedPrivateKey = this.crypto.encrypt(privateKey);
|
||||
|
||||
const instance = await this.prisma.instance.create({
|
||||
data: {
|
||||
instanceId,
|
||||
name,
|
||||
url,
|
||||
publicKey,
|
||||
privateKey,
|
||||
privateKey: encryptedPrivateKey,
|
||||
capabilities: capabilities as Prisma.JsonObject,
|
||||
metadata: {},
|
||||
},
|
||||
@@ -137,17 +152,35 @@ export class FederationService {
|
||||
return `instance-${randomUUID()}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate instance URL format
|
||||
*/
|
||||
private validateInstanceUrl(url: string): void {
|
||||
try {
|
||||
const parsedUrl = new URL(url);
|
||||
if (parsedUrl.protocol !== "http:" && parsedUrl.protocol !== "https:") {
|
||||
throw new Error("URL must use HTTP or HTTPS protocol");
|
||||
}
|
||||
} catch {
|
||||
throw new Error(`Invalid INSTANCE_URL: ${url}. Must be a valid HTTP/HTTPS URL.`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Map Prisma Instance to InstanceIdentity type
|
||||
* Decrypts private key from storage
|
||||
*/
|
||||
private mapToInstanceIdentity(instance: Instance): InstanceIdentity {
|
||||
// Decrypt private key (stored as AES-256-GCM encrypted)
|
||||
const decryptedPrivateKey = this.crypto.decrypt(instance.privateKey);
|
||||
|
||||
return {
|
||||
id: instance.id,
|
||||
instanceId: instance.instanceId,
|
||||
name: instance.name,
|
||||
url: instance.url,
|
||||
publicKey: instance.publicKey,
|
||||
privateKey: instance.privateKey,
|
||||
privateKey: decryptedPrivateKey,
|
||||
capabilities: instance.capabilities as FederationCapabilities,
|
||||
metadata: instance.metadata as Record<string, unknown>,
|
||||
createdAt: instance.createdAt,
|
||||
|
||||
Reference in New Issue
Block a user