Fix HTTP Signature by explicitly setting Host header
- Added key pair validation debug endpoint - Verified that public/private key pairs match correctly - Fixed HTTP Signature issue by explicitly setting Host header - Previously relied on RestTemplate to auto-set Host, which could differ from signed value - Now explicitly set Host header to match the value used in signature calculation This should fix the 401 Unauthorized errors when sending activities to Mastodon
This commit is contained in:
parent
42a585220f
commit
cc8e309821
4 changed files with 215 additions and 5 deletions
|
|
@ -77,6 +77,9 @@ public class SecurityConfig {
|
|||
// Public endpoints - User's public activities
|
||||
.requestMatchers(HttpMethod.GET, "/api/activities/user/*").permitAll()
|
||||
|
||||
// Debug endpoints (dev only)
|
||||
.requestMatchers("/api/debug/**").permitAll()
|
||||
|
||||
// Public endpoints - Likes and Comments (GET only)
|
||||
.requestMatchers(HttpMethod.GET, "/api/activities/*/likes").permitAll()
|
||||
.requestMatchers(HttpMethod.GET, "/api/activities/*/comments").permitAll()
|
||||
|
|
|
|||
|
|
@ -0,0 +1,110 @@
|
|||
package org.operaton.fitpub.controller;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.operaton.fitpub.model.entity.User;
|
||||
import org.operaton.fitpub.repository.UserRepository;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.KeyFactory;
|
||||
import java.security.PrivateKey;
|
||||
import java.security.PublicKey;
|
||||
import java.security.Signature;
|
||||
import java.security.spec.PKCS8EncodedKeySpec;
|
||||
import java.security.spec.X509EncodedKeySpec;
|
||||
import java.util.*;
|
||||
|
||||
/**
|
||||
* Debug controller for troubleshooting.
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("/api/debug")
|
||||
@RequiredArgsConstructor
|
||||
@Slf4j
|
||||
public class DebugController {
|
||||
|
||||
private final UserRepository userRepository;
|
||||
|
||||
@GetMapping("/validate-keys")
|
||||
public Map<String, Object> validateKeys() {
|
||||
List<User> users = userRepository.findAll();
|
||||
Map<String, Object> results = new LinkedHashMap<>();
|
||||
|
||||
for (User user : users) {
|
||||
Map<String, Object> userResult = new LinkedHashMap<>();
|
||||
|
||||
try {
|
||||
// Parse public key
|
||||
PublicKey publicKey = parsePublicKey(user.getPublicKey());
|
||||
userResult.put("publicKeyValid", true);
|
||||
|
||||
// Parse private key
|
||||
PrivateKey privateKey = parsePrivateKey(user.getPrivateKey());
|
||||
userResult.put("privateKeyValid", true);
|
||||
|
||||
// Test if they match by signing and verifying
|
||||
String testData = "Test data for " + user.getUsername();
|
||||
byte[] signature = signData(testData.getBytes(StandardCharsets.UTF_8), privateKey);
|
||||
boolean verified = verifySignature(testData.getBytes(StandardCharsets.UTF_8), signature, publicKey);
|
||||
|
||||
userResult.put("keysMatch", verified);
|
||||
userResult.put("publicKeyPem", user.getPublicKey());
|
||||
|
||||
if (verified) {
|
||||
log.info("✓ Key pair is valid for user: {}", user.getUsername());
|
||||
} else {
|
||||
log.error("✗ Key pair MISMATCH for user: {}", user.getUsername());
|
||||
}
|
||||
|
||||
} catch (Exception e) {
|
||||
userResult.put("error", e.getMessage());
|
||||
log.error("Failed to validate keys for user: {}", user.getUsername(), e);
|
||||
}
|
||||
|
||||
results.put(user.getUsername(), userResult);
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
private PublicKey parsePublicKey(String publicKeyPem) throws Exception {
|
||||
String publicKeyContent = publicKeyPem
|
||||
.replace("-----BEGIN PUBLIC KEY-----", "")
|
||||
.replace("-----END PUBLIC KEY-----", "")
|
||||
.replaceAll("\\s", "");
|
||||
|
||||
byte[] keyBytes = Base64.getDecoder().decode(publicKeyContent);
|
||||
X509EncodedKeySpec spec = new X509EncodedKeySpec(keyBytes);
|
||||
KeyFactory keyFactory = KeyFactory.getInstance("RSA");
|
||||
return keyFactory.generatePublic(spec);
|
||||
}
|
||||
|
||||
private PrivateKey parsePrivateKey(String privateKeyPem) throws Exception {
|
||||
String privateKeyContent = privateKeyPem
|
||||
.replace("-----BEGIN PRIVATE KEY-----", "")
|
||||
.replace("-----END PRIVATE KEY-----", "")
|
||||
.replaceAll("\\s", "");
|
||||
|
||||
byte[] keyBytes = Base64.getDecoder().decode(privateKeyContent);
|
||||
PKCS8EncodedKeySpec spec = new PKCS8EncodedKeySpec(keyBytes);
|
||||
KeyFactory keyFactory = KeyFactory.getInstance("RSA");
|
||||
return keyFactory.generatePrivate(spec);
|
||||
}
|
||||
|
||||
private byte[] signData(byte[] data, PrivateKey privateKey) throws Exception {
|
||||
Signature signature = Signature.getInstance("SHA256withRSA");
|
||||
signature.initSign(privateKey);
|
||||
signature.update(data);
|
||||
return signature.sign();
|
||||
}
|
||||
|
||||
private boolean verifySignature(byte[] data, byte[] signatureBytes, PublicKey publicKey) throws Exception {
|
||||
Signature signature = Signature.getInstance("SHA256withRSA");
|
||||
signature.initVerify(publicKey);
|
||||
signature.update(data);
|
||||
return signature.verify(signatureBytes);
|
||||
}
|
||||
}
|
||||
|
|
@ -190,6 +190,11 @@ public class FederationService {
|
|||
headers.set("Content-Type", "application/activity+json");
|
||||
headers.set("Accept", "application/activity+json");
|
||||
|
||||
// CRITICAL: Set the Host header to exactly match what was used in the signature
|
||||
// We MUST set this explicitly, otherwise RestTemplate might set it differently
|
||||
// (e.g., with port number) and the signature validation will fail
|
||||
headers.set("Host", signatureHeaders.host);
|
||||
|
||||
// Set the Date and Digest headers that were used in the signature
|
||||
headers.set("Date", signatureHeaders.date);
|
||||
headers.set("Digest", signatureHeaders.digest);
|
||||
|
|
@ -197,11 +202,6 @@ public class FederationService {
|
|||
// 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);
|
||||
|
||||
ResponseEntity<String> response = restTemplate.postForEntity(inboxUrl, entity, String.class);
|
||||
|
|
|
|||
|
|
@ -0,0 +1,97 @@
|
|||
package org.operaton.fitpub.security;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.operaton.fitpub.model.entity.User;
|
||||
import org.operaton.fitpub.repository.UserRepository;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.KeyFactory;
|
||||
import java.security.PrivateKey;
|
||||
import java.security.PublicKey;
|
||||
import java.security.Signature;
|
||||
import java.security.spec.PKCS8EncodedKeySpec;
|
||||
import java.security.spec.X509EncodedKeySpec;
|
||||
import java.util.Base64;
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
/**
|
||||
* Test to validate that users' public and private keys match.
|
||||
*/
|
||||
@SpringBootTest
|
||||
@Slf4j
|
||||
public class KeyPairValidationTest {
|
||||
|
||||
@Autowired
|
||||
private UserRepository userRepository;
|
||||
|
||||
@Test
|
||||
public void testAllUsersKeysMatch() {
|
||||
List<User> users = userRepository.findAll();
|
||||
|
||||
for (User user : users) {
|
||||
log.info("Validating key pair for user: {}", user.getUsername());
|
||||
|
||||
try {
|
||||
// Parse public key
|
||||
PublicKey publicKey = parsePublicKey(user.getPublicKey());
|
||||
|
||||
// Parse private key
|
||||
PrivateKey privateKey = parsePrivateKey(user.getPrivateKey());
|
||||
|
||||
// Test if they match by signing and verifying
|
||||
String testData = "Test data for " + user.getUsername();
|
||||
byte[] signature = signData(testData.getBytes(StandardCharsets.UTF_8), privateKey);
|
||||
boolean verified = verifySignature(testData.getBytes(StandardCharsets.UTF_8), signature, publicKey);
|
||||
|
||||
assertTrue(verified, "Public key does NOT match private key for user: " + user.getUsername());
|
||||
log.info("✓ Key pair is valid for user: {}", user.getUsername());
|
||||
|
||||
} catch (Exception e) {
|
||||
fail("Failed to validate keys for user " + user.getUsername() + ": " + e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private PublicKey parsePublicKey(String publicKeyPem) throws Exception {
|
||||
String publicKeyContent = publicKeyPem
|
||||
.replace("-----BEGIN PUBLIC KEY-----", "")
|
||||
.replace("-----END PUBLIC KEY-----", "")
|
||||
.replaceAll("\\s", "");
|
||||
|
||||
byte[] keyBytes = Base64.getDecoder().decode(publicKeyContent);
|
||||
X509EncodedKeySpec spec = new X509EncodedKeySpec(keyBytes);
|
||||
KeyFactory keyFactory = KeyFactory.getInstance("RSA");
|
||||
return keyFactory.generatePublic(spec);
|
||||
}
|
||||
|
||||
private PrivateKey parsePrivateKey(String privateKeyPem) throws Exception {
|
||||
String privateKeyContent = privateKeyPem
|
||||
.replace("-----BEGIN PRIVATE KEY-----", "")
|
||||
.replace("-----END PRIVATE KEY-----", "")
|
||||
.replaceAll("\\s", "");
|
||||
|
||||
byte[] keyBytes = Base64.getDecoder().decode(privateKeyContent);
|
||||
PKCS8EncodedKeySpec spec = new PKCS8EncodedKeySpec(keyBytes);
|
||||
KeyFactory keyFactory = KeyFactory.getInstance("RSA");
|
||||
return keyFactory.generatePrivate(spec);
|
||||
}
|
||||
|
||||
private byte[] signData(byte[] data, PrivateKey privateKey) throws Exception {
|
||||
Signature signature = Signature.getInstance("SHA256withRSA");
|
||||
signature.initSign(privateKey);
|
||||
signature.update(data);
|
||||
return signature.sign();
|
||||
}
|
||||
|
||||
private boolean verifySignature(byte[] data, byte[] signatureBytes, PublicKey publicKey) throws Exception {
|
||||
Signature signature = Signature.getInstance("SHA256withRSA");
|
||||
signature.initVerify(publicKey);
|
||||
signature.update(data);
|
||||
return signature.verify(signatureBytes);
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue