Working prototype
This commit is contained in:
@@ -12,6 +12,12 @@ server:
|
||||
# e.g. domain name in e-mails.
|
||||
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:
|
||||
|
@@ -22,6 +22,8 @@ package io.kamax.mxisd.config
|
||||
|
||||
import io.kamax.mxisd.exception.ConfigurationException
|
||||
import org.apache.commons.lang.StringUtils
|
||||
import org.slf4j.Logger
|
||||
import org.slf4j.LoggerFactory
|
||||
import org.springframework.beans.factory.InitializingBean
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties
|
||||
import org.springframework.context.annotation.Configuration
|
||||
@@ -30,7 +32,11 @@ import org.springframework.context.annotation.Configuration
|
||||
@ConfigurationProperties(prefix = "server")
|
||||
class ServerConfig implements InitializingBean {
|
||||
|
||||
private Logger log = LoggerFactory.getLogger(ServerConfig.class);
|
||||
|
||||
private String name
|
||||
private int port
|
||||
private String publicUrl
|
||||
|
||||
String getName() {
|
||||
return name
|
||||
@@ -40,11 +46,43 @@ class ServerConfig implements InitializingBean {
|
||||
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
|
||||
void afterPropertiesSet() throws Exception {
|
||||
if (StringUtils.isBlank(getName())) {
|
||||
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 io.kamax.matrix.MatrixID
|
||||
import io.kamax.mxisd.config.ServerConfig
|
||||
import io.kamax.mxisd.controller.v1.io.ThreePidInviteReplyIO
|
||||
import io.kamax.mxisd.invitation.IThreePidInvite
|
||||
import io.kamax.mxisd.invitation.IThreePidInviteReply
|
||||
@@ -50,6 +51,9 @@ class InvitationController {
|
||||
@Autowired
|
||||
private KeyManager keyMgr
|
||||
|
||||
@Autowired
|
||||
private ServerConfig srvCfg
|
||||
|
||||
private Gson gson = new Gson()
|
||||
|
||||
@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)
|
||||
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.NotImplementedException
|
||||
import io.kamax.mxisd.key.KeyManager
|
||||
import org.apache.commons.lang.StringUtils
|
||||
import org.slf4j.Logger
|
||||
import org.slf4j.LoggerFactory
|
||||
import org.springframework.beans.factory.annotation.Autowired
|
||||
import org.springframework.web.bind.annotation.PathVariable
|
||||
import org.springframework.web.bind.annotation.RequestMapping
|
||||
import org.springframework.web.bind.annotation.RequestParam
|
||||
import org.springframework.web.bind.annotation.RestController
|
||||
|
||||
import javax.servlet.http.HttpServletRequest
|
||||
@@ -49,6 +51,7 @@ class KeyController {
|
||||
throw new BadRequestException("Invalid algorithm: " + keyType)
|
||||
}
|
||||
|
||||
log.info("Key {}:{} was requested", keyType, keyId)
|
||||
return JsonOutput.toJson([
|
||||
public_key: keyMgr.getPublicKeyBase64(keyId)
|
||||
])
|
||||
@@ -62,10 +65,14 @@ class KeyController {
|
||||
}
|
||||
|
||||
@RequestMapping(value = "/_matrix/identity/api/v1/pubkey/isvalid", method = GET)
|
||||
String checkKeyValidity(HttpServletRequest request) {
|
||||
log.error("{} was requested but not implemented", request.getRequestURL())
|
||||
String checkKeyValidity(HttpServletRequest request, @RequestParam("public_key") String pubKey) {
|
||||
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 java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
public class ThreePidInviteReplyIO {
|
||||
@@ -12,20 +11,40 @@ public class ThreePidInviteReplyIO {
|
||||
private List<Key> public_keys;
|
||||
private String display_name;
|
||||
|
||||
public ThreePidInviteReplyIO(IThreePidInviteReply reply, String pubKey) {
|
||||
public ThreePidInviteReplyIO(IThreePidInviteReply reply, String pubKey, String publicUrl) {
|
||||
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();
|
||||
}
|
||||
|
||||
public String getToken() {
|
||||
return token;
|
||||
}
|
||||
|
||||
public List<Key> getPublic_keys() {
|
||||
return public_keys;
|
||||
}
|
||||
|
||||
public String getDisplay_name() {
|
||||
return display_name;
|
||||
}
|
||||
|
||||
public class Key {
|
||||
private String key_validity_url;
|
||||
private String public_key;
|
||||
|
||||
public Key(String key) {
|
||||
this.key_validity_url = "https://example.org/_matrix/fixme"; // FIXME have a proper URL even if synapse does not check
|
||||
public Key(String key, String publicUrl) {
|
||||
this.key_validity_url = publicUrl + "/_matrix/identity/api/v1/pubkey/isvalid";
|
||||
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.http.client.methods.CloseableHttpResponse;
|
||||
import org.apache.http.client.methods.HttpPost;
|
||||
import org.apache.http.config.Registry;
|
||||
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.NoopHostnameVerifier;
|
||||
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.impl.client.CloseableHttpClient;
|
||||
import org.apache.http.impl.client.HttpClientBuilder;
|
||||
import org.apache.http.impl.conn.PoolingHttpClientConnectionManager;
|
||||
import org.apache.http.impl.client.HttpClients;
|
||||
import org.apache.http.ssl.SSLContextBuilder;
|
||||
import org.json.JSONArray;
|
||||
import org.json.JSONObject;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
@@ -38,8 +35,6 @@ import javax.net.ssl.HostnameVerifier;
|
||||
import javax.net.ssl.SSLContext;
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.cert.CertificateException;
|
||||
import java.security.cert.X509Certificate;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
@@ -65,40 +60,12 @@ public class InvitationManager {
|
||||
private void postConstruct() {
|
||||
gson = new Gson();
|
||||
|
||||
// FIXME export such madness into matrix-java-sdk with a nice wrapper to talk to a homeserver
|
||||
try {
|
||||
HttpClientBuilder b = HttpClientBuilder.create();
|
||||
|
||||
// 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.
|
||||
//
|
||||
SSLContext sslContext = SSLContextBuilder.create().loadTrustMaterial(new TrustSelfSignedStrategy()).build();
|
||||
HostnameVerifier hostnameVerifier = new NoopHostnameVerifier();
|
||||
SSLConnectionSocketFactory sslSocketFactory = new SSLConnectionSocketFactory(sslContext, hostnameVerifier);
|
||||
Registry<ConnectionSocketFactory> socketFactoryRegistry = RegistryBuilder.<ConnectionSocketFactory>create()
|
||||
.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();
|
||||
client = HttpClients.custom().setSSLSocketFactory(sslSocketFactory).build();
|
||||
} catch (Exception e) {
|
||||
// FIXME do better...
|
||||
throw new RuntimeException(e);
|
||||
@@ -196,31 +163,52 @@ public class InvitationManager {
|
||||
if (!hsUrlOpt.isPresent()) {
|
||||
log.warn("No HS found for domain {} - ignoring publishing", domain);
|
||||
} else {
|
||||
HttpPost req = new HttpPost(hsUrlOpt.get() + "/_matrix/federation/v1/3pid/onbind");
|
||||
JSONObject obj = new JSONObject(); // TODO use Gson instead
|
||||
obj.put("mxisd", threePid.getMxid());
|
||||
obj.put("token", reply.getToken());
|
||||
String mapping = gson.toJson(signMgr.signMessage(obj.toString())); // FIXME we shouldn't need to be doign this
|
||||
// TODO this is needed as this will block if called during authentication cycle due to synapse implementation
|
||||
new Thread(() -> { // FIXME need to make this retryable and within a general background working pool
|
||||
HttpPost req = new HttpPost(hsUrlOpt.get() + "/_matrix/federation/v1/3pid/onbind");
|
||||
// Expected body: https://matrix.to/#/!HUeDbmFUsWAhxHHvFG:matrix.org/$150469846739DCLWc:matrix.trancendances.fr
|
||||
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
|
||||
content.put("invites", Collections.singletonList(mapping));
|
||||
content.put("medium", threePid.getMedium());
|
||||
content.put("address", threePid.getValue());
|
||||
content.put("mxid", threePid.getMxid());
|
||||
JSONObject objUp = new JSONObject();
|
||||
objUp.put("mxid", threePid.getMxid());
|
||||
objUp.put("medium", threePid.getMedium());
|
||||
objUp.put("address", threePid.getValue());
|
||||
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);
|
||||
entity.setContentType("application/json");
|
||||
req.setEntity(entity);
|
||||
try {
|
||||
log.info("Posting onBind event to {}", req.getURI());
|
||||
CloseableHttpResponse response = client.execute(req);
|
||||
response.close();
|
||||
} catch (IOException e) {
|
||||
log.warn("Unable to tell HS {} about invite being mapped", domain, e);
|
||||
}
|
||||
String mapping = gson.toJson(objUp); // FIXME we shouldn't need to be doign this
|
||||
|
||||
JSONObject content = new JSONObject(); // TODO use Gson instead
|
||||
JSONArray invites = new JSONArray();
|
||||
invites.put(objUp);
|
||||
content.put("invites", invites);
|
||||
content.put("medium", threePid.getMedium());
|
||||
content.put("address", threePid.getValue());
|
||||
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.key.KeyManager
|
||||
import net.i2p.crypto.eddsa.EdDSAEngine
|
||||
import org.json.JSONObject
|
||||
import org.springframework.beans.factory.InitializingBean
|
||||
import org.springframework.beans.factory.annotation.Autowired
|
||||
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
|
||||
void afterPropertiesSet() throws Exception {
|
||||
signEngine = new EdDSAEngine(MessageDigest.getInstance(keyMgr.getSpecs().getHashAlgorithm()))
|
||||
|
Reference in New Issue
Block a user