aboutsummaryrefslogtreecommitdiff
path: root/juick-server/src
diff options
context:
space:
mode:
authorGravatar Vitaly Takmazov2018-09-05 22:46:23 +0300
committerGravatar Vitaly Takmazov2018-09-05 22:46:23 +0300
commit3ad367ca8a9a11c026938459f7b852f6391bd341 (patch)
tree263f1309e83c5dc5cd9b7e5cc97459f936d6895d /juick-server/src
parent302200828e48932dba38dc36fd41dd23fb6dfc76 (diff)
KeystoreManager shares keystore between XMPP and ActivityPub
Diffstat (limited to 'juick-server/src')
-rw-r--r--juick-server/src/main/java/com/juick/server/KeystoreManager.java116
-rw-r--r--juick-server/src/main/java/com/juick/server/XMPPServer.java16
-rw-r--r--juick-server/src/main/java/com/juick/server/api/activity/Profile.java8
-rw-r--r--juick-server/src/main/java/com/juick/server/api/activity/model/ActivityObject.java10
-rw-r--r--juick-server/src/main/java/com/juick/server/api/activity/model/Key.java22
-rw-r--r--juick-server/src/main/java/com/juick/server/api/activity/model/Person.java9
-rw-r--r--juick-server/src/test/java/com/juick/server/tests/ServerTests.java6
7 files changed, 170 insertions, 17 deletions
diff --git a/juick-server/src/main/java/com/juick/server/KeystoreManager.java b/juick-server/src/main/java/com/juick/server/KeystoreManager.java
new file mode 100644
index 00000000..bb0de79d
--- /dev/null
+++ b/juick-server/src/main/java/com/juick/server/KeystoreManager.java
@@ -0,0 +1,116 @@
+package com.juick.server;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.stereotype.Component;
+import org.springframework.util.Base64Utils;
+import sun.security.x509.*;
+
+import javax.annotation.PostConstruct;
+import javax.net.ssl.KeyManagerFactory;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.InputStream;
+import java.math.BigInteger;
+import java.security.*;
+import java.security.cert.Certificate;
+import java.security.cert.CertificateException;
+import java.security.cert.X509Certificate;
+import java.util.Date;
+
+@Component
+public class KeystoreManager {
+ private static final Logger logger = LoggerFactory.getLogger("com.juick.server");
+ @Value("${keystore:juick.p12}")
+ private String keystore;
+ @Value("${keystore_password:secret}")
+ private String keystorePassword;
+
+ private KeyStore ks;
+
+ private KeyManagerFactory kmf;
+
+ @PostConstruct
+ public void init() throws GeneralSecurityException, IOException {
+ try (InputStream ksIs = new FileInputStream(keystore)) {
+ ks = KeyStore.getInstance("PKCS12");
+ ks.load(ksIs, keystorePassword.toCharArray());
+ kmf = KeyManagerFactory.getInstance(KeyManagerFactory
+ .getDefaultAlgorithm());
+ kmf.init(ks, keystorePassword.toCharArray());
+ } catch (IOException | KeyStoreException | NoSuchAlgorithmException | UnrecoverableKeyException | CertificateException e) {
+ logger.warn("Keystore error, creating self-signed", e);
+ KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA");
+ keyPairGenerator.initialize(4096);
+ KeyPair keyPair = keyPairGenerator.generateKeyPair();
+
+ Certificate[] chain = {generateCertificate("cn=localhost", keyPair, 365, "SHA256withRSA")};
+
+ ks = KeyStore.getInstance(KeyStore.getDefaultType());
+ ks.load(null, null);
+ ks.setKeyEntry("1", keyPair.getPrivate(), keystorePassword.toCharArray(), chain);
+ kmf = KeyManagerFactory.getInstance(KeyManagerFactory
+ .getDefaultAlgorithm());
+ kmf.init(ks, keystorePassword.toCharArray());
+ }
+ }
+
+ public KeyStore getKeystore() {
+ return ks;
+ }
+
+ public KeyManagerFactory getKeymanagerFactory() {
+ return kmf;
+ }
+
+ private KeyPair getKeyPair() {
+ Key privateKey = null;
+ try {
+ privateKey = ks.getKey("1", keystorePassword.toCharArray());
+ Certificate certificate = ks.getCertificate("1");
+ return new KeyPair(certificate.getPublicKey(), (PrivateKey) privateKey);
+ } catch (KeyStoreException | NoSuchAlgorithmException | UnrecoverableKeyException e) {
+ e.printStackTrace();
+ }
+ return null;
+ }
+ public String getPublicKey() {
+ return String.format("-----BEGIN RSA PUBLIC KEY-----\n%s\\n-----END RSA PUBLIC KEY-----\\n",
+ new String(Base64Utils.encode(getKeyPair().getPublic().getEncoded())));
+ }
+ private X509Certificate generateCertificate(String dn, KeyPair keyPair, int validity, String sigAlgName) throws GeneralSecurityException, IOException {
+ PrivateKey privateKey = keyPair.getPrivate();
+
+ X509CertInfo info = new X509CertInfo();
+
+ Date from = new Date();
+ Date to = new Date(from.getTime() + validity * 1000L * 24L * 60L * 60L);
+
+ CertificateValidity interval = new CertificateValidity(from, to);
+ BigInteger serialNumber = new BigInteger(64, new SecureRandom());
+ X500Name owner = new X500Name(dn);
+ AlgorithmId sigAlgId = new AlgorithmId(AlgorithmId.sha256WithRSAEncryption_oid);
+
+ info.set(X509CertInfo.VALIDITY, interval);
+ info.set(X509CertInfo.SERIAL_NUMBER, new CertificateSerialNumber(serialNumber));
+ info.set(X509CertInfo.SUBJECT, owner);
+ info.set(X509CertInfo.ISSUER, owner);
+ info.set(X509CertInfo.KEY, new CertificateX509Key(keyPair.getPublic()));
+ info.set(X509CertInfo.VERSION, new CertificateVersion(CertificateVersion.V3));
+ info.set(X509CertInfo.ALGORITHM_ID, new CertificateAlgorithmId(sigAlgId));
+
+ // Sign the cert to identify the algorithm that's used.
+ X509CertImpl certificate = new X509CertImpl(info);
+ certificate.sign(privateKey, sigAlgName);
+
+ // Update the algorith, and resign.
+ sigAlgId = (AlgorithmId) certificate.get(X509CertImpl.SIG_ALG);
+ info.set(CertificateAlgorithmId.NAME + "." + CertificateAlgorithmId.ALGORITHM, sigAlgId);
+ certificate = new X509CertImpl(info);
+ certificate.sign(privateKey, sigAlgName);
+
+ return certificate;
+ }
+}
diff --git a/juick-server/src/main/java/com/juick/server/XMPPServer.java b/juick-server/src/main/java/com/juick/server/XMPPServer.java
index 675d79da..f2e41750 100644
--- a/juick-server/src/main/java/com/juick/server/XMPPServer.java
+++ b/juick-server/src/main/java/com/juick/server/XMPPServer.java
@@ -70,10 +70,6 @@ public class XMPPServer implements ConnectionListener {
private Jid jid;
@Value("${s2s_port:5269}")
private int s2sPort;
- @Value("${keystore:juick.p12}")
- public String keystore;
- @Value("${keystore_password:secret}")
- public String keystorePassword;
@Value("${broken_ssl_hosts:}")
public String[] brokenSSLhosts;
@Value("${banned_hosts:}")
@@ -111,21 +107,17 @@ public class XMPPServer implements ConnectionListener {
private BasicXmppSession session;
@Inject
private UserService userService;
+ @Inject
+ private KeystoreManager keystoreManager;
@PostConstruct
public void init() throws KeyStoreException {
closeFlag.set(false);
- KeyStore ks = KeyStore.getInstance("PKCS12");
- try (InputStream ksIs = new FileInputStream(keystore)) {
- ks.load(ksIs, keystorePassword.toCharArray());
- KeyManagerFactory kmf = KeyManagerFactory.getInstance(KeyManagerFactory
- .getDefaultAlgorithm());
- kmf.init(ks, keystorePassword.toCharArray());
+ try {
sc = SSLContext.getInstance("TLSv1.2");
- sc.init(kmf.getKeyManagers(), trustAllCerts, new SecureRandom());
+ sc.init(keystoreManager.getKeymanagerFactory().getKeyManagers(), trustAllCerts, new SecureRandom());
TrustManagerFactory trustManagerFactory =
TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
-
Set<TrustAnchor> ca = new HashSet<>();
trustManagerFactory.init((KeyStore)null);
Arrays.stream(trustManagerFactory.getTrustManagers()).forEach(t -> Arrays.stream(((X509TrustManager)t).getAcceptedIssuers()).forEach(cert -> ca.add(new TrustAnchor(cert, null))));
diff --git a/juick-server/src/main/java/com/juick/server/api/activity/Profile.java b/juick-server/src/main/java/com/juick/server/api/activity/Profile.java
index c0dcc3f6..0d987b58 100644
--- a/juick-server/src/main/java/com/juick/server/api/activity/Profile.java
+++ b/juick-server/src/main/java/com/juick/server/api/activity/Profile.java
@@ -1,6 +1,7 @@
package com.juick.server.api.activity;
import com.juick.User;
+import com.juick.server.KeystoreManager;
import com.juick.server.api.activity.model.*;
import com.juick.server.util.HttpNotFoundException;
import com.juick.server.util.UserUtils;
@@ -28,6 +29,8 @@ public class Profile {
private UserService userService;
@Inject
private MessagesService messagesService;
+ @Inject
+ private KeystoreManager keystoreManager;
@Value("${web_domain:localhost}")
private String domain;
@Value("${ap_base_uri:http://localhost:8080/}")
@@ -44,6 +47,11 @@ public class Profile {
uri.replacePath(String.format("/u/%s", userName));
person.setId(uri.toUriString());
person.setName(userName);
+ Key publicKey = new Key();
+ publicKey.setId(person.getId() + "#main-key");
+ publicKey.setOwner(person.getId());
+ publicKey.setPublicKeyPem(keystoreManager.getPublicKey());
+ person.setPublicKey(publicKey);
uri.replacePath("/post");
person.setInbox(uri.toUriString());
person.setOutbox(uri.replacePath(String.format("/u/%s/blog/toc", userName)).toUriString());
diff --git a/juick-server/src/main/java/com/juick/server/api/activity/model/ActivityObject.java b/juick-server/src/main/java/com/juick/server/api/activity/model/ActivityObject.java
index 7859be86..84a3e018 100644
--- a/juick-server/src/main/java/com/juick/server/api/activity/model/ActivityObject.java
+++ b/juick-server/src/main/java/com/juick/server/api/activity/model/ActivityObject.java
@@ -3,6 +3,9 @@ package com.juick.server.api.activity.model;
import com.fasterxml.jackson.annotation.JsonProperty;
import java.time.Instant;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
public abstract class ActivityObject {
@@ -23,11 +26,12 @@ public abstract class ActivityObject {
}
@JsonProperty("@context")
- public String getContext() {
- return CONTEXT_URI;
+ public List<String> getContext() {
+ return Arrays.asList(ACTIVITY_STREAMS_URI, SECURITY_URI);
}
- public final static String CONTEXT_URI = "https://www.w3.org/ns/activitystreams";
+ public final static String ACTIVITY_STREAMS_URI = "https://www.w3.org/ns/activitystreams";
+ public final static String SECURITY_URI = "https://w3id.org/security/v1";
public final static String LD_JSON_MEDIA_TYPE = "application/ld+json; profile=\"https://www.w3.org/ns/activitystreams\"";
public final static String ACTIVITY_JSON_MEDIA_TYPE = "application/activity+json; profile=\"https://www.w3.org/ns/activitystreams\"";
diff --git a/juick-server/src/main/java/com/juick/server/api/activity/model/Key.java b/juick-server/src/main/java/com/juick/server/api/activity/model/Key.java
new file mode 100644
index 00000000..32417778
--- /dev/null
+++ b/juick-server/src/main/java/com/juick/server/api/activity/model/Key.java
@@ -0,0 +1,22 @@
+package com.juick.server.api.activity.model;
+
+public class Key extends ActivityObject {
+ private String owner;
+ private String publicKeyPem;
+
+ public String getOwner() {
+ return owner;
+ }
+
+ public void setOwner(String owner) {
+ this.owner = owner;
+ }
+
+ public String getPublicKeyPem() {
+ return publicKeyPem;
+ }
+
+ public void setPublicKeyPem(String publicKeyPem) {
+ this.publicKeyPem = publicKeyPem;
+ }
+}
diff --git a/juick-server/src/main/java/com/juick/server/api/activity/model/Person.java b/juick-server/src/main/java/com/juick/server/api/activity/model/Person.java
index f4312ffc..97fa5ae8 100644
--- a/juick-server/src/main/java/com/juick/server/api/activity/model/Person.java
+++ b/juick-server/src/main/java/com/juick/server/api/activity/model/Person.java
@@ -9,6 +9,7 @@ public class Person extends ActivityObject {
private String following;
private String followers;
private Link url;
+ private Key publicKey;
@Override
public String getType() {
@@ -70,4 +71,12 @@ public class Person extends ActivityObject {
public void setUrl(Link url) {
this.url = url;
}
+
+ public Key getPublicKey() {
+ return publicKey;
+ }
+
+ public void setPublicKey(Key publicKey) {
+ this.publicKey = publicKey;
+ }
}
diff --git a/juick-server/src/test/java/com/juick/server/tests/ServerTests.java b/juick-server/src/test/java/com/juick/server/tests/ServerTests.java
index 3f196461..1931f13b 100644
--- a/juick-server/src/test/java/com/juick/server/tests/ServerTests.java
+++ b/juick-server/src/test/java/com/juick/server/tests/ServerTests.java
@@ -156,6 +156,8 @@ public class ServerTests {
private ImagesService imagesService;
@Inject
private ServerManager serverManager;
+ @Inject
+ private KeystoreManager keystoreManager;
@Value("${hostname:localhost}")
private Jid jid;
@Value("${xmppbot_jid:juick@localhost}")
@@ -1342,8 +1344,8 @@ public class ServerTests {
public void userProfileAndBlogShouldBeExposedAsActivityStream() throws Exception {
mockMvc.perform(get("/u/ugnich").accept(ActivityObject.LD_JSON_MEDIA_TYPE))
.andExpect(status().isOk())
- .andExpect(jsonPath("$.@context", is(ActivityObject.CONTEXT_URI)))
- .andExpect(jsonPath("$.icon.url", is("http://localhost:8080/i/a/1.png")));
+ .andExpect(jsonPath("$.icon.url", is("http://localhost:8080/i/a/1.png")))
+ .andExpect(jsonPath("$.publicKey.publicKeyPem", is(keystoreManager.getPublicKey())));
jdbcTemplate.execute("DELETE FROM messages");
List<Integer> mids = IteratorUtils.toList(IntStream.rangeClosed(1, 30)
.mapToObj(i -> messagesService.createMessage(ugnich.getUid(),