Skip to content

Fix BCryptImportKeyPair: phKey passed by value, and the error path frees the wrong field - #4

Open
mckuipers wants to merge 5 commits into
DataFlex-dev:production/stablefrom
mckuipers:fix/bcryptimportkeypair-phkey-out-parameter
Open

Fix BCryptImportKeyPair: phKey passed by value, and the error path frees the wrong field#4
mckuipers wants to merge 5 commits into
DataFlex-dev:production/stablefrom
mckuipers:fix/bcryptimportkeypair-phkey-out-parameter

Conversation

@mckuipers

Copy link
Copy Markdown

BCryptImportKeyPair cannot succeed as written. Nothing in the library or the demo calls it,
which is why it has gone unnoticed — we hit it calling it directly to verify a WebAuthn
assertion signature.

The bug

bcrypt.h.pkg declares the fourth parameter correctly:

External_Function WinAPI_BCryptImportKeyPair "BCryptImportKeyPair" Bcrypt.dll ;
    Handle   hAlgorithm ;
    Handle   hImportKey ;
    Pointer  pszBlobType ;
    Pointer  phKey ;          // <- out-parameter: BCRYPT_KEY_HANDLE *phKey
    Pointer  pbInput ;
    UInteger cbInput ;
    UInteger dwFlags ;
    Returns Integer

The call site passes a Handle by value into it:

Move 0 to KeyObject.hHandle
Move (WinAPI_BCryptImportKeyPair(hAlgorithm, hImportKey, AddressOf(ucaBlobType),
        KeyObject.hHandle,        // <- 0, by value
        AddressOf(ucaBlob), SizeOfArray(ucaBlob), dwFlags)) to iStatus

So the provider writes the new key handle to address 0. The function returns hHandle = 0
on success, and on x64 the write itself is an access violation.

The intended idiom is established either side of it in the same file — BCryptImportKey
(line 1180), BCryptGenerateKeyPair (1049) and BCryptOpenAlgorithmProvider (1248) all pass
AddressOf(...).

The second defect

The error path frees KeyObject.hHandle, which is definitionally 0 at that point, and
leaves the Alloc'd KeyObject.pPointer behind:

If (iStatus <> STATUS_SUCCESS) Begin
    Move (Free(KeyObject.hHandle)) to void     // frees nothing; leaks pPointer
    Move 0 to KeyObject.hHandle
End

BCryptImportKey frees pPointer, which is the correct field.

The third, in the second commit

BCryptImportKeyPair has no pbKeyObject/cbKeyObject parameter — the two the sibling
BCryptImportKey uses for a caller-supplied key object are absent from the seven this API
declares. The provider allocates the key object internally and BCryptDestroyKey releases it.

The Alloc(cbKeyObject) was therefore never passed to the API and never reachable: on success
the block came back in pPointer for the caller to free, while the key object the provider
actually used was somewhere else entirely.

cCngKey.Destroy_Object already guards its release with If (pbKeyObject <> 0), so leaving
pPointer at 0 needs no change on the consuming side.

The two commits are separable — take the first alone if you would rather keep the allocation.

Base branch

Raised against 26-0-package-manager so it sits on top of #3's BCRYPT_PROVIDER_HANDLE fix,
which is in the same file and the same family (an out-parameter/handle-width mistake that only
bites on x64). Both commits also cherry-pick onto production/stable as a pair, if you would
rather land it on the release line first — verified.

production/stable is currently tag 1.0.0, which is what the package manager installs, so
as things stand neither this nor #3 reaches anyone using the published packages.

How far this is verified — and how far it is not

Being precise, because the distinction matters:

Verified against real Windows CNG. We are running the corrected call pattern in a passkey
implementation on x64. Because the wrapper is unusable as shipped, we declared our own
External_Function against the same BCryptImportKeyPair export in Bcrypt.dll with
Pointer phKey, and call it with AddressOf(...). With that one change and nothing else, an
ECDSA P-256 public key imported from a WebAuthn attestation as BCRYPT_ECCPUBLIC_BLOB yields a
non-zero key handle, and BCryptVerifySignature returns STATUS_SUCCESS for a good assertion
signature and STATUS_INVALID_SIGNATURE for a tampered one. That is the evidence that
AddressOf is the fix.

Not verified. I have not compiled this patched file into a program that exercises
cCryptoApiNextGen.BCryptImportKeyPair through the class, because nothing in the library
reaches it — there is no caller to route through and no test for it. The change is a
by-inspection correction against your own declaration and your own BCryptImportKey, backed by
the independent binding above.

If it would help, the natural home for a regression test is an ImportKeyPair on
cCngAlgorithmProvider mirroring the existing ImportKey, which would also give the function
its first caller. Happy to add that here if you want it in the same PR.

HarmWibier and others added 5 commits January 22, 2026 14:56
BCryptDuplicateHash and BCryptDuplicateKey fetched BCRYPT_PROVIDER_HANDLE
via BCryptNamedUIntegerPropertyValue, which only accepts 4-byte values.
BCRYPT_PROVIDER_HANDLE is a native Handle, 8 bytes on x64, so the size
check silently returned 0 and the next BCryptGetProperty call
dereferenced a null handle.

Both call sites now fetch the handle via raw BCryptNamedPropertyValue
and copy it in only when the returned size matches SizeOfType(Handle).

BCryptGetProperty's blanket 8-to-4 truncation is removed since it was
corrupting the newly fetched 8-byte handle. The same tolerance is
re-added scoped to BCryptNamedUIntegerPropertyValue, the one caller
that legitimately wants 4 bytes. NCryptNamedUIntegerPropertyValue gets
the same tolerance for symmetry, though it has no current callers.
…ge-manager

Fix BCRYPT_PROVIDER_HANDLE access violation in CNG.pkg
… field

BCryptImportKeyPair's fourth parameter is BCRYPT_KEY_HANDLE *phKey, an
out-parameter, and bcrypt.h.pkg declares it correctly as `Pointer phKey`.
The call site passed `KeyObject.hHandle` by value instead, so the provider
wrote the new key handle to address 0 rather than into the struct. The
function therefore returned hHandle = 0 on success, and on x64 the write
itself is an access violation.

The surrounding code already shows the intended idiom: BCryptImportKey
immediately above passes AddressOf(KeyObject.hHandle), as do
BCryptGenerateKeyPair and BCryptOpenAlgorithmProvider.

The error path had the matching mistake in reverse: it freed
KeyObject.hHandle, which is definitionally 0 at that point, and left the
Alloc'd KeyObject.pPointer behind. It now frees pPointer, as
BCryptImportKey does.

Found while importing an ECDSA P-256 public key from a WebAuthn
attestation to verify an assertion signature. Nothing in the library or
the demo calls BCryptImportKeyPair, which is why it went unnoticed.
BCryptImportKeyPair has no pbKeyObject/cbKeyObject parameter - bcrypt.h.pkg
declares seven parameters, and the two the sibling BCryptImportKey uses for
a caller-supplied key object are not among them. The provider allocates the
key object internally and BCryptDestroyKey releases it.

The Alloc was therefore never passed to the API and never reachable: on
success the block was returned in pPointer for the caller to free, and the
key object the provider actually used was somewhere else entirely.

cCngKey.Destroy_Object already guards its Free with `If (pbKeyObject <> 0)`,
so leaving pPointer at 0 needs no change on the consuming side.
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