Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

chore: add more support for signatures #249

Merged
merged 2 commits into from
May 7, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -305,14 +305,54 @@ public void writeTo(@NonNull final MessageDigest digest, final int offset, final
digest.update(buffer, Math.toIntExact(start + offset), length);
}

/**
* Same as {@link #updateSignature(Signature, int, int)} with offset 0 and length equal to the length of this
* {@link Bytes} object.
*/
public void updateSignature(@NonNull final Signature signature) throws SignatureException {
signature.update(buffer, start, length);
}

/**
* A helper method for efficient copy of our data into a Signature without creating a defensive copy of the data.
* The implementation relies on a well-behaved Signature that doesn't modify the buffer data.
* The implementation relies on a well-behaved Signature that doesn't modify the buffer data. Calls the
* {@link Signature#update(byte[], int, int)} method with all the data in this {@link Bytes} object. This method
* should be used when the data in the buffer should be validated or signed.
*
* @param signature the Signature to copy into
* @param signature The Signature to update
* @param offset The offset from the start of this {@link Bytes} object to get the bytes from
* @param length The number of bytes to extract
* @throws SignatureException If the Signature instance throws this exception
*/
public void writeTo(@NonNull final Signature signature) throws SignatureException {
signature.update(buffer, start, length);
public void updateSignature(@NonNull final Signature signature, final int offset, final int length)
throws SignatureException {
validateOffsetLength(offset, length);
signature.update(buffer, calculateOffset(offset), length);
}

/**
* Same as {@link #verifySignature(Signature, int, int)} with offset 0 and length equal to the length of this
* {@link Bytes} object.
*/
public void verifySignature(@NonNull final Signature signature) throws SignatureException {
signature.verify(buffer, start, length);
}

/**
* A helper method for efficient copy of our data into a Signature without creating a defensive copy of the data.
* The implementation relies on a well-behaved Signature that doesn't modify the buffer data. Calls the
* {@link Signature#verify(byte[], int, int)} method with all the data in this {@link Bytes} object. This method
* should be used when the data in the buffer is a signature that should be verified.
*
* @param signature the Signature to use to verify
* @param offset The offset from the start of this {@link Bytes} object to get the bytes from
* @param length The number of bytes to extract
* @throws SignatureException If the Signature instance throws this exception
*/
public void verifySignature(@NonNull final Signature signature, final int offset, final int length)
throws SignatureException {
validateOffsetLength(offset, length);
signature.verify(buffer, calculateOffset(offset), length);
}

/**
Expand Down Expand Up @@ -593,6 +633,34 @@ private void validateOffset(final long offset) {
}
}

/**
* Validates whether the offset and length supplied to a method are within the bounds of the Bytes object.
*
* @param suppliedOffset the offset supplied
* @param suppliedLength the length supplied
*/
private void validateOffsetLength(final long suppliedOffset, final long suppliedLength) {
if (suppliedOffset < 0 || suppliedLength < 0) {
throw new IllegalArgumentException("Negative length or offset not allowed");
}
if (suppliedOffset + suppliedLength > length) {
throw new IndexOutOfBoundsException(
"The offset(%d) and length(%d) provided are out of bounds for this Bytes object, which has a length of %d"
.formatted(suppliedOffset, suppliedLength, length)
);
}
}

/**
* Calculates the offset from the start for the given supplied offset.
*
* @param suppliedOffset the offset supplied
* @return the calculated offset
*/
private int calculateOffset(final long suppliedOffset) {
return Math.toIntExact(start + suppliedOffset);
}

/** Sorts {@link Bytes} according to their byte values, lower valued bytes first.
* Bytes are compared using the passed in Byte Comparator
*/
Expand Down
Original file line number Diff line number Diff line change
@@ -1,45 +1,47 @@
package com.hedera.pbj.runtime.io.buffer;

import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;

import com.hedera.pbj.runtime.io.DataEncodingException;
import com.hedera.pbj.runtime.io.ReadableSequentialData;
import com.hedera.pbj.runtime.io.WritableSequentialData;
import com.hedera.pbj.runtime.io.stream.WritableStreamingData;
import edu.umd.cs.findbugs.annotations.NonNull;

import java.io.OutputStream;
import java.nio.ByteBuffer;
import java.security.InvalidKeyException;
import java.security.PublicKey;
import java.security.Signature;
import java.security.SignatureException;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.CsvSource;
import org.junit.jupiter.params.provider.MethodSource;

import java.io.BufferedOutputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.nio.BufferUnderflowException;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.security.InvalidKeyException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.PublicKey;
import java.security.Signature;
import java.security.SignatureException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.BiConsumer;
import java.util.function.Function;
import java.util.stream.Stream;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.CsvSource;
import org.junit.jupiter.params.provider.MethodSource;
import org.junit.jupiter.params.provider.ValueSource;
import org.mockito.Mockito;

import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.*;

final class BytesTest {

// ================================================================================================================
Expand Down Expand Up @@ -428,36 +430,125 @@ void writeToMessageDigestNo0OffsetPartial() throws NoSuchAlgorithmException {
assertArrayEquals(exp, res);
}

/**
* Mock a Signature object for testing.
*
* @return a mock Signature object
*/
private static Signature mockSignature() throws InvalidKeyException {
final Signature signature = Mockito.mock(Signature.class);
// Signature.update() throws unless we call initVerify() first
signature.initVerify(Mockito.mock(PublicKey.class));
Mockito.verify(signature).initVerify(Mockito.any(PublicKey.class));
return signature;
}

@Test
@DisplayName("Write to Signature")
void writeToSignature() throws SignatureException, InvalidKeyException {
@DisplayName("Update Signature")
void updateSignature() throws SignatureException, InvalidKeyException {
byte[] byteArray = {0, 1, 2, 3, 4, 5};
final Bytes bytes = Bytes.wrap(byteArray);

final Signature signature = Mockito.mock(Signature.class);
// Signature.writeTo() throws unless we call initVerify() first
signature.initVerify(Mockito.mock(PublicKey.class));
bytes.writeTo(signature);
Mockito.verify(signature).initVerify(Mockito.any(PublicKey.class));
final Signature signature = mockSignature();
bytes.updateSignature(signature);
Mockito.verify(signature).update(byteArray, 0, 6);
Mockito.verifyNoMoreInteractions(signature);
}

@Test
@DisplayName("Write to Signature no 0 Offset")
void writeToSignatureNo0Offset() throws InvalidKeyException, SignatureException {
@DisplayName("Update Signature no 0 Offset")
void updateSignatureNo0Offset() throws InvalidKeyException, SignatureException {
final byte[] byteArray = {9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 0, 1, 2, 3, 4, 5};
final Bytes bytes = Bytes.wrap(byteArray, 10, 6);

final Signature signature = Mockito.mock(Signature.class);
// Signature.writeTo() throws unless we call initVerify() first
signature.initVerify(Mockito.mock(PublicKey.class));
bytes.writeTo(signature);
Mockito.verify(signature).initVerify(Mockito.any(PublicKey.class));
final Signature signature = mockSignature();
bytes.updateSignature(signature);
Mockito.verify(signature).update(byteArray, 10, 6);
Mockito.verifyNoMoreInteractions(signature);
}

@Test
@DisplayName("Update Signature no 0 Offset, partial")
void updateSignatureNo0OffsetPartial() throws InvalidKeyException, SignatureException {
final byte[] byteArray = {9, 9, 9, 9, 9, 0, 1, 2, 3, 4, 5, 9, 9, 9, 9, 9};
final Bytes bytes = Bytes.wrap(byteArray, 5, 6);

final Signature signature = mockSignature();
bytes.updateSignature(signature, 1, 4);
Mockito.verify(signature).update(byteArray, 6, 4);
Mockito.verifyNoMoreInteractions(signature);
}

@Test
@DisplayName("Check updateSignature() index bounds")
void updateSignatureBoundsChecks() throws InvalidKeyException {
byte[] byteArray = {1, 2, 3, 4, 5};
final Bytes bytes = Bytes.wrap(byteArray);

final Signature signature = mockSignature();
assertThrows(IndexOutOfBoundsException.class, () -> bytes.updateSignature(signature, 3, 10));
assertThrows(IndexOutOfBoundsException.class, () -> bytes.updateSignature(signature, 0, 6));
assertThrows(IndexOutOfBoundsException.class, () -> bytes.updateSignature(signature, 1, 5));
assertThrows(IllegalArgumentException.class, () -> bytes.updateSignature(signature, 0, -5));
assertThrows(IllegalArgumentException.class, () -> bytes.updateSignature(signature, -1, 2));
Mockito.verifyNoMoreInteractions(signature);
assertDoesNotThrow(() -> bytes.updateSignature(signature, 0, 5));
assertDoesNotThrow(() -> bytes.updateSignature(signature, 5, 0));
}

@Test
@DisplayName("Verify Signature")
void verifySignature() throws SignatureException, InvalidKeyException {
byte[] byteArray = {0, 1, 2, 3, 4, 5};
final Bytes bytes = Bytes.wrap(byteArray);

final Signature signature = mockSignature();
bytes.verifySignature(signature);
Mockito.verify(signature).verify(byteArray, 0, 6);
Mockito.verifyNoMoreInteractions(signature);
}

@Test
@DisplayName("Verify Signature no 0 Offset")
void verifySignatureNo0Offset() throws InvalidKeyException, SignatureException {
final byte[] byteArray = {9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 0, 1, 2, 3, 4, 5};
final Bytes bytes = Bytes.wrap(byteArray, 10, 6);

final Signature signature = mockSignature();
bytes.verifySignature(signature);
Mockito.verify(signature).verify(byteArray, 10, 6);
Mockito.verifyNoMoreInteractions(signature);
}

@Test
@DisplayName("Verify Signature no 0 Offset, partial")
void verifySignatureNo0OffsetPartial() throws InvalidKeyException, SignatureException {
final byte[] byteArray = {9, 9, 9, 9, 9, 0, 1, 2, 3, 4, 5, 9, 9, 9, 9, 9};
final Bytes bytes = Bytes.wrap(byteArray, 5, 6);

final Signature signature = mockSignature();
bytes.verifySignature(signature, 1, 4);
Mockito.verify(signature).verify(byteArray, 6, 4);
Mockito.verifyNoMoreInteractions(signature);
}

@Test
@DisplayName("Check verifySignature() index bounds")
void verifySignatureBoundsChecks() throws InvalidKeyException {
byte[] byteArray = {1, 2, 3, 4, 5};
final Bytes bytes = Bytes.wrap(byteArray);

final Signature signature = mockSignature();
assertThrows(IndexOutOfBoundsException.class, () -> bytes.verifySignature(signature, 3, 10));
assertThrows(IndexOutOfBoundsException.class, () -> bytes.verifySignature(signature, 0, 6));
assertThrows(IndexOutOfBoundsException.class, () -> bytes.verifySignature(signature, 1, 5));
assertThrows(IllegalArgumentException.class, () -> bytes.verifySignature(signature, 0, -5));
assertThrows(IllegalArgumentException.class, () -> bytes.verifySignature(signature, -1, 2));
Mockito.verifyNoMoreInteractions(signature);
assertDoesNotThrow(() -> bytes.verifySignature(signature, 0, 5));
assertDoesNotThrow(() -> bytes.verifySignature(signature, 5, 0));
}

// asUtf8String throws with null (no offset here? That's wierd. Should have offset, or we should have non-offset
// versions of everything else Or at least "getBytes").

Expand Down