Working prototype
This commit is contained in:
@@ -12,6 +12,12 @@ server:
|
|||||||
# e.g. domain name in e-mails.
|
# e.g. domain name in e-mails.
|
||||||
name: 'example.org'
|
name: 'example.org'
|
||||||
|
|
||||||
|
# Public URL to reach this identity server
|
||||||
|
#
|
||||||
|
# This is used with 3PID invites in room and other Homeserver key verification workflow.
|
||||||
|
# If left unconfigured, it will be generated from the server name
|
||||||
|
# publicUrl: 'https://example.org'
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
key:
|
key:
|
||||||
|
|||||||
@@ -22,6 +22,8 @@ package io.kamax.mxisd.config
|
|||||||
|
|
||||||
import io.kamax.mxisd.exception.ConfigurationException
|
import io.kamax.mxisd.exception.ConfigurationException
|
||||||
import org.apache.commons.lang.StringUtils
|
import org.apache.commons.lang.StringUtils
|
||||||
|
import org.slf4j.Logger
|
||||||
|
import org.slf4j.LoggerFactory
|
||||||
import org.springframework.beans.factory.InitializingBean
|
import org.springframework.beans.factory.InitializingBean
|
||||||
import org.springframework.boot.context.properties.ConfigurationProperties
|
import org.springframework.boot.context.properties.ConfigurationProperties
|
||||||
import org.springframework.context.annotation.Configuration
|
import org.springframework.context.annotation.Configuration
|
||||||
@@ -30,7 +32,11 @@ import org.springframework.context.annotation.Configuration
|
|||||||
@ConfigurationProperties(prefix = "server")
|
@ConfigurationProperties(prefix = "server")
|
||||||
class ServerConfig implements InitializingBean {
|
class ServerConfig implements InitializingBean {
|
||||||
|
|
||||||
|
private Logger log = LoggerFactory.getLogger(ServerConfig.class);
|
||||||
|
|
||||||
private String name
|
private String name
|
||||||
|
private int port
|
||||||
|
private String publicUrl
|
||||||
|
|
||||||
String getName() {
|
String getName() {
|
||||||
return name
|
return name
|
||||||
@@ -40,11 +46,43 @@ class ServerConfig implements InitializingBean {
|
|||||||
this.name = name
|
this.name = name
|
||||||
}
|
}
|
||||||
|
|
||||||
|
int getPort() {
|
||||||
|
return port
|
||||||
|
}
|
||||||
|
|
||||||
|
void setPort(int port) {
|
||||||
|
this.port = port
|
||||||
|
}
|
||||||
|
|
||||||
|
String getPublicUrl() {
|
||||||
|
return publicUrl
|
||||||
|
}
|
||||||
|
|
||||||
|
void setPublicUrl(String publicUrl) {
|
||||||
|
this.publicUrl = publicUrl
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
void afterPropertiesSet() throws Exception {
|
void afterPropertiesSet() throws Exception {
|
||||||
if (StringUtils.isBlank(getName())) {
|
if (StringUtils.isBlank(getName())) {
|
||||||
throw new ConfigurationException("server.name")
|
throw new ConfigurationException("server.name")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (StringUtils.isBlank(getPublicUrl())) {
|
||||||
|
log.warn("Public URL is empty, generating from name {}", getName())
|
||||||
|
publicUrl = "https://${getName()}"
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
new URL(getPublicUrl())
|
||||||
|
} catch (MalformedURLException e) {
|
||||||
|
log.warn("Public URL is not valid: {}", StringUtils.defaultIfBlank(e.getMessage(), "<no reason provided>"))
|
||||||
|
}
|
||||||
|
|
||||||
|
log.info("--- Server config ---")
|
||||||
|
log.info("Name: {}", getName())
|
||||||
|
log.info("Port: {}", getPort())
|
||||||
|
log.info("Public URL: {}", getPublicUrl())
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ package io.kamax.mxisd.controller.v1
|
|||||||
|
|
||||||
import com.google.gson.Gson
|
import com.google.gson.Gson
|
||||||
import io.kamax.matrix.MatrixID
|
import io.kamax.matrix.MatrixID
|
||||||
|
import io.kamax.mxisd.config.ServerConfig
|
||||||
import io.kamax.mxisd.controller.v1.io.ThreePidInviteReplyIO
|
import io.kamax.mxisd.controller.v1.io.ThreePidInviteReplyIO
|
||||||
import io.kamax.mxisd.invitation.IThreePidInvite
|
import io.kamax.mxisd.invitation.IThreePidInvite
|
||||||
import io.kamax.mxisd.invitation.IThreePidInviteReply
|
import io.kamax.mxisd.invitation.IThreePidInviteReply
|
||||||
@@ -50,6 +51,9 @@ class InvitationController {
|
|||||||
@Autowired
|
@Autowired
|
||||||
private KeyManager keyMgr
|
private KeyManager keyMgr
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private ServerConfig srvCfg
|
||||||
|
|
||||||
private Gson gson = new Gson()
|
private Gson gson = new Gson()
|
||||||
|
|
||||||
@RequestMapping(value = "/_matrix/identity/api/v1/store-invite", method = POST)
|
@RequestMapping(value = "/_matrix/identity/api/v1/store-invite", method = POST)
|
||||||
@@ -62,7 +66,7 @@ class InvitationController {
|
|||||||
IThreePidInvite invite = new ThreePidInvite(new MatrixID(sender), medium, address, roomId)
|
IThreePidInvite invite = new ThreePidInvite(new MatrixID(sender), medium, address, roomId)
|
||||||
IThreePidInviteReply reply = mgr.storeInvite(invite)
|
IThreePidInviteReply reply = mgr.storeInvite(invite)
|
||||||
|
|
||||||
return gson.toJson(new ThreePidInviteReplyIO(reply, keyMgr.getPublicKeyBase64(keyMgr.getCurrentIndex())))
|
return gson.toJson(new ThreePidInviteReplyIO(reply, keyMgr.getPublicKeyBase64(keyMgr.getCurrentIndex()), srvCfg.getPublicUrl()))
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,11 +24,13 @@ import groovy.json.JsonOutput
|
|||||||
import io.kamax.mxisd.exception.BadRequestException
|
import io.kamax.mxisd.exception.BadRequestException
|
||||||
import io.kamax.mxisd.exception.NotImplementedException
|
import io.kamax.mxisd.exception.NotImplementedException
|
||||||
import io.kamax.mxisd.key.KeyManager
|
import io.kamax.mxisd.key.KeyManager
|
||||||
|
import org.apache.commons.lang.StringUtils
|
||||||
import org.slf4j.Logger
|
import org.slf4j.Logger
|
||||||
import org.slf4j.LoggerFactory
|
import org.slf4j.LoggerFactory
|
||||||
import org.springframework.beans.factory.annotation.Autowired
|
import org.springframework.beans.factory.annotation.Autowired
|
||||||
import org.springframework.web.bind.annotation.PathVariable
|
import org.springframework.web.bind.annotation.PathVariable
|
||||||
import org.springframework.web.bind.annotation.RequestMapping
|
import org.springframework.web.bind.annotation.RequestMapping
|
||||||
|
import org.springframework.web.bind.annotation.RequestParam
|
||||||
import org.springframework.web.bind.annotation.RestController
|
import org.springframework.web.bind.annotation.RestController
|
||||||
|
|
||||||
import javax.servlet.http.HttpServletRequest
|
import javax.servlet.http.HttpServletRequest
|
||||||
@@ -49,6 +51,7 @@ class KeyController {
|
|||||||
throw new BadRequestException("Invalid algorithm: " + keyType)
|
throw new BadRequestException("Invalid algorithm: " + keyType)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
log.info("Key {}:{} was requested", keyType, keyId)
|
||||||
return JsonOutput.toJson([
|
return JsonOutput.toJson([
|
||||||
public_key: keyMgr.getPublicKeyBase64(keyId)
|
public_key: keyMgr.getPublicKeyBase64(keyId)
|
||||||
])
|
])
|
||||||
@@ -62,10 +65,14 @@ class KeyController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@RequestMapping(value = "/_matrix/identity/api/v1/pubkey/isvalid", method = GET)
|
@RequestMapping(value = "/_matrix/identity/api/v1/pubkey/isvalid", method = GET)
|
||||||
String checkKeyValidity(HttpServletRequest request) {
|
String checkKeyValidity(HttpServletRequest request, @RequestParam("public_key") String pubKey) {
|
||||||
log.error("{} was requested but not implemented", request.getRequestURL())
|
log.info("Validating public key {}", pubKey)
|
||||||
|
|
||||||
throw new NotImplementedException()
|
// TODO do in manager
|
||||||
|
boolean valid = StringUtils.equals(pubKey, keyMgr.getPublicKeyBase64(keyMgr.getCurrentIndex()))
|
||||||
|
return JsonOutput.toJson(
|
||||||
|
valid: valid
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,8 +2,7 @@ package io.kamax.mxisd.controller.v1.io;
|
|||||||
|
|
||||||
import io.kamax.mxisd.invitation.IThreePidInviteReply;
|
import io.kamax.mxisd.invitation.IThreePidInviteReply;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.Collections;
|
||||||
import java.util.Arrays;
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
public class ThreePidInviteReplyIO {
|
public class ThreePidInviteReplyIO {
|
||||||
@@ -12,20 +11,40 @@ public class ThreePidInviteReplyIO {
|
|||||||
private List<Key> public_keys;
|
private List<Key> public_keys;
|
||||||
private String display_name;
|
private String display_name;
|
||||||
|
|
||||||
public ThreePidInviteReplyIO(IThreePidInviteReply reply, String pubKey) {
|
public ThreePidInviteReplyIO(IThreePidInviteReply reply, String pubKey, String publicUrl) {
|
||||||
this.token = reply.getToken();
|
this.token = reply.getToken();
|
||||||
this.public_keys = new ArrayList<>(Arrays.asList(new Key(pubKey)));
|
this.public_keys = Collections.singletonList(new Key(pubKey, publicUrl));
|
||||||
this.display_name = reply.getDisplayName();
|
this.display_name = reply.getDisplayName();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public String getToken() {
|
||||||
|
return token;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<Key> getPublic_keys() {
|
||||||
|
return public_keys;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getDisplay_name() {
|
||||||
|
return display_name;
|
||||||
|
}
|
||||||
|
|
||||||
public class Key {
|
public class Key {
|
||||||
private String key_validity_url;
|
private String key_validity_url;
|
||||||
private String public_key;
|
private String public_key;
|
||||||
|
|
||||||
public Key(String key) {
|
public Key(String key, String publicUrl) {
|
||||||
this.key_validity_url = "https://example.org/_matrix/fixme"; // FIXME have a proper URL even if synapse does not check
|
this.key_validity_url = publicUrl + "/_matrix/identity/api/v1/pubkey/isvalid";
|
||||||
this.public_key = key;
|
this.public_key = key;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public String getKey_validity_url() {
|
||||||
|
return key_validity_url;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getPublic_key() {
|
||||||
|
return public_key;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,17 +12,14 @@ import io.kamax.mxisd.signature.SignatureManager;
|
|||||||
import org.apache.commons.lang.RandomStringUtils;
|
import org.apache.commons.lang.RandomStringUtils;
|
||||||
import org.apache.http.client.methods.CloseableHttpResponse;
|
import org.apache.http.client.methods.CloseableHttpResponse;
|
||||||
import org.apache.http.client.methods.HttpPost;
|
import org.apache.http.client.methods.HttpPost;
|
||||||
import org.apache.http.config.Registry;
|
import org.apache.http.conn.ssl.NoopHostnameVerifier;
|
||||||
import org.apache.http.config.RegistryBuilder;
|
|
||||||
import org.apache.http.conn.socket.ConnectionSocketFactory;
|
|
||||||
import org.apache.http.conn.socket.PlainConnectionSocketFactory;
|
|
||||||
import org.apache.http.conn.ssl.SSLConnectionSocketFactory;
|
import org.apache.http.conn.ssl.SSLConnectionSocketFactory;
|
||||||
import org.apache.http.conn.ssl.TrustStrategy;
|
import org.apache.http.conn.ssl.TrustSelfSignedStrategy;
|
||||||
import org.apache.http.entity.StringEntity;
|
import org.apache.http.entity.StringEntity;
|
||||||
import org.apache.http.impl.client.CloseableHttpClient;
|
import org.apache.http.impl.client.CloseableHttpClient;
|
||||||
import org.apache.http.impl.client.HttpClientBuilder;
|
import org.apache.http.impl.client.HttpClients;
|
||||||
import org.apache.http.impl.conn.PoolingHttpClientConnectionManager;
|
|
||||||
import org.apache.http.ssl.SSLContextBuilder;
|
import org.apache.http.ssl.SSLContextBuilder;
|
||||||
|
import org.json.JSONArray;
|
||||||
import org.json.JSONObject;
|
import org.json.JSONObject;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
@@ -38,8 +35,6 @@ import javax.net.ssl.HostnameVerifier;
|
|||||||
import javax.net.ssl.SSLContext;
|
import javax.net.ssl.SSLContext;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.nio.charset.StandardCharsets;
|
import java.nio.charset.StandardCharsets;
|
||||||
import java.security.cert.CertificateException;
|
|
||||||
import java.security.cert.X509Certificate;
|
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
import java.util.concurrent.ConcurrentHashMap;
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
|
||||||
@@ -65,40 +60,12 @@ public class InvitationManager {
|
|||||||
private void postConstruct() {
|
private void postConstruct() {
|
||||||
gson = new Gson();
|
gson = new Gson();
|
||||||
|
|
||||||
|
// FIXME export such madness into matrix-java-sdk with a nice wrapper to talk to a homeserver
|
||||||
try {
|
try {
|
||||||
HttpClientBuilder b = HttpClientBuilder.create();
|
SSLContext sslContext = SSLContextBuilder.create().loadTrustMaterial(new TrustSelfSignedStrategy()).build();
|
||||||
|
HostnameVerifier hostnameVerifier = new NoopHostnameVerifier();
|
||||||
// setup a Trust Strategy that allows all certificates.
|
|
||||||
//
|
|
||||||
SSLContext sslContext = new SSLContextBuilder().loadTrustMaterial(null, new TrustStrategy() {
|
|
||||||
public boolean isTrusted(X509Certificate[] arg0, String arg1) throws CertificateException {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}).build();
|
|
||||||
b.setSslcontext(sslContext);
|
|
||||||
|
|
||||||
// don't check Hostnames, either.
|
|
||||||
// -- use SSLConnectionSocketFactory.getDefaultHostnameVerifier(), if you don't want to weaken
|
|
||||||
HostnameVerifier hostnameVerifier = SSLConnectionSocketFactory.ALLOW_ALL_HOSTNAME_VERIFIER;
|
|
||||||
|
|
||||||
// here's the special part:
|
|
||||||
// -- need to create an SSL Socket Factory, to use our weakened "trust strategy";
|
|
||||||
// -- and create a Registry, to register it.
|
|
||||||
//
|
|
||||||
SSLConnectionSocketFactory sslSocketFactory = new SSLConnectionSocketFactory(sslContext, hostnameVerifier);
|
SSLConnectionSocketFactory sslSocketFactory = new SSLConnectionSocketFactory(sslContext, hostnameVerifier);
|
||||||
Registry<ConnectionSocketFactory> socketFactoryRegistry = RegistryBuilder.<ConnectionSocketFactory>create()
|
client = HttpClients.custom().setSSLSocketFactory(sslSocketFactory).build();
|
||||||
.register("http", PlainConnectionSocketFactory.getSocketFactory())
|
|
||||||
.register("https", sslSocketFactory)
|
|
||||||
.build();
|
|
||||||
|
|
||||||
// now, we create connection-manager using our Registry.
|
|
||||||
// -- allows multi-threaded use
|
|
||||||
PoolingHttpClientConnectionManager connMgr = new PoolingHttpClientConnectionManager(socketFactoryRegistry);
|
|
||||||
b.setConnectionManager(connMgr);
|
|
||||||
|
|
||||||
// finally, build the HttpClient;
|
|
||||||
// -- done!
|
|
||||||
client = b.build();
|
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
// FIXME do better...
|
// FIXME do better...
|
||||||
throw new RuntimeException(e);
|
throw new RuntimeException(e);
|
||||||
@@ -196,31 +163,52 @@ public class InvitationManager {
|
|||||||
if (!hsUrlOpt.isPresent()) {
|
if (!hsUrlOpt.isPresent()) {
|
||||||
log.warn("No HS found for domain {} - ignoring publishing", domain);
|
log.warn("No HS found for domain {} - ignoring publishing", domain);
|
||||||
} else {
|
} else {
|
||||||
HttpPost req = new HttpPost(hsUrlOpt.get() + "/_matrix/federation/v1/3pid/onbind");
|
// TODO this is needed as this will block if called during authentication cycle due to synapse implementation
|
||||||
JSONObject obj = new JSONObject(); // TODO use Gson instead
|
new Thread(() -> { // FIXME need to make this retryable and within a general background working pool
|
||||||
obj.put("mxisd", threePid.getMxid());
|
HttpPost req = new HttpPost(hsUrlOpt.get() + "/_matrix/federation/v1/3pid/onbind");
|
||||||
obj.put("token", reply.getToken());
|
// Expected body: https://matrix.to/#/!HUeDbmFUsWAhxHHvFG:matrix.org/$150469846739DCLWc:matrix.trancendances.fr
|
||||||
String mapping = gson.toJson(signMgr.signMessage(obj.toString())); // FIXME we shouldn't need to be doign this
|
JSONObject obj = new JSONObject(); // TODO use Gson instead
|
||||||
|
obj.put("mxid", threePid.getMxid());
|
||||||
|
obj.put("token", reply.getToken());
|
||||||
|
obj.put("signatures", signMgr.signMessageJson(obj.toString()));
|
||||||
|
|
||||||
JSONObject content = new JSONObject(); // TODO use Gson instead
|
JSONObject objUp = new JSONObject();
|
||||||
content.put("invites", Collections.singletonList(mapping));
|
objUp.put("mxid", threePid.getMxid());
|
||||||
content.put("medium", threePid.getMedium());
|
objUp.put("medium", threePid.getMedium());
|
||||||
content.put("address", threePid.getValue());
|
objUp.put("address", threePid.getValue());
|
||||||
content.put("mxid", threePid.getMxid());
|
objUp.put("sender", reply.getInvite().getSender().getId());
|
||||||
|
objUp.put("room_id", reply.getInvite().getRoomId());
|
||||||
|
objUp.put("signed", obj);
|
||||||
|
|
||||||
StringEntity entity = new StringEntity(content.toString(), StandardCharsets.UTF_8);
|
String mapping = gson.toJson(objUp); // FIXME we shouldn't need to be doign this
|
||||||
entity.setContentType("application/json");
|
|
||||||
req.setEntity(entity);
|
JSONObject content = new JSONObject(); // TODO use Gson instead
|
||||||
try {
|
JSONArray invites = new JSONArray();
|
||||||
log.info("Posting onBind event to {}", req.getURI());
|
invites.put(objUp);
|
||||||
CloseableHttpResponse response = client.execute(req);
|
content.put("invites", invites);
|
||||||
response.close();
|
content.put("medium", threePid.getMedium());
|
||||||
} catch (IOException e) {
|
content.put("address", threePid.getValue());
|
||||||
log.warn("Unable to tell HS {} about invite being mapped", domain, e);
|
content.put("mxid", threePid.getMxid());
|
||||||
}
|
|
||||||
|
content.put("signatures", signMgr.signMessageJson(content.toString()));
|
||||||
|
|
||||||
|
log.info("Will send following JSON to {}: {}", domain, content.toString());
|
||||||
|
StringEntity entity = new StringEntity(content.toString(), StandardCharsets.UTF_8);
|
||||||
|
entity.setContentType("application/json");
|
||||||
|
req.setEntity(entity);
|
||||||
|
try {
|
||||||
|
log.info("Posting onBind event to {}", req.getURI());
|
||||||
|
CloseableHttpResponse response = client.execute(req);
|
||||||
|
log.info("Answer code: {}", response.getStatusLine().getStatusCode());
|
||||||
|
response.close();
|
||||||
|
} catch (IOException e) {
|
||||||
|
log.warn("Unable to tell HS {} about invite being mapped", domain, e);
|
||||||
|
}
|
||||||
|
invitations.remove(key);
|
||||||
|
}).start();
|
||||||
}
|
}
|
||||||
|
|
||||||
invitations.remove(key);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ package io.kamax.mxisd.signature
|
|||||||
import io.kamax.mxisd.config.ServerConfig
|
import io.kamax.mxisd.config.ServerConfig
|
||||||
import io.kamax.mxisd.key.KeyManager
|
import io.kamax.mxisd.key.KeyManager
|
||||||
import net.i2p.crypto.eddsa.EdDSAEngine
|
import net.i2p.crypto.eddsa.EdDSAEngine
|
||||||
|
import org.json.JSONObject
|
||||||
import org.springframework.beans.factory.InitializingBean
|
import org.springframework.beans.factory.InitializingBean
|
||||||
import org.springframework.beans.factory.annotation.Autowired
|
import org.springframework.beans.factory.annotation.Autowired
|
||||||
import org.springframework.stereotype.Component
|
import org.springframework.stereotype.Component
|
||||||
@@ -51,6 +52,18 @@ class SignatureManager implements InitializingBean {
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
JSONObject signMessageJson(String message) {
|
||||||
|
byte[] signRaw = signEngine.signOneShot(message.getBytes())
|
||||||
|
String sign = Base64.getEncoder().encodeToString(signRaw)
|
||||||
|
|
||||||
|
JSONObject keySignature = new JSONObject()
|
||||||
|
keySignature.put("ed25519:${keyMgr.getCurrentIndex()}", sign)
|
||||||
|
JSONObject signature = new JSONObject()
|
||||||
|
signature.put("${srvCfg.getName()}", keySignature)
|
||||||
|
|
||||||
|
return signature
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
void afterPropertiesSet() throws Exception {
|
void afterPropertiesSet() throws Exception {
|
||||||
signEngine = new EdDSAEngine(MessageDigest.getInstance(keyMgr.getSpecs().getHashAlgorithm()))
|
signEngine = new EdDSAEngine(MessageDigest.getInstance(keyMgr.getSpecs().getHashAlgorithm()))
|
||||||
|
|||||||
Reference in New Issue
Block a user