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:
Tim Zöller 2025-12-02 21:46:08 +01:00
parent 42a585220f
commit cc8e309821
4 changed files with 215 additions and 5 deletions

View file

@ -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);
}
}