Add async counterparts to the enclave provider hierarchy (Phase 3, partial) - #4541
Add async counterparts to the enclave provider hierarchy (Phase 3, partial)#4541cheenamalhotra wants to merge 1 commit into
Conversation
…rtial)
Implements the enclave-provider portion of Phase 3 of the async Always
Encrypted spec (specs/002-async-always-encrypted/spec.md).
SqlColumnEncryptionEnclaveProvider gains four `virtual` async counterparts
whose default implementations defer to the existing sync overloads, mirroring
the pattern already established for SqlColumnEncryptionKeyStoreProvider in
Phase 1. Because C# forbids `out` parameters on async methods, the two members
that report multiple values return tuples instead (spec Design Decision 4).
The two providers that perform real network I/O explicitly override the
defaults rather than inheriting the blocking fallback, which removes both
sync-over-async blocking calls in this hierarchy:
* AzureAttestationBasedEnclaveProvider now awaits
ConfigurationManager.GetConfigurationAsync instead of blocking on .Result.
* HostGuardianServiceEnclaveProvider now awaits GetStreamAsync,
JsonSerializer.DeserializeAsync and the retry backoff instead of blocking
on .GetAwaiter().GetResult() and Thread.Sleep.
FR-015: the attestation gate (AutoResetEvent) has no awaitable wait, so the
async path gets its own SemaphoreSlim gate via GetEnclaveSessionHelperAsync.
The gates are deliberately independent so that a synchronous caller can never
block a thread for the duration of an awaited attestation round trip. The five
`lock` statements called out in the spec are left as-is: their bodies only
touch MemoryCache and flags, and the awaited attestation happens before session
storage rather than inside those regions.
Unlike the sync path, the async gate is also released when the caller cancels,
since a cancelled caller never goes on to create the session.
FR-010: no existing sync code path is modified. Every change is an insertion.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: cf57fdc0-1c77-40f1-a1d5-7c0bd98c1a89
There was a problem hiding this comment.
Pull request overview
This PR adds internal async counterparts to the Always Encrypted enclave provider hierarchy, enabling non-blocking attestation/session creation (notably for the HTTP-based providers) while preserving existing sync behavior via default virtual fallbacks. It lays groundwork for later phases to wire async enclave attestation into the command execution flow without introducing public API surface changes.
Changes:
- Added
GetEnclaveSessionAsync,GetAttestationParametersAsync,CreateEnclaveSessionAsync, andInvalidateEnclaveSessionAsynctoSqlColumnEncryptionEnclaveProvider, with default implementations delegating to the sync members and supporting cancellation. - Introduced an async attestation gate in
EnclaveProviderBase(viaSemaphoreSlim) and implemented truly-async network paths for Azure Attestation and HGS/VSM signing certificate retrieval. - Added a new unit test suite validating default fallback behavior, cancellation, and concurrency characteristics across sync/async paths.
Reviewed changes
Copilot reviewed 7 out of 7 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
| src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlColumnEncryptionEnclaveProvider.cs | Adds the new internal virtual async members with sync-fallback defaults, cancellation, and exception-to-faulted-task behavior. |
| src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/EnclaveProviderBase.cs | Implements async session gating + async helper APIs (tuple returns) to support non-blocking attestation flows. |
| src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/AzureAttestationBasedEnclaveProvider.cs | Overrides async members to avoid sync-over-async blocking and propagate cancellation through network calls. |
| src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/VirtualSecureModeEnclaveProviderBase.cs | Adds async counterparts for VSM attestation logic, including async signing cert retrieval via an abstract MakeRequestAsync. |
| src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/VirtualSecureModeEnclaveProvider.cs | Implements truly async HTTP request + retry/backoff + async JSON deserialization for HGS signing cert retrieval. |
| src/Microsoft.Data.SqlClient/tests/UnitTests/Microsoft/Data/SqlClient/AlwaysEncrypted/SqlColumnEncryptionEnclaveProviderAsyncShould.cs | Adds unit tests for fallback dispatch, cancellation semantics, gate behavior, and concurrency on sync/async paths. |
| doc/snippets/Microsoft.Data.SqlClient/SqlColumnEncryptionEnclaveProvider.xml | Documents the new async members and the “don’t mix sync/async within one attestation sequence” constraint. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## main #4541 +/- ##
==========================================
- Coverage 64.78% 62.90% -1.88%
==========================================
Files 288 284 -4
Lines 44418 67766 +23348
==========================================
+ Hits 28774 42628 +13854
- Misses 15644 25138 +9494
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
Description
Implements the enclave-provider portion of Phase 3 of
specs/002-async-always-encrypted/spec.md, building on theSqlColumnEncryptionKeyStoreProviderasync members that landed in Phase 1 (#3673).The motivation is that the Always Encrypted enclave path currently blocks a thread on real network I/O even when the caller invoked an async API. Two call sites perform sync-over-async, and the attestation gate that serialises concurrent cold starts is an
AutoResetEventwith a 15 second timeout, so contending callers park a thread pool thread for up to 15 seconds each. This PR lays the async groundwork to remove all three.API changes, backwards compatibility
None.
SqlColumnEncryptionEnclaveProviderisinternal abstractand appears in neithernetcore/ref/nornetfx/ref/, so there is no public API surface and no binary compatibility constraint. The hierarchy is closed and entirely internal.Functionality
1. Four async counterparts on the base type.
GetEnclaveSessionAsync,GetAttestationParametersAsync,CreateEnclaveSessionAsyncandInvalidateEnclaveSessionAsync. They arevirtual, with defaults that defer to the existing sync overloads, so providers with no I/O need no boilerplate. This mirrors the Phase 1Task.FromCanceled/Task.FromResult/Task.FromException+ADP.IsCatchableExceptionTypepattern exactly.C# forbids
outparameters on async methods, so the two members that report multiple values return tuples instead (spec Design Decision 4):2. Both HTTP providers are genuinely async. A
virtualdefault would leave a real network call blocking a thread, so the two providers that perform I/O explicitly override rather than inheriting the fallback. This removes both sync-over-async blocking calls in the hierarchy:AzureAttestationBasedEnclaveProviderGetConfigurationAsync(CancellationToken.None).Resultawait GetConfigurationAsync(cancellationToken)HostGuardianServiceEnclaveProviderGetStreamAsync(url).ConfigureAwait(false).GetAwaiter().GetResult()await GetStreamAsync(url, cancellationToken)The HGS path also awaits
JsonSerializer.DeserializeAsyncand replacesThread.Sleepin the retry backoff withTask.Delay.MakeRequestAsyncis declaredprotected abstractrather thanvirtualso no derived provider can silently inherit a blocking implementation.Left on the base defaults deliberately:
NoneAttestationEnclaveProvider(no I/O), and the CPU boundGetAttestationParameters/InvalidateEnclaveSessionoperations (ECDH key generation, nonce generation,MemoryCacheaccess).3. FR-015, attestation gating. The spec calls for converting
locktoSemaphoreSlimwhere an async operation precedes session storage. On inspection, the fivelockstatements named in the spec were not the ones that needed converting: their bodies only touchMemoryCacheand flags, noawaitoccurs inside them, and none can, because the awaited attestation happens beforeAddEnclaveSessionToCacheis called.lockremains correct and cheaper there.The construct that genuinely blocks is
EnclaveProviderBase.sessionLockEvent, anAutoResetEventwith no awaitable wait.GetEnclaveSessionHelperAsyncuses aSemaphoreSlimgate instead.The sync and async gates are deliberately independent, per the requirement never to hold a gate across an awaited network call that a synchronous caller can also block on. The trade off is that a concurrent sync plus async cold start may perform two attestations. The existing design already tolerates duplicates (see the documented lock timeout cases) and the session cache is idempotent.
Behavioral differences worth reviewer attention
UpdateEnclaveSessionLockStatusreleases only when a session was created. The async version also releases when the token is cancelled, otherwise a caller cancelled mid attestation would stall every other async caller for the full 15 second lock timeout. The sync path has no equivalent case because it has no cancellation.SemaphoreSlim.Release()throws when the semaphore is full, unlikeAutoResetEvent.Set()which is a harmless no-op. Releases are therefore guarded by ans_isAsyncSessionLockAcquiredflag, preserving the sync design's "any caller may signal" semantics while staying idempotent.GetEnclaveSessionAsync->GetAttestationParametersAsync->CreateEnclaveSessionAsyncis one unit. Pairing an async Get with a sync Create leaks a gate because the two use different gates. This is documented in the snippet XML<remarks>and is the main thing to watch during Phase 5 integration.ThreadRetryCacheis keyed byManagedThreadId, which is not stable acrossawaitboundaries. A caller resuming on a different thread may miss the same thread retry optimisation, costing an extra attestation. Never a correctness or deadlock issue. Worth replacing with a correlation token in Phase 5.FR-010, sync paths unchanged
Every source change is an insertion.
git diff --statreports+690 / -1across the five source files, and the single deletion is in the docs XML (a<remarks>line that was expanded). No existing sync statement was moved, reordered or edited. Each new async method was verified statement by statement against its sync twin.Documentation
XML docs for the new members use
<include>intodoc/snippets/Microsoft.Data.SqlClient/SqlColumnEncryptionEnclaveProvider.xml, consistent with the existing sync members. The class level<remarks>gained the three call attestation sequence and the do-not-mix-sync-and-async contract. No localization impact.Not in scope
EnclaveDelegateasync dispatchers,SqlSecurityUtility.DecryptSymmetricKeyAsyncand theSqlCommand.Encryption.cscall sites are Phase 3 remainder through Phase 5. Nothing calls these new members yet, so there is no end to end behaviour change from this PR. It is purely additive groundwork.Issues
Implements part of Phase 3 of
specs/002-async-always-encrypted/spec.md. Follows on from #3673 (Phase 1).No GitHub issue is closed by this PR.
Testing
12 new unit tests in
src/Microsoft.Data.SqlClient/tests/UnitTests/Microsoft/Data/SqlClient/AlwaysEncrypted/SqlColumnEncryptionEnclaveProviderAsyncShould.cs, using four in-file test doubles so no SQL Server or attestation service is required:Task.FromExceptionAttestation took 00:00:15.05, so it is a proven regression guard rather than an assumed oneMakeRequestAsynccancellation, and mapping of request failure toSqlExceptionResults:
No integration or manual tests were added. The new members are not yet reachable from any public API (see "Not in scope"), so there is no end to end path to exercise. Integration coverage against a live enclave belongs with the Phase 5 call site work, where
SqlCommandactually invokes these methods.Additional verification against the spec's hard requirements:
awaitin library code uses.ConfigureAwait(false)(Design Decision 5)CancellationTokenis propagated through all new async methodssrc/Microsoft.Data.SqlClient/src/, none innetfx/src/ornetcore/src/