Security fixes: swarm signature verification and error handling

This commit is contained in:
Clawd Deploy Bot
2026-01-30 16:50:49 +01:00
parent 495a037eb1
commit 50355b740a
21 changed files with 850 additions and 467 deletions
+13 -1
View File
@@ -64,11 +64,23 @@ export async function buildAnnouncement(): Promise<SwarmAnnouncement> {
/**
* Announce this node to a remote node
*
* SECURITY: Signs the announcement with the node's private key
*/
export async function announceToNode(targetDomain: string): Promise<{ success: boolean; error?: string }> {
try {
const announcement = await buildAnnouncement();
// SECURITY: Sign the announcement with our private key
const { signPayload, getNodePrivateKey } = await import('./signature');
const privateKey = await getNodePrivateKey();
const signature = signPayload(announcement, privateKey);
const signedAnnouncement = {
...announcement,
signature,
};
const baseUrl = targetDomain.startsWith('http') ? targetDomain : `https://${targetDomain}`;
const url = `${baseUrl}/api/swarm/announce`;
@@ -78,7 +90,7 @@ export async function announceToNode(targetDomain: string): Promise<{ success: b
'Content-Type': 'application/json',
'Accept': 'application/json',
},
body: JSON.stringify(announcement),
body: JSON.stringify(signedAnnouncement),
});
if (!response.ok) {
+13 -1
View File
@@ -109,6 +109,8 @@ export async function processGossip(
/**
* Send gossip to a specific node
*
* SECURITY: Signs the gossip payload with the node's private key
*/
export async function gossipToNode(
targetDomain: string,
@@ -119,6 +121,16 @@ export async function gossipToNode(
try {
const payload = await buildGossipPayload(since);
// SECURITY: Sign the gossip payload with our private key
const { signPayload, getNodePrivateKey } = await import('./signature');
const privateKey = await getNodePrivateKey();
const signature = signPayload(payload, privateKey);
const signedPayload = {
...payload,
signature,
};
const baseUrl = targetDomain.startsWith('http') ? targetDomain : `https://${targetDomain}`;
const url = `${baseUrl}/api/swarm/gossip`;
@@ -128,7 +140,7 @@ export async function gossipToNode(
'Content-Type': 'application/json',
'Accept': 'application/json',
},
body: JSON.stringify(payload),
body: JSON.stringify(signedPayload),
});
const durationMs = Date.now() - startTime;
+68 -47
View File
@@ -153,64 +153,80 @@ export async function getNodesSince(since: Date, limit = 100): Promise<SwarmNode
/**
* Mark a node as having failed contact
*
* @throws Error if database operation fails (after logging)
*/
export async function markNodeFailure(domain: string): Promise<void> {
if (!db) return;
const node = await db.query.swarmNodes.findFirst({
where: eq(swarmNodes.domain, domain),
});
try {
const node = await db.query.swarmNodes.findFirst({
where: eq(swarmNodes.domain, domain),
});
if (!node) return;
if (!node) return;
const newFailures = node.consecutiveFailures + 1;
const newTrust = Math.max(
SWARM_CONFIG.minTrustScore,
node.trustScore + SWARM_CONFIG.trustScoreOnFailure
);
const isActive = newFailures < SWARM_CONFIG.maxConsecutiveFailures;
const newFailures = node.consecutiveFailures + 1;
const newTrust = Math.max(
SWARM_CONFIG.minTrustScore,
node.trustScore + SWARM_CONFIG.trustScoreOnFailure
);
const isActive = newFailures < SWARM_CONFIG.maxConsecutiveFailures;
await db.update(swarmNodes)
.set({
consecutiveFailures: newFailures,
trustScore: newTrust,
isActive,
updatedAt: new Date(),
})
.where(eq(swarmNodes.domain, domain));
await db.update(swarmNodes)
.set({
consecutiveFailures: newFailures,
trustScore: newTrust,
isActive,
updatedAt: new Date(),
})
.where(eq(swarmNodes.domain, domain));
} catch (error) {
console.error(`[Swarm] Failed to mark node failure for ${domain}:`, error);
throw error;
}
}
/**
* Mark a node as successfully contacted
*
* @throws Error if database operation fails (after logging)
*/
export async function markNodeSuccess(domain: string): Promise<void> {
if (!db) return;
const node = await db.query.swarmNodes.findFirst({
where: eq(swarmNodes.domain, domain),
});
try {
const node = await db.query.swarmNodes.findFirst({
where: eq(swarmNodes.domain, domain),
});
if (!node) return;
if (!node) return;
const newTrust = Math.min(
SWARM_CONFIG.maxTrustScore,
node.trustScore + SWARM_CONFIG.trustScoreOnSuccess
);
const newTrust = Math.min(
SWARM_CONFIG.maxTrustScore,
node.trustScore + SWARM_CONFIG.trustScoreOnSuccess
);
await db.update(swarmNodes)
.set({
consecutiveFailures: 0,
trustScore: newTrust,
isActive: true,
lastSeenAt: new Date(),
lastSyncAt: new Date(),
updatedAt: new Date(),
})
.where(eq(swarmNodes.domain, domain));
await db.update(swarmNodes)
.set({
consecutiveFailures: 0,
trustScore: newTrust,
isActive: true,
lastSeenAt: new Date(),
lastSyncAt: new Date(),
updatedAt: new Date(),
})
.where(eq(swarmNodes.domain, domain));
} catch (error) {
console.error(`[Swarm] Failed to mark node success for ${domain}:`, error);
throw error;
}
}
/**
* Log a sync operation
*
* @throws Error if database operation fails (after logging)
*/
export async function logSync(
remoteDomain: string,
@@ -219,17 +235,22 @@ export async function logSync(
): Promise<void> {
if (!db) return;
await db.insert(swarmSyncLog).values({
remoteDomain,
direction,
nodesReceived: result.nodesReceived,
nodesSent: result.nodesSent,
handlesReceived: result.handlesReceived,
handlesSent: result.handlesSent,
success: result.success,
errorMessage: result.error,
durationMs: result.durationMs,
});
try {
await db.insert(swarmSyncLog).values({
remoteDomain,
direction,
nodesReceived: result.nodesReceived,
nodesSent: result.nodesSent,
handlesReceived: result.handlesReceived,
handlesSent: result.handlesSent,
success: result.success,
errorMessage: result.error,
durationMs: result.durationMs,
});
} catch (error) {
console.error(`[Swarm] Failed to log sync for ${remoteDomain}:`, error);
throw error;
}
}
/**
+6 -3
View File
@@ -12,11 +12,13 @@ export interface RemoteProfile {
/**
* Upsert a remote user into the local database for caching/display purposes.
*
* @throws Error if database operation fails (after logging)
*/
export async function upsertRemoteUser(profile: RemoteProfile) {
try {
if (!db) return;
export async function upsertRemoteUser(profile: RemoteProfile): Promise<void> {
if (!db) return;
try {
// Check if user already exists
const existing = await db.query.users.findFirst({
where: eq(users.did, profile.did),
@@ -50,5 +52,6 @@ export async function upsertRemoteUser(profile: RemoteProfile) {
}
} catch (error) {
console.error(`[User Cache] Failed to upsert ${profile.handle}:`, error);
throw error;
}
}