aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorGravatar Vitaly Takmazov2020-11-10 16:36:02 +0300
committerGravatar Vitaly Takmazov2020-11-10 16:36:02 +0300
commit07ebf86ab279811c365e8174807dbf36fc2f4ca4 (patch)
tree9413da0d168bda99a8921eda9c97d4dd9341eb52 /src
parenta0a70fd3c0b031426a34b4d62a7b3e11f1a90c64 (diff)
ActivityPub: Digest header is mandatory now for POST requests
Diffstat (limited to 'src')
-rw-r--r--src/main/java/com/juick/ActivityPubManager.java11
-rw-r--r--src/main/java/com/juick/SignatureManager.java42
-rw-r--r--src/test/java/com/juick/server/tests/ServerTests.java25
3 files changed, 57 insertions, 21 deletions
diff --git a/src/main/java/com/juick/ActivityPubManager.java b/src/main/java/com/juick/ActivityPubManager.java
index e3b1ac8e..22fe826f 100644
--- a/src/main/java/com/juick/ActivityPubManager.java
+++ b/src/main/java/com/juick/ActivityPubManager.java
@@ -54,6 +54,7 @@ import java.io.IOException;
import java.io.StringWriter;
import java.io.Writer;
import java.net.URI;
+import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
@@ -104,7 +105,7 @@ public class ActivityPubManager implements ActivityListener, NotificationListene
signatureManager.post(me, follower, accept);
socialService.addFollower(followedUser, follower.getId());
logger.info("Follower added for {}", followedUser.getName());
- } catch (IOException e) {
+ } catch (IOException | NoSuchAlgorithmException e) {
logger.info("activitypub exception", e);
}
}
@@ -145,7 +146,7 @@ public class ActivityPubManager implements ActivityListener, NotificationListene
try {
logger.info("Deletion to follower {}", follower.getId());
signatureManager.post(me, follower, delete);
- } catch (IOException e) {
+ } catch (IOException | NoSuchAlgorithmException e) {
logger.warn("activitypub exception", e);
}
});
@@ -194,7 +195,7 @@ public class ActivityPubManager implements ActivityListener, NotificationListene
try {
logger.info("Update to follower {}", follower.getId());
signatureManager.post(me, follower, update);
- } catch (IOException e) {
+ } catch (IOException | NoSuchAlgorithmException e) {
logger.warn("activitypub exception", e);
}
});
@@ -239,7 +240,7 @@ public class ActivityPubManager implements ActivityListener, NotificationListene
create.setObject(note);
try {
signatureManager.post(me, follower, create);
- } catch (IOException e) {
+ } catch (IOException | NoSuchAlgorithmException e) {
logger.warn("activitypub exception", e);
}
}
@@ -386,7 +387,7 @@ public class ActivityPubManager implements ActivityListener, NotificationListene
try {
logger.info("Announcing top: {}", message.getMid());
signatureManager.post(me, (Person)person, announce);
- } catch (IOException e) {
+ } catch (IOException | NoSuchAlgorithmException e) {
logger.warn("activitypub exception", e);
}
}, () -> logger.warn("Follower not found: {}", acct));
diff --git a/src/main/java/com/juick/SignatureManager.java b/src/main/java/com/juick/SignatureManager.java
index fc92f39a..fed6c368 100644
--- a/src/main/java/com/juick/SignatureManager.java
+++ b/src/main/java/com/juick/SignatureManager.java
@@ -26,6 +26,7 @@ import com.juick.www.api.activity.model.Context;
import com.juick.www.api.activity.model.objects.Person;
import com.juick.www.api.webfinger.model.Account;
import com.juick.www.api.webfinger.model.Link;
+import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpEntity;
@@ -34,6 +35,8 @@ import org.springframework.http.HttpMethod;
import org.springframework.http.ResponseEntity;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.util.UriComponentsBuilder;
+import org.tomitribe.auth.signatures.Base64;
+import org.tomitribe.auth.signatures.MissingRequiredHeaderException;
import org.tomitribe.auth.signatures.Signature;
import org.tomitribe.auth.signatures.Signer;
import org.tomitribe.auth.signatures.Verifier;
@@ -43,10 +46,13 @@ import javax.inject.Inject;
import java.io.IOException;
import java.net.URI;
import java.security.Key;
+import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.SignatureException;
import java.time.Instant;
+import java.util.Arrays;
import java.util.HashMap;
+import java.util.List;
import java.util.Map;
import java.util.Optional;
@@ -63,35 +69,51 @@ public class SignatureManager {
@Inject
private RestTemplate apClient;
- public void post(Person from, Person to, Context data) throws IOException {
+ public void post(Person from, Person to, Context data) throws IOException, NoSuchAlgorithmException {
UriComponentsBuilder uriComponentsBuilder = UriComponentsBuilder.fromUriString(to.getInbox());
URI inbox = uriComponentsBuilder.build().toUri();
Instant now = Instant.now();
String requestDate = DateFormattersHolder.getHttpDateFormatter().format(now);
String host = inbox.getPort() > 0 ? String.format("%s:%d", inbox.getHost(), inbox.getPort()) : inbox.getHost();
- String signatureString = addSignature(from, host, "POST", inbox.getPath(), requestDate);
+ var finalContext = Context.build(data);
+ var payload = jsonMapper.writeValueAsString(finalContext);
+ final byte[] digest = MessageDigest.getInstance("SHA-256").digest(payload.getBytes()); // (1)
+ final String digestHeader = "SHA-256=" + new String(Base64.encodeBase64(digest));
+ String signatureString = addSignature(from, host, "POST", inbox.getPath(), requestDate, digestHeader);
HttpHeaders requestHeaders = new HttpHeaders();
requestHeaders.add("Content-Type", Context.ACTIVITYSTREAMS_PROFILE_MEDIA_TYPE);
requestHeaders.add("Date", requestDate);
requestHeaders.add("Host", host);
+ requestHeaders.add("Digest", digestHeader);
requestHeaders.add("Signature", signatureString);
- HttpEntity<Context> request = new HttpEntity<>(Context.build(data), requestHeaders);
- logger.info("Sending context to {}: {}", to.getId(), jsonMapper.writeValueAsString(data));
+ HttpEntity<Context> request = new HttpEntity<>(finalContext, requestHeaders);
+ logger.info("Sending context to {}: {}", to.getId(), payload);
ResponseEntity<Void> response = apClient.postForEntity(inbox, request, Void.class);
logger.info("Remote response: {}", response.getStatusCodeValue());
}
- public String addSignature(Person from, String host, String method, String path, String dateString) throws IOException {
- return addSignature(from, host, method, path, dateString, keystoreManager);
+ public String addSignature(Person from, String host, String method,
+ String path, String dateString, String digestHeader)
+ throws IOException {
+ return addSignature(from, host, method, path, dateString,
+ digestHeader, keystoreManager);
}
- public String addSignature(Person from, String host, String method, String path, String dateString, KeystoreManager keystoreManager) throws IOException {
- Signature templateSignature = new Signature(from.getPublicKey().getId(), "rsa-sha256", null,
- "(request-target)", "host", "date");
+ public String addSignature(Person from, String host, String method,
+ String path, String dateString, String digestHeader,
+ KeystoreManager keystoreManager) throws IOException {
+ List<String> requiredHeaders = StringUtils.isEmpty(digestHeader) ?
+ Arrays.asList("(request-target)", "host", "date")
+ : Arrays.asList("(request-target)", "host", "date", "digest");
+ Signature templateSignature = new Signature(from.getPublicKey().getId(),
+ "rsa-sha256", null, requiredHeaders);
Map<String, String> headers = new HashMap<>();
headers.put("host", host);
headers.put("date", dateString);
+ if (StringUtils.isNotEmpty(digestHeader)) {
+ headers.put("digest", digestHeader);
+ }
Signer signer = new Signer(keystoreManager.getPrivateKey(), templateSignature);
Signature signature = signer.sign(method, path, headers);
// remove "Signature: " from result
@@ -122,7 +144,7 @@ public class SignatureManager {
} else {
return AnonymousUser.INSTANCE;
}
- } catch (NoSuchAlgorithmException | SignatureException | IOException e) {
+ } catch (NoSuchAlgorithmException | SignatureException | MissingRequiredHeaderException | IOException e) {
logger.warn("Invalid signature {}", signatureString);
}
} else {
diff --git a/src/test/java/com/juick/server/tests/ServerTests.java b/src/test/java/com/juick/server/tests/ServerTests.java
index 320ba6fd..925d42f6 100644
--- a/src/test/java/com/juick/server/tests/ServerTests.java
+++ b/src/test/java/com/juick/server/tests/ServerTests.java
@@ -104,6 +104,7 @@ import org.springframework.web.client.ResourceAccessException;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.util.UriComponents;
import org.springframework.web.util.UriComponentsBuilder;
+import org.tomitribe.auth.signatures.Base64;
import org.w3c.dom.*;
import org.xml.sax.SAXException;
import rocks.xmpp.addr.Jid;
@@ -1889,7 +1890,7 @@ public class ServerTests {
}
@Test
- public void signingSpec() throws IOException {
+ public void signingSpec() throws IOException, NoSuchAlgorithmException {
Person from = (Person) signatureManager.getContext(URI.create("http://localhost:8080/u/freefd")).get();
Person to = (Person) signatureManager.getContext(URI.create("http://localhost:8080/u/ugnich")).get();
Follow follow = new Follow();
@@ -1910,7 +1911,7 @@ public class ServerTests {
Person ugnichPerson = profileController.getUser("ugnich");
now = Instant.now();
requestDate = DateFormattersHolder.getHttpDateFormatter().format(now);
- String signatureString = signatureManager.addSignature(ugnichPerson, testHost, "GET", meUri, requestDate);
+ String signatureString = signatureManager.addSignature(ugnichPerson, testHost, "GET", meUri, requestDate, StringUtils.EMPTY);
MvcResult me = mockMvc.perform(get("/api/me")
.header("Host", testHost)
.header("Date", requestDate)
@@ -1924,24 +1925,28 @@ public class ServerTests {
URI testuserUri = URI.create("https://example.com/u/testuser");
URI testuserkeyUri = URI.create("https://example.com/u/testuser#main-key");
MockRestServiceServer restServiceServer = MockRestServiceServer.createServer(apClient);
- restServiceServer.expect(times(3), requestTo(testuserUri))
+ restServiceServer.expect(times(4), requestTo(testuserUri))
.andRespond(withSuccess(testuserResponseString, MediaType.APPLICATION_JSON));
- restServiceServer.expect(times(3), requestTo(testuserkeyUri))
+ restServiceServer.expect(times(4), requestTo(testuserkeyUri))
.andRespond(withSuccess(testuserResponseString, MediaType.APPLICATION_JSON));
Person testuser = (Person) signatureManager.getContext(testuserUri).get();
assertThat(testuser.getPublicKey().getPublicKeyPem(), is(testKeystoreManager.getPublicKeyPem()));
Instant now2 = Instant.now();
String testRequestDate = DateFormattersHolder.getHttpDateFormatter().format(now2);
String inboxUri = "/api/inbox";
+ var payload = IOUtils.toByteArray(testfollowRequest.getInputStream());
+ final byte[] digest = MessageDigest.getInstance("SHA-256").digest(payload); // (1)
+ final String digestHeader = "SHA-256=" + new String(Base64.encodeBase64(digest));
String testSignatureString =
signatureManager.addSignature(testuser, testHost, "POST",
- inboxUri, testRequestDate, testKeystoreManager);
+ inboxUri, testRequestDate, digestHeader, testKeystoreManager);
mockMvc.perform(post(inboxUri)
.header("Host", testHost)
.header("Date", testRequestDate)
+ .header("Digest", digestHeader)
.header("Signature", testSignatureString)
.contentType(Context.LD_JSON_MEDIA_TYPE)
- .content(IOUtils.toByteArray(testfollowRequest.getInputStream())))
+ .content(payload))
.andExpect(status().isAccepted());
mockMvc.perform(post(inboxUri)
.header("Host", "wronghost")
@@ -1950,6 +1955,14 @@ public class ServerTests {
.contentType(Context.LD_JSON_MEDIA_TYPE)
.content(IOUtils.toByteArray(testfollowRequest.getInputStream())))
.andExpect(status().isUnauthorized());
+ // digest required but not present
+ mockMvc.perform(post(inboxUri)
+ .header("Host", testHost)
+ .header("Date", testRequestDate)
+ .header("Signature", testSignatureString)
+ .contentType(Context.LD_JSON_MEDIA_TYPE)
+ .content(payload))
+ .andExpect(status().isUnauthorized());
apClient.setRequestFactory(originalRequestFactory);
}