From ec515626385c74d8956010cbe2d3357c7b653df5 Mon Sep 17 00:00:00 2001 From: Silberengel Date: Wed, 18 Feb 2026 11:59:17 +0100 Subject: [PATCH] more ssh key implementation --- docs/SSH_KEY_ATTESTATION.md | 17 +++++++--- src/lib/services/security/audit-logger.ts | 20 ++++++++++++ src/routes/api/user/ssh-keys/+server.ts | 31 +++++++++++++++++++ .../api/user/ssh-keys/verify/+server.ts | 18 +++++++++++ 4 files changed, 82 insertions(+), 4 deletions(-) diff --git a/docs/SSH_KEY_ATTESTATION.md b/docs/SSH_KEY_ATTESTATION.md index 699ee10..94f8246 100644 --- a/docs/SSH_KEY_ATTESTATION.md +++ b/docs/SSH_KEY_ATTESTATION.md @@ -120,6 +120,8 @@ const response = await fetch('/api/user/ssh-keys', { } ``` +**Note**: You can have multiple SSH keys attested. All active (non-revoked) keys will be returned, sorted by creation date (newest first). + ### Verify SSH Key **Endpoint**: `POST /api/user/ssh-keys/verify` @@ -223,10 +225,17 @@ git remote add origin git@your-gitrepublic-server.com:repos/{npub}/{repo}.git - In production, use Redis or a database for persistent storage - Only users with "unlimited access" can create attestations +## Current Status + +✅ **Implemented:** +- Support for multiple SSH keys per user (users can attest multiple SSH keys) +- Rate limiting (10 attestations per hour per user) +- Revocation support +- HMAC-based fingerprint lookup for security +- Audit logging for SSH key attestation operations (submit, revoke, verify) + ## Future Improvements -- Persistent storage (Redis/database) for attestations -- SSH server implementation -- Support for multiple SSH keys per user +- Persistent storage (Redis/database) for attestations (currently in-memory) +- SSH server implementation (API is ready, server integration needed) - Key expiration/rotation policies -- Audit logging for SSH operations diff --git a/src/lib/services/security/audit-logger.ts b/src/lib/services/security/audit-logger.ts index 0e3e78d..99a4dbd 100644 --- a/src/lib/services/security/audit-logger.ts +++ b/src/lib/services/security/audit-logger.ts @@ -337,6 +337,26 @@ export class AuditLogger { metadata: { originalRepo } }); } + + /** + * Log SSH key attestation operation + */ + logSSHKeyAttestation( + user: string, + action: 'submit' | 'revoke' | 'verify', + fingerprint: string, + result: 'success' | 'failure', + error?: string + ): void { + this.log({ + user, + action: `ssh.attestation.${action}`, + resource: fingerprint, + result, + error, + metadata: { fingerprint: fingerprint.slice(0, 20) + '...' } + }); + } } // Singleton instance diff --git a/src/routes/api/user/ssh-keys/+server.ts b/src/routes/api/user/ssh-keys/+server.ts index cdca3de..e1303be 100644 --- a/src/routes/api/user/ssh-keys/+server.ts +++ b/src/routes/api/user/ssh-keys/+server.ts @@ -10,6 +10,7 @@ import type { RequestHandler } from './$types'; import { extractRequestContext } from '$lib/utils/api-context.js'; import { storeAttestation, getUserAttestations, verifyAttestation, calculateSSHKeyFingerprint } from '$lib/services/ssh/ssh-key-attestation.js'; import { getCachedUserLevel } from '$lib/services/security/user-level-cache.js'; +import { auditLogger } from '$lib/services/security/audit-logger.js'; import { verifyEvent } from 'nostr-tools'; import type { NostrEvent } from '$lib/types/nostr.js'; import { KIND } from '$lib/types/nostr.js'; @@ -27,6 +28,7 @@ import logger from '$lib/services/logger.js'; export const POST: RequestHandler = async (event) => { const requestContext = extractRequestContext(event); const clientIp = requestContext.clientIp || 'unknown'; + let attestationFingerprint: string | null = null; try { if (!requestContext.userPubkeyHex) { @@ -39,6 +41,15 @@ export const POST: RequestHandler = async (event) => { } const attestationEvent: NostrEvent = body.event; + + // Calculate fingerprint for audit logging (before storing) + try { + if (attestationEvent.content) { + attestationFingerprint = calculateSSHKeyFingerprint(attestationEvent.content); + } + } catch { + // Ignore fingerprint calculation errors + } // Verify event signature if (!verifyEvent(attestationEvent)) { @@ -64,6 +75,14 @@ export const POST: RequestHandler = async (event) => { // Store attestation const attestation = storeAttestation(attestationEvent); + // Audit log + auditLogger.logSSHKeyAttestation( + requestContext.userPubkeyHex, + attestation.revoked ? 'revoke' : 'submit', + attestation.sshKeyFingerprint, + 'success' + ); + logger.info({ userPubkey: requestContext.userPubkeyHex.slice(0, 16) + '...', fingerprint: attestation.sshKeyFingerprint.slice(0, 20) + '...', @@ -84,6 +103,18 @@ export const POST: RequestHandler = async (event) => { }); } catch (e) { const errorMessage = e instanceof Error ? e.message : 'Failed to store SSH key attestation'; + + // Audit log failure (if we have user context and fingerprint) + if (requestContext.userPubkeyHex && attestationFingerprint) { + auditLogger.logSSHKeyAttestation( + requestContext.userPubkeyHex, + 'submit', + attestationFingerprint, + 'failure', + errorMessage + ); + } + logger.error({ error: e, clientIp }, 'Failed to store SSH key attestation'); if (errorMessage.includes('Rate limit')) { diff --git a/src/routes/api/user/ssh-keys/verify/+server.ts b/src/routes/api/user/ssh-keys/verify/+server.ts index 070ff29..be8886c 100644 --- a/src/routes/api/user/ssh-keys/verify/+server.ts +++ b/src/routes/api/user/ssh-keys/verify/+server.ts @@ -8,6 +8,7 @@ import { json, error } from '@sveltejs/kit'; import type { RequestHandler } from './$types'; import { extractRequestContext } from '$lib/utils/api-context.js'; import { verifyAttestation } from '$lib/services/ssh/ssh-key-attestation.js'; +import { auditLogger } from '$lib/services/security/audit-logger.js'; import logger from '$lib/services/logger.js'; /** @@ -30,12 +31,29 @@ export const POST: RequestHandler = async (event) => { const attestation = verifyAttestation(body.fingerprint); if (!attestation) { + // Audit log failed verification + auditLogger.logSSHKeyAttestation( + 'unknown', + 'verify', + body.fingerprint, + 'failure', + 'SSH key not attested or attestation revoked' + ); + return json({ valid: false, message: 'SSH key not attested or attestation revoked' }); } + // Audit log successful verification + auditLogger.logSSHKeyAttestation( + attestation.userPubkey, + 'verify', + body.fingerprint, + 'success' + ); + return json({ valid: true, attestation: {