diff --git a/src/main/java/org/operaton/fitpub/FitPubApplication.java b/src/main/java/org/operaton/fitpub/FitPubApplication.java index 3dc8b27..1979ece 100644 --- a/src/main/java/org/operaton/fitpub/FitPubApplication.java +++ b/src/main/java/org/operaton/fitpub/FitPubApplication.java @@ -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); } } diff --git a/src/main/java/org/operaton/fitpub/config/ActivityPubHttpRequestInterceptor.java b/src/main/java/org/operaton/fitpub/config/ActivityPubHttpRequestInterceptor.java deleted file mode 100644 index be1e5c5..0000000 --- a/src/main/java/org/operaton/fitpub/config/ActivityPubHttpRequestInterceptor.java +++ /dev/null @@ -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); - } -} diff --git a/src/main/java/org/operaton/fitpub/service/FederationService.java b/src/main/java/org/operaton/fitpub/service/FederationService.java index 4cf8749..e6a594f 100644 --- a/src/main/java/org/operaton/fitpub/service/FederationService.java +++ b/src/main/java/org/operaton/fitpub/service/FederationService.java @@ -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 entity = new HttpEntity<>(activityJson, headers);