/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.waveprotocol.box.server.waveserver;
import com.google.common.base.Preconditions;
import com.google.common.collect.ArrayListMultimap;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.collect.Multimap;
import com.google.inject.Inject;
import com.google.inject.name.Named;
import com.google.protobuf.ByteString;
import com.google.protobuf.InvalidProtocolBufferException;
import org.apache.commons.codec.binary.Hex;
import org.waveprotocol.box.server.CoreSettings;
import org.waveprotocol.box.server.common.CoreWaveletOperationSerializer;
import org.waveprotocol.wave.crypto.CertPathStore;
import org.waveprotocol.wave.crypto.SignatureException;
import org.waveprotocol.wave.crypto.SignerInfo;
import org.waveprotocol.wave.crypto.UnknownSignerException;
import org.waveprotocol.wave.crypto.WaveSignatureVerifier;
import org.waveprotocol.wave.federation.FederationErrors;
import org.waveprotocol.wave.federation.WaveletFederationProvider;
import org.waveprotocol.wave.federation.FederationErrorProto.FederationError;
import org.waveprotocol.wave.federation.Proto.ProtocolSignature;
import org.waveprotocol.wave.federation.Proto.ProtocolSignedDelta;
import org.waveprotocol.wave.federation.Proto.ProtocolSignerInfo;
import org.waveprotocol.wave.federation.Proto.ProtocolWaveletDelta;
import org.waveprotocol.wave.federation.WaveletFederationProvider.DeltaSignerInfoResponseListener;
import org.waveprotocol.wave.model.id.WaveletName;
import org.waveprotocol.wave.model.version.HashedVersion;
import org.waveprotocol.wave.model.wave.ParticipantId;
import org.waveprotocol.wave.util.logging.Log;
import java.util.List;
import java.util.Map;
/**
* Default implementation of {@link CertificateManager}.
*/
public class CertificateManagerImpl implements CertificateManager {
private static final Log LOG = Log.get(CertificateManagerImpl.class);
private final SignatureHandler waveSigner;
private final ImmutableSet<String> localDomains;
private final WaveSignatureVerifier verifier;
private final CertPathStore certPathStore;
private final boolean disableVerfication;
/**
* Map of signer ids to requests for the signer info for those ids. Each signer id is mapped to
* a multimap: a domain mapped to a list of callbacks for that domain, called when the signer info
* is available for the signer id. It is arranged by domain to facilitate the optimisation where
* exactly 1 signer request is sent per domain.
*/
private final Map<ByteString, Multimap<String, SignerInfoPrefetchResultListener>>
signerInfoRequests;
@Inject
public CertificateManagerImpl(
@Named(CoreSettings.WAVESERVER_DISABLE_VERIFICATION) boolean disableVerfication,
SignatureHandler signer, WaveSignatureVerifier verifier, CertPathStore certPathStore) {
this.disableVerfication = disableVerfication;
this.waveSigner = signer;
// for now, we just support a single signer
this.localDomains = ImmutableSet.of(signer.getDomain());
this.verifier = verifier;
this.certPathStore = certPathStore;
this.signerInfoRequests = Maps.newHashMap();
if (disableVerfication) {
LOG.warning("** SIGNATURE VERIFICATION DISABLED ** "
+ "see flag \"" + CoreSettings.WAVESERVER_DISABLE_VERIFICATION + "\"");
}
}
@Override
public ImmutableSet<String> getLocalDomains() {
return localDomains;
}
@Override
public SignatureHandler getLocalSigner() {
return waveSigner;
}
@Override
public ProtocolSignedDelta signDelta(ByteStringMessage<ProtocolWaveletDelta> delta) {
// TODO: support extended address paths. For now, there will be exactly
// one signature, and we don't support federated groups.
Preconditions.checkState(delta.getMessage().getAddressPathCount() == 0);
ProtocolSignedDelta.Builder signedDelta = ProtocolSignedDelta.newBuilder();
signedDelta.setDelta(delta.getByteString());
signedDelta.addAllSignature(waveSigner.sign(delta));
return signedDelta.build();
}
@Override
public ByteStringMessage<ProtocolWaveletDelta> verifyDelta(ProtocolSignedDelta signedDelta)
throws SignatureException, UnknownSignerException {
ByteStringMessage<ProtocolWaveletDelta> delta;
try {
delta = ByteStringMessage.parseProtocolWaveletDelta(signedDelta.getDelta());
} catch (InvalidProtocolBufferException e) {
throw new IllegalArgumentException("signed delta does not contain valid delta", e);
}
if (disableVerfication) {
return delta;
}
List<String> domains = getParticipantDomains(delta.getMessage());
if (domains.size() != signedDelta.getSignatureCount()) {
throw new SignatureException("found " + domains.size() + " domains in " +
"extended address path, but " + signedDelta.getSignatureCount() +
" signatures.");
}
for (int i = 0; i < domains.size(); i++) {
String domain = domains.get(i);
ProtocolSignature signature = signedDelta.getSignature(i);
verifySingleSignature(delta, signature, domain);
}
return delta;
}
/**
* Verifies a single signature.
* @param delta the payload that we're verifying the signature on.
* @param signature the signature on the payload
* @param domain the authority (domain name) that should have signed the
* payload.
* @throws SignatureException if the signature doesn't verify.
*/
private void verifySingleSignature(ByteStringMessage<ProtocolWaveletDelta> delta,
ProtocolSignature signature, String domain)
throws SignatureException, UnknownSignerException {
verifier.verify(delta.getByteString().toByteArray(), signature, domain);
}
/**
* Returns the domains of all the addresses in the extended address path.
*/
private List<String> getParticipantDomains(ProtocolWaveletDelta delta) {
Iterable<String> addresses = getExtendedAddressPath(delta);
return getDeDupedDomains(addresses);
}
/**
* Extracts the domains from user addresses, and removes duplicates.
*/
private List<String> getDeDupedDomains(Iterable<String> addresses) {
List<String> domains = Lists.newArrayList();
for (String address : addresses) {
String participantDomain = new ParticipantId(address).getDomain();
if (!domains.contains(participantDomain)) {
domains.add(participantDomain);
}
}
return domains;
}
/**
* Returns the extended address path, i.e., the addresses in the delta's
* address path, plus the author of the delta.
*/
private Iterable<String> getExtendedAddressPath(ProtocolWaveletDelta delta) {
return Iterables.concat(delta.getAddressPathList(),
ImmutableList.of(delta.getAuthor()));
}
@Override
public synchronized void storeSignerInfo(ProtocolSignerInfo signerInfo)
throws SignatureException {
verifier.verifySignerInfo(new SignerInfo(signerInfo));
certPathStore.putSignerInfo(signerInfo);
}
@Override
public synchronized ProtocolSignerInfo retrieveSignerInfo(ByteString signerId) {
SignerInfo signerInfo;
try {
signerInfo = certPathStore.getSignerInfo(signerId.toByteArray());
// null is acceptable for retrieveSignerInfo. The user of the certificate manager should call
// prefetchDeltaSignerInfo for the mechanism to actually populate the certificate manager.
return signerInfo == null ? null : signerInfo.toProtoBuf();
} catch (SignatureException e) {
/*
* TODO: This may result in the server endlessly requesting the signer info from the
* remote server, a more graceful failure needs to be implemented.
*/
LOG.severe("Failed to retreive signer info for "
+ new String(Hex.encodeHex(signerId.toByteArray())), e);
return null;
}
}
@Override
public synchronized void prefetchDeltaSignerInfo(WaveletFederationProvider provider,
ByteString signerId, WaveletName waveletName, HashedVersion deltaEndVersion,
SignerInfoPrefetchResultListener callback) {
ProtocolSignerInfo signerInfo = retrieveSignerInfo(signerId);
if (signerInfo != null) {
callback.onSuccess(signerInfo);
} else {
enqueueSignerInfoRequest(provider, signerId, waveletName, deltaEndVersion, callback);
}
}
/**
* Enqueue a signer info request for a signed delta on a given domain.
*/
private synchronized void enqueueSignerInfoRequest(final WaveletFederationProvider provider,
final ByteString signerId, final WaveletName waveletName,
HashedVersion deltaEndVersion, SignerInfoPrefetchResultListener callback) {
final String domain = waveletName.waveletId.getDomain();
Multimap<String, SignerInfoPrefetchResultListener> domainCallbacks =
signerInfoRequests.get(signerId);
if (domainCallbacks == null) {
domainCallbacks = ArrayListMultimap.create();
signerInfoRequests.put(signerId, domainCallbacks);
}
// The thing is, we need to add multiple callbacks for the same domain, but we only want to
// have one outstanding request per domain
domainCallbacks.put(domain, callback);
if (domainCallbacks.get(domain).size() == 1) {
provider.getDeltaSignerInfo(signerId, waveletName,
(deltaEndVersion == null)
? null : CoreWaveletOperationSerializer.serialize(deltaEndVersion),
new DeltaSignerInfoResponseListener() {
@Override public void onFailure(FederationError error) {
LOG.warning("getDeltaSignerInfo failed: " + error);
// Fail all requests on this domain
dequeueSignerInfoRequestForDomain(signerId, error, domain);
}
@Override public void onSuccess(ProtocolSignerInfo signerInfo) {
try {
storeSignerInfo(signerInfo);
dequeueSignerInfoRequest(signerId, null);
} catch (SignatureException e) {
LOG.warning("Failed to verify signer info", e);
dequeueSignerInfoRequest(signerId, FederationErrors.badRequest(e.toString()));
}
}});
}
}
/**
* Dequeue all signer info requests for a given signer id.
*
* @param signerId to dequeue requests for
* @param error if there was an error, null for success
*/
private synchronized void dequeueSignerInfoRequest(ByteString signerId, FederationError error) {
List<String> domains = ImmutableList.copyOf(signerInfoRequests.get(signerId).keySet());
for (String domain : domains) {
dequeueSignerInfoRequestForDomain(signerId, error, domain);
}
}
/**
* Dequeue all signer info requests for a given signer id and a specific domain.
*
* @param signerId to dequeue requests for
* @param error if there was an error, null for success
* @param domain to dequeue the signer requests for
*/
private synchronized void dequeueSignerInfoRequestForDomain(ByteString signerId,
FederationError error, String domain) {
Multimap<String, SignerInfoPrefetchResultListener> domainListeners =
signerInfoRequests.get(signerId);
if (domainListeners == null) {
LOG.info("There are no domain listeners for signer " + signerId + " domain "+ domain);
return;
} else {
LOG.info("Dequeuing " + domainListeners.size() + " listeners for domain " + domain);
}
for (SignerInfoPrefetchResultListener listener : domainListeners.get(domain)) {
if (error == null) {
listener.onSuccess(retrieveSignerInfo(signerId));
} else {
listener.onFailure(error);
}
}
domainListeners.removeAll(domain);
if (domainListeners.isEmpty()) {
// No listeners for any domains, delete the signer id for the overall map
signerInfoRequests.remove(signerId);
}
}
}