[Openvpn-devel,1/4] Import some sample certificates into Windows store for testing

Message ID 20230315013516.1256700-2-selva.nair@gmail.com
State Accepted
Headers show
Series Add some tests for cryptoapi.c functions | expand

Commit Message

Selva Nair March 15, 2023, 1:35 a.m. UTC
From: Selva Nair <selva.nair@gmail.com>

- A few sample certificates are defined and imported into
  Windows certificate store (user store).
  This only tests the import process. Use of these certs to test the
  core functionality of 'cryptoapicert' are in following commits.

Change-Id: Ida5fc12c5bad5fde202da0bf0e8cdc71efe548c2
Signed-off-by: Selva Nair <selva.nair@gmail.com>
---
 tests/unit_tests/openvpn/cert_data.h      | 166 ++++++++++++++++++++++
 tests/unit_tests/openvpn/test_cryptoapi.c | 160 ++++++++++++++++++++-
 2 files changed, 324 insertions(+), 2 deletions(-)
 create mode 100644 tests/unit_tests/openvpn/cert_data.h

Comments

Gert Doering March 16, 2023, 9:27 a.m. UTC | #1
Acked-by: Gert Doering <gert@greenie.muc.de>

This is a welcome addition so we can have actual cryptoapi unit 
tests.  The code looks reasonable, and GHA says "it passes"...

	Run ./unittests/cryptoapi_testdriver.exe
	[==========] Running 2 test(s).
	[ RUN      ] test_parse_hexstring
	[       OK ] test_parse_hexstring
	[ RUN      ] import_certs
	[  PASSED  ] 2 test(s).
	[       OK ] import_certs
	[==========] 2 test(s) run.


I have tried testing this on a "real" Win10 VM, but I am missing some
intricacies with the cert store - I wanted to see if certs are properly
cleaned up, but I can't even see my own .p12 I have imported, so I am
doing something wrong... 

Your patch has been applied to the master and release/2.6 branch.

commit d6cf0239e835d98b66c71d701e70128db9ca7e9a (master)
commit e6011868c40aaf91d30d084b1b454f7709c855fd (release/2.6)
Author: Selva Nair
Date:   Tue Mar 14 21:35:13 2023 -0400

     Import some sample certificates into Windows store for testing

     Signed-off-by: Selva Nair <selva.nair@gmail.com>
     Acked-by: Gert Doering <gert@greenie.muc.de>
     Message-Id: <20230315013516.1256700-2-selva.nair@gmail.com>
     URL: https://www.mail-archive.com/openvpn-devel@lists.sourceforge.net/msg26417.html
     Signed-off-by: Gert Doering <gert@greenie.muc.de>


--
kind regards,

Gert Doering
Selva Nair March 16, 2023, 11:59 a.m. UTC | #2
>
>
> I have tried testing this on a "real" Win10 VM, but I am missing some
> intricacies with the cert store - I wanted to see if certs are properly
> cleaned up, but I can't even see my own .p12 I have imported, so I am
> doing something wrong...


This works for me:
Run "certmgr" from a user command prompt. The UI that opens up will show
the "Certificates - Current User" snapin. Under it select
"Personal->Certificates", and a list of all imported certificates will open
up. This will not include any Root certificates that may have been
imported. Those go to "Trusted Root Certificates" listed below "Personal".

I'm not entirely sure whether it's possible to select a wrong destination
during import, causing client certificates to go into root certificates.
During import I only select the store (user or machine) and let it decide
the appropriate location within the store.

Selva
Gert Doering March 16, 2023, 1:11 p.m. UTC | #3
Hi,

On Thu, Mar 16, 2023 at 07:59:42AM -0400, Selva Nair wrote:
> > I have tried testing this on a "real" Win10 VM, but I am missing some
> > intricacies with the cert store - I wanted to see if certs are properly
> > cleaned up, but I can't even see my own .p12 I have imported, so I am
> > doing something wrong...
> 
> This works for me:
> Run "certmgr" from a user command prompt. The UI that opens up will show
> the "Certificates - Current User" snapin. Under it select
> "Personal->Certificates", and a list of all imported certificates will open
> up. This will not include any Root certificates that may have been
> imported. Those go to "Trusted Root Certificates" listed below "Personal".

THAT was the clue I needed.  When you type "certificates" into the
search box, you are presented the same snap-in, but it will only do 
"Certificates / Local Computer" (after an UAC confirmation).

So, cmd -> certmgr -> "current user \ personal certificates \ certificates"
will show what I imported from .p12

(And it does *not* show any of the test certificates, so I assume that
cleanup() worked correctly.  I might try a test run that skips cleanup()
("core dumped"), just to see what happens, but as far as I understand
the code, the *next* run will run the cleanup, so this is not of major
importance)

thanks,

gert

Patch

diff --git a/tests/unit_tests/openvpn/cert_data.h b/tests/unit_tests/openvpn/cert_data.h
new file mode 100644
index 00000000..33de35ec
--- /dev/null
+++ b/tests/unit_tests/openvpn/cert_data.h
@@ -0,0 +1,166 @@ 
+/*
+ *  OpenVPN -- An application to securely tunnel IP networks
+ *             over a single UDP port, with support for SSL/TLS-based
+ *             session authentication and key exchange,
+ *             packet encryption, packet authentication, and
+ *             packet compression.
+ *
+ *  Copyright (C) 2023 Selva Nair <selva.nair@gmail.com>
+ *
+ *  This program is free software; you can redistribute it and/or modify
+ *  it under the terms of the GNU General Public License as published by the
+ *  Free Software Foundation, either version 2 of the License,
+ *  or (at your option) any later version.
+ *
+ *  This program is distributed in the hope that it will be useful,
+ *  but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *  GNU General Public License for more details.
+ *
+ *  You should have received a copy of the GNU General Public License along
+ *  with this program; if not, write to the Free Software Foundation, Inc.,
+ *  51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ */
+
+#ifndef CERT_DATA_H
+#define CERT_DATA_H
+
+/* Some certificates and their private keys for testing cryptoapi.c.
+ * Two certificates, cert1 (EC) and cert3 (RSA) are signed by one CA
+ * and the other two, cert2 (EC) and cert4 (RSA), by another to have a
+ * different issuer name. The common name of cert4 is the same as
+ * that of cert3 but the former has expired. It is used to test
+ * retrieval of valid certificate by name when an expired one with same
+ * common name exists.
+ * To reduce data volume, certs of same keytype use the same private key.
+ */
+
+/* sample-ec.crt */
+static const char *const cert1 =
+    "-----BEGIN CERTIFICATE-----\n"
+    "MIIClzCCAX+gAwIBAgIRAIJr3cy95V63CPEtaAA8JN4wDQYJKoZIhvcNAQELBQAw\n"
+    "GDEWMBQGA1UEAwwNT1ZQTiBURVNUIENBMTAgFw0yMzAzMTMxNjExMjhaGA8yMTIz\n"
+    "MDIxNzE2MTEyOFowGDEWMBQGA1UEAwwNb3Zwbi10ZXN0LWVjMTBZMBMGByqGSM49\n"
+    "AgEGCCqGSM49AwEHA0IABHhJG+dK4Z0mY+K0pupwVtyDLOwwGWHjBY6u3LgjRmUh\n"
+    "fFjaoSfJvdgrPg50wbOkrsUt9Bl6EeDosZuVwuzgRbujgaQwgaEwCQYDVR0TBAIw\n"
+    "ADAdBgNVHQ4EFgQUPWeU5BEmD8VEOSKeNf9kAvhcVuowUwYDVR0jBEwwSoAU3MLD\n"
+    "NDOK13DqflQ8ra7FeGBXK06hHKQaMBgxFjAUBgNVBAMMDU9WUE4gVEVTVCBDQTGC\n"
+    "FD55ErHXpK2JXS3WkfBm0NB1r3vKMBMGA1UdJQQMMAoGCCsGAQUFBwMCMAsGA1Ud\n"
+    "DwQEAwIHgDANBgkqhkiG9w0BAQsFAAOCAQEAhH/wOFqP4R+FK5QvU+oW/XacFMku\n"
+    "+qT8lL9J7BG28WhZ0ZcAy/AmtnyynkDyuZSwnlzGgJ5m4L/RfwTzJKhEHiSU3BvB\n"
+    "5C1Z1Q8k67MHSfb565iCn8GzPUQLK4zsILCoTkJPvimv2bJ/RZmNaD+D4LWiySD4\n"
+    "tuOEdHKrxIrbJ5eAaN0WxRrvDdwGlyPvbMFvfhXzd/tbkP4R2xvlm7S2DPeSTJ8s\n"
+    "srXMaPe0lAea4etMSZsjIRPwGRMXBrwbRmb6iN2Cq40867HdaJoAryYig7IiDwSX\n"
+    "htCbOA6sX+60+FEOYDEx5cmkogl633Pw7LJ3ICkyzIrUSEt6BOT1Gsc1eQ==\n"
+    "-----END CERTIFICATE-----\n";
+static const char *const key1 =
+    "-----BEGIN PRIVATE KEY-----\n"
+    "MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQg5Xpw/lLvBrWjAWDq\n"
+    "L6dm/4a1or6AQ6O3yXYgw78B23ihRANCAAR4SRvnSuGdJmPitKbqcFbcgyzsMBlh\n"
+    "4wWOrty4I0ZlIXxY2qEnyb3YKz4OdMGzpK7FLfQZehHg6LGblcLs4EW7\n"
+    "-----END PRIVATE KEY-----\n";
+static const char *const hash1 = "A4B74F1D68AF50691F62CBD675E24C8655369567";
+static const char *const cname1 = "ovpn-test-ec1";
+
+static const char *const cert2 =
+    "-----BEGIN CERTIFICATE-----\n"
+    "MIIClzCCAX+gAwIBAgIRAN9fIkTDOjX0Bd9adHVcLx8wDQYJKoZIhvcNAQELBQAw\n"
+    "GDEWMBQGA1UEAwwNT1ZQTiBURVNUIENBMjAgFw0yMzAzMTMxODAzMzFaGA8yMTIz\n"
+    "MDIxNzE4MDMzMVowGDEWMBQGA1UEAwwNb3Zwbi10ZXN0LWVjMjBZMBMGByqGSM49\n"
+    "AgEGCCqGSM49AwEHA0IABHhJG+dK4Z0mY+K0pupwVtyDLOwwGWHjBY6u3LgjRmUh\n"
+    "fFjaoSfJvdgrPg50wbOkrsUt9Bl6EeDosZuVwuzgRbujgaQwgaEwCQYDVR0TBAIw\n"
+    "ADAdBgNVHQ4EFgQUPWeU5BEmD8VEOSKeNf9kAvhcVuowUwYDVR0jBEwwSoAUyX3c\n"
+    "tpRP5cKlESsG80rOGhEphsGhHKQaMBgxFjAUBgNVBAMMDU9WUE4gVEVTVCBDQTKC\n"
+    "FBc8ra53hwYrlIkdY3Ay1WCrrHJ8MBMGA1UdJQQMMAoGCCsGAQUFBwMCMAsGA1Ud\n"
+    "DwQEAwIHgDANBgkqhkiG9w0BAQsFAAOCAQEAWmA40BvEgBbKb1ReKlKzk64xi2ak\n"
+    "4tyr3sW9wIYQ2N1zkSomwEV6wGEawLqPADRbXiYdjtAqLz12OJvBnBwgxN3dVmqL\n"
+    "6UN4ZIwMWJ4fSW9vK/Nt+JNwebN+Jgw/nIXvSdK95ha4iusZZOIZ4qDj3DWwjhjV\n"
+    "L5/m6zP09L9G9/79j1Tsu4Stl5SI1XxtYc0eVn29vJEMBfpsS7pPD6V9JpY3Y1f3\n"
+    "HeTsAlHjfFEReVDiNCI9vMQLKFKKWnAorT2+iyRueA3bt2gchf863BBhZvJddL7Q\n"
+    "KBa0osXw+eGBRAwsm7m1qCho3b3fN2nFAa+k07ptRkOeablmFdXE81nVlA==\n"
+    "-----END CERTIFICATE-----\n";
+static const char *const key2 = key1;
+static const char *const hash2 = "FA18FD34BAABE47D6E2910E080F421C109CA97F5";
+static const char *const cname2 = "ovpn-test-ec2";
+
+static const char *const cert3 =
+    "-----BEGIN CERTIFICATE-----\n"
+    "MIIDYzCCAkugAwIBAgIRALrXTx4lqa8QgF7uGjISxmcwDQYJKoZIhvcNAQELBQAw\n"
+    "GDEWMBQGA1UEAwwNT1ZQTiBURVNUIENBMTAgFw0yMzAzMTMxNjA5MThaGA8yMTIz\n"
+    "MDIxNzE2MDkxOFowGTEXMBUGA1UEAwwOb3Zwbi10ZXN0LXJzYTEwggEiMA0GCSqG\n"
+    "SIb3DQEBAQUAA4IBDwAwggEKAoIBAQC7xFoR6fmoyfsJIQDKKgbYgFw0MzVuDAmp\n"
+    "Rx6KTEihgTchkQx9fHddWbKiOUbcEnQi3LNux7P4QVl/4dRR3skisBug6Vd5LXeB\n"
+    "GZqmpu5XZiF4DgLz1lX21G0aOogFWkie2qGEcso40159x9FBDl5A3sLP18ubeex0\n"
+    "pd/BzDFv6SLOTyVWO/GCNc8IX/i0uN4mLvoVU00SeqwTPnS+CRXrSq4JjGDJLsXl\n"
+    "0/PlxkjsgU0yOOA0Z2d8Fzk3wClwP6Hc49BOMWKstUIhLbG2DcIv8l29EuEj2w3j\n"
+    "u/7gkewol96XQ2twpPvpoVAaiVh/m7hQUcQORQCD6eJcDjOZVCArAgMBAAGjgaQw\n"
+    "gaEwCQYDVR0TBAIwADAdBgNVHQ4EFgQUqYnRaBHrZmKLtMZES5AuwqzJkGYwUwYD\n"
+    "VR0jBEwwSoAU3MLDNDOK13DqflQ8ra7FeGBXK06hHKQaMBgxFjAUBgNVBAMMDU9W\n"
+    "UE4gVEVTVCBDQTGCFD55ErHXpK2JXS3WkfBm0NB1r3vKMBMGA1UdJQQMMAoGCCsG\n"
+    "AQUFBwMCMAsGA1UdDwQEAwIHgDANBgkqhkiG9w0BAQsFAAOCAQEAZVcXrezA9Aby\n"
+    "sfUNHAsMxrex/EO0PrIPSrmSmc9sCiD8cCIeB6kL8c5iPPigoWW0uLA9zteDRFes\n"
+    "ez+Z8wBY6g8VQ0tFPURDooUg5011GZPDcuw7/PsI4+I2J9q6LHEp+6Oo4faSn/kl\n"
+    "yWYCLjM4FZdGXbOijDacQJiN6HcRv0UdodBrEVRf7YHJJmMCbCI7ZUGW2zef/+rO\n"
+    "e4Lkxh0MLYqCkNKH5ZfoGTC4Oeb0xKykswAanqgR60r+upaLU8PFuI2L9M3vc6KU\n"
+    "F6MgVGSxl6eylJgDYckvJiAbmcp2PD/LRQQOxQA0yqeAMg2cbdvclETuYD6zoFfu\n"
+    "Y8aO7dvDlw==\n"
+    "-----END CERTIFICATE-----\n";
+static const char *const key3 =
+    "-----BEGIN PRIVATE KEY-----\n"
+    "MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQC7xFoR6fmoyfsJ\n"
+    "IQDKKgbYgFw0MzVuDAmpRx6KTEihgTchkQx9fHddWbKiOUbcEnQi3LNux7P4QVl/\n"
+    "4dRR3skisBug6Vd5LXeBGZqmpu5XZiF4DgLz1lX21G0aOogFWkie2qGEcso40159\n"
+    "x9FBDl5A3sLP18ubeex0pd/BzDFv6SLOTyVWO/GCNc8IX/i0uN4mLvoVU00SeqwT\n"
+    "PnS+CRXrSq4JjGDJLsXl0/PlxkjsgU0yOOA0Z2d8Fzk3wClwP6Hc49BOMWKstUIh\n"
+    "LbG2DcIv8l29EuEj2w3ju/7gkewol96XQ2twpPvpoVAaiVh/m7hQUcQORQCD6eJc\n"
+    "DjOZVCArAgMBAAECggEACqkuWAAJ3cyCBVWrXs8eDmLTWV9i9DmYvtS75ixIn2rf\n"
+    "v3cl12YevN0f6FgKLuqZT3Vqdqq+DCVhuIIQ9QkKMH8BQpSdE9NCCsFyZ23o8Gtr\n"
+    "EQ7ymfecb+RFwYx7NpqWrvZI32VJGArgPZH/zorLTTGYrAZbmBtHEqRsXOuEDw97\n"
+    "slwwcWaa9ztaYC8/N/7fgsnydaCFSaOByRlWuyvSmHvn6ZwLv8ANOshY6fstC0Jb\n"
+    "BW0GpSe9eZPjpl71VT2RtpghqLV5+iAoFDHoT+eZvBospcUGtfcZSU7RrBjKB8+a\n"
+    "U1d6hwKhduVs2peIQzl+FiOSdWriLcsZv79q4sBhsQKBgQDUDVTf5BGJ8apOs/17\n"
+    "YVk+Ad8Ey8sXvsfk49psmlCRa8Z4g0LVXfrP94qzhtl8U5kE9hs3nEF4j/kX1ZWG\n"
+    "k11tdsNTZN5x5bbAgEgPA6Ap6J/uto0HS8G0vSv0lyBymdKA3p/i5Dx+8Nc9cGns\n"
+    "LGI9MvviLX7pQFIkvbaCkdKwYwKBgQDirowjWZnm7BgVhF0G1m3DY9nQTYYU185W\n"
+    "UESaO5/nVzwUrA+FypJamD+AvmlSuY8rJeQAGAS6nQr9G8/617r+GwJnzRtxC6Vl\n"
+    "4OF5BJRsD70oX4CFOOlycMoJ8tzcYVH7NI8KVocjxb+QW82hqSvEwSsvnwwn3eOW\n"
+    "nr5u5vIHmQKBgCuc3lL6Dl1ntdZgEIdau0cUjXDoFUo589TwxBDIID/4gaZxoMJP\n"
+    "hPFXAVDxMDPw4azyjSB/47tPKTUsuYcnMfT8kynIujOEwnSPLcLgxQU5kgM/ynuw\n"
+    "qhNpQOwaVRMc7f2RTCMXPBYDpNE/GJn5eu8JWGLpZovEreBeoHX0VffvAoGAVrWn\n"
+    "+3mxykhzaf+oyg3KDNysG+cbq+tlDVVE+K5oG0kePVYX1fjIBQmJ+QhdJ3y9jCbB\n"
+    "UVveqzeZVXqHEw/kgoD4aZZmsdZfnVnpRa5/y9o1ZDUr50n+2nzUe/u/ijlb77iK\n"
+    "Is04gnGJNoI3ZWhdyrSNfXjcYH+bKClu9OM4n7kCgYAorc3PAX7M0bsQrrqYxUS8\n"
+    "56UU0YdhAgYitjM7Fm/0iIm0vDpSevxL9js4HnnsSMVR77spCBAGOCCZrTcI3Ejg\n"
+    "xKDYzh1xlfMRjJBuBu5Pd55ZAv9NXFGpsX5SO8fDZQJMwpcbQH36+UdqRRFDpjJ0\n"
+    "ZbX6nKcJ7jciJVKJds59Jg==\n"
+    "-----END PRIVATE KEY-----\n";
+static const char *const hash3 = "2463628674E362578113F508BA05F29EF142E979";
+static const char *const cname3 = "ovpn-test-rsa1";
+
+static const char *const cert4 =
+    "-----BEGIN CERTIFICATE-----\n"
+    "MIIDYTCCAkmgAwIBAgIRAPTJucQy27qoIv0oYoE71z8wDQYJKoZIhvcNAQELBQAw\n"
+    "GDEWMBQGA1UEAwwNT1ZQTiBURVNUIENBMjAeFw0yMzAzMTMxNzQ2MDNaFw0yMzAz\n"
+    "MTQxNzQ2MDNaMBkxFzAVBgNVBAMMDm92cG4tdGVzdC1yc2ExMIIBIjANBgkqhkiG\n"
+    "9w0BAQEFAAOCAQ8AMIIBCgKCAQEAu8RaEen5qMn7CSEAyioG2IBcNDM1bgwJqUce\n"
+    "ikxIoYE3IZEMfXx3XVmyojlG3BJ0Ityzbsez+EFZf+HUUd7JIrAboOlXeS13gRma\n"
+    "pqbuV2YheA4C89ZV9tRtGjqIBVpIntqhhHLKONNefcfRQQ5eQN7Cz9fLm3nsdKXf\n"
+    "wcwxb+kizk8lVjvxgjXPCF/4tLjeJi76FVNNEnqsEz50vgkV60quCYxgyS7F5dPz\n"
+    "5cZI7IFNMjjgNGdnfBc5N8ApcD+h3OPQTjFirLVCIS2xtg3CL/JdvRLhI9sN47v+\n"
+    "4JHsKJfel0NrcKT76aFQGolYf5u4UFHEDkUAg+niXA4zmVQgKwIDAQABo4GkMIGh\n"
+    "MAkGA1UdEwQCMAAwHQYDVR0OBBYEFKmJ0WgR62Zii7TGREuQLsKsyZBmMFMGA1Ud\n"
+    "IwRMMEqAFMl93LaUT+XCpRErBvNKzhoRKYbBoRykGjAYMRYwFAYDVQQDDA1PVlBO\n"
+    "IFRFU1QgQ0EyghQXPK2ud4cGK5SJHWNwMtVgq6xyfDATBgNVHSUEDDAKBggrBgEF\n"
+    "BQcDAjALBgNVHQ8EBAMCB4AwDQYJKoZIhvcNAQELBQADggEBAFjJvZFwhY77UOWu\n"
+    "O6n5yLxcG6/VNWMbD0CazZP8pBqCGJRU9Rq0vXxZ00E0WSYTJLZoq1aFmeWIX0vZ\n"
+    "sudVkdbfWLdiwuQZDWBS+qC4SkIcnNe5FYSSUlXlvpSUN2CgGCLmryP+SZKHp8YV\n"
+    "e37pQxDjImXCu5Jdk5AhK6pkFm5IMskdTKfWJjjR69lBgWHPoM2WAwkV8vxKdpy8\n"
+    "0Bqef8MZZM+qVYw7OguAFos2Am7waLpa3q9SYqCRYctq4Q2++p2WjINv3nkXIwYS\n"
+    "353PpJJ9s2b/Fqoc4d7udqhQogA7jqbayTKhJxbT134l2NzqDROzuS0kXbX8bXCi\n"
+    "mXSa4c8=\n"
+    "-----END CERTIFICATE-----\n";
+static const char *const key4 = key3;
+static const char *const hash4 = "E1401D4497C944783E3D62CDBD2A1F69F5E5071E";
+static const char *const cname4 = cname3; /* same CN as that of cert3 */
+
+#endif /* CERT_DATA_H */
diff --git a/tests/unit_tests/openvpn/test_cryptoapi.c b/tests/unit_tests/openvpn/test_cryptoapi.c
index 73ef34e9..54dbd094 100644
--- a/tests/unit_tests/openvpn/test_cryptoapi.c
+++ b/tests/unit_tests/openvpn/test_cryptoapi.c
@@ -32,6 +32,7 @@ 
 #include "manage.h"
 #include "integer.h"
 #include "xkey_common.h"
+#include "cert_data.h"
 
 #if defined(HAVE_XKEY_PROVIDER) && defined (ENABLE_CRYPTOAPI)
 #include <setjmp.h>
@@ -40,6 +41,7 @@ 
 #include <openssl/pem.h>
 #include <openssl/core_names.h>
 #include <openssl/evp.h>
+#include <openssl/pkcs12.h>
 
 #include <cryptoapi.h>
 #include <cryptoapi.c> /* pull-in the whole file to test static functions */
@@ -84,6 +86,157 @@  static const char *invalid_str[] = {
     "7738x5001e9648c6570baec0b796f9664d5fd0b7",   /* non hex character */
 };
 
+/* Test certificate database: data for cert1, cert2 .. key1, key2 etc.
+ * are stashed away in cert_data.h
+ */
+static struct test_cert
+{
+    const char *const cert;             /* certificate as PEM */
+    const char *const key;              /* key as unencrypted PEM */
+    const char *const cname;            /* common-name */
+    const char *const issuer;           /* issuer common-name */
+    const char *const friendly_name;    /* identifies certs loaded to the store -- keep unique */
+    const char *hash;                   /* SHA1 fingerprint */
+    int valid;                          /* nonzero if certificate has not expired */
+} certs[] = {
+    {cert1,  key1,  cname1,  "OVPN TEST CA1",  "OVPN Test Cert 1",  hash1,  1},
+    {cert2,  key2,  cname2,  "OVPN TEST CA2",  "OVPN Test Cert 2",  hash2,  1},
+    {cert3,  key3,  cname3,  "OVPN TEST CA1",  "OVPN Test Cert 3",  hash3,  1},
+    {cert4,  key4,  cname4,  "OVPN TEST CA2",  "OVPN Test Cert 4",  hash4,  0},
+    {}
+};
+
+static bool certs_loaded;
+static HCERTSTORE user_store;
+
+/* Lookup a certificate in our certificate/key db */
+static struct test_cert *
+lookup_cert(const char *friendly_name)
+{
+    struct test_cert *c = certs;
+    while (c->cert && strcmp(c->friendly_name, friendly_name))
+    {
+        c++;
+    }
+    return c->cert ? c : NULL;
+}
+
+/* import sample certificates into windows cert store */
+static void
+import_certs(void **state)
+{
+    (void) state;
+    if (certs_loaded)
+    {
+        return;
+    }
+    user_store = CertOpenStore(CERT_STORE_PROV_SYSTEM, 0, 0, CERT_SYSTEM_STORE_CURRENT_USER
+                               |CERT_STORE_OPEN_EXISTING_FLAG, L"MY");
+    assert_non_null(user_store);
+    for (struct test_cert *c = certs; c->cert; c++)
+    {
+        /* Convert PEM cert & key to pkcs12 and import */
+        const char *pass = "opensesame";        /* some password */
+        const wchar_t *wpass = L"opensesame";   /* same as a wide string */
+
+        X509 *x509 = NULL;
+        EVP_PKEY *pkey = NULL;
+
+        BIO *buf = BIO_new_mem_buf(c->cert, -1);
+        if (buf)
+        {
+            x509 = PEM_read_bio_X509(buf, NULL, NULL, NULL);
+        }
+        BIO_free(buf);
+
+        buf = BIO_new_mem_buf(c->key, -1);
+        if (buf)
+        {
+            pkey = PEM_read_bio_PrivateKey(buf, NULL, NULL, NULL);
+        }
+        BIO_free(buf);
+
+        if (!x509 || !pkey)
+        {
+            fail_msg("Failed to parse certificate/key data: <%s>", c->friendly_name);
+            return;
+        }
+
+        PKCS12 *p12 = PKCS12_create(pass, c->friendly_name, pkey, x509, NULL, 0, 0, 0, 0, 0);
+        X509_free(x509);
+        EVP_PKEY_free(pkey);
+        if (!p12)
+        {
+            fail_msg("Failed to convert to PKCS12: <%s>", c->friendly_name);
+            return;
+        }
+
+        CRYPT_DATA_BLOB blob = {.cbData = 0, .pbData = NULL};
+        int len = i2d_PKCS12(p12, &blob.pbData); /* pbData will be allocated by OpenSSL */
+        if (len <= 0)
+        {
+            fail_msg("Failed to DER encode PKCS12: <%s>", c->friendly_name);
+            return;
+        }
+        blob.cbData = len;
+
+        DWORD flags = PKCS12_ALLOW_OVERWRITE_KEY|PKCS12_ALWAYS_CNG_KSP;
+        HCERTSTORE tmp_store = PFXImportCertStore(&blob, wpass, flags);
+        PKCS12_free(p12);
+        OPENSSL_free(blob.pbData);
+
+        assert_non_null(tmp_store);
+
+        /* The cert and key get imported into a temp store. We have to move it to
+         * user's store to accumulate all certs in one place and use them for tests.
+         * It seems there is no API to directly import a p12 blob into an existing store.
+         * Nothing in Windows is ever easy.
+         */
+
+        const CERT_CONTEXT *ctx = CertEnumCertificatesInStore(tmp_store, NULL);
+        assert_non_null(ctx);
+        bool added = CertAddCertificateContextToStore(user_store, ctx,
+                                                      CERT_STORE_ADD_REPLACE_EXISTING, NULL);
+        assert_true(added);
+
+        CertFreeCertificateContext(ctx);
+        CertCloseStore(tmp_store, 0);
+    }
+    certs_loaded = true;
+}
+
+static int
+cleanup(void **state)
+{
+    (void) state;
+    struct gc_arena gc = gc_new();
+    if (user_store) /* delete all certs we imported */
+    {
+        const CERT_CONTEXT *ctx = NULL;
+        while ((ctx = CertEnumCertificatesInStore(user_store, ctx)))
+        {
+            char *friendly_name = get_cert_name(ctx, &gc);
+            if (!lookup_cert(friendly_name)) /* not our cert */
+            {
+                continue;
+            }
+
+            /* create a dup context to not destroy the state of loop iterator */
+            const CERT_CONTEXT *ctx_dup = CertDuplicateCertificateContext(ctx);
+            if (ctx_dup)
+            {
+                CertDeleteCertificateFromStore(ctx_dup);
+                /* the above also releases ctx_dup */
+            }
+        }
+        CertCloseStore(user_store, 0);
+    }
+    user_store = NULL;
+    certs_loaded = false;
+    gc_free(&gc);
+    return 0;
+}
+
 static void
 test_parse_hexstring(void **state)
 {
@@ -108,9 +261,12 @@  test_parse_hexstring(void **state)
 int
 main(void)
 {
-    const struct CMUnitTest tests[] = { cmocka_unit_test(test_parse_hexstring) };
+    const struct CMUnitTest tests[] = {
+        cmocka_unit_test(test_parse_hexstring),
+        cmocka_unit_test(import_certs),
+    };
 
-    int ret = cmocka_run_group_tests_name("cryptoapi tests", tests, NULL, NULL);
+    int ret = cmocka_run_group_tests_name("cryptoapi tests", tests, NULL, cleanup);
 
     return ret;
 }