blob: 48b6d624b93f94ec9f4f1cfc7c6249c2f48f7b2c [file] [log] [blame]
// Copyright 2017 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 com.google.crypto.tink.mac;
import static com.google.common.truth.Truth.assertThat;
import static com.google.crypto.tink.internal.Util.toBytesFromPrintableAscii;
import static java.nio.charset.StandardCharsets.UTF_8;
import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertThrows;
import com.google.crypto.tink.CryptoFormat;
import com.google.crypto.tink.Mac;
import com.google.crypto.tink.PrimitiveSet;
import com.google.crypto.tink.SecretKeyAccess;
import com.google.crypto.tink.internal.KeyParser;
import com.google.crypto.tink.internal.MutableMonitoringRegistry;
import com.google.crypto.tink.internal.MutableSerializationRegistry;
import com.google.crypto.tink.internal.ProtoKeySerialization;
import com.google.crypto.tink.internal.testing.FakeMonitoringClient;
import com.google.crypto.tink.monitoring.MonitoringAnnotations;
import com.google.crypto.tink.proto.KeyData;
import com.google.crypto.tink.proto.KeyData.KeyMaterialType;
import com.google.crypto.tink.proto.KeyStatusType;
import com.google.crypto.tink.proto.Keyset.Key;
import com.google.crypto.tink.proto.OutputPrefixType;
import com.google.crypto.tink.subtle.Bytes;
import com.google.crypto.tink.subtle.Random;
import com.google.crypto.tink.testing.TestUtil;
import com.google.protobuf.ByteString;
import java.security.GeneralSecurityException;
import java.util.Arrays;
import java.util.List;
import javax.annotation.Nullable;
import org.junit.BeforeClass;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
/** Tests for MacWrapper. */
@RunWith(JUnit4.class)
public class MacWrapperTest {
private static final int HMAC_KEY_SIZE = 20;
@BeforeClass
public static void setUp() throws Exception {
MacConfig.register();
}
@Test
public void testMultipleKeysWithoutAnnotation() throws Exception {
FakeMonitoringClient fakeMonitoringClient = new FakeMonitoringClient();
MutableMonitoringRegistry.globalInstance().clear();
MutableMonitoringRegistry.globalInstance().registerMonitoringClient(fakeMonitoringClient);
byte[] keyValue = Random.randBytes(HMAC_KEY_SIZE);
Key tink = TestUtil.createKey(
TestUtil.createHmacKeyData(keyValue, 16),
42,
KeyStatusType.ENABLED,
OutputPrefixType.TINK);
Key legacy = TestUtil.createKey(
TestUtil.createHmacKeyData(keyValue, 16),
43,
KeyStatusType.ENABLED,
OutputPrefixType.LEGACY);
Key raw = TestUtil.createKey(
TestUtil.createHmacKeyData(keyValue, 16),
44,
KeyStatusType.ENABLED,
OutputPrefixType.RAW);
Key crunchy = TestUtil.createKey(
TestUtil.createHmacKeyData(keyValue, 16),
45,
KeyStatusType.ENABLED,
OutputPrefixType.CRUNCHY);
Key[] keys = new Key[] {tink, legacy, raw, crunchy};
int j = keys.length;
for (int i = 0; i < j; i++) {
PrimitiveSet<Mac> primitives =
TestUtil.createPrimitiveSet(
TestUtil.createKeyset(
keys[i], keys[(i + 1) % j], keys[(i + 2) % j], keys[(i + 3) % j]),
Mac.class);
Mac mac = new MacWrapper().wrap(primitives);
byte[] plaintext = "plaintext".getBytes(UTF_8);
byte[] tag = mac.computeMac(plaintext);
if (!keys[i].getOutputPrefixType().equals(OutputPrefixType.RAW)) {
byte[] prefix = Arrays.copyOf(tag, CryptoFormat.NON_RAW_PREFIX_SIZE);
assertArrayEquals(prefix, CryptoFormat.getOutputPrefix(keys[i]));
}
try {
mac.verifyMac(tag, plaintext);
} catch (GeneralSecurityException e) {
throw new AssertionError("Valid MAC, should not throw exception: " + i, e);
}
// Modify plaintext or tag and make sure the verifyMac failed.
byte[] plaintextAndTag = Bytes.concat(plaintext, tag);
for (int b = 0; b < plaintextAndTag.length; b++) {
for (int bit = 0; bit < 8; bit++) {
byte[] modified = Arrays.copyOf(plaintextAndTag, plaintextAndTag.length);
modified[b] ^= (byte) (1 << bit);
assertThrows(
GeneralSecurityException.class,
() ->
mac.verifyMac(
Arrays.copyOfRange(modified, plaintext.length, modified.length),
Arrays.copyOf(modified, plaintext.length)));
}
}
// mac with a non-primary RAW key, verify with the keyset
PrimitiveSet<Mac> primitives2 =
TestUtil.createPrimitiveSet(TestUtil.createKeyset(raw, legacy, tink, crunchy), Mac.class);
Mac mac2 = new MacWrapper().wrap(primitives2);
tag = mac2.computeMac(plaintext);
try {
mac.verifyMac(tag, plaintext);
} catch (GeneralSecurityException e) {
throw new AssertionError("Valid MAC, should not throw exception", e);
}
// mac with a random key not in the keyset, verify with the keyset should fail
byte[] keyValue2 = Random.randBytes(HMAC_KEY_SIZE);
Key random = TestUtil.createKey(
TestUtil.createHmacKeyData(keyValue2, 16),
44,
KeyStatusType.ENABLED,
OutputPrefixType.TINK);
PrimitiveSet<Mac> primitives3 =
TestUtil.createPrimitiveSet(TestUtil.createKeyset(random), Mac.class);
mac2 = new MacWrapper().wrap(primitives3);
byte[] tag2 = mac2.computeMac(plaintext);
assertThrows(GeneralSecurityException.class, () -> mac.verifyMac(tag2, plaintext));
}
// Without annotations, nothing gets logged.
assertThat(fakeMonitoringClient.getLogEntries()).isEmpty();
assertThat(fakeMonitoringClient.getLogFailureEntries()).isEmpty();
}
@Test
public void testSmallPlaintextWithRawKey() throws Exception {
byte[] keyValue = Random.randBytes(HMAC_KEY_SIZE);
Key primary = TestUtil.createKey(
TestUtil.createHmacKeyData(keyValue, 16),
42,
KeyStatusType.ENABLED,
OutputPrefixType.RAW);
PrimitiveSet<Mac> primitives =
TestUtil.createPrimitiveSet(TestUtil.createKeyset(primary), Mac.class);
Mac mac = new MacWrapper().wrap(primitives);
byte[] plaintext = "blah".getBytes(UTF_8);
byte[] tag = mac.computeMac(plaintext);
// no prefix
assertThat(tag).hasLength(16);
try {
mac.verifyMac(tag, plaintext);
} catch (GeneralSecurityException e) {
throw new AssertionError("Valid MAC, should not throw exception", e);
}
}
@Test
public void testWithAnnotation_hasMonitoring() throws Exception {
FakeMonitoringClient fakeMonitoringClient = new FakeMonitoringClient();
MutableMonitoringRegistry.globalInstance().clear();
MutableMonitoringRegistry.globalInstance().registerMonitoringClient(fakeMonitoringClient);
Key tinkKey =
TestUtil.createKey(
TestUtil.createHmacKeyData(Random.randBytes(HMAC_KEY_SIZE), 16),
42,
KeyStatusType.ENABLED,
OutputPrefixType.TINK);
Key rawKey =
TestUtil.createKey(
TestUtil.createHmacKeyData(Random.randBytes(HMAC_KEY_SIZE), 16),
43,
KeyStatusType.ENABLED,
OutputPrefixType.RAW);
Key legacyKey =
TestUtil.createKey(
TestUtil.createHmacKeyData(Random.randBytes(HMAC_KEY_SIZE), 16),
44,
KeyStatusType.ENABLED,
OutputPrefixType.LEGACY);
MonitoringAnnotations annotations =
MonitoringAnnotations.newBuilder().add("annotation_name", "annotation_value").build();
Mac rawMac =
new MacWrapper()
.wrap(
TestUtil.createPrimitiveSetWithAnnotations(
TestUtil.createKeyset(rawKey), annotations, Mac.class));
Mac legacyMac =
new MacWrapper()
.wrap(
TestUtil.createPrimitiveSetWithAnnotations(
TestUtil.createKeyset(legacyKey), annotations, Mac.class));
Mac mac =
new MacWrapper()
.wrap(
TestUtil.createPrimitiveSetWithAnnotations(
TestUtil.createKeyset(tinkKey, rawKey, legacyKey), annotations, Mac.class));
byte[] plaintext = "plaintext".getBytes(UTF_8);
byte[] tinkTag = mac.computeMac(plaintext);
byte[] rawTag = rawMac.computeMac(plaintext);
byte[] legacyTag = legacyMac.computeMac(plaintext);
mac.verifyMac(tinkTag, plaintext);
mac.verifyMac(rawTag, plaintext);
mac.verifyMac(legacyTag, plaintext);
assertThrows(GeneralSecurityException.class, () -> mac.verifyMac(tinkTag, new byte[0]));
List<FakeMonitoringClient.LogEntry> logEntries = fakeMonitoringClient.getLogEntries();
assertThat(logEntries).hasSize(6);
FakeMonitoringClient.LogEntry tinkComputeEntry = logEntries.get(0);
assertThat(tinkComputeEntry.getKeyId()).isEqualTo(42);
assertThat(tinkComputeEntry.getPrimitive()).isEqualTo("mac");
assertThat(tinkComputeEntry.getApi()).isEqualTo("compute");
assertThat(tinkComputeEntry.getNumBytesAsInput()).isEqualTo(plaintext.length);
assertThat(tinkComputeEntry.getKeysetInfo().getAnnotations()).isEqualTo(annotations);
FakeMonitoringClient.LogEntry rawComputeEntry = logEntries.get(1);
assertThat(rawComputeEntry.getKeyId()).isEqualTo(43);
assertThat(rawComputeEntry.getPrimitive()).isEqualTo("mac");
assertThat(rawComputeEntry.getApi()).isEqualTo("compute");
assertThat(rawComputeEntry.getNumBytesAsInput()).isEqualTo(plaintext.length);
assertThat(rawComputeEntry.getKeysetInfo().getAnnotations()).isEqualTo(annotations);
FakeMonitoringClient.LogEntry legacyComputeEntry = logEntries.get(2);
assertThat(legacyComputeEntry.getKeyId()).isEqualTo(44);
assertThat(legacyComputeEntry.getPrimitive()).isEqualTo("mac");
assertThat(legacyComputeEntry.getApi()).isEqualTo("compute");
// legacy mac appends one byte to the input data, therefore the input length is one longer.
assertThat(legacyComputeEntry.getNumBytesAsInput()).isEqualTo(plaintext.length + 1);
assertThat(legacyComputeEntry.getKeysetInfo().getAnnotations()).isEqualTo(annotations);
FakeMonitoringClient.LogEntry tinkVerifyEntry = logEntries.get(3);
assertThat(tinkVerifyEntry.getKeyId()).isEqualTo(42);
assertThat(tinkVerifyEntry.getPrimitive()).isEqualTo("mac");
assertThat(tinkVerifyEntry.getApi()).isEqualTo("verify");
assertThat(tinkVerifyEntry.getNumBytesAsInput()).isEqualTo(plaintext.length);
assertThat(tinkVerifyEntry.getKeysetInfo().getAnnotations()).isEqualTo(annotations);
FakeMonitoringClient.LogEntry rawVerifyEntry = logEntries.get(4);
assertThat(rawVerifyEntry.getKeyId()).isEqualTo(43);
assertThat(rawVerifyEntry.getPrimitive()).isEqualTo("mac");
assertThat(rawVerifyEntry.getApi()).isEqualTo("verify");
assertThat(rawVerifyEntry.getNumBytesAsInput()).isEqualTo(plaintext.length);
assertThat(rawVerifyEntry.getKeysetInfo().getAnnotations()).isEqualTo(annotations);
FakeMonitoringClient.LogEntry legacyVerifyEntry = logEntries.get(5);
assertThat(legacyVerifyEntry.getKeyId()).isEqualTo(44);
assertThat(legacyVerifyEntry.getPrimitive()).isEqualTo("mac");
assertThat(legacyVerifyEntry.getApi()).isEqualTo("verify");
// legacy mac appends one byte to the input data, therefore the input length is one longer.
assertThat(legacyVerifyEntry.getNumBytesAsInput()).isEqualTo(plaintext.length + 1);
assertThat(legacyVerifyEntry.getKeysetInfo().getAnnotations()).isEqualTo(annotations);
List<FakeMonitoringClient.LogFailureEntry> failures =
fakeMonitoringClient.getLogFailureEntries();
assertThat(failures).hasSize(1);
FakeMonitoringClient.LogFailureEntry verifyFailure = failures.get(0);
assertThat(verifyFailure.getPrimitive()).isEqualTo("mac");
assertThat(verifyFailure.getApi()).isEqualTo("verify");
assertThat(verifyFailure.getKeysetInfo().getPrimaryKeyId()).isEqualTo(42);
assertThat(verifyFailure.getKeysetInfo().getAnnotations()).isEqualTo(annotations);
}
public static class AlwaysFailingMac implements Mac {
@Override
public byte[] computeMac(final byte[] data) throws GeneralSecurityException {
throw new GeneralSecurityException("fail");
}
@Override
public void verifyMac(final byte[] mac, final byte[] data) throws GeneralSecurityException {
throw new GeneralSecurityException("fail");
}
}
@Test
public void testAlwaysFailingWithAnnotation_hasMonitoring() throws Exception {
FakeMonitoringClient fakeMonitoringClient = new FakeMonitoringClient();
MutableMonitoringRegistry.globalInstance().clear();
MutableMonitoringRegistry.globalInstance().registerMonitoringClient(fakeMonitoringClient);
MonitoringAnnotations annotations =
MonitoringAnnotations.newBuilder().add("annotation_name", "annotation_value").build();
PrimitiveSet<Mac> primitives =
PrimitiveSet.newBuilder(Mac.class)
.setAnnotations(annotations)
.addPrimaryPrimitive(
new AlwaysFailingMac(),
TestUtil.createKey(
TestUtil.createHmacKeyData(Random.randBytes(HMAC_KEY_SIZE), 16),
42,
KeyStatusType.ENABLED,
OutputPrefixType.TINK))
.build();
Mac mac = new MacWrapper().wrap(primitives);
byte[] data = "some data".getBytes(UTF_8);
byte[] invalidTag = "an invalid tag".getBytes(UTF_8);
assertThrows(GeneralSecurityException.class, () -> mac.computeMac(data));
assertThrows(GeneralSecurityException.class, () -> mac.verifyMac(invalidTag, data));
// Test short tags, because there is a different code path for this.
byte[] shortInvalidTag = "t".getBytes(UTF_8);
assertThrows(GeneralSecurityException.class, () -> mac.verifyMac(shortInvalidTag, data));
assertThat(fakeMonitoringClient.getLogEntries()).isEmpty();
List<FakeMonitoringClient.LogFailureEntry> failures =
fakeMonitoringClient.getLogFailureEntries();
assertThat(failures).hasSize(3);
FakeMonitoringClient.LogFailureEntry compFailure = failures.get(0);
assertThat(compFailure.getPrimitive()).isEqualTo("mac");
assertThat(compFailure.getApi()).isEqualTo("compute");
assertThat(compFailure.getKeysetInfo().getPrimaryKeyId()).isEqualTo(42);
assertThat(compFailure.getKeysetInfo().getAnnotations()).isEqualTo(annotations);
FakeMonitoringClient.LogFailureEntry verifyFailure = failures.get(1);
assertThat(verifyFailure.getPrimitive()).isEqualTo("mac");
assertThat(verifyFailure.getApi()).isEqualTo("verify");
assertThat(verifyFailure.getKeysetInfo().getPrimaryKeyId()).isEqualTo(42);
assertThat(verifyFailure.getKeysetInfo().getAnnotations()).isEqualTo(annotations);
FakeMonitoringClient.LogFailureEntry verifyFailure2 = failures.get(2);
assertThat(verifyFailure2.getPrimitive()).isEqualTo("mac");
assertThat(verifyFailure2.getApi()).isEqualTo("verify");
assertThat(verifyFailure2.getKeysetInfo().getPrimaryKeyId()).isEqualTo(42);
assertThat(verifyFailure2.getKeysetInfo().getAnnotations()).isEqualTo(annotations);
}
// Returns a key whose output prefix is taken from
private static com.google.crypto.tink.Key parseKeyWithProgrammableOutputPrefix(
ProtoKeySerialization serialization, @Nullable SecretKeyAccess access) {
com.google.crypto.tink.util.Bytes outputPrefix =
com.google.crypto.tink.util.Bytes.copyFrom(serialization.getValue().toByteArray());
return new MacKey() {
@Override
public com.google.crypto.tink.util.Bytes getOutputPrefix() {
return outputPrefix;
}
@Override
public MacParameters getParameters() {
return new MacParameters() {
@Override
public boolean hasIdRequirement() {
throw new UnsupportedOperationException("Not needed in test");
}
};
}
@Override
public Integer getIdRequirementOrNull() {
throw new UnsupportedOperationException("Not needed in test");
}
@Override
public boolean equalsKey(com.google.crypto.tink.Key k) {
throw new UnsupportedOperationException("Not needed in test");
}
};
}
private static final String TYPE_URL_FOR_PROGRAMMABLE_KEY = "typeUrlForProgrammableKey";
@BeforeClass
public static void registerProgrammableOutputPrefixKey() throws Exception {
MutableSerializationRegistry.globalInstance()
.registerKeyParser(
KeyParser.create(
MacWrapperTest::parseKeyWithProgrammableOutputPrefix,
toBytesFromPrintableAscii(TYPE_URL_FOR_PROGRAMMABLE_KEY),
ProtoKeySerialization.class));
}
@Test
public void correctOutputPrefixInKey_creatingWorks() throws Exception {
Key key =
Key.newBuilder()
.setOutputPrefixType(OutputPrefixType.TINK)
.setKeyData(
KeyData.newBuilder()
.setTypeUrl(TYPE_URL_FOR_PROGRAMMABLE_KEY)
.setValue(ByteString.copyFrom(new byte[] {0x01, 0x01, 0x02, 0x03, 0x04}))
.setKeyMaterialType(KeyMaterialType.SYMMETRIC))
.setKeyId(0x01020304)
.setStatus(KeyStatusType.ENABLED)
.build();
// Wrapping would throw before any mac is computed, so we can use any Mac for the test.
PrimitiveSet<Mac> primitives =
PrimitiveSet.newBuilder(Mac.class).addPrimaryPrimitive(new AlwaysFailingMac(), key).build();
new MacWrapper().wrap(primitives);
}
@Test
public void wrongOutputPrefixInKey_creationFails() throws Exception {
Key key =
Key.newBuilder()
.setOutputPrefixType(OutputPrefixType.TINK)
.setKeyData(
KeyData.newBuilder()
.setTypeUrl(TYPE_URL_FOR_PROGRAMMABLE_KEY)
.setValue(ByteString.copyFrom(new byte[] {0x01, 0x01, 0x02, 0x03, 0x04}))
.setKeyMaterialType(KeyMaterialType.SYMMETRIC))
.setKeyId(0x01020305)
.setStatus(KeyStatusType.ENABLED)
.build();
// Wrapping throws before any mac is computed, so we can use an AlwaysFailingMac for the test.
PrimitiveSet<Mac> primitives =
PrimitiveSet.newBuilder(Mac.class).addPrimaryPrimitive(new AlwaysFailingMac(), key).build();
GeneralSecurityException e =
assertThrows(GeneralSecurityException.class, () -> new MacWrapper().wrap(primitives));
assertThat(e).hasMessageThat().contains("has wrong output prefix");
}
}