[PATCH v2 0/2] MR10561: Draft: Fix handling by SChannel for CNG/NCrypt client certificates for mTLS
Part 3 of my attempt to run the [Niko Home Control programming software](https://appdb.winehq.org/objectManager.php?sClass=application&iId=21635) under Wine. If I understand correctly, the current implementation only supports retrieving via the legacy CryptoAPI (CAPI). The Niko app uses Cryptography Next Generation (CNG/NCrypt) so this PRs adds support for it in acquire_credentials_handle. -- v2: Fix windows tests https://gitlab.winehq.org/wine/wine/-/merge_requests/10561
From: Benoît Legat <benoit.legat@gmail.com> --- dlls/secur32/Makefile.in | 2 +- dlls/secur32/schannel.c | 140 +++++++++++++++++++++++++++++++++- dlls/secur32/tests/schannel.c | 54 +++++++++++++ 3 files changed, 194 insertions(+), 2 deletions(-) diff --git a/dlls/secur32/Makefile.in b/dlls/secur32/Makefile.in index 2120a9c0e02..23704ad6935 100644 --- a/dlls/secur32/Makefile.in +++ b/dlls/secur32/Makefile.in @@ -2,7 +2,7 @@ MODULE = secur32.dll IMPORTLIB = secur32 UNIXLIB = secur32.so IMPORTS = advapi32 -DELAYIMPORTS = crypt32 +DELAYIMPORTS = crypt32 ncrypt UNIX_CFLAGS = $(GNUTLS_CFLAGS) SOURCES = \ diff --git a/dlls/secur32/schannel.c b/dlls/secur32/schannel.c index 83aea0b4737..7d31713e7a5 100644 --- a/dlls/secur32/schannel.c +++ b/dlls/secur32/schannel.c @@ -32,6 +32,8 @@ #include "sspi.h" #define SCHANNEL_USE_BLACKLISTS #include "schannel.h" +#include "bcrypt.h" +#include "ncrypt.h" #include "wine/unixlib.h" #include "wine/debug.h" @@ -561,6 +563,141 @@ static BYTE *get_key_blob(const CERT_CONTEXT *ctx, DWORD *size) return ret; } +static void reverse_bytes(BYTE *buf, ULONG len) +{ + BYTE tmp; + ULONG i; + for (i = 0; i < len / 2; i++) + { + tmp = buf[i]; + buf[i] = buf[len - i - 1]; + buf[len - i - 1] = tmp; + } +} + +/* Convert BCRYPT_RSAKEY_BLOB (big-endian) to CAPI PRIVATEKEYBLOB (little-endian) */ +static BYTE *convert_bcrypt_to_capi(const BYTE *bcrypt_blob, DWORD bcrypt_size, DWORD *out_size) +{ + const BCRYPT_RSAKEY_BLOB *hdr = (const BCRYPT_RSAKEY_BLOB *)bcrypt_blob; + DWORD bitlen, half, modlen, capi_size; + BLOBHEADER *blob_hdr; + RSAPUBKEY *rsa_hdr; + const BYTE *src; + BYTE *buf, *dst; + + if (bcrypt_size < sizeof(*hdr)) return NULL; + if (hdr->Magic != BCRYPT_RSAFULLPRIVATE_MAGIC) + { + TRACE("unexpected magic %#lx\n", (unsigned long)hdr->Magic); + return NULL; + } + + bitlen = hdr->BitLength; + modlen = bitlen / 8; + half = bitlen / 16; + + /* CAPI blob: BLOBHEADER + RSAPUBKEY + modulus + p + q + e1 + e2 + coeff + d */ + capi_size = sizeof(BLOBHEADER) + sizeof(RSAPUBKEY) + modlen + 5 * half + modlen; + + if (!(buf = malloc(capi_size + MAX_LEAD_BYTES))) return NULL; + + /* BLOBHEADER */ + blob_hdr = (BLOBHEADER *)buf; + blob_hdr->bType = 0x07; /* PRIVATEKEYBLOB */ + blob_hdr->bVersion = 0x02; /* CUR_BLOB_VERSION */ + blob_hdr->reserved = 0; + blob_hdr->aiKeyAlg = 0xa400; /* CALG_RSA_KEYX */ + + /* RSAPUBKEY */ + rsa_hdr = (RSAPUBKEY *)(buf + sizeof(BLOBHEADER)); + rsa_hdr->magic = 0x32415352; /* "RSA2" */ + rsa_hdr->bitlen = bitlen; + /* public exponent: BCRYPT stores big-endian variable-length, CAPI stores as DWORD */ + rsa_hdr->pubexp = 0; + src = bcrypt_blob + sizeof(*hdr); + if (hdr->cbPublicExp <= 4) + { + DWORD i; + for (i = 0; i < hdr->cbPublicExp; i++) + rsa_hdr->pubexp = (rsa_hdr->pubexp << 8) | src[i]; + } + + /* Skip past public exponent in BCRYPT blob */ + src += hdr->cbPublicExp; + + dst = (BYTE *)(rsa_hdr + 1); + + /* Modulus: copy and reverse (big-endian to little-endian) */ + memcpy(dst, src, modlen); reverse_bytes(dst, modlen); + src += hdr->cbModulus; dst += modlen; + + /* Prime1 (p) */ + memcpy(dst, src, half); reverse_bytes(dst, half); + src += hdr->cbPrime1; dst += half; + + /* Prime2 (q) */ + memcpy(dst, src, half); reverse_bytes(dst, half); + src += hdr->cbPrime2; dst += half; + + /* Exponent1 (dp) */ + memcpy(dst, src, half); reverse_bytes(dst, half); + src += hdr->cbPrime1; dst += half; + + /* Exponent2 (dq) */ + memcpy(dst, src, half); reverse_bytes(dst, half); + src += hdr->cbPrime2; dst += half; + + /* Coefficient (InverseQ) */ + memcpy(dst, src, half); reverse_bytes(dst, half); + src += hdr->cbPrime1; dst += half; + + /* PrivateExponent (d) */ + memcpy(dst, src, modlen); reverse_bytes(dst, modlen); + + *out_size = capi_size + MAX_LEAD_BYTES; + return buf; +} + +static BYTE *get_key_blob_ncrypt(const CERT_CONTEXT *ctx, DWORD *size) +{ + CERT_KEY_CONTEXT keyctx; + DWORD ctx_size = sizeof(keyctx); + NCRYPT_KEY_HANDLE key; + DWORD blob_size; + BYTE *bcrypt_buf, *capi_buf; + SECURITY_STATUS status; + + if (!CertGetCertificateContextProperty(ctx, CERT_KEY_CONTEXT_PROP_ID, &keyctx, &ctx_size)) + return NULL; + if (keyctx.dwKeySpec != CERT_NCRYPT_KEY_SPEC) + return NULL; + + key = keyctx.hCryptProv; + + status = NCryptExportKey(key, 0, BCRYPT_RSAFULLPRIVATE_BLOB, NULL, NULL, 0, &blob_size, 0); + if (status) + { + TRACE("NCryptExportKey size query failed: %#lx\n", status); + return NULL; + } + + if (!(bcrypt_buf = malloc(blob_size))) return NULL; + + status = NCryptExportKey(key, 0, BCRYPT_RSAFULLPRIVATE_BLOB, NULL, bcrypt_buf, blob_size, &blob_size, 0); + if (status) + { + TRACE("NCryptExportKey failed: %#lx\n", status); + free(bcrypt_buf); + return NULL; + } + + /* Convert BCRYPT blob to CAPI PRIVATEKEYBLOB format */ + capi_buf = convert_bcrypt_to_capi(bcrypt_buf, blob_size, size); + free(bcrypt_buf); + + return capi_buf; +} + static SECURITY_STATUS acquire_credentials_handle(ULONG fCredentialUse, const SCHANNEL_CRED *schanCred, PCredHandle phCredential, PTimeStamp ptsExpiry) { @@ -614,7 +751,8 @@ static SECURITY_STATUS acquire_credentials_handle(ULONG fCredentialUse, creds->credential_use = fCredentialUse; creds->enabled_protocols = enabled_protocols; - if (cert && !(key_blob = get_key_blob(cert, &key_size))) goto fail; + if (cert && !(key_blob = get_key_blob(cert, &key_size)) + && !(key_blob = get_key_blob_ncrypt(cert, &key_size))) goto fail; params.c = creds; if (cert) { diff --git a/dlls/secur32/tests/schannel.c b/dlls/secur32/tests/schannel.c index 04ef9ca6880..a3c8b63547e 100644 --- a/dlls/secur32/tests/schannel.c +++ b/dlls/secur32/tests/schannel.c @@ -2051,6 +2051,59 @@ static void test_connection_shutdown(void) FreeCredentialsHandle( &cred_handle ); } +static void test_ncrypt_key_credentials(void) +{ + SCHANNEL_CRED schanCred; + CredHandle cred; + SECURITY_STATUS st; + CRYPT_DATA_BLOB pfx; + HCERTSTORE store; + const CERT_CONTEXT *cert; + CERT_KEY_CONTEXT key_ctx; + DWORD size; + BOOL ret; + + pfx.pbData = (BYTE *)pfxdata; + pfx.cbData = sizeof(pfxdata); + store = PFXImportCertStore(&pfx, NULL, PKCS12_NO_PERSIST_KEY | PKCS12_ALWAYS_CNG_KSP); + ok(store != NULL, "PFXImportCertStore failed: %lu\n", GetLastError()); + if (!store) return; + + cert = CertFindCertificateInStore(store, X509_ASN_ENCODING, 0, CERT_FIND_ANY, NULL, NULL); + ok(cert != NULL, "CertFindCertificateInStore failed: %lu\n", GetLastError()); + if (!cert) + { + CertCloseStore(store, 0); + return; + } + + /* Verify the key is NCrypt. */ + size = sizeof(key_ctx); + key_ctx.hCryptProv = key_ctx.dwKeySpec = 0; + ret = CertGetCertificateContextProperty(cert, CERT_KEY_CONTEXT_PROP_ID, &key_ctx, &size); + ok(ret, "CertGetCertificateContextProperty failed: %lu\n", GetLastError()); + todo_wine + ok(key_ctx.dwKeySpec == CERT_NCRYPT_KEY_SPEC, + "expected CERT_NCRYPT_KEY_SPEC, got %lu\n", key_ctx.dwKeySpec); + + /* AcquireCredentialsHandle should succeed with an NCrypt key. */ + init_cred(&schanCred); + schanCred.cCreds = 1; + schanCred.paCred = &cert; + st = AcquireCredentialsHandleA(NULL, (SEC_CHAR *)UNISP_NAME_A, SECPKG_CRED_OUTBOUND, + NULL, &schanCred, NULL, NULL, &cred, NULL); + ok(st == SEC_E_OK, "AcquireCredentialsHandleA outbound with NCrypt key failed: %08lx\n", st); + if (st == SEC_E_OK) FreeCredentialsHandle(&cred); + + st = AcquireCredentialsHandleA(NULL, (SEC_CHAR *)UNISP_NAME_A, SECPKG_CRED_INBOUND, + NULL, &schanCred, NULL, NULL, &cred, NULL); + ok(st == SEC_E_OK, "AcquireCredentialsHandleA inbound with NCrypt key failed: %08lx\n", st); + if (st == SEC_E_OK) FreeCredentialsHandle(&cred); + + CertFreeCertificateContext(cert); + CertCloseStore(store, 0); +} + START_TEST(schannel) { WSADATA wsa_data; @@ -2059,6 +2112,7 @@ START_TEST(schannel) test_cread_attrs(); testAcquireSecurityContext(); + test_ncrypt_key_credentials(); test_InitializeSecurityContext(); test_communication(); test_application_protocol_negotiation(); -- GitLab https://gitlab.winehq.org/wine/wine/-/merge_requests/10561
From: Benoît Legat <benoit.legat@gmail.com> --- dlls/secur32/tests/schannel.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dlls/secur32/tests/schannel.c b/dlls/secur32/tests/schannel.c index a3c8b63547e..3bfde3be406 100644 --- a/dlls/secur32/tests/schannel.c +++ b/dlls/secur32/tests/schannel.c @@ -2065,7 +2065,7 @@ static void test_ncrypt_key_credentials(void) pfx.pbData = (BYTE *)pfxdata; pfx.cbData = sizeof(pfxdata); - store = PFXImportCertStore(&pfx, NULL, PKCS12_NO_PERSIST_KEY | PKCS12_ALWAYS_CNG_KSP); + store = PFXImportCertStore(&pfx, NULL, CRYPT_EXPORTABLE | PKCS12_NO_PERSIST_KEY | PKCS12_ALWAYS_CNG_KSP); ok(store != NULL, "PFXImportCertStore failed: %lu\n", GetLastError()); if (!store) return; -- GitLab https://gitlab.winehq.org/wine/wine/-/merge_requests/10561
Hans Leidekker (@hans) commented about dlls/secur32/schannel.c:
+static void reverse_bytes(BYTE *buf, ULONG len) +{ + BYTE tmp; + ULONG i; + for (i = 0; i < len / 2; i++) + { + tmp = buf[i]; + buf[i] = buf[len - i - 1]; + buf[len - i - 1] = tmp; + } +} + +/* Convert BCRYPT_RSAKEY_BLOB (big-endian) to CAPI PRIVATEKEYBLOB (little-endian) */ +static BYTE *convert_bcrypt_to_capi(const BYTE *bcrypt_blob, DWORD bcrypt_size, DWORD *out_size) +{
CAPI is the old API. It would be better to change the Unix side to accept a BCrypt key blob and then convert from CAPI to BCrypt on the PE side. That also avoids conversion from big-endian to little-endian and back. -- https://gitlab.winehq.org/wine/wine/-/merge_requests/10561#note_135026
participants (3)
-
Benoît Legat -
Benoît Legat (@blegat) -
Hans Leidekker (@hans)