Hi, I'm not convinced of the utility of this. It could be marginally useful in some setups where all users are tied to a domain and choosing any certificate that matches a template is appropriate. I don't have a setup to test this.
Here are some general comments anyway. On Tue, May 25, 2021 at 5:44 AM Heiko Wundram <heiko.wund...@gehrkens.it> wrote: > > 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. > > Signed-off-by: Heiko Wundram <heiko.wund...@gehrkens.it> > --- > src/openvpn/cryptoapi.c | 117 ++++++++++++++++++++++++++++++++++++++-- > 1 file changed, 112 insertions(+), 5 deletions(-) > > diff --git a/src/openvpn/cryptoapi.c b/src/openvpn/cryptoapi.c > index ded8c914..ee4c9014 100644 > --- a/src/openvpn/cryptoapi.c > +++ b/src/openvpn/cryptoapi.c > @@ -732,6 +732,105 @@ err: > > #endif /* OPENSSL_VERSION_NUMBER >= 1.1.0 */ > > +static void * > +decode_object(struct gc_arena *gc, LPCSTR struct_type, const > CRYPT_OBJID_BLOB *val, DWORD flags, DWORD *cb) > +{ > + /* get byte cound for decoding */ typo: count > + BYTE *buf; > + if (!CryptDecodeObject(X509_ASN_ENCODING | PKCS_7_ASN_ENCODING, > struct_type, val->pbData, val->cbData, flags, NULL, cb)) > + return NULL; > + > + /* do the actual decode */ > + buf = gc_malloc(*cb, false, gc); > + if (!CryptDecodeObject(X509_ASN_ENCODING | PKCS_7_ASN_ENCODING, > struct_type, val->pbData, val->cbData, flags, buf, cb)) > + return NULL; > + > + return buf; > +} > + > +static const CRYPT_OID_INFO * > +find_oid(DWORD keytype, const void *key, DWORD groupid, bool fallback) > +{ > + const CRYPT_OID_INFO *info = NULL; > + > + /* force resolve from local as first step */ > + if (groupid != CRYPT_HASH_ALG_OID_GROUP_ID && > + groupid != CRYPT_ENCRYPT_ALG_OID_GROUP_ID && > + groupid != CRYPT_PUBKEY_ALG_OID_GROUP_ID && > + groupid != CRYPT_SIGN_ALG_OID_GROUP_ID && > + groupid != CRYPT_RDN_ATTR_OID_GROUP_ID && > + groupid != CRYPT_EXT_OR_ATTR_OID_GROUP_ID && > + groupid != CRYPT_KDF_OID_GROUP_ID) > + { Why not do the local lookup for all grupids? Anyway you are falling back to domain-wide look up if this fails. > + info = CryptFindOIDInfo(keytype, (void*)key, groupid | > CRYPT_OID_DISABLE_SEARCH_DS_FLAG); > + } > + > + /* try proper resolve if not found yet, also including AD */ > + if (!info) > + { > + info = CryptFindOIDInfo(keytype, (void*)key, groupid); How long would this take to time out -- we try to avoid calling functions that need network access to the DC as that's often not available before the tunnel comes up. > + } > + > + /* fall back to all groups if not found yet and fallback requested */ > + if (!info && fallback && groupid) > + { > + info = CryptFindOIDInfo(keytype, (void*)key, 0); That could be a second network call. Why not combine the two into one call. > + } > + > + return info; > +} > + > +static bool > +test_certificate_template(struct gc_arena *gc, const char *cert_prop, const > CERT_CONTEXT *cert_ctx) > +{ As this is called from a loop for every certificate in the store until a match is found, passing in gc that will be released only after exiting the loop is wasteful memory use. Just create a new gc clear and clear it at exit. You are not using any buffers allocated here after return. > + const CERT_INFO *info = cert_ctx->pCertInfo; > + const CERT_EXTENSION *ext; > + DWORD cbext; > + void *pvext; > + const WCHAR *tmpl_name = wide_string(cert_prop, gc); > + > + /* check for V1 extension (Windows 2K and below) */ > + ext = CertFindExtension(szOID_ENROLL_CERTTYPE_EXTENSION, > info->cExtension, info->rgExtension); > + if (ext) > + { > + pvext = decode_object(gc, X509_UNICODE_ANY_STRING, &ext->Value, 0, > &cbext); > + if (pvext && cbext >= sizeof(CERT_NAME_VALUE)) > + { > + const CERT_NAME_VALUE *nv = (const CERT_NAME_VALUE*)pvext; > + if (!_wcsicmp((const WCHAR*)nv->Value.pbData, tmpl_name)) Is it guaranteed that pbData is NULL terminated? (MS docs are unclear on this). We may have to add a safety check that the data length length is >= wcslen(tmpl_name). > + { > + /* data content matches extension name */ > + return true; > + } > + } > + } > + > + /* check for V2 extension (Windows 2003+) */ > + ext = CertFindExtension(szOID_CERTIFICATE_TEMPLATE, info->cExtension, > info->rgExtension); > + if (ext) > + { > + pvext = decode_object(gc, X509_CERTIFICATE_TEMPLATE, &ext->Value, 0, > &cbext); > + if (pvext && cbext >= sizeof(CERT_TEMPLATE_EXT)) > + { > + const CERT_TEMPLATE_EXT *cte = (const CERT_TEMPLATE_EXT*)pvext; > + const CRYPT_OID_INFO *tmpl_oid = > find_oid(CRYPT_OID_INFO_NAME_KEY, tmpl_name, CRYPT_TEMPLATE_OID_GROUP_ID, > true); > + if (tmpl_oid && !stricmp(tmpl_oid->pszOID, cte->pszObjId)) > + { > + /* found OID match in extension against resolved key */ > + return true; > + } > + else if (!tmpl_oid && !stricmp(cert_prop, cte->pszObjId)) > + { Why not test this first so that the call to find_oid could be avoided if this matches? > + /* found direct OID match with certificate property > specified */ > + return true; > + } > + } > + } > + > + /* no extension found, exit */ > + return false; > +} > + > static const CERT_CONTEXT * > find_certificate_in_store(const char *cert_prop, HCERTSTORE cert_store) > { > @@ -743,17 +842,25 @@ find_certificate_in_store(const char *cert_prop, > HCERTSTORE cert_store) > * The first matching certificate that has not expired is returned. > */ > const CERT_CONTEXT *rv = NULL; > - DWORD find_type; > - const void *find_param; > + DWORD find_type = CERT_FIND_ANY; > + const void *find_param = NULL; > + bool (*find_test)(struct gc_arena*, const char*, const CERT_CONTEXT*) = > NULL; > unsigned char hash[255]; > CRYPT_HASH_BLOB blob = {.cbData = 0, .pbData = hash}; > struct gc_arena gc = gc_new(); > > - if (!strncmp(cert_prop, "SUBJ:", 5)) > + if (!strncmp(cert_prop, "TMPL:", 5)) > + { > + /* skip the tag */ > + cert_prop += 5; > + find_test = &test_certificate_template; > + } > + else if (!strncmp(cert_prop, "SUBJ:", 5)) > { > /* skip the tag */ > - find_param = wide_string(cert_prop + 5, &gc); > + cert_prop += 5; > find_type = CERT_FIND_SUBJECT_STR_W; > + find_param = wide_string(cert_prop, &gc); > } > else if (!strncmp(cert_prop, "THUMB:", 6)) > { > @@ -819,7 +926,7 @@ find_certificate_in_store(const char *cert_prop, > HCERTSTORE cert_store) > { > validity = CertVerifyTimeValidity(NULL, rv->pCertInfo); > } > - if (!rv || validity == 0) > + if (!rv || (validity == 0 && (!find_test || find_test(&gc, > cert_prop, rv)))) > { > break; > } The warning log below this is meant to be printed only when the cert matches but validity period is off. But now it will print a warning for every certificate in the store until a match is found. That's not acceptable. There are some missing {} for if/else, we prefer && at the start of line for multi-line conditions etc. See https://community.openvpn.net/openvpn/wiki/CodeStyle Selva _______________________________________________ Openvpn-devel mailing list Openvpn-devel@lists.sourceforge.net https://lists.sourceforge.net/lists/listinfo/openvpn-devel