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

Use readFully() to read exactly the number of bytes we expect from the Stream #28515

Merged
merged 16 commits into from
May 4, 2018

Conversation

jkakavas
Copy link
Member

@jkakavas jkakavas commented Feb 5, 2018

Instead of using read() and checking that the bytes read are what we expect,
use readFully() which will read exactly the number of bytes
while keep reading until the end of the stream or throw an
EOFException if not all bytes can be read.

This approach keeps the simplicity of using CipherInputStream while
working as expected with all Security Providers (SunJCE, BouncyCastle, BouncyCastleFips )

Calls explicitly doFinal() on the byte array of plaintext and
ciphertext, instead of making use of Cipher{Input,Output}Stream.
While functionally equivelant, CipherInputStream merhod does not
work well with BouncyCastle (FIPS) Security Provider's implementation
of AES GCM as the byte array that is read from the CipherInputStream
backed DataInputStream is always some bytes short (17 vs 20 for
the case of keystore.seed value).

Calls explicitly `doFinal()` on the byte array of plaintext and
ciphertext, instead of making use of Cipher{Input,Output}Stream.

While functionally equivelant, CipherInputStream merhod does not
work well with BouncyCastle (FIPS) Security Provider's implementation
of AES GCM as the byte array that is read from the `CipherInputStream`
backed `DataInputStream` is always some bytes short (17 vs 20 for
the case of `keystore.seed` value).
Copy link
Member

@jaymode jaymode left a comment

Choose a reason for hiding this comment

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

While functionally equivelant, CipherInputStream merhod does not
work well with BouncyCastle (FIPS) Security Provider's implementation
of AES GCM as the byte array that is read from the CipherInputStream
backed DataInputStream is always some bytes short (17 vs 20 for
the case of keystore.seed value).

This sounds like it could be a bug in the BC implementation? Can you create a small reproducer and report it upstream. I am not a fan of adding workarounds like this unless we have a upstream issue so that we can plan to remove the workaround in the future.

If we're going to remove the use of CipherInputStream, we should add it to forbidden apis otherwise it will crop up somewhere else.

@rjernst
Copy link
Member

rjernst commented Feb 6, 2018

+1 to what Jay said.

@jkakavas
Copy link
Member Author

jkakavas commented Feb 7, 2018

Thanks for the comments. I started a discussion upstream and provided them with test cases for reproduction.

. I am not a fan of adding workarounds like this

Apart from this approach being a workaround to the original version, do you see anything that is inherently wrong with using doFinal() explicitly ?

If we're going to remove the use of CipherInputStream, we should add it to forbidden apis otherwise it will crop up somewhere else.

It does seem to affect other AES modes,I was able to reproduce it with AES/CBC/PKCS7Padding too for instance.

@jaymode
Copy link
Member

jaymode commented Feb 7, 2018

I started a discussion upstream and provided them with test cases for reproduction.

Thank you.

do you see anything that is inherently wrong with using doFinal() explicitly

There is nothing wrong with this aspect explicitly. In many instances the Cipher*Stream make things simpler, which is why I prefer their usage. Also, we should add comments in the code about why we do things a certain way with links to things like the upstream discussion so that the next person to come across this has a clue why things were done a certain way.

After receiving feedback from the discussion in the upstream [1],
reverted back to using CipherInputStream. Instead of using
`read()` and checking that the bytes read are what we expect,
use `readFully()` which will read exactly the number of bytes
while keep reading until the end of the stream or throw an
`EOFException` if not all bytes can be read.

This approach keeps the simplicity of using CipherInputStream while
working as expected with both Security Providers

[1] https://www.bouncycastle.org/devmailarchive/msg15559.html
@jkakavas
Copy link
Member Author

jkakavas commented Feb 8, 2018

Based on the feedback from the upstream mailing list, it turns out this has to do with how the data is read from the stream, and thus affects only the CipherInputStream. I changed to a different approach, using readFully() on the DataInputStream , that works as expected for both providers. I plan on renaming this PR in order to keep the history of the discussion, but I might as well close this and open a new one if you think it is cleaner.

Copy link
Member

@jaymode jaymode left a comment

Choose a reason for hiding this comment

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

I think it is fine to keep this PR. Let's just update the title and description to be appropriate of the change. I'd like to have a test for this and don't feel like it should be too difficult to add.

@@ -19,6 +19,7 @@

package org.elasticsearch.common.settings;


Copy link
Member

Choose a reason for hiding this comment

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

nit: unnecessary change?

@@ -317,38 +319,33 @@ public void decrypt(char[] password) throws GeneralSecurityException, IOExceptio
DataInputStream input = new DataInputStream(bytesStream)) {
Copy link
Member

Choose a reason for hiding this comment

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

I think we should add a test that would have failed using the old method. This would probably require a small refactoring to do this; I am thinking instead of new ByteArrayInputStream we call a method that could be overridden in a test. There we would have a special stream where a call to read would not fully populate the byte array

Copy link
Member

Choose a reason for hiding this comment

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

I don't think we need to override any methods. We can form streams of bytes in whatever order we want in tests.
However, if I understand the problem correctly, it has to do with read boundaries. Eg, an input stream may choose to return less bytes to a read call than the array can contain, since it may just copy the rest of an existing buffer (eg a decrypted block in CipherInputStream). By calling readFully, multiple read calls are made to ensure the array is filled completely or EOF is reached. I think this will be very difficult to mimic, given the behavior of read here will be implementation dependent. The best we could do is add tests to trigger each of the possible EOFs from readFully calls by truncating the keystore at certain points (although this would probably be difficult to do within the encrypted bytes).

I do think we should have a test to ensure there cannot be garbage after the encrypted data.

Copy link
Member Author

Choose a reason for hiding this comment

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

Revisiting this after coming back.

I do think we should have a test to ensure there cannot be garbage after the encrypted data

Care to elaborate @rjernst ? You mean attempting to readFully() into a larger array and verifying that we get the EOFException rather than keep reading garbage ?

Copy link
Member

Choose a reason for hiding this comment

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

I mean adding a test with garbage at the end, so that we know the last readFully logic works. The intermediate readFully's are harder, as I described above.

Copy link
Member

Choose a reason for hiding this comment

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

I do think we should have a test to ensure there cannot be garbage after the encrypted data.

If I understand correctly, we should fail if there is garbage right? Currently this is not the case at least based on the test @jkakavas added.

Copy link
Member Author

Choose a reason for hiding this comment

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

readFully reads exactly the number of bytes we instruct it to, or throws an EOFException. I interpreted @rjernst comment above to mean that we need to have a test verifying that even if there more garbage, readFully won't read them, but read the bytes it should(this is what the test I added does, and it should not fail )
Another way to interpret the comment I guess, would be to write less bytes to would be to write fewer bytes to the DataOutputStream than we indicate with the writeInt above and make sure that on decryption, readFully throws the EOFException instead of reading in garbage in order to fill our byte array - I can add that too.
Writing about this, the second interpretation makes much more sense I guess..

Copy link
Member

@rjernst rjernst Mar 14, 2018

Choose a reason for hiding this comment

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

That was not my intention. I meant it the way @jaymode suggested: we should fail when trailing garbage is present, and have a test for that.

@jkakavas jkakavas changed the title Use Cipher.doFinal() instead of CipherOutputStream for enryption and decryption Use readFully() to read exactly the number of bytes we expect from the Stream Feb 8, 2018
@jkakavas
Copy link
Member Author

Added check and test for trailing garbage. As discussed, we can address the slightly more complicated test case of cross buffer boundaries in a follow-up PR, so this is ready for another look @rjernst, @jaymode

Copy link
Member

@jaymode jaymode left a comment

Choose a reason for hiding this comment

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

LGTM

Copy link
Member

@rjernst rjernst left a comment

Choose a reason for hiding this comment

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

LGTM, I left a couple final requests.

throw new SecurityException("Keystore has been corrupted or tampered with");
}
input.readFully(encryptedBytes);
} catch (EOFException e) {
Copy link
Member

Choose a reason for hiding this comment

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

While the exception message was the same in all cases before, we still would have been able to tell where the failed read happened (ie where in the bytes the corrupt/tamped data occurs). Are we sure we want to drop the root cause exception here? I think that at least warrants a comment here as to why.

Copy link
Member Author

@jkakavas jkakavas Apr 17, 2018

Choose a reason for hiding this comment

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

You're right, any of the readX can throw an EOFException and I would have masked it. We should know where the failure occurred.

I passed the original exception as a cause to SecurityException to fix that

throw new SecurityException("Keystore has been corrupted or tampered with");
}
} catch (EOFException e) {
throw new SecurityException("Keystore has been corrupted or tampered with");
Copy link
Member

Choose a reason for hiding this comment

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

Same comment about dropping the root cause as above.

@@ -104,6 +107,57 @@ public void testUpgradeNoop() throws Exception {
assertEquals(seed.toString(), keystore.getString(KeyStoreWrapper.SEED_SETTING.getKey()).toString());
}

public void testFailWhenStreamNotConsumed() throws Exception {
Copy link
Member

Choose a reason for hiding this comment

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

This seems to test just one case. Can it be duplicated/refactored so it tests each of the readFully calls?

Copy link
Member Author

Choose a reason for hiding this comment

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

This one was attempting to test the case where for some reason the stream is not fully consumed. The only case that can be effectively tested in the same manner is the readFully(encryptedBytes) - again leaving bytes in the stream by indicating a smaller size.

If we write a smaller int as an indication of the size of the salt or the iv byte arrays, we'll get unpredictable behavior in the subsequent readInt() calls ( for iv, encryptedBytes ) that might cause NegativeArraySizeException and these are actually the tests I was postponing to the followup

As discussed, we can address the slightly more complicated test case of cross buffer boundaries in a follow-up PR

I did however add a couple of more tests, checking that we fail when these two readFully calls either don't or can't consume the stream.

Adds aditional explicit check for full stream consumption
Adds tests
@jkakavas
Copy link
Member Author

jkakavas commented May 3, 2018

@rjernst I addressed your comments, I think this is ready (pending green CI), I would appreciate a 👍 before merging

Copy link
Member

@rjernst rjernst left a comment

Choose a reason for hiding this comment

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

LGTM

@jkakavas jkakavas merged commit 21bc87a into elastic:master May 4, 2018
jasontedor added a commit to jasontedor/elasticsearch that referenced this pull request May 4, 2018
* master:
  Docs: remove transport_client from CCS role example (elastic#30263)
  [Rollup] Validate timezone in range queries (elastic#30338)
  Use readFully() to read bytes from CipherInputStream (elastic#28515)
  Fix  docs Recently merged elastic#29229 had a doc bug that broke the doc build. This commit fixes.
  Test: remove cluster permission from CCS user (elastic#30262)
  Add Get Settings API support to java high-level rest client (elastic#29229)
  Watcher: Remove unneeded index deletion in tests
jasontedor added a commit that referenced this pull request May 6, 2018
* master: (35 commits)
  DOCS: Correct mapping tags in put-template api
  DOCS: Fix broken link in the put index template api
  Add put index template api to high level rest client (#30400)
  Relax testAckedIndexing to allow document updating
  [Docs] Add snippets for POS stop tags default value
  Move respect accept header on no handler to 6.3.1
  Respect accept header on no handler (#30383)
  [Test] Add analysis-nori plugin to the vagrant tests
  [Docs] Fix bad link
  [Docs] Fix end of section in the korean plugin docs
  Expose the Lucene Korean analyzer module in a plugin (#30397)
  Docs: remove transport_client from CCS role example (#30263)
  [Rollup] Validate timezone in range queries (#30338)
  Use readFully() to read bytes from CipherInputStream (#28515)
  Fix  docs Recently merged #29229 had a doc bug that broke the doc build. This commit fixes.
  Test: remove cluster permission from CCS user (#30262)
  Add Get Settings API support to java high-level rest client (#29229)
  Watcher: Remove unneeded index deletion in tests
  Set the new lucene version for 6.4.0
  [ML][TEST] Clean up jobs in ModelPlotIT
  ...
dnhatn added a commit that referenced this pull request May 8, 2018
* elastic-master:
  Watcher: Mark watcher as started only after loading watches (#30403)
  Pass the task to broadcast actions (#29672)
  Disable REST default settings testing until #29229 is back-ported
  Correct wording in log message (#30336)
  Do not fail snapshot when deleting a missing snapshotted file (#30332)
  AwaitsFix testCreateShrinkIndexToN
  DOCS: Correct mapping tags in put-template api
  DOCS: Fix broken link in the put index template api
  Add put index template api to high level rest client (#30400)
  Relax testAckedIndexing to allow document updating
  [Docs] Add snippets for POS stop tags default value
  Move respect accept header on no handler to 6.3.1
  Respect accept header on no handler (#30383)
  [Test] Add analysis-nori plugin to the vagrant tests
  [Docs] Fix bad link
  [Docs] Fix end of section in the korean plugin docs
  Expose the Lucene Korean analyzer module in a plugin (#30397)
  Docs: remove transport_client from CCS role example (#30263)
  [Rollup] Validate timezone in range queries (#30338)
  Use readFully() to read bytes from CipherInputStream (#28515)
  Fix  docs Recently merged #29229 had a doc bug that broke the doc build. This commit fixes.
  Test: remove cluster permission from CCS user (#30262)
  Add Get Settings API support to java high-level rest client (#29229)
  Watcher: Remove unneeded index deletion in tests
jkakavas added a commit to jkakavas/elasticsearch that referenced this pull request May 16, 2018
Changes how data is read from CipherInputStream

 Instead of using `read()` and checking that the bytes read are what we
expect, use `readFully()` which will read exactly the number of bytes
while keep reading until the end of the stream or throw an
`EOFException` if not all bytes can be read.

This approach keeps the simplicity of using CipherInputStream while
working as expected with both JCE and BCFIPS Security Providers
jkakavas added a commit that referenced this pull request May 16, 2018
Changes how data is read from CipherInputStream in
 KeyStoreWrapper. Instead of using `read()` and checking that the 
bytes read are what we expect, use `readFully()` which will read 
exactly the number of bytes while keep reading until the end of 
the stream or throw an `EOFException` if not all bytes can be read.

This approach keeps the simplicity of using CipherInputStream while
working as expected with both JCE and BCFIPS Security Providers.

See also: #28515
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants