Browse Source

more ssh key implementation

main
Silberengel 4 weeks ago
parent
commit
ec51562638
  1. 17
      docs/SSH_KEY_ATTESTATION.md
  2. 20
      src/lib/services/security/audit-logger.ts
  3. 31
      src/routes/api/user/ssh-keys/+server.ts
  4. 18
      src/routes/api/user/ssh-keys/verify/+server.ts

17
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 ### Verify SSH Key
**Endpoint**: `POST /api/user/ssh-keys/verify` **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 - In production, use Redis or a database for persistent storage
- Only users with "unlimited access" can create attestations - 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 ## Future Improvements
- Persistent storage (Redis/database) for attestations - Persistent storage (Redis/database) for attestations (currently in-memory)
- SSH server implementation - SSH server implementation (API is ready, server integration needed)
- Support for multiple SSH keys per user
- Key expiration/rotation policies - Key expiration/rotation policies
- Audit logging for SSH operations

20
src/lib/services/security/audit-logger.ts

@ -337,6 +337,26 @@ export class AuditLogger {
metadata: { originalRepo } 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 // Singleton instance

31
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 { extractRequestContext } from '$lib/utils/api-context.js';
import { storeAttestation, getUserAttestations, verifyAttestation, calculateSSHKeyFingerprint } from '$lib/services/ssh/ssh-key-attestation.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 { getCachedUserLevel } from '$lib/services/security/user-level-cache.js';
import { auditLogger } from '$lib/services/security/audit-logger.js';
import { verifyEvent } from 'nostr-tools'; import { verifyEvent } from 'nostr-tools';
import type { NostrEvent } from '$lib/types/nostr.js'; import type { NostrEvent } from '$lib/types/nostr.js';
import { KIND } 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) => { export const POST: RequestHandler = async (event) => {
const requestContext = extractRequestContext(event); const requestContext = extractRequestContext(event);
const clientIp = requestContext.clientIp || 'unknown'; const clientIp = requestContext.clientIp || 'unknown';
let attestationFingerprint: string | null = null;
try { try {
if (!requestContext.userPubkeyHex) { if (!requestContext.userPubkeyHex) {
@ -40,6 +42,15 @@ export const POST: RequestHandler = async (event) => {
const attestationEvent: NostrEvent = body.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 // Verify event signature
if (!verifyEvent(attestationEvent)) { if (!verifyEvent(attestationEvent)) {
return error(400, 'Invalid event signature'); return error(400, 'Invalid event signature');
@ -64,6 +75,14 @@ export const POST: RequestHandler = async (event) => {
// Store attestation // Store attestation
const attestation = storeAttestation(attestationEvent); const attestation = storeAttestation(attestationEvent);
// Audit log
auditLogger.logSSHKeyAttestation(
requestContext.userPubkeyHex,
attestation.revoked ? 'revoke' : 'submit',
attestation.sshKeyFingerprint,
'success'
);
logger.info({ logger.info({
userPubkey: requestContext.userPubkeyHex.slice(0, 16) + '...', userPubkey: requestContext.userPubkeyHex.slice(0, 16) + '...',
fingerprint: attestation.sshKeyFingerprint.slice(0, 20) + '...', fingerprint: attestation.sshKeyFingerprint.slice(0, 20) + '...',
@ -84,6 +103,18 @@ export const POST: RequestHandler = async (event) => {
}); });
} catch (e) { } catch (e) {
const errorMessage = e instanceof Error ? e.message : 'Failed to store SSH key attestation'; 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'); logger.error({ error: e, clientIp }, 'Failed to store SSH key attestation');
if (errorMessage.includes('Rate limit')) { if (errorMessage.includes('Rate limit')) {

18
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 type { RequestHandler } from './$types';
import { extractRequestContext } from '$lib/utils/api-context.js'; import { extractRequestContext } from '$lib/utils/api-context.js';
import { verifyAttestation } from '$lib/services/ssh/ssh-key-attestation.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'; import logger from '$lib/services/logger.js';
/** /**
@ -30,12 +31,29 @@ export const POST: RequestHandler = async (event) => {
const attestation = verifyAttestation(body.fingerprint); const attestation = verifyAttestation(body.fingerprint);
if (!attestation) { if (!attestation) {
// Audit log failed verification
auditLogger.logSSHKeyAttestation(
'unknown',
'verify',
body.fingerprint,
'failure',
'SSH key not attested or attestation revoked'
);
return json({ return json({
valid: false, valid: false,
message: 'SSH key not attested or attestation revoked' message: 'SSH key not attested or attestation revoked'
}); });
} }
// Audit log successful verification
auditLogger.logSSHKeyAttestation(
attestation.userPubkey,
'verify',
body.fingerprint,
'success'
);
return json({ return json({
valid: true, valid: true,
attestation: { attestation: {

Loading…
Cancel
Save