[PATCH v7 0/6] MR10532: Draft: crypt32: Implement PFXExportCertStoreEx support
Part 2 of my attempt to run the [Niko Home Control programming software](https://appdb.winehq.org/objectManager.php?sClass=application&iId=21635) under Wine. This .NET application ends up calling \``PFXExportCertStoreEx` (or `PFXExportCertStore` ) since it redirects to it which is a stub at the moment. According to the [documentation](https://learn.microsoft.com/en-us/windows/win32/api/wincrypt/nf-wincrypt-pfx...), if the `EXPORT_PRIVATE_KEYS` flag is set, we should export the private key as well. For this, we use `NCryptExportKey` which is part of `ncrypt` so we have to add `ncrypt` to the `Makefile.in` as well. For exporting, I used GnuTLS library. As it seems to be only available in the unix side, I added a helper in `unixlib.c`. **EDIT** Looking at the behavior of Windows with the tests, it seems that when `cert` is `NULL` we should not fail but instead produce a valid but empty `PKCS#12`. I changed this to reflect Windows' behavior. -- v7: Fix Fix https://gitlab.winehq.org/wine/wine/-/merge_requests/10532
From: Benoît Legat <benoit.legat@gmail.com> --- dlls/crypt32/Makefile.in | 2 +- dlls/crypt32/crypt32_private.h | 12 +++ dlls/crypt32/pfx.c | 118 ++++++++++++++++++++++- dlls/crypt32/tests/store.c | 154 ++++++++++++++++++++++++++++++ dlls/crypt32/unixlib.c | 167 +++++++++++++++++++++++++++++++++ 5 files changed, 450 insertions(+), 3 deletions(-) diff --git a/dlls/crypt32/Makefile.in b/dlls/crypt32/Makefile.in index 7f35b0e4164..9af443bfb36 100644 --- a/dlls/crypt32/Makefile.in +++ b/dlls/crypt32/Makefile.in @@ -2,7 +2,7 @@ EXTRADEFS = -D_CRYPT32_ MODULE = crypt32.dll UNIXLIB = crypt32.so IMPORTLIB = crypt32 -IMPORTS = user32 advapi32 bcrypt +IMPORTS = user32 advapi32 bcrypt ncrypt DELAYIMPORTS = cryptnet UNIX_LIBS = $(SECURITY_LIBS) UNIX_CFLAGS = $(GNUTLS_CFLAGS) diff --git a/dlls/crypt32/crypt32_private.h b/dlls/crypt32/crypt32_private.h index 17253bde695..7b6bdfa9df7 100644 --- a/dlls/crypt32/crypt32_private.h +++ b/dlls/crypt32/crypt32_private.h @@ -513,6 +513,17 @@ struct enum_root_certs_params DWORD *needed; }; +struct export_cert_store_params +{ + const BYTE *cert_data; + DWORD cert_size; + const BYTE *key_blob; + DWORD key_blob_size; + const WCHAR *password; + BYTE *pfx_data; + DWORD *pfx_size; +}; + enum unix_funcs { unix_process_attach, @@ -522,6 +533,7 @@ enum unix_funcs unix_import_store_cert, unix_close_cert_store, unix_enum_root_certs, + unix_export_cert_store, unix_funcs_count, }; diff --git a/dlls/crypt32/pfx.c b/dlls/crypt32/pfx.c index a833fd42366..6d134163f90 100644 --- a/dlls/crypt32/pfx.c +++ b/dlls/crypt32/pfx.c @@ -22,6 +22,8 @@ #include "windef.h" #include "winbase.h" #include "wincrypt.h" +#include "bcrypt.h" +#include "ncrypt.h" #include "snmp.h" #include "crypt32_private.h" @@ -244,6 +246,118 @@ BOOL WINAPI PFXExportCertStore( HCERTSTORE store, CRYPT_DATA_BLOB *pfx, const WC BOOL WINAPI PFXExportCertStoreEx( HCERTSTORE store, CRYPT_DATA_BLOB *pfx, const WCHAR *password, void *reserved, DWORD flags ) { - FIXME( "(%p, %p, %p, %p, %08lx): stub\n", store, pfx, password, reserved, flags ); - return FALSE; + const CERT_CONTEXT *cert; + CERT_KEY_CONTEXT key_ctx; + struct export_cert_store_params params; + DWORD key_ctx_size, key_blob_size, pfx_size; + BYTE *key_blob = NULL; + SECURITY_STATUS sec_status; + NTSTATUS status; + BOOL ret = FALSE; + + TRACE( "(%p, %p, %p, %p, %08lx)\n", store, pfx, password, reserved, flags ); + + if (!store || !pfx) + { + SetLastError( ERROR_INVALID_PARAMETER ); + return FALSE; + } + + /* Find the first certificate in the store (may be NULL for empty stores). */ + cert = CertEnumCertificatesInStore( store, NULL ); + + /* Get the private key if EXPORT_PRIVATE_KEYS is requested. */ + if (cert && (flags & EXPORT_PRIVATE_KEYS)) + { + key_ctx_size = sizeof(key_ctx); + if (!CertGetCertificateContextProperty( cert, CERT_KEY_CONTEXT_PROP_ID, &key_ctx, &key_ctx_size )) + { + WARN( "no key context on certificate, error %08lx\n", GetLastError() ); + if (flags & REPORT_NOT_ABLE_TO_EXPORT_PRIVATE_KEY) + { + SetLastError( NTE_NOT_FOUND ); + CertFreeCertificateContext( cert ); + return FALSE; + } + } + else if (key_ctx.dwKeySpec == CERT_NCRYPT_KEY_SPEC) + { + /* Query key blob size first. */ + sec_status = NCryptExportKey( key_ctx.hCryptProv, 0, BCRYPT_RSAFULLPRIVATE_BLOB, NULL, + NULL, 0, &key_blob_size, 0 ); + if (sec_status) + { + WARN( "NCryptExportKey size query failed %08lx\n", (DWORD)sec_status ); + SetLastError( sec_status ); + CertFreeCertificateContext( cert ); + return FALSE; + } + + key_blob = CryptMemAlloc( key_blob_size ); + if (!key_blob) + { + SetLastError( ERROR_OUTOFMEMORY ); + CertFreeCertificateContext( cert ); + return FALSE; + } + + sec_status = NCryptExportKey( key_ctx.hCryptProv, 0, BCRYPT_RSAFULLPRIVATE_BLOB, NULL, + key_blob, key_blob_size, &key_blob_size, 0 ); + if (sec_status) + { + WARN( "NCryptExportKey failed %08lx\n", (DWORD)sec_status ); + SetLastError( sec_status ); + CryptMemFree( key_blob ); + CertFreeCertificateContext( cert ); + return FALSE; + } + } + else + { + FIXME( "dwKeySpec %lu not supported\n", key_ctx.dwKeySpec ); + if (flags & REPORT_NOT_ABLE_TO_EXPORT_PRIVATE_KEY) + { + SetLastError( NTE_NOT_FOUND ); + CertFreeCertificateContext( cert ); + return FALSE; + } + } + } + + params.cert_data = cert ? cert->pbCertEncoded : NULL; + params.cert_size = cert ? cert->cbCertEncoded : 0; + params.key_blob = key_blob; + params.key_blob_size = key_blob ? key_blob_size : 0; + params.password = password; + + if (!pfx->pbData) + { + /* Size query. */ + pfx_size = 0; + params.pfx_data = NULL; + params.pfx_size = &pfx_size; + } + else + { + /* Actual export. */ + pfx_size = pfx->cbData; + params.pfx_data = pfx->pbData; + params.pfx_size = &pfx_size; + } + + status = CRYPT32_CALL( export_cert_store, ¶ms ); + if (status) + { + WARN( "unix export_cert_store failed %08lx\n", (DWORD)status ); + SetLastError( RtlNtStatusToDosError( status ) ); + } + else + { + pfx->cbData = pfx_size; + ret = TRUE; + } + + CryptMemFree( key_blob ); + CertFreeCertificateContext( cert ); + return ret; } diff --git a/dlls/crypt32/tests/store.c b/dlls/crypt32/tests/store.c index 678d4810d8b..0fe0d3609c3 100644 --- a/dlls/crypt32/tests/store.c +++ b/dlls/crypt32/tests/store.c @@ -3522,6 +3522,159 @@ static void test_PFXImportCertStore(void) CertCloseStore( store, 0 ); } +static void test_PFXExportCertStoreEx(void) +{ + HCERTSTORE store, store2; + CRYPT_DATA_BLOB pfx, exported; + const CERT_CONTEXT *cert, *cert2; + DWORD count; + BOOL ret; + + /* Test NULL parameters. */ + SetLastError( 0xdeadbeef ); + ret = PFXExportCertStoreEx( NULL, NULL, NULL, NULL, 0 ); + ok( !ret, "expected failure\n" ); + ok( GetLastError() == ERROR_INVALID_PARAMETER, "got %lu\n", GetLastError() ); + + exported.pbData = NULL; + exported.cbData = 0; + + SetLastError( 0xdeadbeef ); + ret = PFXExportCertStoreEx( NULL, &exported, NULL, NULL, 0 ); + ok( !ret, "expected failure\n" ); + ok( GetLastError() == ERROR_INVALID_PARAMETER, "got %lu\n", GetLastError() ); + + /* Empty store succeeds (produces a valid but empty PFX). */ + store = CertOpenStore( CERT_STORE_PROV_MEMORY, 0, 0, 0, NULL ); + ok( store != NULL, "CertOpenStore failed %lu\n", GetLastError() ); + + exported.pbData = NULL; + exported.cbData = 0; + ret = PFXExportCertStoreEx( store, &exported, NULL, NULL, 0 ); + ok( ret, "PFXExportCertStoreEx empty store size query failed %lu\n", GetLastError() ); + ok( exported.cbData > 0, "expected nonzero size\n" ); + CertCloseStore( store, 0 ); + + /* Import pfxdata (cert + private key), then export without private key. */ + pfx.pbData = (BYTE *)pfxdata; + pfx.cbData = sizeof(pfxdata); + store = PFXImportCertStore( &pfx, NULL, CRYPT_EXPORTABLE | CRYPT_USER_KEYSET | PKCS12_NO_PERSIST_KEY ); + ok( store != NULL, "PFXImportCertStore failed %lu\n", GetLastError() ); + + /* Size query. */ + exported.pbData = NULL; + exported.cbData = 0; + ret = PFXExportCertStoreEx( store, &exported, L"test", NULL, 0 ); + ok( ret, "PFXExportCertStoreEx size query failed %lu\n", GetLastError() ); + ok( exported.cbData > 0, "expected nonzero size\n" ); + + /* Actual export. */ + exported.pbData = HeapAlloc( GetProcessHeap(), 0, exported.cbData ); + ok( exported.pbData != NULL, "HeapAlloc failed\n" ); + ret = PFXExportCertStoreEx( store, &exported, L"test", NULL, 0 ); + ok( ret, "PFXExportCertStoreEx failed %lu\n", GetLastError() ); + + /* Verify exported blob is valid PFX. */ + ret = PFXIsPFXBlob( &exported ); + ok( ret, "exported blob is not valid PFX\n" ); + + /* Re-import and verify certificate round-trip. */ + store2 = PFXImportCertStore( &exported, L"test", PKCS12_NO_PERSIST_KEY ); + ok( store2 != NULL, "PFXImportCertStore of exported data failed %lu\n", GetLastError() ); + if (store2) + { + count = countCertsInStore( store2 ); + ok( count == 1, "expected 1 cert, got %lu\n", count ); + + cert = CertFindCertificateInStore( store, X509_ASN_ENCODING, 0, CERT_FIND_ANY, NULL, NULL ); + cert2 = CertFindCertificateInStore( store2, X509_ASN_ENCODING, 0, CERT_FIND_ANY, NULL, NULL ); + ok( cert != NULL && cert2 != NULL, "failed to find certs\n" ); + if (cert && cert2) + { + ok( cert->cbCertEncoded == cert2->cbCertEncoded, + "cert size mismatch: %lu vs %lu\n", cert->cbCertEncoded, cert2->cbCertEncoded ); + ok( !memcmp( cert->pbCertEncoded, cert2->pbCertEncoded, cert->cbCertEncoded ), + "cert data mismatch\n" ); + } + if (cert) CertFreeCertificateContext( cert ); + if (cert2) CertFreeCertificateContext( cert2 ); + CertCloseStore( store2, 0 ); + } + HeapFree( GetProcessHeap(), 0, exported.pbData ); + + /* Export with EXPORT_PRIVATE_KEYS. */ + exported.pbData = NULL; + exported.cbData = 0; + ret = PFXExportCertStoreEx( store, &exported, L"test", NULL, EXPORT_PRIVATE_KEYS ); + ok( ret, "PFXExportCertStoreEx size query failed %lu\n", GetLastError() ); + ok( exported.cbData > 0, "expected nonzero size\n" ); + + exported.pbData = HeapAlloc( GetProcessHeap(), 0, exported.cbData ); + ok( exported.pbData != NULL, "HeapAlloc failed\n" ); + ret = PFXExportCertStoreEx( store, &exported, L"test", NULL, EXPORT_PRIVATE_KEYS ); + ok( ret, "PFXExportCertStoreEx with EXPORT_PRIVATE_KEYS failed %lu\n", GetLastError() ); + + ret = PFXIsPFXBlob( &exported ); + ok( ret, "exported blob with private key is not valid PFX\n" ); + + /* Re-import with private key and verify it's present. */ + store2 = PFXImportCertStore( &exported, L"test", PKCS12_NO_PERSIST_KEY ); + ok( store2 != NULL, "PFXImportCertStore failed %lu\n", GetLastError() ); + if (store2) + { + CERT_KEY_CONTEXT key; + DWORD size; + + cert2 = CertFindCertificateInStore( store2, X509_ASN_ENCODING, 0, CERT_FIND_ANY, NULL, NULL ); + ok( cert2 != NULL, "no cert in re-imported store %lu\n", GetLastError() ); + if (cert2) + { + size = sizeof(key); + ret = CertGetCertificateContextProperty( cert2, CERT_KEY_CONTEXT_PROP_ID, &key, &size ); + ok( ret, "no key context on re-imported cert %lu\n", GetLastError() ); + if (ret) + { + ok( key.hCryptProv != 0, "expected non-zero hCryptProv\n" ); + } + CertFreeCertificateContext( cert2 ); + } + CertCloseStore( store2, 0 ); + } + HeapFree( GetProcessHeap(), 0, exported.pbData ); + + /* Test cert-only export (import pfx_cert_only which has no private key). */ + CertCloseStore( store, 0 ); + pfx.pbData = (BYTE *)pfx_cert_only; + pfx.cbData = sizeof(pfx_cert_only); + store = PFXImportCertStore( &pfx, L"", 0 ); + ok( store != NULL, "PFXImportCertStore failed %lu\n", GetLastError() ); + + exported.pbData = NULL; + exported.cbData = 0; + ret = PFXExportCertStoreEx( store, &exported, L"", NULL, 0 ); + ok( ret, "PFXExportCertStoreEx cert-only size query failed %lu\n", GetLastError() ); + ok( exported.cbData > 0, "expected nonzero size\n" ); + + exported.pbData = HeapAlloc( GetProcessHeap(), 0, exported.cbData ); + ret = PFXExportCertStoreEx( store, &exported, L"", NULL, 0 ); + ok( ret, "PFXExportCertStoreEx cert-only failed %lu\n", GetLastError() ); + + ret = PFXIsPFXBlob( &exported ); + ok( ret, "cert-only exported blob is not valid PFX\n" ); + + store2 = PFXImportCertStore( &exported, L"", 0 ); + ok( store2 != NULL, "PFXImportCertStore of cert-only export failed %lu\n", GetLastError() ); + if (store2) + { + count = countCertsInStore( store2 ); + ok( count == 1, "expected 1 cert, got %lu\n", count ); + CertCloseStore( store2, 0 ); + } + HeapFree( GetProcessHeap(), 0, exported.pbData ); + + CertCloseStore( store, 0 ); +} + static void test_CryptQueryObject(void) { CRYPT_DATA_BLOB pfx; @@ -3586,5 +3739,6 @@ START_TEST(store) test_I_UpdateStore(); test_PFXImportCertStore(); + test_PFXExportCertStoreEx(); test_CryptQueryObject(); } diff --git a/dlls/crypt32/unixlib.c b/dlls/crypt32/unixlib.c index 5cfc54bf026..6355625bf58 100644 --- a/dlls/crypt32/unixlib.c +++ b/dlls/crypt32/unixlib.c @@ -41,6 +41,7 @@ #include "winbase.h" #include "winternl.h" #include "wincrypt.h" +#include "bcrypt.h" #include "crypt32_private.h" #include "wine/debug.h" @@ -65,10 +66,15 @@ MAKE_FUNCPTR(gnutls_pkcs12_bag_get_count); MAKE_FUNCPTR(gnutls_pkcs12_bag_get_data); MAKE_FUNCPTR(gnutls_pkcs12_bag_get_type); MAKE_FUNCPTR(gnutls_pkcs12_bag_init); +MAKE_FUNCPTR(gnutls_pkcs12_bag_set_crt); +MAKE_FUNCPTR(gnutls_pkcs12_bag_set_privkey); MAKE_FUNCPTR(gnutls_pkcs12_deinit); +MAKE_FUNCPTR(gnutls_pkcs12_export2); +MAKE_FUNCPTR(gnutls_pkcs12_generate_mac2); MAKE_FUNCPTR(gnutls_pkcs12_get_bag); MAKE_FUNCPTR(gnutls_pkcs12_import); MAKE_FUNCPTR(gnutls_pkcs12_init); +MAKE_FUNCPTR(gnutls_pkcs12_set_bag); MAKE_FUNCPTR(gnutls_pkcs12_verify_mac); MAKE_FUNCPTR(gnutls_x509_crt_deinit); MAKE_FUNCPTR(gnutls_x509_crt_export); @@ -78,6 +84,7 @@ MAKE_FUNCPTR(gnutls_x509_privkey_deinit); MAKE_FUNCPTR(gnutls_x509_privkey_export_rsa_raw2); MAKE_FUNCPTR(gnutls_x509_privkey_get_pk_algorithm2); MAKE_FUNCPTR(gnutls_x509_privkey_import_pkcs8); +MAKE_FUNCPTR(gnutls_x509_privkey_import_rsa_raw); MAKE_FUNCPTR(gnutls_x509_privkey_init); #undef MAKE_FUNCPTR @@ -125,10 +132,15 @@ static NTSTATUS process_attach( void *args ) LOAD_FUNCPTR(gnutls_pkcs12_bag_get_data) LOAD_FUNCPTR(gnutls_pkcs12_bag_get_type) LOAD_FUNCPTR(gnutls_pkcs12_bag_init) + LOAD_FUNCPTR(gnutls_pkcs12_bag_set_crt) + LOAD_FUNCPTR(gnutls_pkcs12_bag_set_privkey) LOAD_FUNCPTR(gnutls_pkcs12_deinit) + LOAD_FUNCPTR(gnutls_pkcs12_export2) + LOAD_FUNCPTR(gnutls_pkcs12_generate_mac2) LOAD_FUNCPTR(gnutls_pkcs12_get_bag) LOAD_FUNCPTR(gnutls_pkcs12_import) LOAD_FUNCPTR(gnutls_pkcs12_init) + LOAD_FUNCPTR(gnutls_pkcs12_set_bag) LOAD_FUNCPTR(gnutls_pkcs12_verify_mac) LOAD_FUNCPTR(gnutls_x509_crt_deinit) LOAD_FUNCPTR(gnutls_x509_crt_export) @@ -138,6 +150,7 @@ static NTSTATUS process_attach( void *args ) LOAD_FUNCPTR(gnutls_x509_privkey_export_rsa_raw2) LOAD_FUNCPTR(gnutls_x509_privkey_get_pk_algorithm2) LOAD_FUNCPTR(gnutls_x509_privkey_import_pkcs8) + LOAD_FUNCPTR(gnutls_x509_privkey_import_rsa_raw) LOAD_FUNCPTR(gnutls_x509_privkey_init) #undef LOAD_FUNCPTR @@ -521,6 +534,130 @@ static NTSTATUS close_cert_store( void *args ) return STATUS_SUCCESS; } +static NTSTATUS export_cert_store( void *args ) +{ + struct export_cert_store_params *params = args; + gnutls_pkcs12_t p12 = NULL; + gnutls_pkcs12_bag_t cert_bag = NULL, key_bag = NULL; + gnutls_x509_crt_t crt = NULL; + gnutls_x509_privkey_t key = NULL; + gnutls_datum_t out = { NULL, 0 }; + char *pwd = NULL; + int ret; + NTSTATUS status = STATUS_INVALID_PARAMETER; + + if (!libgnutls_handle) return STATUS_DLL_NOT_FOUND; + + if (params->password && !(pwd = password_to_ascii( params->password ))) + return STATUS_NO_MEMORY; + + /* Import the certificate if provided. */ + if (params->cert_data && params->cert_size) + { + if ((ret = pgnutls_x509_crt_init( &crt )) < 0) goto error; + { + gnutls_datum_t cert_datum = { (unsigned char *)params->cert_data, params->cert_size }; + if ((ret = pgnutls_x509_crt_import( crt, &cert_datum, GNUTLS_X509_FMT_DER )) < 0) goto error; + } + + /* Create cert bag. */ + if ((ret = pgnutls_pkcs12_bag_init( &cert_bag )) < 0) goto error; + if ((ret = pgnutls_pkcs12_bag_set_crt( cert_bag, crt )) < 0) goto error; + } + + /* Import private key from BCRYPT_RSAKEY_BLOB if provided. */ + if (params->key_blob && params->key_blob_size) + { + const BCRYPT_RSAKEY_BLOB *hdr = (const BCRYPT_RSAKEY_BLOB *)params->key_blob; + const BYTE *ptr = params->key_blob + sizeof(*hdr); + gnutls_datum_t m, e, d, p, q, u; + + if (hdr->Magic != BCRYPT_RSAFULLPRIVATE_MAGIC) + { + WARN( "unexpected key blob magic %08lx\n", (unsigned long)hdr->Magic ); + status = STATUS_INVALID_PARAMETER; + goto done; + } + + /* Layout after header: PublicExp, Modulus, Prime1, Prime2, Exponent1, Exponent2, Coefficient, PrivateExponent */ + e.data = (unsigned char *)ptr; e.size = hdr->cbPublicExp; + ptr += hdr->cbPublicExp; + m.data = (unsigned char *)ptr; m.size = hdr->cbModulus; + ptr += hdr->cbModulus; + p.data = (unsigned char *)ptr; p.size = hdr->cbPrime1; + ptr += hdr->cbPrime1; + q.data = (unsigned char *)ptr; q.size = hdr->cbPrime2; + ptr += hdr->cbPrime2; + /* Skip Exponent1 and Exponent2 - GnuTLS computes them. */ + ptr += hdr->cbPrime1; /* Exponent1 */ + ptr += hdr->cbPrime2; /* Exponent2 */ + u.data = (unsigned char *)ptr; u.size = hdr->cbPrime1; + ptr += hdr->cbPrime1; + d.data = (unsigned char *)ptr; d.size = hdr->cbModulus; + + if ((ret = pgnutls_x509_privkey_init( &key )) < 0) goto error; + if ((ret = pgnutls_x509_privkey_import_rsa_raw( key, &m, &e, &d, &p, &q, &u )) < 0) + { + pgnutls_x509_privkey_deinit( key ); + key = NULL; + goto error; + } + + /* Create key bag. */ + if ((ret = pgnutls_pkcs12_bag_init( &key_bag )) < 0) goto error; + if ((ret = pgnutls_pkcs12_bag_set_privkey( key_bag, key, pwd ? pwd : "", 0 )) < 0) goto error; + } + + /* Build PKCS#12. */ + if ((ret = pgnutls_pkcs12_init( &p12 )) < 0) goto error; + if (cert_bag) + { + if ((ret = pgnutls_pkcs12_set_bag( p12, cert_bag )) < 0) goto error; + } + if (key_bag) + { + if ((ret = pgnutls_pkcs12_set_bag( p12, key_bag )) < 0) goto error; + } + + /* Generate MAC. */ + if ((ret = pgnutls_pkcs12_generate_mac2( p12, GNUTLS_MAC_SHA256, pwd ? pwd : "" )) < 0) goto error; + + /* Export. */ + if ((ret = pgnutls_pkcs12_export2( p12, GNUTLS_X509_FMT_DER, &out )) < 0) goto error; + + if (!params->pfx_data) + { + /* Size query. */ + *params->pfx_size = out.size; + status = STATUS_SUCCESS; + } + else if (*params->pfx_size < out.size) + { + *params->pfx_size = out.size; + status = STATUS_BUFFER_TOO_SMALL; + } + else + { + memcpy( params->pfx_data, out.data, out.size ); + *params->pfx_size = out.size; + status = STATUS_SUCCESS; + } + goto done; + +error: + pgnutls_perror( ret ); + status = STATUS_INVALID_PARAMETER; +done: + free( out.data ); + if (p12) pgnutls_pkcs12_deinit( p12 ); + if (key_bag) pgnutls_pkcs12_bag_deinit( key_bag ); + if (cert_bag) pgnutls_pkcs12_bag_deinit( cert_bag ); + if (key) pgnutls_x509_privkey_deinit( key ); + if (crt) pgnutls_x509_crt_deinit( crt ); + free( pwd ); + return status; +} + #else /* SONAME_LIBGNUTLS */ static NTSTATUS process_attach( void *args ) { return STATUS_SUCCESS; } @@ -529,6 +666,7 @@ static NTSTATUS open_cert_store( void *args ) { return STATUS_DLL_NOT_FOUND; } static NTSTATUS import_store_key( void *args ) { return STATUS_DLL_NOT_FOUND; } static NTSTATUS import_store_cert( void *args ) { return STATUS_DLL_NOT_FOUND; } static NTSTATUS close_cert_store( void *args ) { return STATUS_DLL_NOT_FOUND; } +static NTSTATUS export_cert_store( void *args ) { return STATUS_DLL_NOT_FOUND; } #endif /* SONAME_LIBGNUTLS */ @@ -860,6 +998,7 @@ const unixlib_entry_t __wine_unix_call_funcs[] = import_store_cert, close_cert_store, enum_root_certs, + export_cert_store, }; C_ASSERT( ARRAYSIZE(__wine_unix_call_funcs) == unix_funcs_count ); @@ -956,6 +1095,33 @@ static NTSTATUS wow64_enum_root_certs( void *args ) return enum_root_certs( ¶ms ); } +static NTSTATUS wow64_export_cert_store( void *args ) +{ + struct + { + PTR32 cert_data; + DWORD cert_size; + PTR32 key_blob; + DWORD key_blob_size; + PTR32 password; + PTR32 pfx_data; + PTR32 pfx_size; + } const *params32 = args; + + struct export_cert_store_params params = + { + ULongToPtr( params32->cert_data ), + params32->cert_size, + ULongToPtr( params32->key_blob ), + params32->key_blob_size, + ULongToPtr( params32->password ), + ULongToPtr( params32->pfx_data ), + ULongToPtr( params32->pfx_size ) + }; + + return export_cert_store( ¶ms ); +} + const unixlib_entry_t __wine_unix_call_wow64_funcs[] = { process_attach, @@ -965,6 +1131,7 @@ const unixlib_entry_t __wine_unix_call_wow64_funcs[] = wow64_import_store_cert, close_cert_store, wow64_enum_root_certs, + wow64_export_cert_store, }; C_ASSERT( ARRAYSIZE(__wine_unix_call_wow64_funcs) == unix_funcs_count ); -- GitLab https://gitlab.winehq.org/wine/wine/-/merge_requests/10532
From: Benoît Legat <benoit.legat@gmail.com> --- dlls/crypt32/pfx.c | 116 +++++++++++++++++++++++++++++++++++++++++ dlls/crypt32/unixlib.c | 7 ++- 2 files changed, 121 insertions(+), 2 deletions(-) diff --git a/dlls/crypt32/pfx.c b/dlls/crypt32/pfx.c index 6d134163f90..3d0caaf8cdd 100644 --- a/dlls/crypt32/pfx.c +++ b/dlls/crypt32/pfx.c @@ -243,6 +243,108 @@ BOOL WINAPI PFXExportCertStore( HCERTSTORE store, CRYPT_DATA_BLOB *pfx, const WC return PFXExportCertStoreEx( store, pfx, password, NULL, flags ); } +static void reverse_bytes( const BYTE *src, BYTE *dst, DWORD len ) +{ + DWORD i; + for (i = 0; i < len; i++) dst[i] = src[len - i - 1]; +} + +static BYTE *export_capi_key( HCRYPTPROV prov, DWORD key_spec, DWORD *out_size ) +{ + HCRYPTKEY hkey; + BYTE *capi_blob = NULL, *bcrypt_blob = NULL; + DWORD capi_size, bitlen, mod_len, prime_len, pos, out_pos; + BLOBHEADER *hdr; + RSAPUBKEY *rsakey; + BCRYPT_RSAKEY_BLOB *rsa_hdr; + + if (!CryptGetUserKey( prov, key_spec, &hkey )) + { + WARN( "CryptGetUserKey failed %08lx\n", GetLastError() ); + return NULL; + } + + /* Query size. */ + capi_size = 0; + if (!CryptExportKey( hkey, 0, PRIVATEKEYBLOB, 0, NULL, &capi_size )) + { + WARN( "CryptExportKey size query failed %08lx\n", GetLastError() ); + CryptDestroyKey( hkey ); + return NULL; + } + + capi_blob = CryptMemAlloc( capi_size ); + if (!capi_blob) + { + CryptDestroyKey( hkey ); + return NULL; + } + + if (!CryptExportKey( hkey, 0, PRIVATEKEYBLOB, 0, capi_blob, &capi_size )) + { + WARN( "CryptExportKey failed %08lx\n", GetLastError() ); + CryptMemFree( capi_blob ); + CryptDestroyKey( hkey ); + return NULL; + } + CryptDestroyKey( hkey ); + + hdr = (BLOBHEADER *)capi_blob; + rsakey = (RSAPUBKEY *)(hdr + 1); + bitlen = rsakey->bitlen; + mod_len = bitlen / 8; + prime_len = bitlen / 16; + + /* Build BCRYPT_RSAFULLPRIVATE_BLOB. */ + *out_size = sizeof(BCRYPT_RSAKEY_BLOB) + sizeof(rsakey->pubexp) + mod_len * 2 + prime_len * 5; + bcrypt_blob = CryptMemAlloc( *out_size ); + if (!bcrypt_blob) + { + CryptMemFree( capi_blob ); + return NULL; + } + + rsa_hdr = (BCRYPT_RSAKEY_BLOB *)bcrypt_blob; + rsa_hdr->Magic = BCRYPT_RSAFULLPRIVATE_MAGIC; + rsa_hdr->BitLength = bitlen; + rsa_hdr->cbPublicExp = sizeof(rsakey->pubexp); + rsa_hdr->cbModulus = mod_len; + rsa_hdr->cbPrime1 = prime_len; + rsa_hdr->cbPrime2 = prime_len; + + out_pos = sizeof(BCRYPT_RSAKEY_BLOB); + /* PublicExp (big-endian). */ + reverse_bytes( (const BYTE *)&rsakey->pubexp, bcrypt_blob + out_pos, sizeof(rsakey->pubexp) ); + out_pos += sizeof(rsakey->pubexp); + + /* CAPI data starts after BLOBHEADER + RSAPUBKEY. */ + pos = sizeof(BLOBHEADER) + sizeof(RSAPUBKEY); + + /* Modulus */ + reverse_bytes( capi_blob + pos, bcrypt_blob + out_pos, mod_len ); + pos += mod_len; out_pos += mod_len; + /* Prime1 */ + reverse_bytes( capi_blob + pos, bcrypt_blob + out_pos, prime_len ); + pos += prime_len; out_pos += prime_len; + /* Prime2 */ + reverse_bytes( capi_blob + pos, bcrypt_blob + out_pos, prime_len ); + pos += prime_len; out_pos += prime_len; + /* Exponent1 */ + reverse_bytes( capi_blob + pos, bcrypt_blob + out_pos, prime_len ); + pos += prime_len; out_pos += prime_len; + /* Exponent2 */ + reverse_bytes( capi_blob + pos, bcrypt_blob + out_pos, prime_len ); + pos += prime_len; out_pos += prime_len; + /* Coefficient */ + reverse_bytes( capi_blob + pos, bcrypt_blob + out_pos, prime_len ); + pos += prime_len; out_pos += prime_len; + /* PrivateExponent */ + reverse_bytes( capi_blob + pos, bcrypt_blob + out_pos, mod_len ); + + CryptMemFree( capi_blob ); + return bcrypt_blob; +} + BOOL WINAPI PFXExportCertStoreEx( HCERTSTORE store, CRYPT_DATA_BLOB *pfx, const WCHAR *password, void *reserved, DWORD flags ) { @@ -312,6 +414,20 @@ BOOL WINAPI PFXExportCertStoreEx( HCERTSTORE store, CRYPT_DATA_BLOB *pfx, const return FALSE; } } + else if (key_ctx.dwKeySpec == AT_KEYEXCHANGE || key_ctx.dwKeySpec == AT_SIGNATURE) + { + key_blob = export_capi_key( key_ctx.hCryptProv, key_ctx.dwKeySpec, &key_blob_size ); + if (!key_blob) + { + WARN( "failed to export CAPI key\n" ); + if (flags & REPORT_NOT_ABLE_TO_EXPORT_PRIVATE_KEY) + { + SetLastError( NTE_NOT_FOUND ); + CertFreeCertificateContext( cert ); + return FALSE; + } + } + } else { FIXME( "dwKeySpec %lu not supported\n", key_ctx.dwKeySpec ); diff --git a/dlls/crypt32/unixlib.c b/dlls/crypt32/unixlib.c index 6355625bf58..ad78996eeee 100644 --- a/dlls/crypt32/unixlib.c +++ b/dlls/crypt32/unixlib.c @@ -619,8 +619,11 @@ static NTSTATUS export_cert_store( void *args ) if ((ret = pgnutls_pkcs12_set_bag( p12, key_bag )) < 0) goto error; } - /* Generate MAC. */ - if ((ret = pgnutls_pkcs12_generate_mac2( p12, GNUTLS_MAC_SHA256, pwd ? pwd : "" )) < 0) goto error; + /* Generate MAC (only if there are bags, GnuTLS fails on empty PKCS#12). */ + if (cert_bag || key_bag) + { + if ((ret = pgnutls_pkcs12_generate_mac2( p12, GNUTLS_MAC_SHA256, pwd ? pwd : "" )) < 0) goto error; + } /* Export. */ if ((ret = pgnutls_pkcs12_export2( p12, GNUTLS_X509_FMT_DER, &out )) < 0) goto error; -- GitLab https://gitlab.winehq.org/wine/wine/-/merge_requests/10532
From: Benoît Legat <benoit.legat@gmail.com> --- dlls/crypt32/pfx.c | 31 +++++++++++++++++++++++++++++-- dlls/crypt32/unixlib.c | 31 +++++++++++-------------------- 2 files changed, 40 insertions(+), 22 deletions(-) diff --git a/dlls/crypt32/pfx.c b/dlls/crypt32/pfx.c index 3d0caaf8cdd..0b92761a802 100644 --- a/dlls/crypt32/pfx.c +++ b/dlls/crypt32/pfx.c @@ -440,8 +440,35 @@ BOOL WINAPI PFXExportCertStoreEx( HCERTSTORE store, CRYPT_DATA_BLOB *pfx, const } } - params.cert_data = cert ? cert->pbCertEncoded : NULL; - params.cert_size = cert ? cert->cbCertEncoded : 0; + if (!cert && !key_blob) + { + /* Empty store: return a minimal valid PKCS#12 (version 3, empty content). */ + static const BYTE empty_pfx[] = { + 0x30, 0x15, /* SEQUENCE */ + 0x02, 0x01, 0x03, /* INTEGER 3 (version) */ + 0x30, 0x10, /* SEQUENCE (authSafe) */ + 0x06, 0x09, 0x2a, 0x86, 0x48, 0x86, /* OID 1.2.840.113549.1.7.1 */ + 0xf7, 0x0d, 0x01, 0x07, 0x01, + 0xa0, 0x03, 0x04, 0x01, 0x00, /* [0] OCTET STRING (empty) */ + }; + if (!pfx->pbData) + { + pfx->cbData = sizeof(empty_pfx); + return TRUE; + } + if (pfx->cbData < sizeof(empty_pfx)) + { + SetLastError( ERROR_MORE_DATA ); + pfx->cbData = sizeof(empty_pfx); + return FALSE; + } + memcpy( pfx->pbData, empty_pfx, sizeof(empty_pfx) ); + pfx->cbData = sizeof(empty_pfx); + return TRUE; + } + + params.cert_data = cert->pbCertEncoded; + params.cert_size = cert->cbCertEncoded; params.key_blob = key_blob; params.key_blob_size = key_blob ? key_blob_size : 0; params.password = password; diff --git a/dlls/crypt32/unixlib.c b/dlls/crypt32/unixlib.c index ad78996eeee..fe7b48750c5 100644 --- a/dlls/crypt32/unixlib.c +++ b/dlls/crypt32/unixlib.c @@ -551,20 +551,17 @@ static NTSTATUS export_cert_store( void *args ) if (params->password && !(pwd = password_to_ascii( params->password ))) return STATUS_NO_MEMORY; - /* Import the certificate if provided. */ - if (params->cert_data && params->cert_size) + /* Import the certificate. */ + if ((ret = pgnutls_x509_crt_init( &crt )) < 0) goto error; { - if ((ret = pgnutls_x509_crt_init( &crt )) < 0) goto error; - { - gnutls_datum_t cert_datum = { (unsigned char *)params->cert_data, params->cert_size }; - if ((ret = pgnutls_x509_crt_import( crt, &cert_datum, GNUTLS_X509_FMT_DER )) < 0) goto error; - } - - /* Create cert bag. */ - if ((ret = pgnutls_pkcs12_bag_init( &cert_bag )) < 0) goto error; - if ((ret = pgnutls_pkcs12_bag_set_crt( cert_bag, crt )) < 0) goto error; + gnutls_datum_t cert_datum = { (unsigned char *)params->cert_data, params->cert_size }; + if ((ret = pgnutls_x509_crt_import( crt, &cert_datum, GNUTLS_X509_FMT_DER )) < 0) goto error; } + /* Create cert bag. */ + if ((ret = pgnutls_pkcs12_bag_init( &cert_bag )) < 0) goto error; + if ((ret = pgnutls_pkcs12_bag_set_crt( cert_bag, crt )) < 0) goto error; + /* Import private key from BCRYPT_RSAKEY_BLOB if provided. */ if (params->key_blob && params->key_blob_size) { @@ -610,20 +607,14 @@ static NTSTATUS export_cert_store( void *args ) /* Build PKCS#12. */ if ((ret = pgnutls_pkcs12_init( &p12 )) < 0) goto error; - if (cert_bag) - { - if ((ret = pgnutls_pkcs12_set_bag( p12, cert_bag )) < 0) goto error; - } + if ((ret = pgnutls_pkcs12_set_bag( p12, cert_bag )) < 0) goto error; if (key_bag) { if ((ret = pgnutls_pkcs12_set_bag( p12, key_bag )) < 0) goto error; } - /* Generate MAC (only if there are bags, GnuTLS fails on empty PKCS#12). */ - if (cert_bag || key_bag) - { - if ((ret = pgnutls_pkcs12_generate_mac2( p12, GNUTLS_MAC_SHA256, pwd ? pwd : "" )) < 0) goto error; - } + /* Generate MAC. */ + if ((ret = pgnutls_pkcs12_generate_mac2( p12, GNUTLS_MAC_SHA256, pwd ? pwd : "" )) < 0) goto error; /* Export. */ if ((ret = pgnutls_pkcs12_export2( p12, GNUTLS_X509_FMT_DER, &out )) < 0) goto error; -- GitLab https://gitlab.winehq.org/wine/wine/-/merge_requests/10532
From: Benoît Legat <benoit.legat@gmail.com> --- dlls/crypt32/pfx.c | 31 ++----------------------------- dlls/crypt32/unixlib.c | 19 +++++++++++-------- 2 files changed, 13 insertions(+), 37 deletions(-) diff --git a/dlls/crypt32/pfx.c b/dlls/crypt32/pfx.c index 0b92761a802..3d0caaf8cdd 100644 --- a/dlls/crypt32/pfx.c +++ b/dlls/crypt32/pfx.c @@ -440,35 +440,8 @@ BOOL WINAPI PFXExportCertStoreEx( HCERTSTORE store, CRYPT_DATA_BLOB *pfx, const } } - if (!cert && !key_blob) - { - /* Empty store: return a minimal valid PKCS#12 (version 3, empty content). */ - static const BYTE empty_pfx[] = { - 0x30, 0x15, /* SEQUENCE */ - 0x02, 0x01, 0x03, /* INTEGER 3 (version) */ - 0x30, 0x10, /* SEQUENCE (authSafe) */ - 0x06, 0x09, 0x2a, 0x86, 0x48, 0x86, /* OID 1.2.840.113549.1.7.1 */ - 0xf7, 0x0d, 0x01, 0x07, 0x01, - 0xa0, 0x03, 0x04, 0x01, 0x00, /* [0] OCTET STRING (empty) */ - }; - if (!pfx->pbData) - { - pfx->cbData = sizeof(empty_pfx); - return TRUE; - } - if (pfx->cbData < sizeof(empty_pfx)) - { - SetLastError( ERROR_MORE_DATA ); - pfx->cbData = sizeof(empty_pfx); - return FALSE; - } - memcpy( pfx->pbData, empty_pfx, sizeof(empty_pfx) ); - pfx->cbData = sizeof(empty_pfx); - return TRUE; - } - - params.cert_data = cert->pbCertEncoded; - params.cert_size = cert->cbCertEncoded; + params.cert_data = cert ? cert->pbCertEncoded : NULL; + params.cert_size = cert ? cert->cbCertEncoded : 0; params.key_blob = key_blob; params.key_blob_size = key_blob ? key_blob_size : 0; params.password = password; diff --git a/dlls/crypt32/unixlib.c b/dlls/crypt32/unixlib.c index fe7b48750c5..569de5fcaaf 100644 --- a/dlls/crypt32/unixlib.c +++ b/dlls/crypt32/unixlib.c @@ -551,17 +551,20 @@ static NTSTATUS export_cert_store( void *args ) if (params->password && !(pwd = password_to_ascii( params->password ))) return STATUS_NO_MEMORY; - /* Import the certificate. */ - if ((ret = pgnutls_x509_crt_init( &crt )) < 0) goto error; + /* Create cert bag (always initialized, even for empty stores). */ + if ((ret = pgnutls_pkcs12_bag_init( &cert_bag )) < 0) goto error; + + /* Import the certificate if provided. */ + if (params->cert_data && params->cert_size) { - gnutls_datum_t cert_datum = { (unsigned char *)params->cert_data, params->cert_size }; - if ((ret = pgnutls_x509_crt_import( crt, &cert_datum, GNUTLS_X509_FMT_DER )) < 0) goto error; + if ((ret = pgnutls_x509_crt_init( &crt )) < 0) goto error; + { + gnutls_datum_t cert_datum = { (unsigned char *)params->cert_data, params->cert_size }; + if ((ret = pgnutls_x509_crt_import( crt, &cert_datum, GNUTLS_X509_FMT_DER )) < 0) goto error; + } + if ((ret = pgnutls_pkcs12_bag_set_crt( cert_bag, crt )) < 0) goto error; } - /* Create cert bag. */ - if ((ret = pgnutls_pkcs12_bag_init( &cert_bag )) < 0) goto error; - if ((ret = pgnutls_pkcs12_bag_set_crt( cert_bag, crt )) < 0) goto error; - /* Import private key from BCRYPT_RSAKEY_BLOB if provided. */ if (params->key_blob && params->key_blob_size) { -- GitLab https://gitlab.winehq.org/wine/wine/-/merge_requests/10532
From: Benoît Legat <benoit.legat@gmail.com> --- dlls/crypt32/pfx.c | 36 ++++++++++++++++++++++-------------- 1 file changed, 22 insertions(+), 14 deletions(-) diff --git a/dlls/crypt32/pfx.c b/dlls/crypt32/pfx.c index 3d0caaf8cdd..5ece0bc9080 100644 --- a/dlls/crypt32/pfx.c +++ b/dlls/crypt32/pfx.c @@ -448,29 +448,37 @@ BOOL WINAPI PFXExportCertStoreEx( HCERTSTORE store, CRYPT_DATA_BLOB *pfx, const if (!pfx->pbData) { - /* Size query. */ pfx_size = 0; params.pfx_data = NULL; params.pfx_size = &pfx_size; + status = CRYPT32_CALL( export_cert_store, ¶ms ); + if (status) + { + WARN( "unix export_cert_store failed %08lx\n", (DWORD)status ); + SetLastError( RtlNtStatusToDosError( status ) ); + } + else + { + pfx->cbData = pfx_size; + ret = TRUE; + } } else { - /* Actual export. */ pfx_size = pfx->cbData; params.pfx_data = pfx->pbData; params.pfx_size = &pfx_size; - } - - status = CRYPT32_CALL( export_cert_store, ¶ms ); - if (status) - { - WARN( "unix export_cert_store failed %08lx\n", (DWORD)status ); - SetLastError( RtlNtStatusToDosError( status ) ); - } - else - { - pfx->cbData = pfx_size; - ret = TRUE; + status = CRYPT32_CALL( export_cert_store, ¶ms ); + if (status) + { + WARN( "unix export_cert_store failed %08lx\n", (DWORD)status ); + SetLastError( RtlNtStatusToDosError( status ) ); + } + else + { + pfx->cbData = pfx_size; + ret = TRUE; + } } CryptMemFree( key_blob ); -- GitLab https://gitlab.winehq.org/wine/wine/-/merge_requests/10532
From: Benoît Legat <benoit.legat@gmail.com> --- dlls/crypt32/pfx.c | 43 ++++++++++++++++++++++++++++++++++--------- 1 file changed, 34 insertions(+), 9 deletions(-) diff --git a/dlls/crypt32/pfx.c b/dlls/crypt32/pfx.c index 5ece0bc9080..bc245497d4c 100644 --- a/dlls/crypt32/pfx.c +++ b/dlls/crypt32/pfx.c @@ -446,8 +446,14 @@ BOOL WINAPI PFXExportCertStoreEx( HCERTSTORE store, CRYPT_DATA_BLOB *pfx, const params.key_blob_size = key_blob ? key_blob_size : 0; params.password = password; - if (!pfx->pbData) + /* GnuTLS encrypts the private key with a random salt, so the output + * size can vary between calls. Do a single export into a temporary + * buffer and then handle the size query / copy on the Windows side. */ { + DWORD alloc_size; + BYTE *tmp; + + /* Get an estimate of the required size. */ pfx_size = 0; params.pfx_data = NULL; params.pfx_size = &pfx_size; @@ -456,31 +462,50 @@ BOOL WINAPI PFXExportCertStoreEx( HCERTSTORE store, CRYPT_DATA_BLOB *pfx, const { WARN( "unix export_cert_store failed %08lx\n", (DWORD)status ); SetLastError( RtlNtStatusToDosError( status ) ); + goto done; } - else + + /* Allocate with extra room for size variation. */ + alloc_size = pfx_size + 256; + tmp = CryptMemAlloc( alloc_size ); + if (!tmp) { - pfx->cbData = pfx_size; - ret = TRUE; + SetLastError( ERROR_OUTOFMEMORY ); + goto done; } - } - else - { - pfx_size = pfx->cbData; - params.pfx_data = pfx->pbData; + + pfx_size = alloc_size; + params.pfx_data = tmp; params.pfx_size = &pfx_size; status = CRYPT32_CALL( export_cert_store, ¶ms ); if (status) { WARN( "unix export_cert_store failed %08lx\n", (DWORD)status ); SetLastError( RtlNtStatusToDosError( status ) ); + CryptMemFree( tmp ); + goto done; + } + + if (!pfx->pbData) + { + pfx->cbData = pfx_size; + ret = TRUE; + } + else if (pfx->cbData < pfx_size) + { + pfx->cbData = pfx_size; + SetLastError( ERROR_MORE_DATA ); } else { + memcpy( pfx->pbData, tmp, pfx_size ); pfx->cbData = pfx_size; ret = TRUE; } + CryptMemFree( tmp ); } +done: CryptMemFree( key_blob ); CertFreeCertificateContext( cert ); return ret; -- GitLab https://gitlab.winehq.org/wine/wine/-/merge_requests/10532
participants (2)
-
Benoît Legat -
Benoît Legat (@blegat)