aboutsummaryrefslogtreecommitdiff
path: root/src/main/java/com/juick/SignatureManager.java
diff options
context:
space:
mode:
authorGravatar Vitaly Takmazov2020-04-04 01:15:01 +0300
committerGravatar Vitaly Takmazov2020-04-04 01:15:01 +0300
commita608baeed738894433aacfa041e2617f60ce959f (patch)
tree1e0de7056417ff0833ae3d4600de9fec6eb81631 /src/main/java/com/juick/SignatureManager.java
parent7a2f89266c8f6337e4e81a2fd8488e0f80f4f9bd (diff)
Initialize all components from configuration
Diffstat (limited to 'src/main/java/com/juick/SignatureManager.java')
-rw-r--r--src/main/java/com/juick/SignatureManager.java169
1 files changed, 169 insertions, 0 deletions
diff --git a/src/main/java/com/juick/SignatureManager.java b/src/main/java/com/juick/SignatureManager.java
new file mode 100644
index 00000000..959242f5
--- /dev/null
+++ b/src/main/java/com/juick/SignatureManager.java
@@ -0,0 +1,169 @@
+/*
+ * Copyright (C) 2008-2020, Juick
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package com.juick;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.juick.model.User;
+import com.juick.model.AnonymousUser;
+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 com.juick.service.UserService;
+import com.juick.util.DateFormattersHolder;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.http.HttpEntity;
+import org.springframework.http.HttpHeaders;
+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.Signature;
+import org.tomitribe.auth.signatures.Signer;
+import org.tomitribe.auth.signatures.Verifier;
+import rocks.xmpp.addr.Jid;
+
+import javax.inject.Inject;
+import java.io.IOException;
+import java.net.URI;
+import java.security.Key;
+import java.security.NoSuchAlgorithmException;
+import java.security.SignatureException;
+import java.time.Instant;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Optional;
+
+import static com.juick.www.api.activity.model.Context.ACTIVITY_MEDIA_TYPE;
+
+public class SignatureManager {
+ private static final Logger logger = LoggerFactory.getLogger("ActivityPub");
+ @Inject
+ private KeystoreManager keystoreManager;
+ @Inject
+ private ObjectMapper jsonMapper;
+ @Inject
+ private UserService userService;
+ @Inject
+ private RestTemplate apClient;
+
+ public void post(Person from, Person to, Context data) throws IOException {
+ 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);
+
+ HttpHeaders requestHeaders = new HttpHeaders();
+ requestHeaders.add("Content-Type", Context.ACTIVITYSTREAMS_PROFILE_MEDIA_TYPE);
+ requestHeaders.add("Date", requestDate);
+ requestHeaders.add("Host", host);
+ requestHeaders.add("Signature", signatureString);
+ HttpEntity<Context> request = new HttpEntity<>(Context.build(data), requestHeaders);
+ logger.info("Sending context to {}: {}", to.getId(), jsonMapper.writeValueAsString(data));
+ 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, KeystoreManager keystoreManager) throws IOException {
+ Signature templateSignature = new Signature(from.getPublicKey().getId(), "rsa-sha256", null,
+ "(request-target)", "host", "date");
+ Map<String, String> headers = new HashMap<>();
+ headers.put("host", host);
+ headers.put("date", dateString);
+ Signer signer = new Signer(keystoreManager.getPrivateKey(), templateSignature);
+ Signature signature = signer.sign(method, path, headers);
+ // remove "Signature: " from result
+ return signature.toString().substring(10);
+ }
+
+ public User verifySignature(String method, String path, Map<String, String> headers) {
+ String signatureString = headers.get("signature");
+ logger.info("Signature: {}", signatureString);
+ Signature signature = Signature.fromString(signatureString);
+ Optional<Context> context = getContext(UriComponentsBuilder.fromUriString(signature.getKeyId())
+ .fragment(null).build().toUri());
+ if (context.isPresent() && context.get() instanceof Person) {
+ Person person = (Person) context.get();
+ Key key = KeystoreManager.publicKeyOf(person);
+
+ Verifier verifier = new Verifier(key, signature);
+ try {
+ boolean result = verifier.verify(method, path, headers);
+ logger.info("signature of {} is valid: {}", signature.getKeyId(), result);
+ if (result) {
+ User user = new User();
+ user.setUri(URI.create(person.getId()));
+ if (key.equals(keystoreManager.getPublicKey())) {
+ return userService.getUserByName(person.getName());
+ }
+ return user;
+ } else {
+ return AnonymousUser.INSTANCE;
+ }
+ } catch (NoSuchAlgorithmException | SignatureException | IOException e) {
+ logger.warn("Invalid signature {}", signatureString);
+ }
+ } else {
+ logger.warn("Unknown keyId");
+ }
+ return AnonymousUser.INSTANCE;
+ }
+ public Optional<Context> getContext(URI contextUri) {
+ try {
+ Context context = apClient.getForEntity(contextUri, Context.class).getBody();
+ if (context == null) {
+ logger.warn("Cannot identify {}", contextUri);
+ return Optional.empty();
+ }
+ return Optional.of(context);
+ } catch (Exception e) {
+ logger.warn("REST Exception on {}: {}", contextUri, e.getMessage());
+ }
+ return Optional.empty();
+ }
+ public Optional<Context> discoverPerson(String acct) {
+ Jid acctId = Jid.of(acct);
+ URI resourceUri = UriComponentsBuilder.fromPath("/.well-known/webfinger")
+ .host(acctId.getDomain())
+ .scheme("https")
+ .queryParam("resource", String.format("%s", acctId.toEscapedString())).build().toUri();
+ HttpHeaders headers = new HttpHeaders();
+ headers.add("Accept", "application/jrd+json");
+ HttpEntity<Void> webfingerRequest = new HttpEntity<>(headers);
+ ResponseEntity<Account> response = apClient.exchange(
+ resourceUri, HttpMethod.GET, webfingerRequest, Account.class);
+ if (response.getStatusCode().is2xxSuccessful()) {
+ Account acctData = response.getBody();
+ if (acctData != null) {
+ for (Link l : acctData.getLinks()) {
+ if (l.getRel().equals("self") && l.getType().equals(ACTIVITY_MEDIA_TYPE)) {
+ return getContext(URI.create(l.getHref()));
+ }
+ }
+ }
+ }
+ return Optional.empty();
+ }
+}