Skip to content

Conversation

edwardneal
Copy link
Contributor

Description

I'm trying to improve query performance in AE scenarios, and part of this means a review of the column encryption providers. These were written prior to a number of cryptography improvements in .NET 5.0; as a result of these improvements, we can save a little memory.

Besides the performance improvements, each provider reproduces the same serialization logic. These primitives centralise that logic; they use the column master key parameters to perform encryption and decryption of a column encryption key with an RSA instance. The only thing that the three SqlColumnEncryptionKeyStoreProvider derivatives need to do is retrieve an RSA instance using the master key path.

I considered moving this logic into a protected method on SqlColumnEncryptionKeyStoreProvider, but a third-party provider isn't guaranteed to perform encryption/decryption of the CEK using an RSA instance (or indeed, on the same server - SqlColumnEncryptionAzureKeyVaultProvider passes that duty to Azure Key Vault.)

A subsequent PR will refactor the column encryption providers to use these primitives, and the benchmark results for this are below. To summarise: no significant variations in execution time, and around a 15% reduction in AlwaysEncrypted memory usage.

Benchmarks

SqlColumnEncryptionCertificateStoreProvider

Method Branch Mean Error StdDev Ratio Allocated Alloc Ratio
DecryptColumnEncryptionKey main 13.79 ms 0.256 ms 0.227 ms 1.00 18.17 KB 1.00
DecryptColumnEncryptionKey PR 13.16 ms 0.180 ms 0.150 ms 0.95 15.43 KB 0.85
EncryptColumnEncryptionKey main 12.59 ms 0.224 ms 0.375 ms 1.00 18.94 KB 1.00
EncryptColumnEncryptionKey PR 12.58 ms 0.246 ms 0.450 ms 1.00 16.01 KB 0.85

SqlColumnEncryptionCngProvider

Method Branch Mean Error StdDev Ratio Allocated Alloc Ratio
DecryptColumnEncryptionKey main 3.303 ms 0.0639 ms 0.1661 ms 1.00 2.51 KB 1.00
DecryptColumnEncryptionKey PR 3.285 ms 0.0648 ms 0.0842 ms 0.99 901 B 0.36
EncryptColumnEncryptionKey main 3.214 ms 0.0395 ms 0.0330 ms 1.00 3.11 KB 1.00
EncryptColumnEncryptionKey PR 3.332 ms 0.0645 ms 0.0603 ms 1.03 1.61 KB 0.52

SqlColumnEncryptionCspProvider

Method Branch Mean Error StdDev Ratio Allocated Alloc Ratio
DecryptColumnEncryptionKey main 2.775 ms 0.0411 ms 0.0520 ms 1.00 3.59 KB 1.00
DecryptColumnEncryptionKey PR 2.737 ms 0.0256 ms 0.0200 ms 0.99 3.01 KB 0.84
EncryptColumnEncryptionKey main 2.815 ms 0.0556 ms 0.0571 ms 1.00 4.01 KB 1.00
EncryptColumnEncryptionKey PR 2.847 ms 0.0464 ms 0.0434 ms 1.01 3.23 KB 0.81

Testing

These primitives aren't used anywhere yet, so there's no test coverage. The existing providers have this coverage though, so the new code will gain coverage from the follow-up PR. I have a local branch where the column encryption providers use this code, and almost all tests pass. The two failing tests are capitalisation errors in exception messages produced by the CSP provider, which will be fixed at the same time.

@edwardneal edwardneal requested a review from a team as a code owner August 9, 2025 15:52
@edwardneal edwardneal changed the title Performance | Introduce low-allocation AlwaysEncrypted primitives Performance | Introduce lower-allocation AlwaysEncrypted primitives Aug 9, 2025
Comment on lines +80 to +81
providerName.AsSpan().ToLowerInvariant(masterKeyMetadataSpan);
masterKeyPath.AsSpan().ToLowerInvariant(masterKeyMetadataSpan.Slice(providerName.Length));
Copy link
Contributor

Choose a reason for hiding this comment

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

Are the characters in the input variable restricted such that it is certain that the output lower invariant replacement version is certain to be of exactly the same length?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes, indirectly. This is due to be used by the certificate store provider, and the constructor takes an RSA instance. The only way to get this instance is by using the master key path to select a valid certificate, and the only valid master key paths are ASCII text in a few formats:

  • [LocalMachine|CurrentUser]/My/[40 character SHA1 thumbprint]
  • My/[40 character SHA1 thumbprint]
  • [40 character SHA1 thumbprint]

Copy link
Contributor

Choose a reason for hiding this comment

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

Excellent. It might be worth leaving a comment in the code somewhere so anyone who reads the code in future knows that there are input restrictions.

Add a comment and a debug assertion to make it clear that the invariant lowercase value of masterKeyPath should be the same length as the original string.
@paulmedynski paulmedynski self-assigned this Aug 27, 2025
@paulmedynski
Copy link
Contributor

/azp run

Copy link

Azure Pipelines successfully started running 2 pipeline(s).

Copy link
Contributor

@paulmedynski paulmedynski left a comment

Choose a reason for hiding this comment

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

Consolidation looks good overall, with some questions/suggestions.

@apoorvdeshmukh apoorvdeshmukh self-assigned this Aug 27, 2025
Add/updated XML documentation to the public APIs.
Add intermediary variable containing the signature size for clarity.
Use AlgorithmOffset rather than hardcoded offset in EncryptedColumnEncryptionKeyParameters.
Specifically define using block in ColumnMasterKeyMetadata.
class -> struct
@paulmedynski
Copy link
Contributor

/azp run

Copy link

Azure Pipelines successfully started running 2 pipeline(s).

Copy link
Contributor

@paulmedynski paulmedynski left a comment

Choose a reason for hiding this comment

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

Thanks for the previous updates. Just a couple more and one question about disposal.

@edwardneal
Copy link
Contributor Author

Thanks. Responded, and merged following the merge of #3014.

Copy link
Contributor

@paulmedynski paulmedynski left a comment

Choose a reason for hiding this comment

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

Thanks for the updates! RSA.SignHash() will throw CryptographicException if "An error occurred creating the signature.", so need to document that one as well.

@edwardneal
Copy link
Contributor Author

I'd missed that - added to ColumnMasterKeyMetadata.Sign and EncryptedColumnEncryptionKeyParameters.Encrypt.

@paulmedynski
Copy link
Contributor

/azp run

Copy link

Azure Pipelines successfully started running 2 pipeline(s).

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