@@ -1021,14 +1021,23 @@ export class FederationImpl<TContextData>
10211021 validateCryptoKey ( privateKey , "private" ) ;
10221022 if ( rsaKey == null && privateKey . algorithm . name === "RSASSA-PKCS1-v1_5" ) {
10231023 rsaKey = { keyId, privateKey } ;
1024- continue ;
10251024 }
1026- if ( privateKey . algorithm . name === "Ed25519" ) {
1027- activity = await signObject ( activity , privateKey , keyId , {
1028- contextLoader,
1029- tracerProvider : this . tracerProvider ,
1030- } ) ;
1031- proofCreated = true ;
1025+ }
1026+ // If Object Integrity Proofs were already created before fanout (e.g., in
1027+ // sendActivityInternal()), skip signing to avoid duplicates.
1028+ for await ( const _ of activity . getProofs ( { contextLoader } ) ) {
1029+ proofCreated = true ;
1030+ break ;
1031+ }
1032+ if ( ! proofCreated ) {
1033+ for ( const { keyId, privateKey } of keys ) {
1034+ if ( privateKey . algorithm . name === "Ed25519" ) {
1035+ activity = await signObject ( activity , privateKey , keyId , {
1036+ contextLoader,
1037+ tracerProvider : this . tracerProvider ,
1038+ } ) ;
1039+ proofCreated = true ;
1040+ }
10321041 }
10331042 }
10341043 let jsonLd = await activity . toJsonLd ( {
@@ -1920,6 +1929,7 @@ export class ContextImpl<TContextData> implements Context<TContextData> {
19201929 }
19211930 const owner = this . getActorUri ( identifier ) ;
19221931 const result = [ ] ;
1932+ let i = 1 ;
19231933 for ( const keyPair of keyPairs ) {
19241934 const newPair : ActorKeyPair = {
19251935 ...keyPair ,
@@ -1929,12 +1939,13 @@ export class ContextImpl<TContextData> implements Context<TContextData> {
19291939 publicKey : keyPair . publicKey ,
19301940 } ) ,
19311941 multikey : new Multikey ( {
1932- id : keyPair . keyId ,
1942+ id : new URL ( `#multikey- ${ i } ` , owner ) ,
19331943 controller : owner ,
19341944 publicKey : keyPair . publicKey ,
19351945 } ) ,
19361946 } ;
19371947 result . push ( newPair ) ;
1948+ i ++ ;
19381949 }
19391950 return result ;
19401951 }
@@ -2192,6 +2203,7 @@ export class ContextImpl<TContextData> implements Context<TContextData> {
21922203 const logger = getLogger ( [ "fedify" , "federation" , "outbox" ] ) ;
21932204 let keys : SenderKeyPair [ ] ;
21942205 let identifier : string | null = null ;
2206+ let actorKeyPairs : ActorKeyPair [ ] | null = null ;
21952207 if ( "identifier" in sender || "username" in sender || "handle" in sender ) {
21962208 if ( "identifier" in sender ) {
21972209 identifier = sender . identifier ;
@@ -2225,12 +2237,19 @@ export class ContextImpl<TContextData> implements Context<TContextData> {
22252237 }
22262238 }
22272239 span . setAttribute ( "fedify.actor.identifier" , identifier ) ;
2228- keys = await this . getKeyPairsFromIdentifier ( identifier ) ;
2229- if ( keys . length < 1 ) {
2240+ if ( this . federation . actorCallbacks ?. keyPairsDispatcher == null ) {
2241+ throw new Error ( "No actor key pairs dispatcher registered." ) ;
2242+ }
2243+ actorKeyPairs = await this . getActorKeyPairs ( identifier ) ;
2244+ if ( actorKeyPairs . length < 1 ) {
22302245 throw new Error (
22312246 `No key pair found for actor ${ JSON . stringify ( identifier ) } .` ,
22322247 ) ;
22332248 }
2249+ keys = actorKeyPairs . map ( ( kp ) => ( {
2250+ keyId : kp . keyId ,
2251+ privateKey : kp . privateKey ,
2252+ } ) ) ;
22342253 } else if ( Array . isArray ( sender ) ) {
22352254 if ( sender . length < 1 ) {
22362255 throw new Error ( "The sender's key pairs are empty." ) ;
@@ -2288,6 +2307,22 @@ export class ContextImpl<TContextData> implements Context<TContextData> {
22882307 "The activity to send must have at least one actor property." ,
22892308 ) ;
22902309 }
2310+ // Pre-sign with Object Integrity Proofs before fanout so that all
2311+ // recipients receive the same signed activity. Uses Multikey IDs so that
2312+ // verifiers can look up the correct key type in the actor document.
2313+ if ( actorKeyPairs != null ) {
2314+ const contextLoader = this . contextLoader ;
2315+ for ( const kp of actorKeyPairs ) {
2316+ if (
2317+ kp . privateKey . algorithm . name !== "Ed25519" ||
2318+ kp . multikey . id == null
2319+ ) continue ;
2320+ activity = await signObject ( activity , kp . privateKey , kp . multikey . id , {
2321+ contextLoader,
2322+ tracerProvider : this . tracerProvider ,
2323+ } ) ;
2324+ }
2325+ }
22912326 const inboxes = extractInboxes ( {
22922327 recipients : expandedRecipients ,
22932328 preferSharedInbox : options . preferSharedInbox ,
@@ -2840,12 +2875,16 @@ export class InboxContextImpl<TContextData> extends ContextImpl<TContextData>
28402875 identifier = mapped ;
28412876 }
28422877 }
2843- keys = await this . getKeyPairsFromIdentifier ( identifier ) ;
2844- if ( keys . length < 1 ) {
2878+ const actorKeyPairs = await this . getActorKeyPairs ( identifier ) ;
2879+ if ( actorKeyPairs . length < 1 ) {
28452880 throw new Error (
28462881 `No key pair found for actor ${ JSON . stringify ( identifier ) } .` ,
28472882 ) ;
28482883 }
2884+ keys = actorKeyPairs . map ( ( kp ) => ( {
2885+ keyId : kp . keyId ,
2886+ privateKey : kp . privateKey ,
2887+ } ) ) ;
28492888 } else if ( Array . isArray ( forwarder ) ) {
28502889 if ( forwarder . length < 1 ) {
28512890 throw new Error ( "The forwarder's key pairs are empty." ) ;
0 commit comments