Reworked MSC1915. Add request validation.

This commit is contained in:
Anatoly Sablin
2019-07-27 15:51:01 +03:00
parent a96920f533
commit a1f64f5159
28 changed files with 419 additions and 236 deletions

View File

@@ -118,7 +118,7 @@ public class Mxisd {
idStrategy = new RecursivePriorityLookupStrategy(cfg.getLookup(), ThreePidProviders.get(), bridgeFetcher);
pMgr = new ProfileManager(ProfileProviders.get(), clientDns, httpClient);
notifMgr = new NotificationManager(cfg.getNotification(), NotificationHandlers.get());
sessMgr = new SessionManager(cfg.getSession(), cfg.getMatrix(), store, notifMgr);
sessMgr = new SessionManager(cfg.getSession(), cfg.getMatrix(), store, notifMgr, resolver, httpClient, signMgr);
invMgr = new InvitationManager(cfg, store, idStrategy, keyMgr, signMgr, resolver, notifMgr, pMgr);
authMgr = new AuthManager(cfg, AuthProviders.get(), idStrategy, invMgr, clientDns, httpClient);
dirMgr = new DirectoryManager(cfg.getDirectory(), clientDns, httpClient, DirectoryProviders.get());

View File

@@ -62,6 +62,7 @@ public class MatrixConfig {
private transient final Logger log = LoggerFactory.getLogger(MatrixConfig.class);
private String domain;
private String trustedIdServer;
private Identity identity = new Identity();
public String getDomain() {
@@ -72,6 +73,14 @@ public class MatrixConfig {
this.domain = domain;
}
public String getTrustedIdServer() {
return trustedIdServer;
}
public void setTrustedIdServer(String trustedIdServer) {
this.trustedIdServer = trustedIdServer;
}
public Identity getIdentity() {
return identity;
}

View File

@@ -46,30 +46,15 @@ public class SessionConfig {
public static class PolicyUnbind {
public static class PolicyUnbindFraudulent {
private boolean enabled = true;
private boolean sendWarning = true;
public boolean getSendWarning() {
return sendWarning;
}
public void setSendWarning(boolean sendWarning) {
this.sendWarning = sendWarning;
}
public boolean getEnabled() {
return enabled;
}
private PolicyUnbindFraudulent fraudulent = new PolicyUnbindFraudulent();
public PolicyUnbindFraudulent getFraudulent() {
return fraudulent;
public void setEnabled(boolean enabled) {
this.enabled = enabled;
}
public void setFraudulent(PolicyUnbindFraudulent fraudulent) {
this.fraudulent = fraudulent;
}
}
public Policy() {

View File

@@ -115,24 +115,10 @@ public class EmailSendGridConfig {
public static class Templates {
public static class TemplateSessionUnbind {
private EmailTemplate fraudulent = new EmailTemplate();
public EmailTemplate getFraudulent() {
return fraudulent;
}
public void setFraudulent(EmailTemplate fraudulent) {
this.fraudulent = fraudulent;
}
}
public static class TemplateSession {
private EmailTemplate validation = new EmailTemplate();
private TemplateSessionUnbind unbind = new TemplateSessionUnbind();
private EmailTemplate unbind = new EmailTemplate();
public EmailTemplate getValidation() {
return validation;
@@ -142,11 +128,11 @@ public class EmailSendGridConfig {
this.validation = validation;
}
public TemplateSessionUnbind getUnbind() {
public EmailTemplate getUnbind() {
return unbind;
}
public void setUnbind(TemplateSessionUnbind unbind) {
public void setUnbind(EmailTemplate unbind) {
this.unbind = unbind;
}

View File

@@ -31,7 +31,8 @@ public class EmailTemplateConfig extends GenericTemplateConfig {
setInvite("classpath:/threepids/email/invite-template.eml");
getGeneric().put("matrixId", "classpath:/threepids/email/mxid-template.eml");
getSession().setValidation("classpath:/threepids/email/validate-template.eml");
getSession().getUnbind().setFraudulent("classpath:/threepids/email/unbind-fraudulent.eml");
getSession().getUnbind().setValidation("classpath:/threepids/email/unbind-template.eml");
getSession().getUnbind().setNotification("classpath:/threepids/email/unbind-notification.eml");
}
public EmailTemplateConfig build() {
@@ -40,7 +41,8 @@ public class EmailTemplateConfig extends GenericTemplateConfig {
log.info("Session:");
log.info(" Validation: {}", getSession().getValidation());
log.info(" Unbind:");
log.info(" Fraudulent: {}", getSession().getUnbind().getFraudulent());
log.info(" Validation: {}", getSession().getUnbind().getValidation());
log.info(" Notification: {}", getSession().getUnbind().getNotification());
return this;
}

View File

@@ -41,16 +41,25 @@ public class GenericTemplateConfig {
public static class SessionUnbind {
private String fraudulent;
private String validation;
public String getFraudulent() {
return fraudulent;
private String notification;
public String getValidation() {
return validation;
}
public void setFraudulent(String fraudulent) {
this.fraudulent = fraudulent;
public void setValidation(String validation) {
this.validation = validation;
}
public String getNotification() {
return notification;
}
public void setNotification(String notification) {
this.notification = notification;
}
}
private String validation;

View File

@@ -30,7 +30,8 @@ public class PhoneSmsTemplateConfig extends GenericTemplateConfig {
public PhoneSmsTemplateConfig() {
setInvite("classpath:/threepids/sms/invite-template.txt");
getSession().setValidation("classpath:/threepids/sms/validate-template.txt");
getSession().getUnbind().setFraudulent("classpath:/threepids/sms/unbind-fraudulent.txt");
getSession().getUnbind().setValidation("classpath:/threepids/sms/unbind-validation.txt");
getSession().getUnbind().setNotification("classpath:/threepids/sms/unbind-notification.txt");
}
public PhoneSmsTemplateConfig build() {
@@ -39,7 +40,8 @@ public class PhoneSmsTemplateConfig extends GenericTemplateConfig {
log.info("Session:");
log.info(" Validation: {}", getSession().getValidation());
log.info(" Unbind:");
log.info(" Fraudulent: {}", getSession().getUnbind().getFraudulent());
log.info(" Validation: {}", getSession().getUnbind().getValidation());
log.info(" Notification: {}", getSession().getUnbind().getNotification());
return this;
}

View File

@@ -58,5 +58,4 @@ public class CryptoFactory {
public static SignatureManager getSignatureManager(MxisdConfig cfg, Ed25519KeyManager keyMgr) {
return new Ed25519SignatureManager(cfg, keyMgr);
}
}

View File

@@ -26,6 +26,7 @@ import io.kamax.matrix.event.EventKey;
import io.kamax.matrix.json.MatrixJson;
import java.nio.charset.StandardCharsets;
import java.security.PublicKey;
import java.util.Objects;
public interface SignatureManager {
@@ -106,4 +107,13 @@ public interface SignatureManager {
*/
Signature sign(byte[] data);
/**
* Verify the data.
*
* @param publicKey public key to verify
* @param signature signature to verify
* @param data the data to verify
* @return {@code true} if signature is valid, else {@code false}
*/
boolean verify(PublicKey publicKey, String signature, byte[] data);
}

View File

@@ -33,7 +33,9 @@ import net.i2p.crypto.eddsa.EdDSAEngine;
import java.security.InvalidKeyException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.PublicKey;
import java.security.SignatureException;
import java.util.Base64;
public class Ed25519SignatureManager implements SignatureManager {
@@ -92,4 +94,15 @@ public class Ed25519SignatureManager implements SignatureManager {
}
}
@Override
public boolean verify(PublicKey publicKey, String signature, byte[] data) {
try {
EdDSAEngine signEngine = new EdDSAEngine(MessageDigest.getInstance(keyMgr.getKeySpecs().getHashAlgorithm()));
signEngine.initVerify(publicKey);
signEngine.update(data);
return signEngine.verify(Base64.getDecoder().decode(signature));
} catch (NoSuchAlgorithmException | InvalidKeyException | SignatureException e) {
throw new RuntimeException(e);
}
}
}

View File

@@ -21,13 +21,10 @@
package io.kamax.mxisd.http.undertow.handler.identity.v1;
import com.google.gson.JsonObject;
import io.kamax.mxisd.exception.BadRequestException;
import io.kamax.mxisd.exception.NotAllowedException;
import io.kamax.mxisd.http.IsAPIv1;
import io.kamax.mxisd.http.undertow.handler.BasicHttpHandler;
import io.kamax.mxisd.session.SessionManager;
import io.undertow.server.HttpServerExchange;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -46,20 +43,9 @@ public class SessionTpidUnbindHandler extends BasicHttpHandler {
@Override
public void handleRequest(HttpServerExchange exchange) {
String auth = exchange.getRequestHeaders().getFirst("Authorization");
if (StringUtils.isNotEmpty(auth)) {
// We have a auth header to process
if (StringUtils.startsWith(auth, "X-Matrix ")) {
log.warn("A remote host attempted to unbind without proper authorization. Request was denied");
log.warn("See https://github.com/kamax-matrix/mxisd/wiki/mxisd-and-your-privacy for more info");
throw new NotAllowedException("3PID can only be removed via 3PID sessions, not via Homeserver signature");
} else {
throw new BadRequestException("Illegal authorization type");
}
}
JsonObject body = parseJsonObject(exchange);
sessionMgr.unbind(body);
sessionMgr.unbind(auth, body);
writeBodyAsUtf8(exchange, "{}");
}
}

View File

@@ -37,6 +37,6 @@ public interface NotificationHandler {
void sendForValidation(IThreePidSession session);
void sendForFraudulentUnbind(ThreePid tpid);
void sendForUnbind(ThreePid tpid);
}

View File

@@ -78,8 +78,8 @@ public class NotificationManager {
ensureMedium(session.getThreePid().getMedium()).sendForValidation(session);
}
public void sendForFraudulentUnbind(ThreePid tpid) throws NotImplementedException {
ensureMedium(tpid.getMedium()).sendForFraudulentUnbind(tpid);
public void sendForUnbind(ThreePid tpid) throws NotImplementedException {
ensureMedium(tpid.getMedium()).sendForUnbind(tpid);
}
}

View File

@@ -20,33 +20,49 @@
package io.kamax.mxisd.session;
import static io.kamax.mxisd.config.SessionConfig.Policy.PolicyTemplate;
import com.google.gson.JsonObject;
import io.kamax.matrix.MatrixID;
import io.kamax.matrix.ThreePid;
import io.kamax.matrix._MatrixID;
import io.kamax.matrix.json.GsonUtil;
import io.kamax.matrix.json.MatrixJson;
import io.kamax.mxisd.config.MatrixConfig;
import io.kamax.mxisd.config.SessionConfig;
import io.kamax.mxisd.crypto.SignatureManager;
import io.kamax.mxisd.exception.BadRequestException;
import io.kamax.mxisd.exception.NotAllowedException;
import io.kamax.mxisd.exception.RemoteHomeServerException;
import io.kamax.mxisd.exception.SessionNotValidatedException;
import io.kamax.mxisd.exception.SessionUnknownException;
import io.kamax.mxisd.lookup.SingleLookupReply;
import io.kamax.mxisd.lookup.SingleLookupRequest;
import io.kamax.mxisd.lookup.ThreePidValidation;
import io.kamax.mxisd.matrix.HomeserverFederationResolver;
import io.kamax.mxisd.notification.NotificationManager;
import io.kamax.mxisd.storage.IStorage;
import io.kamax.mxisd.storage.dao.IThreePidSessionDao;
import io.kamax.mxisd.threepid.session.ThreePidSession;
import net.i2p.crypto.eddsa.EdDSAPublicKey;
import net.i2p.crypto.eddsa.spec.EdDSANamedCurveSpec;
import net.i2p.crypto.eddsa.spec.EdDSANamedCurveTable;
import net.i2p.crypto.eddsa.spec.EdDSAPublicKeySpec;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.RandomStringUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.impl.client.CloseableHttpClient;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.Base64;
import java.util.Calendar;
import java.util.Optional;
import static io.kamax.mxisd.config.SessionConfig.Policy.PolicyTemplate;
public class SessionManager {
private static final Logger log = LoggerFactory.getLogger(SessionManager.class);
@@ -55,17 +71,26 @@ public class SessionManager {
private MatrixConfig mxCfg;
private IStorage storage;
private NotificationManager notifMgr;
private HomeserverFederationResolver resolver;
private CloseableHttpClient client;
private SignatureManager signatureManager;
public SessionManager(
SessionConfig cfg,
MatrixConfig mxCfg,
IStorage storage,
NotificationManager notifMgr
SessionConfig cfg,
MatrixConfig mxCfg,
IStorage storage,
NotificationManager notifMgr,
HomeserverFederationResolver resolver,
CloseableHttpClient client,
SignatureManager signatureManager
) {
this.cfg = cfg;
this.mxCfg = mxCfg;
this.storage = storage;
this.notifMgr = notifMgr;
this.resolver = resolver;
this.client = client;
this.signatureManager = signatureManager;
}
private ThreePidSession getSession(String sid, String secret) {
@@ -98,7 +123,8 @@ public class SessionManager {
ThreePidSession session = new ThreePidSession(dao.get());
log.info("We already have a session for {}: {}", tpid, session.getId());
if (session.getAttempt() < attempt) {
log.info("Received attempt {} is greater than stored attempt {}, sending validation communication", attempt, session.getAttempt());
log.info("Received attempt {} is greater than stored attempt {}, sending validation communication", attempt,
session.getAttempt());
notifMgr.sendForValidation(session);
log.info("Sent validation notification to {}", tpid);
session.increaseAttempt();
@@ -166,7 +192,7 @@ public class SessionManager {
}
log.info("Session {}: Binding of {}:{} to Matrix ID {} is accepted",
session.getId(), session.getThreePid().getMedium(), session.getThreePid().getAddress(), mxid.getId());
session.getId(), session.getThreePid().getMedium(), session.getThreePid().getAddress(), mxid.getId());
SingleLookupRequest request = new SingleLookupRequest();
request.setType(session.getThreePid().getMedium());
@@ -174,7 +200,7 @@ public class SessionManager {
return new SingleLookupReply(request, mxid);
}
public void unbind(JsonObject reqData) {
public void unbind(String auth, JsonObject reqData) {
_MatrixID mxid;
try {
mxid = MatrixID.asAcceptable(GsonUtil.getStringOrThrow(reqData, "mxid"));
@@ -186,6 +212,128 @@ public class SessionManager {
String secret = GsonUtil.getStringOrNull(reqData, "client_secret");
ThreePid tpid = GsonUtil.get().fromJson(GsonUtil.getObj(reqData, "threepid"), ThreePid.class);
if (StringUtils.isNotBlank(sid) && StringUtils.isNotBlank(secret)) {
checkSession(sid, secret, tpid, mxid);
} else if (StringUtils.isNotBlank(auth)) {
checkAuthorization(auth, reqData);
} else {
throw new NotAllowedException("Unable to validate request");
}
// TODO make invalid all 3PID with specified medium and address.
}
private void checkAuthorization(String auth, JsonObject reqData) {
if (!auth.startsWith("X-Matrix ")) {
throw new NotAllowedException("Wrong authorization header");
}
if (StringUtils.isBlank(mxCfg.getTrustedIdServer())) {
throw new NotAllowedException("Unable to verify request, missing `matrix.trustedIdServer` variable");
}
String[] params = auth.substring("X-Matrix ".length()).split(",");
String origin = null;
String key = null;
String sig = null;
for (String param : params) {
String[] paramItems = param.split("=");
String paramKey = paramItems[0];
String paramValue = paramItems[1];
switch (paramKey) {
case "origin":
origin = removeQuotes(paramValue);
break;
case "key":
key = removeQuotes(paramValue);
break;
case "sig":
sig = removeQuotes(paramValue);
break;
default:
log.error("Unknown parameter: {}", param);
throw new BadRequestException("Authorization with unknown parameter");
}
}
if (StringUtils.isBlank(origin) || StringUtils.isBlank(key) || StringUtils.isBlank(sig)) {
log.error("Missing required parameters");
throw new BadRequestException("Missing required header parameters");
}
JsonObject jsonObject = new JsonObject();
jsonObject.addProperty("method", "POST");
jsonObject.addProperty("uri", "/_matrix/identity/api/v1/3pid/unbind");
jsonObject.addProperty("origin", origin);
jsonObject.addProperty("destination_is", mxCfg.getTrustedIdServer());
jsonObject.add("content", reqData);
String canonical = MatrixJson.encodeCanonical(jsonObject);
String originUrl = resolver.resolve(origin).toString();
validateServerKey(key, sig, canonical, originUrl);
}
private String removeQuotes(String origin) {
return origin.startsWith("\"") && origin.endsWith("\"") ? origin.substring(1, origin.length() - 1) : origin;
}
private void validateServerKey(String key, String signature, String canonical, String originUrl) {
HttpGet request = new HttpGet(originUrl + "/_matrix/key/v2/server");
log.info("Get keys from the server {}", request.getURI());
try (CloseableHttpResponse response = client.execute(request)) {
int statusCode = response.getStatusLine().getStatusCode();
log.info("Answer code: {}", statusCode);
if (statusCode == 200) {
verifyKey(key, signature, canonical, response);
} else {
throw new RemoteHomeServerException("Unable to fetch server keys.");
}
} catch (IOException e) {
String message = "Unable to get server keys: " + originUrl;
log.error(message, e);
throw new IllegalArgumentException(message);
}
}
private void verifyKey(String key, String signature, String canonical, CloseableHttpResponse response) throws IOException {
final String content = IOUtils.toString(response.getEntity().getContent(), StandardCharsets.UTF_8);
log.info("Answer body: {}", content);
final JsonObject responseObject = GsonUtil.parseObj(content);
final long validUntilTs = GsonUtil.getLong(responseObject, "valid_until_ts");
final Calendar calendar = Calendar.getInstance();
calendar.setTimeInMillis(validUntilTs);
if (calendar.before(Calendar.getInstance())) {
final String msg = "Key is expired";
log.error(msg);
throw new RemoteHomeServerException(msg);
}
final JsonObject verifyKeys = GsonUtil.getObj(responseObject, "verify_keys");
final JsonObject keyObject = GsonUtil.getObj(verifyKeys, key);
final String publicKey = GsonUtil.getStringOrNull(keyObject, "key");
if (StringUtils.isBlank(publicKey)) {
throw new RemoteHomeServerException("Missing server key.");
}
EdDSANamedCurveSpec ed25519CurveSpec = EdDSANamedCurveTable.ED_25519_CURVE_SPEC;
EdDSAPublicKeySpec publicKeySpec = new EdDSAPublicKeySpec(Base64.getDecoder().decode(publicKey), ed25519CurveSpec);
EdDSAPublicKey dsaPublicKey = new EdDSAPublicKey(publicKeySpec);
final boolean verificationResult = signatureManager.verify(dsaPublicKey, signature, canonical.getBytes(StandardCharsets.UTF_8));
log.info("Verification result: {}", verificationResult);
if (!verificationResult) {
throw new RemoteHomeServerException("Unable to verify request.");
}
log.info("Request was authorized.");
}
private void checkSession(String sid, String secret, ThreePid tpid, _MatrixID mxid) {
// We ensure the session was validated
ThreePidSession session = getSessionIfValidated(sid, secret);
@@ -199,8 +347,6 @@ public class SessionManager {
throw new NotAllowedException("Only Matrix IDs from domain " + mxCfg.getDomain() + " can be unbound");
}
log.info("Session {}: Unbinding of {}:{} to Matrix ID {} is accepted",
session.getId(), session.getThreePid().getMedium(), session.getThreePid().getAddress(), mxid.getId());
log.info("Request was authorized.");
}
}

View File

@@ -79,9 +79,9 @@ public abstract class GenericTemplateNotificationGenerator extends PlaceholderNo
}
@Override
public String getForFraudulentUnbind(ThreePid tpid) {
log.info("Generating notification content for fraudulent unbind");
return populateForFraudulentUndind(tpid, getTemplateContent(cfg.getSession().getUnbind().getFraudulent()));
public String getForNotificationUnbind(ThreePid tpid) {
log.info("Generating notification content for unbind");
return populateForNotificationUndind(tpid, getTemplateContent(cfg.getSession().getUnbind().getNotification()));
}
}

View File

@@ -37,6 +37,6 @@ public interface NotificationGenerator {
String getForValidation(IThreePidSession session);
String getForFraudulentUnbind(ThreePid tpid);
String getForNotificationUnbind(ThreePid tpid);
}

View File

@@ -127,7 +127,7 @@ public abstract class PlaceholderNotificationGenerator {
.replace("%NEXT_URL%", validationLink);
}
protected String populateForFraudulentUndind(ThreePid tpid, String input) {
protected String populateForNotificationUndind(ThreePid tpid, String input) {
return populateForCommon(tpid, input);
}

View File

@@ -73,8 +73,8 @@ public abstract class GenericNotificationHandler<A extends ThreePidConnector, B
}
@Override
public void sendForFraudulentUnbind(ThreePid tpid) {
send(connector, tpid.getAddress(), generator.getForFraudulentUnbind(tpid));
public void sendForUnbind(ThreePid tpid) {
send(connector, tpid.getAddress(), generator.getForNotificationUnbind(tpid));
}
}

View File

@@ -129,10 +129,10 @@ public class EmailSendGridNotificationHandler extends PlaceholderNotificationGen
}
@Override
public void sendForFraudulentUnbind(ThreePid tpid) {
EmailTemplate template = cfg.getTemplates().getSession().getUnbind().getFraudulent();
public void sendForUnbind(ThreePid tpid) {
EmailTemplate template = cfg.getTemplates().getSession().getUnbind();
if (StringUtils.isAllBlank(template.getBody().getText(), template.getBody().getHtml())) {
throw new FeatureNotAvailable("No template has been configured for fraudulent unbind notifications");
throw new FeatureNotAvailable("No template has been configured for unbind notifications");
}
Email email = getEmail();