Fix federation some more

This commit is contained in:
Tim Zöller 2025-12-01 11:10:53 +01:00
parent 6aa3cbb5f6
commit 8fc741e3d6
3 changed files with 22 additions and 113 deletions

View file

@ -33,10 +33,9 @@ public class FitPubApplication {
/**
* REST template for making HTTP requests to remote ActivityPub servers.
* Configured with HTTP Signature interceptor for ActivityPub federation.
*/
@Bean
public RestTemplate restTemplate(org.operaton.fitpub.config.ActivityPubHttpRequestInterceptor interceptor) {
public RestTemplate restTemplate() {
// Use Apache HttpClient with custom configuration
HttpClientConnectionManager connectionManager = PoolingHttpClientConnectionManagerBuilder.create()
.build();
@ -48,11 +47,6 @@ public class FitPubApplication {
HttpComponentsClientHttpRequestFactory requestFactory = new HttpComponentsClientHttpRequestFactory(httpClient);
RestTemplate restTemplate = new RestTemplate(requestFactory);
// Add HTTP Signature interceptor
restTemplate.getInterceptors().add(interceptor);
return restTemplate;
return new RestTemplate(requestFactory);
}
}

View file

@ -1,91 +0,0 @@
package org.operaton.fitpub.config;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.operaton.fitpub.security.HttpSignatureValidator;
import org.springframework.http.HttpRequest;
import org.springframework.http.client.ClientHttpRequestExecution;
import org.springframework.http.client.ClientHttpRequestInterceptor;
import org.springframework.http.client.ClientHttpResponse;
import org.springframework.stereotype.Component;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
/**
* Intercepts outgoing HTTP requests for ActivityPub federation to add HTTP Signatures.
* This interceptor is applied AFTER RestTemplate sets all headers (including Host),
* ensuring the signature matches the actual headers sent.
*/
@Component
@RequiredArgsConstructor
@Slf4j
public class ActivityPubHttpRequestInterceptor implements ClientHttpRequestInterceptor {
private final HttpSignatureValidator signatureValidator;
@Override
public ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution)
throws IOException {
// Check if this request needs HTTP Signature (look for a marker header)
String privateKey = request.getHeaders().getFirst("X-ActivityPub-PrivateKey");
String keyId = request.getHeaders().getFirst("X-ActivityPub-KeyId");
if (privateKey != null && keyId != null) {
// Remove marker headers (they shouldn't be sent)
request.getHeaders().remove("X-ActivityPub-PrivateKey");
request.getHeaders().remove("X-ActivityPub-KeyId");
try {
// Now sign the request with the actual headers that will be sent
String method = request.getMethod().name();
String uri = request.getURI().toString();
String bodyString = new String(body, StandardCharsets.UTF_8);
// Get the actual Host header that RestTemplate set
String host = request.getHeaders().getFirst("Host");
if (host == null) {
host = request.getURI().getHost();
}
// Get the actual Date header that was set
String date = request.getHeaders().getFirst("Date");
// Get the actual Digest header that was set
String digest = request.getHeaders().getFirst("Digest");
// Build the signing string with the ACTUAL header values
String signingString = String.format(
"(request-target): %s %s%s\nhost: %s\ndate: %s\ndigest: %s",
method.toLowerCase(),
request.getURI().getPath(),
request.getURI().getQuery() != null ? "?" + request.getURI().getQuery() : "",
host,
date,
digest
);
// Sign the string
String signatureBase64 = signatureValidator.sign(signingString, privateKey);
// Build signature header
String signatureHeader = String.format(
"keyId=\"%s\",algorithm=\"rsa-sha256\",headers=\"(request-target) host date digest\",signature=\"%s\"",
keyId, signatureBase64
);
// Add signature header
request.getHeaders().set("Signature", signatureHeader);
log.debug("Added HTTP Signature to request: {}", request.getURI());
} catch (Exception e) {
log.error("Failed to sign request", e);
throw new IOException("Failed to sign ActivityPub request", e);
}
}
return execution.execute(request, body);
}
}

View file

@ -166,25 +166,31 @@ public class FederationService {
try {
String activityJson = objectMapper.writeValueAsString(activity);
// Calculate Date and Digest headers
java.time.ZonedDateTime now = java.time.ZonedDateTime.now(java.time.ZoneOffset.UTC);
java.time.format.DateTimeFormatter formatter = java.time.format.DateTimeFormatter.RFC_1123_DATE_TIME;
String date = now.format(formatter);
java.security.MessageDigest digest = java.security.MessageDigest.getInstance("SHA-256");
byte[] hash = digest.digest(activityJson.getBytes(java.nio.charset.StandardCharsets.UTF_8));
String digestValue = "SHA-256=" + java.util.Base64.getEncoder().encodeToString(hash);
// Generate HTTP signature with all required headers
// This calculates what the signature SHOULD be, including the host from the URL
HttpSignatureValidator.SignatureHeaders signatureHeaders = signatureValidator.signRequest(
HttpMethod.POST.name(),
inboxUrl,
activityJson,
sender.getPrivateKey(),
baseUrl + "/users/" + sender.getUsername() + "#main-key"
);
HttpHeaders headers = new HttpHeaders();
headers.set("Content-Type", "application/activity+json");
headers.set("Accept", "application/activity+json");
headers.set("Date", date);
headers.set("Digest", digestValue);
// Set marker headers for the interceptor to use for signing
// These will be removed by the interceptor before sending
headers.set("X-ActivityPub-PrivateKey", sender.getPrivateKey());
headers.set("X-ActivityPub-KeyId", baseUrl + "/users/" + sender.getUsername() + "#main-key");
// Set the Date and Digest headers that were used in the signature
headers.set("Date", signatureHeaders.date);
headers.set("Digest", signatureHeaders.digest);
// Set the Signature header
headers.set("Signature", signatureHeaders.signature);
// NOTE: We do NOT set the Host header here.
// RestTemplate/HttpClient will set it automatically to match the URL.
// The signature was calculated with the correct host (extracted from inboxUrl),
// so when the client sets the Host header, it will match the signature.
HttpEntity<String> entity = new HttpEntity<>(activityJson, headers);