Skip to content

BlobCipherKeyCache::getLatestCipherKey return empty when there's no key#6908

Closed
sfc-gh-yiwu wants to merge 3 commits into
apple:mainfrom
sfc-gh-yiwu:encrypt_valgrind
Closed

BlobCipherKeyCache::getLatestCipherKey return empty when there's no key#6908
sfc-gh-yiwu wants to merge 3 commits into
apple:mainfrom
sfc-gh-yiwu:encrypt_valgrind

Conversation

@sfc-gh-yiwu

@sfc-gh-yiwu sfc-gh-yiwu commented Apr 20, 2022

Copy link
Copy Markdown
Collaborator

Instead of throwing error, BlobCipherKeyCache::getLatestCipherKey() now returns empty key if the latest key doesn't exists.

The PR also fixes valgrind complaining about latestRandomSalt not initialized when getLatestCipherKey() try to print the salt if no key is found.

Tested by running the EncryptionOps test with valgrind, and also updating the flow/BlobCipher unit test.

Code-Reviewer Section

The general guidelines can be found here.

Please check each of the following things and check all boxes before accepting a PR.

  • The PR has a description, explaining both the problem and the solution.
  • The description mentions which forms of testing were done and the testing seems reasonable.
  • Every function/class/actor that was touched is reasonably well documented.

For Release-Branches

If this PR is made against a release-branch, please also check the following:

  • This change/bugfix is a cherry-pick from the next younger branch (younger release-branch or main if this is the youngest branch)
  • There is a good reason why this PR needs to go into a release branch and this reason is documented (either in the description above or in a linked GitHub issue)

@fdb-windows-ci

Copy link
Copy Markdown
Collaborator

Doxense CI Report for Windows 10

@sfc-gh-ahusain

Copy link
Copy Markdown
Collaborator

I intentionally added "encrypt_key_not_found" exception as there is no scenario where the code should hit this scenario; for instance:

  1. INVALID_ID input means there is a logic error somewhere.
  2. Given external KMS is the source of truth for all encryption keys, it is not possible for FDB to get a "key_not_found" from EncryptKeyServer, keyId or domainId not found in external KMS means FDB encryption-key details either got corrupted or tampered; both of this scenario should be caught while authenticating the EncyrptionHeader, hence, only possibilities are: in-memory corruption and/or logic code bug. Hence, throwing this exception seems to be the right thing to ensure the caller doesn't ignore this error (it is even OK not to catch this error leading the process to get restarted)

@sfc-gh-ahusain sfc-gh-ahusain self-assigned this Apr 21, 2022

@sfc-gh-ahusain sfc-gh-ahusain left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Thanks Yi for debugging the Valgrind error.
Please see detailed comment, my vote is against doing this change. Let's talk more if you feel this is a necessary change.
Thanks.

@sfc-gh-yiwu

Copy link
Copy Markdown
Collaborator Author

I intentionally added "encrypt_key_not_found" exception as there is no scenario where the code should hit this scenario; for instance:

  1. INVALID_ID input means there is a logic error somewhere.
  2. Given external KMS is the source of truth for all encryption keys, it is not possible for FDB to get a "key_not_found" from EncryptKeyServer, keyId or domainId not found in external KMS means FDB encryption-key details either got corrupted or tampered; both of this scenario should be caught while authenticating the EncyrptionHeader, hence, only possibilities are: in-memory corruption and/or logic code bug. Hence, throwing this exception seems to be the right thing to ensure the caller doesn't ignore this error (it is even OK not to catch this error leading the process to get restarted)

This is in the BlobCipherKeyCache, and this is how I will use it. The caller will first query BlobCipherKeyCache for latest key, and initially when the cache is empty, the cache will return empty, and the caller will query the EncryptKeyProxy to obtain the latest key and insert into the cache. In this case the cache returning empty should not be considered an error.

@sfc-gh-ahusain

Copy link
Copy Markdown
Collaborator

@sfc-gh-ahusain

Copy link
Copy Markdown
Collaborator

I intentionally added "encrypt_key_not_found" exception as there is no scenario where the code should hit this scenario; for instance:

  1. INVALID_ID input means there is a logic error somewhere.
  2. Given external KMS is the source of truth for all encryption keys, it is not possible for FDB to get a "key_not_found" from EncryptKeyServer, keyId or domainId not found in external KMS means FDB encryption-key details either got corrupted or tampered; both of this scenario should be caught while authenticating the EncyrptionHeader, hence, only possibilities are: in-memory corruption and/or logic code bug. Hence, throwing this exception seems to be the right thing to ensure the caller doesn't ignore this error (it is even OK not to catch this error leading the process to get restarted)

This is in the BlobCipherKeyCache, and this is how I will use it. The caller will first query BlobCipherKeyCache for latest key, and initially when the cache is empty, the cache will return empty, and the caller will query the EncryptKeyProxy to obtain the latest key and insert into the cache. In this case the cache returning empty should not be considered an error.

I see.
But then why are we not throwing the error when DOMAIN_ID is INVALID; it shouldn't happen?

@sfc-gh-ahusain

Copy link
Copy Markdown
Collaborator

I intentionally added "encrypt_key_not_found" exception as there is no scenario where the code should hit this scenario; for instance:

  1. INVALID_ID input means there is a logic error somewhere.
  2. Given external KMS is the source of truth for all encryption keys, it is not possible for FDB to get a "key_not_found" from EncryptKeyServer, keyId or domainId not found in external KMS means FDB encryption-key details either got corrupted or tampered; both of this scenario should be caught while authenticating the EncyrptionHeader, hence, only possibilities are: in-memory corruption and/or logic code bug. Hence, throwing this exception seems to be the right thing to ensure the caller doesn't ignore this error (it is even OK not to catch this error leading the process to get restarted)

This is in the BlobCipherKeyCache, and this is how I will use it. The caller will first query BlobCipherKeyCache for latest key, and initially when the cache is empty, the cache will return empty, and the caller will query the EncryptKeyProxy to obtain the latest key and insert into the cache. In this case the cache returning empty should not be considered an error.

I see. But then why are we not throwing the error when DOMAIN_ID is INVALID; it shouldn't happen?

I agree with the BlobCipherKeyCache usage, but, let's document this behavior in API contracts to avoid any confusion.
Thanks

Comment thread flow/BlobCipher.cpp

Reference<BlobCipherKey> BlobCipherKeyIdCache::getLatestCipherKey() {
if (latestBaseCipherKeyId == ENCRYPT_INVALID_CIPHER_KEY_ID) {
return Reference<BlobCipherKey>();

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

we should be throwing an exception here; this should be a code logic bug?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

ENCRYPT_INVALID_CIPHER_KEY_ID exactly means the cache is empty.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I think it is better not to conflate "invalid_cipher_id" with the "cache empty", a caller can pass "invalid_id" due to some bug even when cache is not-empty. We can add a helper method in the cache to return its size if needed.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

with getLatestCipherKey the caller is not passing in key id, so the ENCRYPT_INVALID_CIPHER_KEY_ID indicate internal state (no latest key). I agree for getCipherKey, it should come with invalid id check, which I'm not updating in this PR.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

If the cache is empty, then BlobCipherKeyIdCache should be invoked?

	auto domainItr = domainCacheMap.find(domainId);
	if (domainItr == domainCacheMap.end()) {
		TraceEvent("GetLatestCipherKey_DomainNotFound").detail("DomainId", domainId);
		throw encrypt_key_not_found();
	}

The domainId check should return empty_reference based on your change? Am I missing anything here.

@sfc-gh-yiwu sfc-gh-yiwu Apr 21, 2022

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Ah, sorry. I look at the valgrind stack again, and it is not EncryptionOps accessing BlobCipherKeyIdCache directly. It is BlobCipherKeyIdCache::insertBaseCipherKey calling getLatestCipherKey() before actually insert the key. In this case the BlobCipherKeyIdCache is created, but it is empty. In this case, do you think it make sense to keep the existing change?

Valgrind stack:

==46== Conditional jump or move depends on uninitialised value(s)
==46==    at 0x6E09DAE: vfprintf (in /usr/lib64/libc-2.17.so)
==46==    by 0x6E37178: vsnprintf (in /usr/lib64/libc-2.17.so)
==46==    by 0x37DDB51: vsformat(std::string&, char const*, __va_list_tag*) (flow.cpp:229)
==46==    by 0x37DE06C: format(char const*, ...) (flow.cpp:266)
==46==    by 0xC00D07: toString (Trace.h:255)
==46==    by 0xC00D07: std::enable_if<Traceable<unsigned long>::value, BaseTraceEvent&>::type BaseTraceEvent::detail<unsigned long>(char const*, unsigned long const&) (Trace.h:434)
==46==    by 0x36D5BF6: BlobCipherKeyIdCache::getCipherByBaseCipherId(unsigned long const&, unsigned long const&) (BlobCipher.cpp:147)
==46==    by 0x36D95A7: getLatestCipherKey (BlobCipher.cpp:137)
==46==    by 0x36D95A7: BlobCipherKeyIdCache::insertBaseCipherKey(unsigned long const&, unsigned char const*, int) (BlobCipher.cpp:161)
==46==    by 0x36D9E0B: BlobCipherKeyCache::insertCipherKey(long const&, unsigned long const&, unsigned char const*, int) (BlobCipher.cpp:253)
==46==    by 0x23EC77E: EncryptionOpsWorkload::setupCipherEssentials() (EncryptionOps.actor.cpp:178)
==46==    by 0x23ED0A5: EncryptionOpsWorkload::start(Database const&) (EncryptionOps.actor.cpp:305)
==46==    by 0x1E5F2EA: CompoundWorkload::start(Database const&) (tester.actor.cpp:301)
==46==    by 0x1E2E193: (anonymous namespace)::RunWorkloadAsyncActorState<(anonymous namespace)::RunWorkloadAsyncActor>::a_body1loopBody1when2(ReplyPromise<Void>&&, int) (tester.actor.cpp:592)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

A try-catch on caller side looks cumbersome. What do you think?

My initial thought that it is better for the caller to handle exceptions from a caching library, some exceptions can be 'acceptable' such as: key_not_found which could be satisfied by reaching out to KMS via EKPServer, however, I see below scheme could work as well:

  1. BlobCipherKeyCache throws key_not_found only for unexpected scenarios such as the one we discussed above. For a cache_miss it is OK to return an emptyReference. However, it is better to have unified behavior of empty-cache as well as cache-missing-a -specific scenario. If we choose not to throw exception, then I would say better not to distinguish these cases as technically they are same in some ways.
  2. As FDB EKs have 1:1 relation with KMS EKs, we can update EncryptKeyServer lookup interface to ensure that all cipher-keys requested are returned back to the client; if not, server throws "encrypt_key_not_found" (never to happen scenario and points to code logic bug/tampering etc.)

#2 I can take care as part of #6898.

Does this sounds reasonable?
Thanks

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

by 0x36D95A7: BlobCipherKeyIdCache::insertBaseCipherKey(unsigned long const&, unsigned char const*, int) (BlobCipher.cpp:161)


		if (cipherKey->getBaseCipherId() == baseCipherId) {
			if (memcmp(cipherKey->rawBaseCipher(), baseCipher, baseCipherLen) == 0) {
				TraceEvent("InsertBaseCipherKey_AlreadyPresent")
				    .detail("BaseCipherKeyId", baseCipherId)
				    .detail("DomainId", domainId);                     <---- line 161
				// Key is already present; nothing more to do.

Something doesn't add up; the above scenario is ONLY possible when caller attempts to insert a "duplicate" baseCipherId with "exact CipherKey" match usecase (hence, no tampering or foul play).

I think the problem is the following line of code -

EncryptionOps.actor.cpp::setupCipherEssentials()
...
		// insert the Encrypt Header cipherKey
		generateRandomBaseCipher(AES_256_KEY_LENGTH, &buff[0], &cipherLen);
		cipherKeyCache->insertCipherKey(
		    ENCRYPT_HEADER_DOMAIN_ID, headerBaseCipherId, buff, cipherLen, headerRandomSalt)
...

As expected inserting a cipher with API accepting 'salt' shouldn't update 'latestBaseCipher' details (as this entry should happen as part of reading encyrption header), however, the testcode is doing it as a 'hack' for "EncryptHeader authToken cipherKey".

Then, below code simulates "cache-miss" randomly for any {"domainId", "baseCipherId"} entry, and when done for domainId == -2 (EncryptHeader auth cipher key), the TraceEvent goes bonkers :)

Reference<BlobCipherKey> getEncryptionKey(const EncryptCipherDomainId& domainId,
	                                          const EncryptCipherBaseKeyId& baseCipherId,
	                                          const EncryptCipherRandomSalt& salt) {
		const bool simCacheMiss = deterministicRandom()->randomInt(1, 100) < 15;

		Reference<BlobCipherKeyCache> cipherKeyCache = BlobCipherKeyCache::getInstance();
		Reference<BlobCipherKey> cipherKey = cipherKeyCache->getCipherKey(domainId, baseCipherId, salt);

		if (simCacheMiss) {
			TraceEvent("SimKeyCacheMiss").detail("EncyrptDomainId", domainId).detail("BaseCipherId", baseCipherId);
			// simulate KeyCache miss that may happen during decryption; insert a CipherKey with known 'salt'
			cipherKeyCache->insertCipherKey(domainId,
			                                baseCipherId,
			                                cipherKey->rawBaseCipher(),
			                                cipherKey->getBaseCipherLen(),
			                                cipherKey->getSalt());
			// Ensure the update was a NOP
			Reference<BlobCipherKey> cKey = cipherKeyCache->getCipherKey(domainId, baseCipherId, salt);
			ASSERT(cKey->isEqual(cipherKey));
		}
		return cipherKey;
	}

I think I know how to fix this issue. Please let me know if it OK to leave the Valgrind fix from this patch and I will fix it separately?

Thanks again for debugging this issue :).

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Sure. Go ahead with the fix. I will close the current PR and subsume it into my upcoming PR.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

A try-catch on caller side looks cumbersome. What do you think?

My initial thought that it is better for the caller to handle exceptions from a caching library, some exceptions can be 'acceptable' such as: key_not_found which could be satisfied by reaching out to KMS via EKPServer, however, I see below scheme could work as well:

  1. BlobCipherKeyCache throws key_not_found only for unexpected scenarios such as the one we discussed above. For a cache_miss it is OK to return an emptyReference. However, it is better to have unified behavior of empty-cache as well as cache-missing-a -specific scenario. If we choose not to throw exception, then I would say better not to distinguish these cases as technically they are same in some ways.
  2. As FDB EKs have 1:1 relation with KMS EKs, we can update EncryptKeyServer lookup interface to ensure that all cipher-keys requested are returned back to the client; if not, server throws "encrypt_key_not_found" (never to happen scenario and points to code logic bug/tampering etc.)

#2 I can take care as part of #6898.

Does this sounds reasonable? Thanks

Make sense to me. I will have #1 done in my upcoming PR.

@sfc-gh-yiwu

Copy link
Copy Markdown
Collaborator Author

I see. But then why are we not throwing the error when DOMAIN_ID is INVALID; it shouldn't happen?

let me add a check for it.

@fdb-windows-ci

Copy link
Copy Markdown
Collaborator

Doxense CI Report for Windows 10

@foundationdb-ci

Copy link
Copy Markdown
Contributor

AWS CodeBuild CI Report for Linux CentOS 7

  • CodeBuild project: foundationdb-pr
  • Commit ID: 80830c6
  • Result: SUCCEEDED
  • Error: N/A
  • Build Logs (available for 30 days)

@foundationdb-ci

Copy link
Copy Markdown
Contributor

AWS CodeBuild CI Report for macOS BigSur 11.5.2

  • CodeBuild project: foundationdb-pr-macos
  • Commit ID: 3d23e9a
  • Result: SUCCEEDED
  • Error: N/A
  • Build Logs (available for 30 days)

@fdb-windows-ci

Copy link
Copy Markdown
Collaborator

Doxense CI Report for Windows 10

@foundationdb-ci

Copy link
Copy Markdown
Contributor

AWS CodeBuild CI Report for macOS BigSur 11.5.2

  • CodeBuild project: foundationdb-pr-macos
  • Commit ID: e6a39b1
  • Result: SUCCEEDED
  • Error: N/A
  • Build Logs (available for 30 days)

@foundationdb-ci

Copy link
Copy Markdown
Contributor

AWS CodeBuild CI Report for Linux CentOS 7

  • CodeBuild project: foundationdb-pr
  • Commit ID: 3d23e9a
  • Result: SUCCEEDED
  • Error: N/A
  • Build Logs (available for 30 days)

@foundationdb-ci

Copy link
Copy Markdown
Contributor

AWS CodeBuild CI Report for Linux CentOS 7

  • CodeBuild project: foundationdb-pr
  • Commit ID: 80830c6
  • Result: FAILED
  • Error: Build has timed out.
  • Build Logs (available for 30 days)

@sfc-gh-yiwu

Copy link
Copy Markdown
Collaborator Author

Closing as discussed.

@foundationdb-ci

Copy link
Copy Markdown
Contributor

AWS CodeBuild CI Report for Linux CentOS 7

  • CodeBuild project: foundationdb-pr
  • Commit ID: e6a39b1
  • Result: FAILED
  • Error: Build has timed out.
  • Build Logs (available for 30 days)

@sfc-gh-yiwu sfc-gh-yiwu deleted the encrypt_valgrind branch April 22, 2022 05:48
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.

4 participants