Working prototype

This commit is contained in:
Maxime Dor
2017-09-06 15:00:43 +02:00
parent a7303fef15
commit a704ba2e6c
7 changed files with 147 additions and 72 deletions

View File

@@ -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:

View File

@@ -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())
}
}

View File

@@ -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()))
}
}

View File

@@ -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
)
}
}

View File

@@ -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;
}
}
}

View File

@@ -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);
}
}

View File

@@ -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()))