From patchwork Fri Jun 4 04:31:25 2021 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: Heiko Wundram X-Patchwork-Id: 1848 Return-Path: Delivered-To: patchwork@openvpn.net Delivered-To: patchwork@openvpn.net Received: from director10.mail.ord1d.rsapps.net ([172.30.191.6]) by backend30.mail.ord1d.rsapps.net with LMTP id v1U3CXs7umAXZAAAIUCqbw (envelope-from ) for ; Fri, 04 Jun 2021 10:40:59 -0400 Received: from proxy10.mail.ord1d.rsapps.net ([172.30.191.6]) by director10.mail.ord1d.rsapps.net with LMTP id uL8vCHs7umBNSwAApN4f7A (envelope-from ) for ; Fri, 04 Jun 2021 10:40:59 -0400 Received: from smtp10.gate.ord1c ([172.30.191.6]) (using TLSv1.2 with cipher ECDHE-RSA-AES256-GCM-SHA384 (256/256 bits)) by proxy10.mail.ord1d.rsapps.net with LMTPS id AKHKB3s7umC4LgAAfSg8FQ (envelope-from ) for ; Fri, 04 Jun 2021 10:40:59 -0400 X-Spam-Threshold: 95 X-Spam-Score: 0 X-Spam-Flag: NO X-Virus-Scanned: OK X-Orig-To: openvpnslackdevel@openvpn.net X-Originating-Ip: [216.105.38.7] Authentication-Results: smtp10.gate.ord1c.rsapps.net; iprev=pass policy.iprev="216.105.38.7"; spf=pass smtp.mailfrom="openvpn-devel-bounces@lists.sourceforge.net" smtp.helo="lists.sourceforge.net"; dkim=fail (signature verification failed) header.d=sourceforge.net; dkim=fail (signature verification failed) header.d=sf.net; dmarc=none (p=nil; dis=none) header.from=gehrkens.it X-Suspicious-Flag: YES X-Classification-ID: dfe2d61c-c542-11eb-b1b5-0026b954785f-1-1 Received: from [216.105.38.7] ([216.105.38.7:53704] helo=lists.sourceforge.net) by smtp10.gate.ord1c.rsapps.net (envelope-from ) (ecelerity 4.2.38.62370 r(:)) with ESMTPS (cipher=DHE-RSA-AES256-GCM-SHA384) id EE/7B-03786-A7B3AB06; Fri, 04 Jun 2021 10:40:58 -0400 Received: from [127.0.0.1] (helo=sfs-ml-2.v29.lw.sourceforge.com) by sfs-ml-2.v29.lw.sourceforge.com with esmtp (Exim 4.92.3) (envelope-from ) id 1lpAzK-0006wV-2J; Fri, 04 Jun 2021 14:39:54 +0000 Received: from [172.30.20.202] (helo=mx.sourceforge.net) by sfs-ml-2.v29.lw.sourceforge.com with esmtps (TLSv1.2:ECDHE-RSA-AES256-GCM-SHA384:256) (Exim 4.92.3) (envelope-from ) id 1lpAzH-0006wI-R2 for openvpn-devel@lists.sourceforge.net; Fri, 04 Jun 2021 14:39:51 +0000 DKIM-Signature: v=1; a=rsa-sha256; q=dns/txt; c=relaxed/relaxed; d=sourceforge.net; s=x; h=Content-Transfer-Encoding:MIME-Version:Message-Id: Date:Subject:Cc:To:From:Sender:Reply-To:Content-Type:Content-ID: Content-Description:Resent-Date:Resent-From:Resent-Sender:Resent-To:Resent-Cc :Resent-Message-ID:In-Reply-To:References:List-Id:List-Help:List-Unsubscribe: List-Subscribe:List-Post:List-Owner:List-Archive; bh=E9bRku97FF921hBEDUT+P6LDk3iUnXFGk5fSsrptMp8=; b=cFuYrtSoErbmIoHinkKYEEFd3n 8x9T4tfQySH7moXJSR/lsuVaAw5BV0GuGZRWVvGDjS2aGFXkBxkaxELJIn4KZEfWQexIUno7oYj9s J1B5mCUBBrOPjRfZ9MGBCe/bR436uVYCz0oT+pyOi1CttfOFV4zEtcJ9C/7gAv8pD9yE=; DKIM-Signature: v=1; a=rsa-sha256; q=dns/txt; c=relaxed/relaxed; d=sf.net; s=x ; h=Content-Transfer-Encoding:MIME-Version:Message-Id:Date:Subject:Cc:To:From :Sender:Reply-To:Content-Type:Content-ID:Content-Description:Resent-Date: Resent-From:Resent-Sender:Resent-To:Resent-Cc:Resent-Message-ID:In-Reply-To: References:List-Id:List-Help:List-Unsubscribe:List-Subscribe:List-Post: List-Owner:List-Archive; bh=E9bRku97FF921hBEDUT+P6LDk3iUnXFGk5fSsrptMp8=; b=I vTgSaevhTGfPqeVaDrKsVk1vbyoQWrN9bhxTZbTdrRPx7J8TTaXznYBuoomC1Uxd4E/BDRD98iynl F8ctC8vSaFzHIHRN+xkRNy4JoasiEnjvAVE8cYejWE+i4Iu6ejGf6kXUnJEOrABewDMjh5LgpMTVk NBhdMFVmlzT9GR6g=; Received: from mailout00.id.gehrkens.it ([83.246.111.105]) by sfi-mx-2.v28.lw.sourceforge.com with esmtps (TLSv1.2:ECDHE-RSA-AES256-GCM-SHA384:256) (Exim 4.92.3) id 1lpAz7-0005S8-8F for openvpn-devel@lists.sourceforge.net; Fri, 04 Jun 2021 14:39:52 +0000 Received: from debdev.id.gehrkens.it (debdev.id.gehrkens.it [IPv6:fdb8:73da:6fd9:9:215:5dff:fefb:2632]) (using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits) key-exchange X25519 server-signature RSA-PSS (2048 bits)) (Client did not present a certificate) by mailout00.id.gehrkens.it (Postfix) with ESMTPS id 05243A811DE; Fri, 4 Jun 2021 16:31:32 +0200 (CEST) Received: by debdev.id.gehrkens.it (Postfix, from userid 66603) id DDBF23610FF; Fri, 4 Jun 2021 16:31:31 +0200 (CEST) From: Heiko Wundram To: openvpn-devel@lists.sourceforge.net Date: Fri, 4 Jun 2021 16:31:25 +0200 Message-Id: <20210604143125.4946-1-heiko.wundram@gehrkens.it> X-Mailer: git-send-email 2.20.1 MIME-Version: 1.0 X-Spam-Report: Spam Filtering performed by mx.sourceforge.net. See http://spamassassin.org/tag/ for more details. 0.4 NO_DNS_FOR_FROM DNS: Envelope sender has no MX or A DNS records 0.0 URIBL_BLOCKED ADMINISTRATOR NOTICE: The query to URIBL was blocked. See http://wiki.apache.org/spamassassin/DnsBlocklists#dnsbl-block for more information. [URIs: gehrkens.it] 0.0 SPF_NONE SPF: sender does not publish an SPF Record 0.0 SPF_HELO_NONE SPF: HELO does not publish an SPF Record X-Headers-End: 1lpAz7-0005S8-8F Subject: [Openvpn-devel] [PATCH] Implement Windows CA template match for Crypto-API selector X-BeenThere: openvpn-devel@lists.sourceforge.net X-Mailman-Version: 2.1.21 Precedence: list List-Id: List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , Errors-To: openvpn-devel-bounces@lists.sourceforge.net X-getmail-retrieved-from-mailbox: Inbox 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 --- src/openvpn/cryptoapi.c | 142 ++++++++++++++++++++++++++++++++++++++-- 1 file changed, 135 insertions(+), 7 deletions(-) diff --git a/src/openvpn/cryptoapi.c b/src/openvpn/cryptoapi.c index ded8c914..3532a9a0 100644 --- a/src/openvpn/cryptoapi.c +++ b/src/openvpn/cryptoapi.c @@ -732,6 +732,115 @@ 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 count for decoding */ + 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) + { + 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); + } + + /* fall back to all groups if not found yet and fallback requested */ + if (!info && fallback && groupid) + { + info = CryptFindOIDInfo(keytype, (void*)key, 0); + } + + return info; +} + +static bool +test_certificate_template(const char *cert_prop, const CERT_CONTEXT *cert_ctx) +{ + const CERT_INFO *info = cert_ctx->pCertInfo; + const CERT_EXTENSION *ext; + DWORD cbext; + void *pvext; + struct gc_arena gc = gc_new(); + 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 (nv->Value.cbData >= sizeof(WCHAR) * (wcslen(tmpl_name) + 1) + && !_wcsicmp((const WCHAR*)nv->Value.pbData, tmpl_name)) + { + /* data content matches extension name */ + gc_free(&gc); + 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 */ + gc_free(&gc); + return true; + } + else if (!tmpl_oid && !stricmp(cert_prop, cte->pszObjId)) + { + /* found direct OID match with certificate property specified */ + gc_free(&gc); + return true; + } + } + } + + /* no extension found, exit */ + gc_free(&gc); + return false; +} + static const CERT_CONTEXT * find_certificate_in_store(const char *cert_prop, HCERTSTORE cert_store) { @@ -743,17 +852,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)(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,12 +936,23 @@ find_certificate_in_store(const char *cert_prop, HCERTSTORE cert_store) { validity = CertVerifyTimeValidity(NULL, rv->pCertInfo); } - if (!rv || validity == 0) + else { break; } - msg(M_WARN, "WARNING: cryptoapicert: ignoring certificate in store %s.", - validity < 0 ? "not yet valid" : "that has expired"); + + if (validity == 0) + { + if (!find_test || find_test(cert_prop, rv)) + { + break; + } + } + else + { + msg(M_WARN, "WARNING: cryptoapicert: ignoring certificate in store %s.", + validity < 0 ? "not yet valid" : "that has expired"); + } } out: