package com.subgraph.orchid.directory.consensus;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.logging.Logger;
import com.subgraph.orchid.ConsensusDocument;
import com.subgraph.orchid.DirectoryServer;
import com.subgraph.orchid.KeyCertificate;
import com.subgraph.orchid.RouterStatus;
import com.subgraph.orchid.Tor;
import com.subgraph.orchid.VoteAuthorityEntry;
import com.subgraph.orchid.crypto.TorPublicKey;
import com.subgraph.orchid.crypto.TorSignature.DigestAlgorithm;
import com.subgraph.orchid.data.HexDigest;
import com.subgraph.orchid.data.Timestamp;
import com.subgraph.orchid.directory.TrustedAuthorities;
public class ConsensusDocumentImpl implements ConsensusDocument {
enum SignatureVerifyStatus { STATUS_UNVERIFIED, STATUS_NEED_CERTS, STATUS_VERIFIED };
private final static Logger logger = Logger.getLogger(ConsensusDocumentImpl.class.getName());
private final static String BW_WEIGHT_SCALE_PARAM = "bwweightscale";
private final static int BW_WEIGHT_SCALE_DEFAULT = 10000;
private final static int BW_WEIGHT_SCALE_MIN = 1;
private final static int BW_WEIGHT_SCALE_MAX = Integer.MAX_VALUE;
private final static String CIRCWINDOW_PARAM = "circwindow";
private final static int CIRCWINDOW_DEFAULT = 1000;
private final static int CIRCWINDOW_MIN = 100;
private final static int CIRCWINDOW_MAX = 1000;
private final static String USE_NTOR_HANDSHAKE_PARAM = "UseNTorHandshake";
private Set<RequiredCertificate> requiredCertificates = new HashSet<RequiredCertificate>();
private int consensusMethod;
private ConsensusFlavor flavor;
private Timestamp validAfter;
private Timestamp freshUntil;
private Timestamp validUntil;
private int distDelaySeconds;
private int voteDelaySeconds;
private Set<String> clientVersions;
private Set<String> serverVersions;
private Set<String> knownFlags;
private HexDigest signingHash;
private HexDigest signingHash256;
private Map<HexDigest, VoteAuthorityEntry> voteAuthorityEntries;
private List<RouterStatus> routerStatusEntries;
private Map<String, Integer> bandwidthWeights;
private Map<String, Integer> parameters;
private int signatureCount;
private boolean isFirstCallToVerifySignatures = true;
private String rawDocumentData;
void setConsensusFlavor(ConsensusFlavor flavor) { this.flavor = flavor; }
void setConsensusMethod(int method) { consensusMethod = method; }
void setValidAfter(Timestamp ts) { validAfter = ts; }
void setFreshUntil(Timestamp ts) { freshUntil = ts; }
void setValidUntil(Timestamp ts) { validUntil = ts; }
void setDistDelaySeconds(int seconds) { distDelaySeconds = seconds; }
void setVoteDelaySeconds(int seconds) { voteDelaySeconds = seconds; }
void addClientVersion(String version) { clientVersions.add(version); }
void addServerVersion(String version) { serverVersions.add(version); }
void addParameter(String name, int value) { parameters.put(name, value); }
void addBandwidthWeight(String name, int value) { bandwidthWeights.put(name, value); }
void addSignature(DirectorySignature signature) {
final VoteAuthorityEntry voteAuthority = voteAuthorityEntries.get(signature.getIdentityDigest());
if(voteAuthority == null) {
logger.warning("Consensus contains signature for source not declared in authority section: "+ signature.getIdentityDigest());
return;
}
final List<DirectorySignature> signatures = voteAuthority.getSignatures();
final DigestAlgorithm newSignatureAlgorithm = signature.getSignature().getDigestAlgorithm();
for(DirectorySignature sig: signatures) {
DigestAlgorithm algo = sig.getSignature().getDigestAlgorithm();
if(algo.equals(newSignatureAlgorithm)) {
logger.warning("Consensus contains two or more signatures for same source with same algorithm");
return;
}
}
signatureCount += 1;
signatures.add(signature);
}
void setSigningHash(HexDigest hash) { signingHash = hash; }
void setSigningHash256(HexDigest hash) { signingHash256 = hash; }
void setRawDocumentData(String rawData) { rawDocumentData = rawData; }
ConsensusDocumentImpl() {
clientVersions = new HashSet<String>();
serverVersions = new HashSet<String>();
knownFlags = new HashSet<String>();
voteAuthorityEntries = new HashMap<HexDigest, VoteAuthorityEntry>();
routerStatusEntries = new ArrayList<RouterStatus>();
bandwidthWeights = new HashMap<String, Integer>();
parameters = new HashMap<String, Integer>();
}
void addKnownFlag(String flag) {
knownFlags.add(flag);
}
void addVoteAuthorityEntry(VoteAuthorityEntry entry) {
voteAuthorityEntries.put(entry.getIdentity(), entry);
}
void addRouterStatusEntry(RouterStatusImpl entry) {
routerStatusEntries.add(entry);
}
public ConsensusFlavor getFlavor() {
return flavor;
}
public Timestamp getValidAfterTime() {
return validAfter;
}
public Timestamp getFreshUntilTime() {
return freshUntil;
}
public Timestamp getValidUntilTime() {
return validUntil;
}
public int getConsensusMethod() {
return consensusMethod;
}
public int getVoteSeconds() {
return voteDelaySeconds;
}
public int getDistSeconds() {
return distDelaySeconds;
}
public Set<String> getClientVersions() {
return clientVersions;
}
public Set<String> getServerVersions() {
return serverVersions;
}
public boolean isLive() {
if(validUntil == null) {
return false;
} else {
return !validUntil.hasPassed();
}
}
public List<RouterStatus> getRouterStatusEntries() {
return Collections.unmodifiableList(routerStatusEntries);
}
public String getRawDocumentData() {
return rawDocumentData;
}
public ByteBuffer getRawDocumentBytes() {
if(getRawDocumentData() == null) {
return ByteBuffer.allocate(0);
} else {
return ByteBuffer.wrap(getRawDocumentData().getBytes(Tor.getDefaultCharset()));
}
}
public boolean isValidDocument() {
return (validAfter != null) && (freshUntil != null) && (validUntil != null) &&
(voteDelaySeconds > 0) && (distDelaySeconds > 0) && (signingHash != null) &&
(signatureCount > 0);
}
public HexDigest getSigningHash() {
return signingHash;
}
public HexDigest getSigningHash256() {
return signingHash256;
}
public synchronized SignatureStatus verifySignatures() {
boolean firstCall = isFirstCallToVerifySignatures;
isFirstCallToVerifySignatures = false;
requiredCertificates.clear();
int verifiedCount = 0;
int certsNeededCount = 0;
final int v3Count = TrustedAuthorities.getInstance().getV3AuthorityServerCount();
final int required = (v3Count / 2) + 1;
for(VoteAuthorityEntry entry: voteAuthorityEntries.values()) {
switch(verifySingleAuthority(entry)) {
case STATUS_FAILED:
break;
case STATUS_NEED_CERTS:
certsNeededCount += 1;
break;
case STATUS_VERIFIED:
verifiedCount += 1;
break;
}
}
if(verifiedCount >= required) {
return SignatureStatus.STATUS_VERIFIED;
} else if(verifiedCount + certsNeededCount >= required) {
if(firstCall) {
logger.info("Certificates need to be retrieved to verify consensus");
}
return SignatureStatus.STATUS_NEED_CERTS;
} else {
return SignatureStatus.STATUS_FAILED;
}
}
private SignatureStatus verifySingleAuthority(VoteAuthorityEntry authority) {
boolean certsNeeded = false;
boolean validSignature = false;
for(DirectorySignature s: authority.getSignatures()) {
DirectoryServer trusted = TrustedAuthorities.getInstance().getAuthorityServerByIdentity(s.getIdentityDigest());
if(trusted == null) {
logger.warning("Consensus signed by unrecognized directory authority: "+ s.getIdentityDigest());
return SignatureStatus.STATUS_FAILED;
} else {
switch(verifySignatureForTrustedAuthority(trusted, s)) {
case STATUS_NEED_CERTS:
certsNeeded = true;
break;
case STATUS_VERIFIED:
validSignature = true;
break;
default:
break;
}
}
}
if(validSignature) {
return SignatureStatus.STATUS_VERIFIED;
} else if(certsNeeded) {
return SignatureStatus.STATUS_NEED_CERTS;
} else {
return SignatureStatus.STATUS_FAILED;
}
}
private SignatureStatus verifySignatureForTrustedAuthority(DirectoryServer trustedAuthority, DirectorySignature signature) {
final KeyCertificate certificate = trustedAuthority.getCertificateByFingerprint(signature.getSigningKeyDigest());
if(certificate == null) {
logger.fine("Missing certificate for signing key: "+ signature.getSigningKeyDigest());
addRequiredCertificateForSignature(signature);
return SignatureStatus.STATUS_NEED_CERTS;
}
if(certificate.isExpired()) {
return SignatureStatus.STATUS_FAILED;
}
final TorPublicKey signingKey = certificate.getAuthoritySigningKey();
final HexDigest d = (signature.useSha256()) ? signingHash256 : signingHash;
if(!signingKey.verifySignature(signature.getSignature(), d)) {
logger.warning("Signature failed on consensus for signing key: "+ signature.getSigningKeyDigest());
return SignatureStatus.STATUS_FAILED;
}
return SignatureStatus.STATUS_VERIFIED;
}
public Set<RequiredCertificate> getRequiredCertificates() {
return requiredCertificates;
}
private void addRequiredCertificateForSignature(DirectorySignature signature) {
requiredCertificates.add(new RequiredCertificateImpl(signature.getIdentityDigest(), signature.getSigningKeyDigest()));
}
public boolean equals(Object o) {
if(!(o instanceof ConsensusDocumentImpl))
return false;
final ConsensusDocumentImpl other = (ConsensusDocumentImpl) o;
return other.getSigningHash().equals(signingHash);
}
public int hashCode() {
return (signingHash == null) ? 0 : signingHash.hashCode();
}
private int getParameterValue(String name, int defaultValue, int minValue, int maxValue) {
if(!parameters.containsKey(name)) {
return defaultValue;
}
final int value = parameters.get(name);
if(value < minValue) {
return minValue;
} else if(value > maxValue) {
return maxValue;
} else {
return value;
}
}
private boolean getBooleanParameterValue(String name, boolean defaultValue) {
if(!parameters.containsKey(name)) {
return defaultValue;
}
final int value = parameters.get(name);
return value != 0;
}
public int getCircWindowParameter() {
return getParameterValue(CIRCWINDOW_PARAM, CIRCWINDOW_DEFAULT, CIRCWINDOW_MIN, CIRCWINDOW_MAX);
}
public int getWeightScaleParameter() {
return getParameterValue(BW_WEIGHT_SCALE_PARAM, BW_WEIGHT_SCALE_DEFAULT, BW_WEIGHT_SCALE_MIN, BW_WEIGHT_SCALE_MAX);
}
public int getBandwidthWeight(String tag) {
if(bandwidthWeights.containsKey(tag)) {
return bandwidthWeights.get(tag);
} else {
return -1;
}
}
public boolean getUseNTorHandshake() {
return getBooleanParameterValue(USE_NTOR_HANDSHAKE_PARAM, false);
}
}