@@ -1073,14 +1073,23 @@ export class FederationImpl<TContextData>
10731073 validateCryptoKey ( privateKey , "private" ) ;
10741074 if ( rsaKey == null && privateKey . algorithm . name === "RSASSA-PKCS1-v1_5" ) {
10751075 rsaKey = { keyId, privateKey } ;
1076- continue ;
10771076 }
1078- if ( privateKey . algorithm . name === "Ed25519" ) {
1079- activity = await signObject ( activity , privateKey , keyId , {
1080- contextLoader,
1081- tracerProvider : this . tracerProvider ,
1082- } ) ;
1083- proofCreated = true ;
1077+ }
1078+ // If Object Integrity Proofs were already created before fanout (e.g., in
1079+ // sendActivityInternal()), skip signing to avoid duplicates.
1080+ for await ( const _ of activity . getProofs ( { contextLoader } ) ) {
1081+ proofCreated = true ;
1082+ break ;
1083+ }
1084+ if ( ! proofCreated ) {
1085+ for ( const { keyId, privateKey } of keys ) {
1086+ if ( privateKey . algorithm . name === "Ed25519" ) {
1087+ activity = await signObject ( activity , privateKey , keyId , {
1088+ contextLoader,
1089+ tracerProvider : this . tracerProvider ,
1090+ } ) ;
1091+ proofCreated = true ;
1092+ }
10841093 }
10851094 }
10861095 let jsonLd = await activity . toJsonLd ( {
@@ -1950,6 +1959,7 @@ export class ContextImpl<TContextData> implements Context<TContextData> {
19501959 }
19511960 const owner = this . getActorUri ( identifier ) ;
19521961 const result = [ ] ;
1962+ let i = 1 ;
19531963 for ( const keyPair of keyPairs ) {
19541964 const newPair : ActorKeyPair = {
19551965 ...keyPair ,
@@ -1959,12 +1969,13 @@ export class ContextImpl<TContextData> implements Context<TContextData> {
19591969 publicKey : keyPair . publicKey ,
19601970 } ) ,
19611971 multikey : new Multikey ( {
1962- id : keyPair . keyId ,
1972+ id : new URL ( `#multikey- ${ i } ` , owner ) ,
19631973 controller : owner ,
19641974 publicKey : keyPair . publicKey ,
19651975 } ) ,
19661976 } ;
19671977 result . push ( newPair ) ;
1978+ i ++ ;
19681979 }
19691980 return result ;
19701981 }
@@ -2208,6 +2219,7 @@ export class ContextImpl<TContextData> implements Context<TContextData> {
22082219 const logger = getLogger ( [ "fedify" , "federation" , "outbox" ] ) ;
22092220 let keys : SenderKeyPair [ ] ;
22102221 let identifier : string | null = null ;
2222+ let actorKeyPairs : ActorKeyPair [ ] | null = null ;
22112223 if ( "identifier" in sender || "username" in sender ) {
22122224 if ( "identifier" in sender ) {
22132225 identifier = sender . identifier ;
@@ -2231,12 +2243,19 @@ export class ContextImpl<TContextData> implements Context<TContextData> {
22312243 }
22322244 }
22332245 span . setAttribute ( "fedify.actor.identifier" , identifier ) ;
2234- keys = await this . getKeyPairsFromIdentifier ( identifier ) ;
2235- if ( keys . length < 1 ) {
2246+ if ( this . federation . actorCallbacks ?. keyPairsDispatcher == null ) {
2247+ throw new Error ( "No actor key pairs dispatcher registered." ) ;
2248+ }
2249+ actorKeyPairs = await this . getActorKeyPairs ( identifier ) ;
2250+ if ( actorKeyPairs . length < 1 ) {
22362251 throw new Error (
22372252 `No key pair found for actor ${ JSON . stringify ( identifier ) } .` ,
22382253 ) ;
22392254 }
2255+ keys = actorKeyPairs . map ( ( kp ) => ( {
2256+ keyId : kp . keyId ,
2257+ privateKey : kp . privateKey ,
2258+ } ) ) ;
22402259 } else if ( Array . isArray ( sender ) ) {
22412260 if ( sender . length < 1 ) {
22422261 throw new Error ( "The sender's key pairs are empty." ) ;
@@ -2300,6 +2319,22 @@ export class ContextImpl<TContextData> implements Context<TContextData> {
23002319 "The activity to send must have at least one actor property." ,
23012320 ) ;
23022321 }
2322+ // Pre-sign with Object Integrity Proofs before fanout so that all
2323+ // recipients receive the same signed activity. Uses Multikey IDs so that
2324+ // verifiers can look up the correct key type in the actor document.
2325+ if ( actorKeyPairs != null ) {
2326+ const contextLoader = this . contextLoader ;
2327+ for ( const kp of actorKeyPairs ) {
2328+ if (
2329+ kp . privateKey . algorithm . name !== "Ed25519" ||
2330+ kp . multikey . id == null
2331+ ) continue ;
2332+ activity = await signObject ( activity , kp . privateKey , kp . multikey . id , {
2333+ contextLoader,
2334+ tracerProvider : this . tracerProvider ,
2335+ } ) ;
2336+ }
2337+ }
23032338 const inboxes = extractInboxes ( {
23042339 recipients : expandedRecipients ,
23052340 preferSharedInbox : options . preferSharedInbox ,
@@ -2841,12 +2876,16 @@ export class InboxContextImpl<TContextData> extends ContextImpl<TContextData>
28412876 identifier = mapped ;
28422877 }
28432878 }
2844- keys = await this . getKeyPairsFromIdentifier ( identifier ) ;
2845- if ( keys . length < 1 ) {
2879+ const actorKeyPairs = await this . getActorKeyPairs ( identifier ) ;
2880+ if ( actorKeyPairs . length < 1 ) {
28462881 throw new Error (
28472882 `No key pair found for actor ${ JSON . stringify ( identifier ) } .` ,
28482883 ) ;
28492884 }
2885+ keys = actorKeyPairs . map ( ( kp ) => ( {
2886+ keyId : kp . keyId ,
2887+ privateKey : kp . privateKey ,
2888+ } ) ) ;
28502889 } else if ( Array . isArray ( forwarder ) ) {
28512890 if ( forwarder . length < 1 ) {
28522891 throw new Error ( "The forwarder's key pairs are empty." ) ;
0 commit comments