/** * Copyright 2009 Google Inc. * * Licensed 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 static org.mockito.Mockito.mock; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyNoMoreInteractions; import static org.mockito.Mockito.verifyZeroInteractions; import static org.waveprotocol.box.server.waveserver.Ticker.EASY_TICKS; import com.google.common.collect.ImmutableList; import com.google.protobuf.ByteString; import junit.framework.TestCase; import org.waveprotocol.box.server.common.CoreWaveletOperationSerializer; import org.waveprotocol.box.server.persistence.memory.MemoryStore; import org.waveprotocol.box.server.waveserver.CertificateManager.SignerInfoPrefetchResultListener; import org.waveprotocol.box.server.waveserver.testing.Certificates; import org.waveprotocol.wave.crypto.CachedCertPathValidator; import org.waveprotocol.wave.crypto.CertPathStore; import org.waveprotocol.wave.crypto.DefaultCacheImpl; import org.waveprotocol.wave.crypto.DefaultTrustRootsProvider; import org.waveprotocol.wave.crypto.DisabledCertPathValidator; import org.waveprotocol.wave.crypto.SignatureException; import org.waveprotocol.wave.crypto.SignerInfo; import org.waveprotocol.wave.crypto.TimeSource; import org.waveprotocol.wave.crypto.TrustRootsProvider; import org.waveprotocol.wave.crypto.UnknownSignerException; import org.waveprotocol.wave.crypto.VerifiedCertChainCache; import org.waveprotocol.wave.crypto.WaveCertPathValidator; import org.waveprotocol.wave.crypto.WaveSignatureVerifier; import org.waveprotocol.wave.crypto.WaveSigner; import org.waveprotocol.wave.crypto.WaveSignerFactory; 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.ProtocolHashedVersion; 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.Proto.ProtocolSignature.SignatureAlgorithm; import org.waveprotocol.wave.model.id.WaveId; import org.waveprotocol.wave.model.id.WaveletId; import org.waveprotocol.wave.model.id.WaveletName; import org.waveprotocol.wave.model.version.HashedVersion; import java.io.ByteArrayInputStream; import java.io.InputStream; import java.security.cert.X509Certificate; import java.util.Calendar; import java.util.Collection; import java.util.Date; import java.util.List; import java.util.TimeZone; public class CertificateManagerImplTest extends TestCase { private static final String OTHER_DOMAIN = "other.org"; private static final FederationError GENERIC_ERROR = FederationErrors.badRequest("It's not my fault!"); private CertPathStore store; private CertificateManager manager; private Ticker ticker; /* * These belong to the example.com domain. */ public static final String DOMAIN = "example.com"; @Override protected void setUp() throws Exception { super.setUp(); store = new MemoryStore(); manager = new CertificateManagerImpl(false, getSigner(), getVerifier(store, true), store); ticker = new Ticker(); } /* * TESTS */ public void testSignature() throws Exception { ProtocolWaveletDelta delta = ProtocolWaveletDelta.newBuilder() .setHashedVersion(getProtocolHashedVersion()) .setAuthor("bob@example.com") .build(); ByteStringMessage<ProtocolWaveletDelta> canonicalDelta = ByteStringMessage.serializeMessage(delta); ProtocolSignedDelta signedDelta = manager.signDelta(canonicalDelta); manager.storeSignerInfo(getSignerInfo().toProtoBuf()); ByteStringMessage<ProtocolWaveletDelta> compare = manager.verifyDelta(signedDelta); assertEquals(canonicalDelta, compare); } public void testSignature_missingSignerInfo() throws Exception { ProtocolWaveletDelta delta = ProtocolWaveletDelta.newBuilder() .setHashedVersion(getProtocolHashedVersion()) .setAuthor("bob@example.com") .build(); ByteStringMessage<ProtocolWaveletDelta> canonicalDelta = ByteStringMessage.serializeMessage(delta); manager = new CertificateManagerImpl(false, getSigner(), getVerifier(store, false), store); ProtocolSignedDelta signedDelta = manager.signDelta(canonicalDelta); try { manager.verifyDelta(signedDelta); fail("expected UnknownSignerException, but didn't get it"); } catch (UnknownSignerException e) { // expected } catch (Exception e) { fail("expected UnknownSignerExeception, but got " + e); } } public void testSignature_authorNotMatching() throws Exception { ProtocolWaveletDelta delta = ProtocolWaveletDelta.newBuilder() .setHashedVersion(getProtocolHashedVersion()) .setAuthor("bob@someotherdomain.com") .build(); ByteStringMessage<ProtocolWaveletDelta> canonicalDelta = ByteStringMessage.serializeMessage(delta); ProtocolSignedDelta signedDelta = manager.signDelta(canonicalDelta); manager.storeSignerInfo(getSignerInfo().toProtoBuf()); try { manager.verifyDelta(signedDelta); fail("expected exception, but didn't get it"); } catch (SignatureException e) { // expected } } public void testRealSignature() throws Exception { manager = new CertificateManagerImpl(false, getSigner(), getRealVerifier(store), store); manager.storeSignerInfo(Certificates.getRealSignerInfo().toProtoBuf()); ByteStringMessage<ProtocolWaveletDelta> compare = manager.verifyDelta(getFakeSignedDelta()); assertEquals(compare, getFakeDelta()); } /** * Test prefetchDeltaSignerInfo for a single request on a single domain, and that subsequent * requests on the same domain return instantly. */ public void test_prefetchDeltaSignerInfo1() throws Exception { SignerInfoPrefetchResultListener mockListener = mock(SignerInfoPrefetchResultListener.class); manager.prefetchDeltaSignerInfo(getSuccessfulProvider(), getRealSignerId(), getFakeWaveletName(CertificateManagerImplTest.DOMAIN), null, mockListener); verify(mockListener).onSuccess(Certificates.getRealSignerInfo().toProtoBuf()); // Shouldn't get a NPE from the null provider because the callback should not be used manager.prefetchDeltaSignerInfo(null, getRealSignerId(), getFakeWaveletName(CertificateManagerImplTest.DOMAIN), null, mockListener); verify(mockListener, times(2)).onSuccess(Certificates.getRealSignerInfo().toProtoBuf()); } /** * Test prefetchDeltaSignerInfo for multiple requests on a single domain where the first one * does not terminate. The entire request should fail. */ public void test_prefetchDeltaSignerInfo2() throws Exception { // The dead listener won't return SignerInfoPrefetchResultListener deadListener = mock(SignerInfoPrefetchResultListener.class); manager.prefetchDeltaSignerInfo(getDeadProvider(), getRealSignerId(), getFakeWaveletName(CertificateManagerImplTest.DOMAIN), null, deadListener); verifyZeroInteractions(deadListener); // But this will. However, it shouldn't be called since the other was added first, and only // 1 request is started per domain SignerInfoPrefetchResultListener aliveListener = mock(SignerInfoPrefetchResultListener.class); manager.prefetchDeltaSignerInfo(getSuccessfulProvider(), getRealSignerId(), getFakeWaveletName(CertificateManagerImplTest.DOMAIN), null, aliveListener); verifyZeroInteractions(aliveListener); } /** * Test prefetchDeltaSignerInfo for multiple requests on different domains where the first one * does not terminate. However the second should terminate, and both callbacks called. */ public void test_prefetchDeltaSignerInfo3() throws Exception { // This will never return, but the callback will run later SignerInfoPrefetchResultListener deadListener = mock(SignerInfoPrefetchResultListener.class); manager.prefetchDeltaSignerInfo(getDeadProvider(), getRealSignerId(), getFakeWaveletName(CertificateManagerImplTest.DOMAIN), null, deadListener); verifyZeroInteractions(deadListener); // This should succeed later, after some number of ticks SignerInfoPrefetchResultListener slowSuccessListener = mock(SignerInfoPrefetchResultListener.class); manager.prefetchDeltaSignerInfo(getSlowSuccessfulProvider(ticker, EASY_TICKS), getRealSignerId(), getFakeWaveletName(OTHER_DOMAIN), null, slowSuccessListener); verifyZeroInteractions(slowSuccessListener); // This would succeed right now if it didn't have to wait for the slow success SignerInfoPrefetchResultListener successListener = mock(SignerInfoPrefetchResultListener.class); manager.prefetchDeltaSignerInfo(getSuccessfulProvider(), getRealSignerId(), getFakeWaveletName(OTHER_DOMAIN), null, successListener); verifyZeroInteractions(successListener); // After ticking, each callback should run ticker.tick(EASY_TICKS); verify(deadListener).onSuccess(Certificates.getRealSignerInfo().toProtoBuf()); verify(slowSuccessListener).onSuccess(Certificates.getRealSignerInfo().toProtoBuf()); verify(successListener).onSuccess(Certificates.getRealSignerInfo().toProtoBuf()); // Subsequent calls should also succeed immediately without calling the callback SignerInfoPrefetchResultListener nullListener = mock(SignerInfoPrefetchResultListener.class); manager.prefetchDeltaSignerInfo(null, getRealSignerId(), getFakeWaveletName(CertificateManagerImplTest.DOMAIN), null, nullListener); verify(nullListener).onSuccess(Certificates.getRealSignerInfo().toProtoBuf()); } /** * Test prefetchDeltaSignerInfo for failing requests -- the failure should be propagated to * the prefetch listener, and requests on the same domain should fail. */ public void test_prefetchDeltaSignerInfo4() throws Exception { // This will fail later SignerInfoPrefetchResultListener failListener = mock(SignerInfoPrefetchResultListener.class); manager.prefetchDeltaSignerInfo(getSlowFailingProvider(ticker, EASY_TICKS), getRealSignerId(), getFakeWaveletName(CertificateManagerImplTest.DOMAIN), null, failListener); // This would succeed later if it weren't for the previous one failing SignerInfoPrefetchResultListener successListener = mock(SignerInfoPrefetchResultListener.class); manager.prefetchDeltaSignerInfo(getSuccessfulProvider(), getRealSignerId(), getFakeWaveletName(CertificateManagerImplTest.DOMAIN), null, successListener); verifyZeroInteractions(failListener); verifyZeroInteractions(successListener); // Both callbacks should fail after ticking ticker.tick(EASY_TICKS); verify(failListener).onFailure(GENERIC_ERROR); verify(successListener).onFailure(GENERIC_ERROR); } /** * Test prefetchDeltaSignerInfo for failing requests where a previous request on a different * domain has already succeeded. The failing request should also appear to succeed. */ public void test_prefetchDeltaSignerInfo5() throws Exception { // This would fail if the next (immediate) request didn't succeed SignerInfoPrefetchResultListener failListener = mock(SignerInfoPrefetchResultListener.class); manager.prefetchDeltaSignerInfo(getSlowFailingProvider(ticker, EASY_TICKS), getRealSignerId(), getFakeWaveletName(CertificateManagerImplTest.DOMAIN), getHashedVersion(), failListener); verifyZeroInteractions(failListener); // This will succeed immediately SignerInfoPrefetchResultListener successListener = mock(SignerInfoPrefetchResultListener.class); manager.prefetchDeltaSignerInfo(getSuccessfulProvider(), getRealSignerId(), getFakeWaveletName(OTHER_DOMAIN), getHashedVersion(), successListener); verify(successListener).onSuccess(Certificates.getRealSignerInfo().toProtoBuf()); verify(failListener).onSuccess(Certificates.getRealSignerInfo().toProtoBuf()); // The failing listener shouldn't do anything, even after the ticks ticker.tick(EASY_TICKS); verifyNoMoreInteractions(failListener); verifyNoMoreInteractions(successListener); } /** * Test prefetchDeltaSignerInfo for failing requests -- even though the first request fails, * the second request on a different domain should succeed. */ public void test_prefetchDeltaSignerInfo6() throws Exception { // This will fail later SignerInfoPrefetchResultListener failListener = mock(SignerInfoPrefetchResultListener.class); manager.prefetchDeltaSignerInfo(getSlowFailingProvider(ticker, EASY_TICKS), getRealSignerId(), getFakeWaveletName(CertificateManagerImplTest.DOMAIN), getHashedVersion(), failListener); verifyZeroInteractions(failListener); // This will succeed later, after the failing one fails SignerInfoPrefetchResultListener successListener = mock(SignerInfoPrefetchResultListener.class); manager.prefetchDeltaSignerInfo(getSlowSuccessfulProvider(ticker, EASY_TICKS * 2), getRealSignerId(), getFakeWaveletName(OTHER_DOMAIN), getHashedVersion(), successListener); verifyZeroInteractions(successListener); // The failing request should fail, but successful request left alone ticker.tick(EASY_TICKS); verifyZeroInteractions(successListener); verify(failListener).onFailure(GENERIC_ERROR); // The successful request should now succeed ticker.tick(EASY_TICKS); verify(successListener).onSuccess(Certificates.getRealSignerInfo().toProtoBuf()); verifyNoMoreInteractions(failListener); } /* * UTILITIES */ private HashedVersion getHashedVersion() { return HashedVersion.unsigned(3L); } private ProtocolHashedVersion getProtocolHashedVersion() { return CoreWaveletOperationSerializer.serialize(getHashedVersion()); } private WaveSignatureVerifier getRealVerifier(CertPathStore store) throws Exception { TrustRootsProvider trustRoots = new DefaultTrustRootsProvider(); VerifiedCertChainCache cache = new DefaultCacheImpl(getFakeTimeSource()); WaveCertPathValidator validator = new CachedCertPathValidator( cache, getFakeTimeSource(), trustRoots); return new WaveSignatureVerifier(validator, store); } private WaveSignatureVerifier getVerifier(CertPathStore store, boolean disableSignerVerification) { VerifiedCertChainCache cache = new DefaultCacheImpl(getFakeTimeSource()); WaveCertPathValidator validator; if (disableSignerVerification) { validator = new DisabledCertPathValidator(); } else { validator = new CachedCertPathValidator( cache, getFakeTimeSource(), getTrustRootsProvider()); } return new WaveSignatureVerifier(validator, store); } private TrustRootsProvider getTrustRootsProvider() { return new TrustRootsProvider() { @Override public Collection<X509Certificate> getTrustRoots() { try { return getSigner().getSignerInfo().getCertificates(); } catch (Exception e) { throw new RuntimeException(e); } } }; } private SigningSignatureHandler getSigner() throws Exception { InputStream keyStream = new ByteArrayInputStream(Certificates.EXAMPLE_PRIVATE_KEY.getBytes()); InputStream certStream = new ByteArrayInputStream(Certificates.EXAMPLE_CERTIFICATE.getBytes()); List<InputStream> certStreams = ImmutableList.of(certStream); WaveSignerFactory factory = new WaveSignerFactory(); WaveSigner signer = factory.getSigner(keyStream, certStreams, CertificateManagerImplTest.DOMAIN); return new SigningSignatureHandler(signer); } private SignerInfo getSignerInfo() throws Exception { return getSigner().getSignerInfo(); } private TimeSource getFakeTimeSource() { return new TimeSource() { @Override public Date now() { return new Date(currentTimeMillis()); } @Override public long currentTimeMillis() { Calendar cal = Calendar.getInstance(TimeZone.getTimeZone("GMT")); cal.set(2009, 11, 1); return cal.getTimeInMillis(); } }; } private ProtocolSignedDelta getFakeSignedDelta() throws Exception { return ProtocolSignedDelta.newBuilder() .setDelta(getFakeDelta().getByteString()) .addSignature(getRealSignature()) .build(); } private ByteStringMessage<ProtocolWaveletDelta> getFakeDelta() throws Exception { ProtocolWaveletDelta delta = ProtocolWaveletDelta.newBuilder() .setHashedVersion(getProtocolHashedVersion()) .setAuthor("bob@initech-corp.com") .build(); return ByteStringMessage.serializeMessage(delta); } private ProtocolSignature getRealSignature() throws Exception { return ProtocolSignature.newBuilder() .setSignerId(ByteString.copyFrom(Certificates.getRealSignerInfo().getSignerId())) .setSignatureAlgorithm(SignatureAlgorithm.SHA1_RSA) .setSignatureBytes(ByteString.copyFrom(Certificates.REAL_SIGNATURE)) .build(); } private WaveletName getFakeWaveletName(String domain) { return WaveletName.of(WaveId.of(domain, "wave"), WaveletId.of(domain, "wavelet")); } private ByteString getRealSignerId() throws Exception { return ByteString.copyFrom(Certificates.getRealSignerInfo().getSignerId()); } /* * Fake WaveletFederationProviders. */ private abstract class WaveletSignerInfoProvider implements WaveletFederationProvider { @Override public void postSignerInfo(String destinationDomain, ProtocolSignerInfo signerInfo, PostSignerInfoResponseListener listener) { throw new UnsupportedOperationException(); } @Override public void requestHistory(WaveletName waveletName, String domain, ProtocolHashedVersion startVersion, ProtocolHashedVersion endVersion, long lengthLimit, HistoryResponseListener listener) { throw new UnsupportedOperationException(); } @Override public void submitRequest(WaveletName waveletName, ProtocolSignedDelta delta, SubmitResultListener listener) { throw new UnsupportedOperationException(); } } private WaveletFederationProvider getSuccessfulProvider() { return new WaveletSignerInfoProvider() { @Override public void getDeltaSignerInfo(ByteString signerId, WaveletName waveletName, ProtocolHashedVersion deltaEndVersion, DeltaSignerInfoResponseListener listener) { try { listener.onSuccess(Certificates.getRealSignerInfo().toProtoBuf()); } catch (Exception e) { throw new RuntimeException(e); } } }; } private WaveletFederationProvider getSlowSuccessfulProvider(final Ticker ticker, final int ticks) { return new WaveletSignerInfoProvider() { @Override public void getDeltaSignerInfo(ByteString signerId, WaveletName waveletName, ProtocolHashedVersion deltaEndVersion, final DeltaSignerInfoResponseListener listener) { ticker.runAt(ticks, new Runnable() { @Override public void run() { try { listener.onSuccess(Certificates.getRealSignerInfo().toProtoBuf()); } catch (Exception e) { throw new RuntimeException(e); } } }); } }; } private WaveletFederationProvider getSlowFailingProvider(final Ticker ticker, final int ticks) { return new WaveletSignerInfoProvider() { @Override public void getDeltaSignerInfo(ByteString signerId, WaveletName waveletName, ProtocolHashedVersion deltaEndVersion, final DeltaSignerInfoResponseListener listener) { ticker.runAt(ticks, new Runnable() { @Override public void run() { listener.onFailure(GENERIC_ERROR); } }); } }; } private WaveletFederationProvider getDeadProvider() { return new WaveletSignerInfoProvider() { @Override public void getDeltaSignerInfo(ByteString signerId, WaveletName waveletName, ProtocolHashedVersion deltaEndVersion, DeltaSignerInfoResponseListener listener) { // Never calls the callback } }; } }