Skip to content

Defining Keyring interface, RawAesKeyring and RawRsaKeyring. #142

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

Merged
merged 9 commits into from
Nov 21, 2019

Conversation

WesleyRosenblum
Copy link
Contributor

Issue #, if available: #102

Description of changes:

This change defines the Keyring interface, RawAesKeyring and RawRsaKeyring.

By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license.

Check any applicable:

  • Were any files moved? Moving files changes their URL, which breaks all hyperlinks to the files.

*Issue #, if available:* #102

*Description of changes:*

This change defines the Keyring interface, RawAesKeyring and RawRsaKeyring.

By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license.

# Check any applicable:
- [ ] Were any files moved? Moving files changes their URL, which breaks all hyperlinks to the files.
@WesleyRosenblum
Copy link
Contributor Author

Some debate on the visibility of RawAesKeyring and RawRsaKeyring is contained here: 4bbe029#r35839832

* @param wrappingKey The AES key input to AES-GCM to encrypt plaintext data keys.
* @return The {@link Keyring}
*/
static Keyring rawAes(String keyNamespace, String keyName, SecretKey wrappingKey) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As noted in other discussion, please move factory methods off of this interface.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll move these to a StandardKeyrings class instead

keyringTrace.add(keyNamespace, keyName,
KeyringTraceFlag.ENCRYPTED_DATA_KEY,
KeyringTraceFlag.SIGNED_ENCRYPTION_CONTEXT,
KeyringTraceFlag.VERIFIED_ENCRYPTION_CONTEXT);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is "VERIFIED" set on Encrypt?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I misunderstood this issue, I'll fix it

void traceOnDecrypt(KeyringTrace keyringTrace) {
keyringTrace.add(keyNamespace, keyName,
KeyringTraceFlag.DECRYPTED_DATA_KEY,
KeyringTraceFlag.SIGNED_ENCRYPTION_CONTEXT,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is "SIGNED" set on decrypt?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as above

import static org.junit.Assert.assertTrue;

@RunWith(MockitoJUnitRunner.class)
public class RawAesKeyringTest {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please add explicit test for the generate case

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added

import static org.mockito.Mockito.when;

@RunWith(MockitoJUnitRunner.class)
public class RawKeyringTest {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I admit this is me not fully trusting mocks, but I'd like to see at least some of these tests duplicated with either (or both) of the AES/RSA keyring tests. Those could extend an abstract base test case which implements most of the tests in a generic way.

The problem is that mocks work exactly how we expect/tell them to rather than how reality might. It's important to know that the logic below works when actually implemented in addition to at the abstract level when mocked out.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've added some tests to the RawAesKeyringTest and RawRsaKeyringTest that work without mocks

SalusaSecondus
SalusaSecondus previously approved these changes Nov 12, 2019
Copy link
Contributor

@SalusaSecondus SalusaSecondus left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM. Please be sure to get a second review by one of our keyring experts.

Copy link
Contributor

@seebees seebees left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some quick feedback to start us off :)

}

public Builder setDataKey(DataKey<?> dataKey) {
this.dataKey = dataKey;
public Builder setCleartextDataKey(SecretKey cleartextDataKey) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the other implementations call this the 'unencrypted data key' and the spec uses plaintext. I would pick one of these :)

At this point, the materials should verify that the data key conforms to algorithm specification.
The encrypt materials make sure that this can not be set more than once.
I would do that here as well.

These set methods generally also build the Keyring Trace.
This makes it harder to return materials with incorrect keyring traces.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the other implementations call this the 'unencrypted data key' and the spec uses plaintext. I would pick one of these :)

I'll go with plaintextDataKey to match the spec

At this point, the materials should verify that the data key conforms to algorithm specification.

What do you mean by "At this point"? During the construction of DecryptionMaterials? The plaintextDataKey would be null at this point, it is set in onDecrypt in the Keyring, which uses the Algorithm in the DecryptionMaterials to create the plaintextDataKey:

decryptionMaterials.setCleartextDataKey(
                            new SecretKeySpec(decryptedKey, decryptionMaterials.getAlgorithm().getDataKeyAlgo()));
                    traceOnDecrypt(decryptionMaterials.getKeyringTrace());

The encrypt materials make sure that this can not be set more than once.
I would do that here as well.

This is just the builder, the actual setter for setCleartextDataKey throws an IllegalStateException if it has already been set.

These set methods generally also build the Keyring Trace.
This makes it harder to return materials with incorrect keyring traces.

I went back and forth on that, I wasn't happy with how it turned out when I did it that way, but let me see what I can do

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+1 on using plaintextDataKey.

The plaintextDataKey would be null at this point, it is set in onDecrypt in the Keyring, which uses the Algorithm in the DecryptionMaterials to create the plaintextDataKey:

I think that we want to ensure that anyone who builds DecryptionMaterials, if they set a plaintextDataKey, can only successfully do so if the plaintextDataKey being set matches the algorithmSuite already set in the decryptionMaterials. i.e. let's make it harder for custom keyrings to create and return a set of materials which are not valid.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed, I'm adding validation to DecryptionMaterials and EncryptionMaterials in both the setPlaintextDataKey and in the Builder.build methods so that it will be impossible to construct an invalid DecryptionMaterials/EncryptionMaterials

Copy link
Contributor Author

@WesleyRosenblum WesleyRosenblum Nov 13, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually, after discussing with @mattsb42-aws, I'm going to hold off on renaming cleartextDataKey, since that will break existing clients that have written their own CryptoMaterialsManager. Also I will only add the validation to the new setCleartextDataKey methods so that existing clients using the builder will not be broken with new validation requirements

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Discussed with @seebees, I'm going to try defining new versions of DecryptionMaterials and EncryptionMaterials for exclusive use in Keyrings

@@ -75,6 +75,14 @@ public SecretKey getCleartextDataKey() {
return cleartextDataKey;
}

public void setCleartextDataKey(SecretKey cleartextDataKey) {
if(this.cleartextDataKey != null) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would validate the length, similar to the decrypt comment above.

}
}

LOGGER.warning("Could not decrypt any data keys");
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the case that onDecrypt gets an empty EDK list the correct behavior is to return. We should not log this warning in that case.

I am also unsure if we want to log a warning here generally. What is the current behavior with the MK/MKP setup?

Copy link
Contributor Author

@WesleyRosenblum WesleyRosenblum Nov 13, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the case that onDecrypt gets an empty EDK list the correct behavior is to return. We should not log this warning in that case.

Good catch

I am also unsure if we want to log a warning here generally. What is the current behavior with the MK/MKP setup?

The Java ESDK basically has no logging anywhere. The current behavior with MK/MKP is to throw a CannotUnwrapDataKeyException if no key can be decrypted, but exception throwing is not specified in the spec so I didn't replicate that.


keyring.onEncrypt(encryptionMaterials);

assertEquals(1, encryptionMaterials.getEncryptedDataKeys().size());
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

RawAES/RSAKeyrings have different behavior for creating edks (specifically how the AES Keyring serializes the provider Info), so I think we should explicitly test that behavior in the RawAES/RSAKeyring tests by checking the fields of the returned EDKs.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've added tests that check that the provider ID is as expected and the provider Information has the keyname as a prefix. I don't want to validate beyond that though, since the JceKeyCipher is separately tested and I want that to remain independent.

Comment on lines +69 to +71
assertEquals(2, encryptionMaterials.getKeyringTrace().getEntries().get(0).getFlags().size());
assertTrue(encryptionMaterials.getKeyringTrace().getEntries().get(0).getFlags().contains(KeyringTraceFlag.ENCRYPTED_DATA_KEY));
assertTrue(encryptionMaterials.getKeyringTrace().getEntries().get(0).getFlags().contains(KeyringTraceFlag.SIGNED_ENCRYPTION_CONTEXT));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This might be overkill, but we can also check that this is the only trace, and that it does not include the "GENERATED" flag.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I do already check that the keyring trace has only 1 entry, and that keyring trace has only 2 flags and those flags are ENCRYPTED_DATA_KEY and SIGNED_ENCRYPTION_CONTEXT

Comment on lines 161 to 162
@Test
public void testDecryptNoValidDataKey() {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The behavior is the same, but I would also add a test for when given an empty EDK list.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added

}

public Builder setDataKey(DataKey<?> dataKey) {
this.dataKey = dataKey;
public Builder setCleartextDataKey(SecretKey cleartextDataKey) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+1 on using plaintextDataKey.

The plaintextDataKey would be null at this point, it is set in onDecrypt in the Keyring, which uses the Algorithm in the DecryptionMaterials to create the plaintextDataKey:

I think that we want to ensure that anyone who builds DecryptionMaterials, if they set a plaintextDataKey, can only successfully do so if the plaintextDataKey being set matches the algorithmSuite already set in the decryptionMaterials. i.e. let's make it harder for custom keyrings to create and return a set of materials which are not valid.

To maintain backward compatibility with MasterKey/MasterKeyProviders,
new EncryptionMaterials and DecryptionMaterials classes are defined for
use in Keyrings, so they can include names inline with the spec and
additional validation. This change also adds test dependencies for
JUnit5.
if (o == null || getClass() != o.getClass()) return false;
DecryptionMaterials that = (DecryptionMaterials) o;
return algorithm == that.algorithm &&
Objects.equals(plaintextDataKey, that.plaintextDataKey) &&
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this constant time? In the JS ESDK, we touch the plaintextDataKey in a similar way. And it is also true that given the interface it does not really matter. However, we still went to the trouble of making the function constant time to avoid cognitive load on security engineers.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SecretKeySpec relies on MessageDigest.isEqual for byte array comparison, which has been timing safe since Java 6 Update 17: https://www.oracle.com/technetwork/java/javase/6u17-141447.html

}

public PublicKey getVerificationKey() {
return verificationKey;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If verificationKey is null because the algorithm is not signed should this throw an error or just return null?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Throwing an error would likely reduce incorrect usage of this class and EncryptionMaterials, so I've gone ahead and made the optional plaintextDataKey, verificationKey, and signingKey throw IllegalStateException if they are not populated. I've also added has* methods so that these properties can be checked if they are populated.

}

public SecretKey getPlaintextDataKey() {
return plaintextDataKey;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the plaintextDataKey is not yet set, is it an error to request it? Or is it just null?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Throws an IllegalStateException now

notNull(encryptedDataKey, "encryptedDataKey is required");
notNull(keyringTraceEntry, "keyringTraceEntry is required");
encryptedDataKeys.add(encryptedDataKey);
keyringTrace.add(keyringTraceEntry);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that there should be some verification that the keyring trace is valid . i.e. the flag DECRYPTED_DATA_KEY is not valid when adding an encrypted data key :(

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would prefer to leave that out of these classes. A user could make their own keyring implementations and decide they don't care about keyring traces, so they shouldn't need to populate it with some flags they don't necessarily care about.

* A data key to be used as input for encryption.
*/
public SecretKey getPlaintextDataKey() {
return plaintextDataKey;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This may not be as important for decrypt, but trying to get the plaintext data key before it is set is really bad.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Throws an IllegalStateException now

import java.util.Objects;

import static org.apache.commons.lang3.Validate.isTrue;
import static org.apache.commons.lang3.Validate.notNull;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not java.lang.Objects.requireNonNull?

(Asked with full awareness that this is probably the idiom for the library since before Java 1.7 :)

Copy link
Contributor Author

@WesleyRosenblum WesleyRosenblum Nov 21, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've been using Validate.notNull since forever so I hadn't seen requireNonNull before :-) I'll switch it.

Actually I'd like to start using Lombok at some point to handle this stuff with annotations, thoughts on that?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've been very wary about using Lombok in our open source projects, since it introduces another dependency. There are known interaction issues between Lombok and Kotlin too, and given the choice I'd prefer to support Kotlin better. :)

this.keyringTrace = result.keyringTrace;
}

public Builder setAlgorithmSuite(CryptoAlgorithm algorithmSuite) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The Java SDK v2 style in these builders is to omit the set, so this should just be algorithmSuite.

See for example: https://docs.aws.amazon.com/sdk-for-java/v2/developer-guide/examples-sqs-messages.html

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's a good example to follow, I'll make the change

public interface Keyring {

/**
* Generate a data key if not present and encrypt it using any available wrapping key
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The phrase "any available wrapping key" isn't particularly meaningful for me there, especially because some keyrings will encrypt with multiple keys.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changed it to be more general

* decryption of data keys using the provided wrapping key.
*
* @param keyNamespace A UTF-8 encoded value that, together with the key name, identifies the wrapping key.
* @param keyName A UTF-8 encoded value that, together with the key namespace, identifies the wrapping key.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit-pick: these are Java Strings, which means they are internally represented in UTF-16. And that's not really relevant anyway because the underlying JceKeyCipher class will call keyName.getBytes(KEY_NAME_ENCODING) (where KEY_NAME_ENCODING == "UTF-8") and pass on the keyNamespace as a String. "A UTF-8 encoded value" really only makes sense if you're describing a byte[].

I do realize the JceKeyCipher class has the same misleading comments, so perhaps you can punt fixing this for a separate PR for consistency.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah I took that from the RawAesKeyring spec. Should it change there too?

I'll fix it here for now

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

per updating the spec: I think that an argument can be made that using Java strings and then erroring if there is an issue converting the string to a UTF-8 encoded byte[] later is still following the behavior defined by the specification.

However, there is the issue that in the Java implementation it is possible to initialize keyrings which are unable to produce EDKs (at least for our "standard" keyrings). Is it considered being out of line with the spec to call something a "keyring" if it is unable to perform the behaviors of a keyring? Or is the fact that such a keyring only results in errors mean it's still "in line" with the spec (following the philosophy that: if you can't follow the spec behavior, you MUST fail). For now it is my opinion that we go with the latter approach. Thoughts?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Interesting point - the specification should probably say "Unicode string" instead of "UTF-8 encoded value". It would be silly to use byte arrays everywhere instead of the language-native String type.

/**
* Contains the cryptographic materials needed for a decryption operation with Keyrings.
*/
public final class DecryptionMaterials {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I worry a bit that having two Encryption/DecryptionMaterials classes in different packages might be a little confusing. The specification assumes they are the same data structure, and using the same name for different structures there would be REALLY confusing and/or verbose.

My specification PR (awslabs/aws-encryption-sdk-specification#56) deals with part of this issue by introducing a Data Key Materials structure that's used at this level.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll leave this for now and we can revisit it after getting some clarity on the spec. I originally had it as the same data structure but there were issues with backwards compatibility that made that undesirable.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I definitely prefer using two different data structures, but if you're going ahead with this I would also cut an issue to track the deviation from the spec - it won't be consistent with either the current version or my PR.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Contributor

@robin-aws robin-aws left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM! My only ask is that we track the deviation from the spec w.r.t. the two different *Materials classes.

@WesleyRosenblum WesleyRosenblum merged commit 36958b8 into keyring Nov 21, 2019
@WesleyRosenblum WesleyRosenblum deleted the rawkeyring branch November 21, 2019 21:57
@mattsb42-aws mattsb42-aws mentioned this pull request Dec 10, 2019
15 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants