Security fixes: swarm signature verification and error handling
This commit is contained in:
@@ -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
@@ -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
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user