From 07ebf86ab279811c365e8174807dbf36fc2f4ca4 Mon Sep 17 00:00:00 2001 From: Vitaly Takmazov Date: Tue, 10 Nov 2020 16:36:02 +0300 Subject: ActivityPub: Digest header is mandatory now for POST requests --- src/main/java/com/juick/ActivityPubManager.java | 11 +++--- src/main/java/com/juick/SignatureManager.java | 42 ++++++++++++++++------ .../java/com/juick/server/tests/ServerTests.java | 25 +++++++++---- 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 request = new HttpEntity<>(Context.build(data), requestHeaders); - logger.info("Sending context to {}: {}", to.getId(), jsonMapper.writeValueAsString(data)); + HttpEntity request = new HttpEntity<>(finalContext, requestHeaders); + logger.info("Sending context to {}: {}", to.getId(), payload); ResponseEntity 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 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 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); } -- cgit v1.2.3