diff --git a/java/google/registry/keyring/api/BUILD b/java/google/registry/keyring/api/BUILD index 56fa9c4d9..315e945c7 100644 --- a/java/google/registry/keyring/api/BUILD +++ b/java/google/registry/keyring/api/BUILD @@ -9,6 +9,7 @@ java_library( srcs = glob(["*.java"]), resources = glob(["*.asc"]), deps = [ + "//java/google/registry/util", "@com_google_code_findbugs_jsr305", "@com_google_dagger", "@com_google_guava", diff --git a/java/google/registry/keyring/api/ComparatorKeyring.java b/java/google/registry/keyring/api/ComparatorKeyring.java new file mode 100644 index 000000000..8b28ad55a --- /dev/null +++ b/java/google/registry/keyring/api/ComparatorKeyring.java @@ -0,0 +1,198 @@ +// Copyright 2017 The Nomulus Authors. All Rights Reserved. +// +// 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 google.registry.keyring.api; + +import com.google.common.annotations.VisibleForTesting; +import com.google.common.base.MoreObjects; + +import google.registry.util.ComparingInvocationHandler; +import google.registry.util.FormattingLogger; + +import java.io.IOException; +import java.lang.reflect.Method; +import java.util.Arrays; +import java.util.Objects; + +import javax.annotation.Nullable; + +import org.bouncycastle.bcpg.BCPGKey; +import org.bouncycastle.bcpg.PublicKeyPacket; +import org.bouncycastle.openpgp.PGPKeyPair; +import org.bouncycastle.openpgp.PGPPrivateKey; +import org.bouncycastle.openpgp.PGPPublicKey; + +/** + * Checks that a second keyring returns the same result as the current one. + * + *

Will behave exactly like the "actualKeyring" - as in will throw / return the exact same values + * - no matter what the "secondKeyring" does. But will log a warning if "secondKeyring" acts + * differently than "actualKeyring". + * + *

If both keyrings threw exceptions, there is no check whether the exeptions are the same. The + * assumption is that an error happened in both, but they might report that error differently. + */ +final class ComparatorKeyring extends ComparingInvocationHandler { + + @VisibleForTesting + static final FormattingLogger logger = FormattingLogger.getLoggerForCallerClass(); + + private ComparatorKeyring(Keyring original, Keyring second) { + super(Keyring.class, original, second); + } + + /** + * Returns an instance of Keyring that is an exact proxy of "original". + * + *

This proxy will log any differences in return value or thrown exceptions with "second". + */ + public static Keyring create(Keyring original, Keyring second) { + return new ComparatorKeyring(original, second).makeProxy(); + } + + @Override + protected void log(Method method, String message) { + logger.severefmt("ComparatorKeyring.%s: %s", method.getName(), message); + } + + /** Implements equals for the PGP classes. */ + @Override + protected boolean compareResults(Method method, @Nullable Object a, @Nullable Object b) { + Class clazz = method.getReturnType(); + if (PGPPublicKey.class.equals(clazz)) { + return compare((PGPPublicKey) a, (PGPPublicKey) b); + } + if (PGPPrivateKey.class.equals(clazz)) { + return compare((PGPPrivateKey) a, (PGPPrivateKey) b); + } + if (PGPKeyPair.class.equals(clazz)) { + return compare((PGPKeyPair) a, (PGPKeyPair) b); + } + return super.compareResults(method, a, b); + } + + /** Implements toString for the PGP classes. */ + @Override + protected String stringifyResult(Method method, @Nullable Object a) { + Class clazz = method.getReturnType(); + if (PGPPublicKey.class.equals(clazz)) { + return stringify((PGPPublicKey) a); + } + if (PGPPrivateKey.class.equals(clazz)) { + return stringify((PGPPrivateKey) a); + } + if (PGPKeyPair.class.equals(clazz)) { + return stringify((PGPKeyPair) a); + } + return super.stringifyResult(method, a); + } + + // .equals implementation for PGP types. + + @VisibleForTesting + static boolean compare(@Nullable PGPKeyPair a, @Nullable PGPKeyPair b) { + if (a == null || b == null) { + return a == null && b == null; + } + return compare(a.getPublicKey(), b.getPublicKey()) + && compare(a.getPrivateKey(), b.getPrivateKey()); + } + + @VisibleForTesting + static boolean compare(@Nullable PGPPublicKey a, @Nullable PGPPublicKey b) { + if (a == null || b == null) { + return a == null && b == null; + } + try { + return Arrays.equals(a.getFingerprint(), b.getFingerprint()) + && Arrays.equals(a.getEncoded(), b.getEncoded()); + } catch (IOException e) { + logger.severefmt("ComparatorKeyring error: PGPPublicKey.getEncoded failed: %s", e); + return false; + } + } + + @VisibleForTesting + static boolean compare(@Nullable PGPPrivateKey a, @Nullable PGPPrivateKey b) { + if (a == null || b == null) { + return a == null && b == null; + } + return a.getKeyID() == b.getKeyID() + && compare(a.getPrivateKeyDataPacket(), b.getPrivateKeyDataPacket()) + && compare(a.getPublicKeyPacket(), b.getPublicKeyPacket()); + } + + @VisibleForTesting + static boolean compare(PublicKeyPacket a, PublicKeyPacket b) { + if (a == null || b == null) { + return a == null && b == null; + } + try { + return Arrays.equals(a.getEncoded(), b.getEncoded()); + } catch (IOException e) { + logger.severefmt("ComparatorKeyring error: PublicKeyPacket.getEncoded failed: %s", e); + return false; + } + } + + @VisibleForTesting + static boolean compare(BCPGKey a, BCPGKey b) { + if (a == null || b == null) { + return a == null && b == null; + } + return Objects.equals(a.getFormat(), b.getFormat()) + && Arrays.equals(a.getEncoded(), b.getEncoded()); + } + + // toString implementations + + @VisibleForTesting + static String stringify(PGPKeyPair a) { + if (a == null) { + return "null"; + } + return MoreObjects.toStringHelper(PGPKeyPair.class) + .addValue(stringify(a.getPublicKey())) + .addValue(stringify(a.getPrivateKey())) + .toString(); + } + + @VisibleForTesting + static String stringify(PGPPublicKey a) { + if (a == null) { + return "null"; + } + + StringBuilder builder = new StringBuilder(""); + for (byte b : a.getFingerprint()) { + builder.append(String.format("%02x:", b)); + } + return MoreObjects.toStringHelper(PGPPublicKey.class) + .add("fingerprint", builder.toString()) + .toString(); + } + + @VisibleForTesting + static String stringify(PGPPrivateKey a) { + if (a == null) { + return "null"; + } + + // We need to be careful what information we output here. The private key should be private, and + // I'm not sure what is safe to put in the logs. + return MoreObjects.toStringHelper(PGPPrivateKey.class) + .add("keyId", a.getKeyID()) + .toString(); + } +}