Browse code

Implement Windows CA template match for Crypto-API selector

The certificate selection process for the Crypto API certificates
is currently fixed to match on subject or identifier. Especially
if certificates that are used for OpenVPN are managed by a Windows CA,
it is appropriate to select the certificate to use by the template
that it is generated from, especially on domain-joined clients which
automatically acquire/renew the corresponding certificate.

The attached match implements the match on TMPL: with either a template
name (which is looked up through CryptFindOIDInfo) or by specifying the
OID of the template directly, which then is matched against the
corresponding X509 extensions specifying the template that the certificate
was generated from.

The logic requires to walk all certificates in the underlying store and
to match the certificate extensions directly. The hook which is
implemented in the certificate selection logic is generic to allow
other Crypto-API certificate matches to also be implemented at some
point in the future.

The logic to match the certificate template is taken from the
implementation in the .NET core runtime, see Pal.Windows/FindPal.cs in
in the implementation of System.Security.Cryptography.X509Certificates.

Change-Id: Ia2c3e4c5c83ecccce1618c43b489dbe811de5351
Signed-off-by: Heiko Wundram <heiko.wundram@gehrkens.it>
Signed-off-by: Hannes Domani <ssbssa@yahoo.de>
Acked-by: Selva Nair <selva.nair@gmail.com>
Message-Id: <20240606103441.26598-1-gert@greenie.muc.de>
URL: https://www.mail-archive.com/openvpn-devel@lists.sourceforge.net/msg28726.html
Signed-off-by: Gert Doering <gert@greenie.muc.de>
(cherry picked from commit 13ee7f902f18e27b981f8e440facd2e6515c6c83)

Heiko Wundram authored on 2024/06/06 19:34:41
Showing 2 changed files
... ...
@@ -55,6 +55,13 @@ Windows-Specific Options
55 55
 
56 56
      cryptoapicert "ISSUER:Sample CA"
57 57
 
58
+  To select a certificate based on a certificate's template name or
59
+  OID of the template:
60
+  ::
61
+
62
+     cryptoapicert "TMPL:Name of Template"
63
+     cryptoapicert "TMPL:1.3.6.1.4..."
64
+
58 65
   The first non-expired certificate found in the user's store or the
59 66
   machine store that matches the select-string is used.
60 67
 
... ...
@@ -178,6 +178,87 @@ parse_hexstring(const char *p, unsigned char *arr, size_t capacity)
178 178
     return i;
179 179
 }
180 180
 
181
+static void *
182
+decode_object(struct gc_arena *gc, LPCSTR struct_type,
183
+              const CRYPT_OBJID_BLOB *val, DWORD flags, DWORD *cb)
184
+{
185
+    /* get byte count for decoding */
186
+    BYTE *buf;
187
+    if (!CryptDecodeObject(X509_ASN_ENCODING | PKCS_7_ASN_ENCODING, struct_type,
188
+                           val->pbData, val->cbData, flags, NULL, cb))
189
+    {
190
+        return NULL;
191
+    }
192
+
193
+    /* do the actual decode */
194
+    buf = gc_malloc(*cb, false, gc);
195
+    if (!CryptDecodeObject(X509_ASN_ENCODING | PKCS_7_ASN_ENCODING, struct_type,
196
+                           val->pbData, val->cbData, flags, buf, cb))
197
+    {
198
+        return NULL;
199
+    }
200
+
201
+    return buf;
202
+}
203
+
204
+static const CRYPT_OID_INFO *
205
+find_oid(DWORD keytype, const void *key, DWORD groupid)
206
+{
207
+    const CRYPT_OID_INFO *info = NULL;
208
+
209
+    /* try proper resolve, also including AD */
210
+    info = CryptFindOIDInfo(keytype, (void *)key, groupid);
211
+
212
+    /* fall back to all groups if not found yet */
213
+    if (!info && groupid)
214
+    {
215
+        info = CryptFindOIDInfo(keytype, (void *)key, 0);
216
+    }
217
+
218
+    return info;
219
+}
220
+
221
+static bool
222
+test_certificate_template(const char *cert_prop, const CERT_CONTEXT *cert_ctx)
223
+{
224
+    const CERT_INFO *info = cert_ctx->pCertInfo;
225
+    const CERT_EXTENSION *ext;
226
+    DWORD cbext;
227
+    void *pvext;
228
+    struct gc_arena gc = gc_new();
229
+    const WCHAR *tmpl_name = wide_string(cert_prop, &gc);
230
+
231
+    /* check for V2 extension (Windows 2003+) */
232
+    ext = CertFindExtension(szOID_CERTIFICATE_TEMPLATE, info->cExtension, info->rgExtension);
233
+    if (ext)
234
+    {
235
+        pvext = decode_object(&gc, X509_CERTIFICATE_TEMPLATE, &ext->Value, 0, &cbext);
236
+        if (pvext && cbext >= sizeof(CERT_TEMPLATE_EXT))
237
+        {
238
+            const CERT_TEMPLATE_EXT *cte = (const CERT_TEMPLATE_EXT *)pvext;
239
+            if (!stricmp(cert_prop, cte->pszObjId))
240
+            {
241
+                /* found direct OID match with certificate property specified */
242
+                gc_free(&gc);
243
+                return true;
244
+            }
245
+
246
+            const CRYPT_OID_INFO *tmpl_oid = find_oid(CRYPT_OID_INFO_NAME_KEY, tmpl_name,
247
+                                                      CRYPT_TEMPLATE_OID_GROUP_ID);
248
+            if (tmpl_oid && !stricmp(tmpl_oid->pszOID, cte->pszObjId))
249
+            {
250
+                /* found OID match in extension against resolved key */
251
+                gc_free(&gc);
252
+                return true;
253
+            }
254
+        }
255
+    }
256
+
257
+    /* no extension found, exit */
258
+    gc_free(&gc);
259
+    return false;
260
+}
261
+
181 262
 static const CERT_CONTEXT *
182 263
 find_certificate_in_store(const char *cert_prop, HCERTSTORE cert_store)
183 264
 {
... ...
@@ -186,6 +267,7 @@ find_certificate_in_store(const char *cert_prop, HCERTSTORE cert_store)
186 186
      * SUBJ:<certificate substring to match>
187 187
      * THUMB:<certificate thumbprint hex value>, e.g.
188 188
      *     THUMB:f6 49 24 41 01 b4 fb 44 0c ce f4 36 ae d0 c4 c9 df 7a b6 28
189
+     * TMPL:<template name or OID>
189 190
      * The first matching certificate that has not expired is returned.
190 191
      */
191 192
     const CERT_CONTEXT *rv = NULL;
... ...
@@ -218,6 +300,12 @@ find_certificate_in_store(const char *cert_prop, HCERTSTORE cert_store)
218 218
             goto out;
219 219
         }
220 220
     }
221
+    else if (!strncmp(cert_prop, "TMPL:", 5))
222
+    {
223
+        cert_prop += 5;
224
+        find_param = NULL;
225
+        find_type = CERT_FIND_HAS_PRIVATE_KEY;
226
+    }
221 227
     else
222 228
     {
223 229
         msg(M_NONFATAL, "Error in cryptoapicert: unsupported certificate specification <%s>", cert_prop);
... ...
@@ -230,11 +318,18 @@ find_certificate_in_store(const char *cert_prop, HCERTSTORE cert_store)
230 230
         /* this frees previous rv, if not NULL */
231 231
         rv = CertFindCertificateInStore(cert_store, X509_ASN_ENCODING | PKCS_7_ASN_ENCODING,
232 232
                                         0, find_type, find_param, rv);
233
-        if (rv)
233
+        if (!rv)
234
+        {
235
+            break;
236
+        }
237
+        /* if searching by template name, check now if it matches */
238
+        if (find_type == CERT_FIND_HAS_PRIVATE_KEY
239
+            && !test_certificate_template(cert_prop, rv))
234 240
         {
235
-            validity = CertVerifyTimeValidity(NULL, rv->pCertInfo);
241
+            continue;
236 242
         }
237
-        if (!rv || validity == 0)
243
+        validity = CertVerifyTimeValidity(NULL, rv->pCertInfo);
244
+        if (validity == 0)
238 245
         {
239 246
             break;
240 247
         }