From 3ad367ca8a9a11c026938459f7b852f6391bd341 Mon Sep 17 00:00:00 2001 From: Vitaly Takmazov Date: Wed, 5 Sep 2018 22:46:23 +0300 Subject: KeystoreManager shares keystore between XMPP and ActivityPub --- .../java/com/juick/server/KeystoreManager.java | 116 +++++++++++++++++++++ .../src/main/java/com/juick/server/XMPPServer.java | 16 +-- .../com/juick/server/api/activity/Profile.java | 8 ++ .../server/api/activity/model/ActivityObject.java | 10 +- .../com/juick/server/api/activity/model/Key.java | 22 ++++ .../juick/server/api/activity/model/Person.java | 9 ++ .../java/com/juick/server/tests/ServerTests.java | 6 +- 7 files changed, 170 insertions(+), 17 deletions(-) create mode 100644 juick-server/src/main/java/com/juick/server/KeystoreManager.java create mode 100644 juick-server/src/main/java/com/juick/server/api/activity/model/Key.java (limited to 'juick-server') 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 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 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 mids = IteratorUtils.toList(IntStream.rangeClosed(1, 30) .mapToObj(i -> messagesService.createMessage(ugnich.getUid(), -- cgit v1.2.3