Needing assistance signing Followers

Problem Description

I’m having some problems signing an ActivityPub Accept activity. The error I’m getting is:

{"error":"Verification failed for russell@podcastperformance.com https://podcastperformance.com/users/russell using rsa-sha256 (RSASSA-PKCS1-v1_5 with SHA-256)","signed_string":"(request-target): post /users/russell/inbox\nhost: podcastindex.social\ndate: Sat, 08 Jun 2024 16:29:26 GMT\ndigest: SHA-256=oLv+MpAd569crts9yw+vajks8CsMZ3R/KqhMZaACVUY=","signature":"d0485QzmCxFPsPIZunc3OKUydYbLaRgPlnjHN4hI1cR4eQ4Y4vrRXvdP2TUfiEaMnnqaXT+Kshv8ugAKyxCw90KSmbyYLxZw+eOHxMaX/xNAhGsgivV6ZKMAwgrO1iyqrOxohCflq/c0hIhw2WMuEAoya0ag7L3dFyPn3hwNYr+eAt/Y1RohEj96cYyizHzX//op1SQ/zW1fWmbksmefiDF0v/Nt0fqU9TGXdTR5VFEZQQLtqBUTb8SFe7nD9u4UTIyhrqAqRzfOe9fLpyUkMZ0lgrjuMvixhqamyBMeS2Sks9TIiEnSdVfoShfUoxeQZ3nCx5Sw6jb5je4H1/0rsw=="}%

Code Details

Function to Handle Follow Activity

async function handleFollowActivity(db, user, activity) {
  await db.followUser(user.username, activity.actor);

  const acceptActivity = {
    "@context": "https://www.w3.org/ns/activitystreams",
    "summary": "User accepted a follow",
    "id": `https://podcastperformance.com/activities/${generateUniqueId()}`,
    "type": "Accept",
    "actor": `https://podcastperformance.com/users/${user.username}`,
    "object": activity.id
  };

  const keyId = `https://podcastperformance.com/users/${user.username}#main-key`;
  const path = `/users/${user.username}/inbox`;
  const host = 'podcastperformance.com';

  const signatureResult = await db.generateSignature(acceptActivity, path, 'POST', host);
  const { signature, date, digest } = signatureResult;

  // Send the Accept activity to the actor's inbox
  await sendActivityToInbox(activity.actor, acceptActivity, signatureResult);
}

Function to Generate Signature

const crypto = require('crypto');

const generateSignature = async (data, path, method, host) => {
  if (!db) throw new Error('Database not initialized');
  const collection = db.collection('podcast_performance_users');
  const user = await collection.findOne({ 'username': 'russell' }, { projection: { secretKey: 1 } });
  if (!user) {
    throw new Error('No User');
  }
  const privateKey = user.secretKey;

  // Generate the digest of the body
  const digest = crypto.createHash('sha256').update(JSON.stringify(data)).digest('base64');

  // Construct the signing string
  const date = new Date().toUTCString();
  const signingString = `(request-target): ${method.toLowerCase()} ${path}\n` +
    `host: ${host}\n` +
    `date: ${date}\n` +
    `digest: SHA-256=${digest}`;

  // Create a sign object and sign the string
  const sign = crypto.createSign('RSA-SHA256');
  sign.update(signingString);
  sign.end();

  // Generate the signature using the private key
  const signature = sign.sign(privateKey, 'base64');

  return {
    signature,
    date,
    digest
  };
}

Function to Send Activity to Inbox

const axios = require('axios');

async function sendActivityToInbox(actor, activity, signatureResult) {
  const inboxUrl = actor.endsWith('/') ? `${actor}inbox` : `${actor}/inbox`;
  const body = JSON.stringify(activity);
  const { signature, date, digest } = signatureResult;
  const host = new URL(inboxUrl).host;

  const headers = {
    'Content-Type': 'application/activity+json',
    'Date': date,
    'Digest': `SHA-256=${digest}`,
    'Signature': `keyId="https://podcastperformance.com/users/russell#main-key",algorithm="rsa-sha256",headers="(request-target) host date digest",signature="${signature}"`
  };

  try {
    const response = await axios.post(inboxUrl, body, { headers });
    console.log('Activity sent successfully:', response.data);
  } catch (error) {
    console.error('Error sending activity:', error.response.data);
  }
}

Importing Crypto Module

import crypto from 'crypto';

Issue

The signature verification fails, and the error message suggests that the generated signature does not match the expected value. The signed_string and signature seem to be correct, but the receiving server rejects them.

Could anyone provide guidance on what might be causing this issue and how to resolve it? Specifically, are there any known issues with the signing process or the headers used in the request? Any help would be greatly appreciated!

You might want to check whether Node.js (I’m assuming this is node.js, not sure) uses RSASSA-PKCS1-v1_5 encoding rather than RSASSA-PSS.
I’m not sure which encoding node.js uses. But if I’m reading this correctly, it seems to be using the correct padding by default: Crypto | Node.js v22.2.0 Documentation

However, you could try your code against this test data: draft-cavage-http-signatures-12
If you code gives the desired output, you at least know your code is valid, and the problem must be somewhere else.

It is using RSASSA-PKCS1-v1_5

It’s strange I can get it working in python, but not nodeJS/ExpressJS

Its good that its working in python. My thoughts on the error were checking the digest calculation and ensuring the private keys were saved using “RSASSA-PKCS1-v1_5”. I grabbed a copy of the code locally, but don’t really have enough local information repro the issue.

here is a slight change, not sure if it fixes the issue but figured id at least toss something tangible your way

 // Generate the digest of the body
const digest = crypto.createHash('sha256').update(JSON.stringify(data)).digest('base64');

// Construct the signing string
const date = new Date().toUTCString();
const signingString = `(request-target): ${method.toLowerCase()} ${path}\n` +
  `host: ${host}\n` +
  `date: ${date}\n` +
  `digest: SHA-256=${digest}`;

// old -----------------------------------
// Create a sign object and sign the string
const sign = crypto.createSign('RSA-SHA256');
sign.update(signingString);
sign.end();

// Generate the signature using the private key
const signature = sign.sign(privateKey, 'base64');


// new -----------------------------------
const buffer = require('buffer'); 
const data = Buffer.from(signingString); 
const sign = crypto.sign("SHA256", data , privateKey); 
const signature = sign.toString('base64'); 

@russellharower Would a working implementation as a reference help?

Since you’re using node, just take a look at how NodeBB signs and verifies HTTP signatures