[PATCH V2 6/6] library: Add openconnect_get_peer_cert_chain()

Kevin Cernekee cernekee at gmail.com
Sun Apr 17 23:05:02 PDT 2016


Allow external validation of the entire certificate chain, not just the
peer_cert.  Tested using a letsencrypt cert on Chrome OS.

Signed-off-by: Kevin Cernekee <cernekee at gmail.com>
---
 gnutls.c                                           | 39 ++++++++++++++-
 java/src/com/example/LibTest.java                  |  2 +
 .../infradead/libopenconnect/LibOpenConnect.java   |  1 +
 jni.c                                              | 49 +++++++++++++++++++
 libopenconnect.map.in                              |  2 +
 openconnect-internal.h                             |  2 +
 openconnect.h                                      | 18 +++++++
 openssl.c                                          | 55 ++++++++++++++++++++--
 8 files changed, 163 insertions(+), 5 deletions(-)

diff --git a/gnutls.c b/gnutls.c
index 2a93dac8ac3e..338f7a78fbed 100644
--- a/gnutls.c
+++ b/gnutls.c
@@ -1948,6 +1948,38 @@ void openconnect_free_cert_info(struct openconnect_info *vpninfo,
 	gnutls_free(buf);
 }
 
+int openconnect_get_peer_cert_chain(struct openconnect_info *vpninfo,
+				    struct oc_cert **chainp)
+{
+	struct oc_cert *chain, *p;
+	const gnutls_datum_t *cert_list = vpninfo->cert_list_handle;
+	int i, cert_list_size = vpninfo->cert_list_size;
+
+	if (!cert_list)
+		return -EINVAL;
+
+	if (cert_list_size <= 0)
+		return -EIO;
+
+	p = chain = calloc(cert_list_size, sizeof(struct oc_cert));
+	if (!chain)
+		return -ENOMEM;
+
+	for (i = 0; i < cert_list_size; i++, p++) {
+		p->der_data = (unsigned char *)cert_list[i].data;
+		p->der_len = cert_list[i].size;
+	}
+
+	*chainp = chain;
+	return cert_list_size;
+}
+
+void openconnect_free_peer_cert_chain(struct openconnect_info *vpninfo,
+				      struct oc_cert *chain)
+{
+	free(chain);
+}
+
 static int verify_peer(gnutls_session_t session)
 {
 	struct openconnect_info *vpninfo = gnutls_session_get_ptr(session);
@@ -2079,10 +2111,13 @@ static int verify_peer(gnutls_session_t session)
 		vpn_progress(vpninfo, PRG_INFO,
 			     _("Server certificate verify failed: %s\n"),
 			     reason);
-		if (vpninfo->validate_peer_cert)
+		if (vpninfo->validate_peer_cert) {
+			vpninfo->cert_list_handle = (void *)cert_list;
+			vpninfo->cert_list_size = cert_list_size;
 			err = vpninfo->validate_peer_cert(vpninfo->cbdata,
 							  reason) ? GNUTLS_E_CERTIFICATE_ERROR : 0;
-		else
+			vpninfo->cert_list_handle = NULL;
+		} else
 			err = GNUTLS_E_CERTIFICATE_ERROR;
 	}
 
diff --git a/java/src/com/example/LibTest.java b/java/src/com/example/LibTest.java
index da073b7bbe5a..78e77b5877ed 100644
--- a/java/src/com/example/LibTest.java
+++ b/java/src/com/example/LibTest.java
@@ -49,6 +49,8 @@ public final class LibTest {
 
 			byte der[] = getPeerCertDER();
 			System.out.println("DER is " + der.length + " bytes long");
+			byte chain[][] = getPeerCertChain();
+			System.out.println("Chain has " + chain.length + " certs");
 
 			System.out.print("\nAccept this certificate? [n] ");
 			String s = getline();
diff --git a/java/src/org/infradead/libopenconnect/LibOpenConnect.java b/java/src/org/infradead/libopenconnect/LibOpenConnect.java
index 3f70b2b15aa2..ce4ffcc26a4b 100644
--- a/java/src/org/infradead/libopenconnect/LibOpenConnect.java
+++ b/java/src/org/infradead/libopenconnect/LibOpenConnect.java
@@ -156,6 +156,7 @@ public abstract class LibOpenConnect {
 	public synchronized native String getPeerCertHash();
 	public synchronized native String getPeerCertDetails();
 	public synchronized native byte[] getPeerCertDER();
+	public synchronized native byte[][] getPeerCertChain();
 
 	/* library info */
 
diff --git a/jni.c b/jni.c
index bfcdaa5abf0d..d72ac2e12376 100644
--- a/jni.c
+++ b/jni.c
@@ -785,6 +785,55 @@ JNIEXPORT jbyteArray JNICALL Java_org_infradead_libopenconnect_LibOpenConnect_ge
 	return jresult;
 }
 
+/* special handling: callee-allocated, caller-freed binary buffer */
+JNIEXPORT jbyteArray JNICALL Java_org_infradead_libopenconnect_LibOpenConnect_getPeerCertChain(
+	JNIEnv *jenv, jobject jobj)
+{
+	struct libctx *ctx = getctx(jenv, jobj);
+	struct oc_cert *chain = NULL, *p;
+	int cert_list_size, i;
+	jobjectArray jresult = NULL;
+	jclass jcls;
+
+	if (!ctx)
+		goto err;
+	cert_list_size = openconnect_get_peer_cert_chain(ctx->vpninfo, &chain);
+	if (cert_list_size <= 0)
+		goto err;
+
+	jcls = (*ctx->jenv)->FindClass(ctx->jenv, "[B");
+	if (!jcls)
+		goto err;
+
+	jresult = (*ctx->jenv)->NewObjectArray(ctx->jenv, cert_list_size, jcls, NULL);
+	if (!jresult)
+		goto err;
+
+	if ((*ctx->jenv)->PushLocalFrame(ctx->jenv, 256) < 0)
+		goto err;
+
+	for (i = 0, p = chain; i < cert_list_size; i++, p++) {
+		jbyteArray cert = (*ctx->jenv)->NewByteArray(ctx->jenv, p->der_len);
+		if (!cert)
+			goto err2;
+		(*ctx->jenv)->SetByteArrayRegion(ctx->jenv, cert, 0, p->der_len, (jbyte *)p->der_data);
+		(*ctx->jenv)->SetObjectArrayElement(ctx->jenv, jresult, i, cert);
+	}
+
+	(*ctx->jenv)->PopLocalFrame(ctx->jenv, NULL);
+	openconnect_free_peer_cert_chain(ctx->vpninfo, chain);
+	return jresult;
+
+err2:
+	(*ctx->jenv)->PopLocalFrame(ctx->jenv, NULL);
+err:
+	if (jresult)
+		(*ctx->jenv)->DeleteLocalRef(ctx->jenv, jresult);
+	if (chain)
+		openconnect_free_peer_cert_chain(ctx->vpninfo, chain);
+	return NULL;
+}
+
 /* special handling: two string arguments */
 JNIEXPORT void JNICALL Java_org_infradead_libopenconnect_LibOpenConnect_setClientCert(
 	JNIEnv *jenv, jobject jobj, jstring jcert, jstring jsslkey)
diff --git a/libopenconnect.map.in b/libopenconnect.map.in
index deaf058da8ce..f832ddad16b2 100644
--- a/libopenconnect.map.in
+++ b/libopenconnect.map.in
@@ -3,6 +3,7 @@ OPENCONNECT_5.0 {
 	openconnect_check_peer_cert_hash;
 	openconnect_clear_cookie;
 	openconnect_free_cert_info;
+	openconnect_free_peer_cert_chain;
 	openconnect_get_cookie;
 	openconnect_get_cstp_cipher;
 	openconnect_get_dnsname;
@@ -11,6 +12,7 @@ OPENCONNECT_5.0 {
 	openconnect_get_ifname;
 	openconnect_get_ip_info;
 	openconnect_get_peer_cert_DER;
+	openconnect_get_peer_cert_chain;
 	openconnect_get_peer_cert_details;
 	openconnect_get_peer_cert_hash;
 	openconnect_get_port;
diff --git a/openconnect-internal.h b/openconnect-internal.h
index b339ef6eb4cb..4ded7612c802 100644
--- a/openconnect-internal.h
+++ b/openconnect-internal.h
@@ -443,6 +443,8 @@ struct openconnect_info {
 
 	void *peer_cert;
 	char *peer_cert_hash;
+	void *cert_list_handle;
+	int cert_list_size;
 
 	char *cookie; /* Pointer to within cookies list */
 	struct oc_vpn_option *cookies;
diff --git a/openconnect.h b/openconnect.h
index d34aae05241a..904a92a8dbe0 100644
--- a/openconnect.h
+++ b/openconnect.h
@@ -46,6 +46,8 @@ extern "C" {
  *  - Add openconnect_set_setup_tun_handler().
  *  - Add openconnect_set_reconnected_handler().
  *  - Add openconnect_get_dnsname().
+ *  - Add openconnect_get_peer_cert_chain() and
+ *        openconnect_free_peer_cert_chain().
  *
  * API version 5.2 (v7.05; 2015-03-10):
  *  - Add openconnect_set_http_auth(), openconnect_set_protocol().
@@ -273,6 +275,12 @@ struct oc_stats {
 	uint64_t rx_bytes;
 };
 
+struct oc_cert {
+	int der_len;
+	unsigned char *der_data;
+	void *reserved;
+};
+
 /****************************************************************************/
 
 #define PRG_ERR		0
@@ -367,6 +375,16 @@ int openconnect_get_peer_cert_DER(struct openconnect_info *vpninfo,
 				  unsigned char **buf);
 void openconnect_free_cert_info(struct openconnect_info *vpninfo,
 				void *buf);
+
+/* Creates a list of all certs in the peer's chain, returning the
+   number of certs in the chain (or <0 on error). Only valid inside the
+   validate_peer_cert callback. The caller should free the chain,
+   but should not modify the contents. */
+int openconnect_get_peer_cert_chain(struct openconnect_info *vpninfo,
+				    struct oc_cert **chain);
+void openconnect_free_peer_cert_chain(struct openconnect_info *vpninfo,
+				      struct oc_cert *chain);
+
 /* Contains a comma-separated list of authentication methods to enabled.
    Currently supported: Negotiate,NTLM,Digest,Basic */
 int openconnect_set_http_auth(struct openconnect_info *vpninfo,
diff --git a/openssl.c b/openssl.c
index 007809bca6c3..8a5ef5606524 100644
--- a/openssl.c
+++ b/openssl.c
@@ -1325,6 +1325,48 @@ static void workaround_openssl_certchain_bug(struct openconnect_info *vpninfo,
 	X509_STORE_CTX_cleanup(&ctx);
 }
 
+int openconnect_get_peer_cert_chain(struct openconnect_info *vpninfo,
+				    struct oc_cert **chainp)
+{
+	struct oc_cert *chain, *p;
+	X509_STORE_CTX *ctx = vpninfo->cert_list_handle;
+	int i, cert_list_size;
+
+	if (!ctx)
+		return -EINVAL;
+
+	cert_list_size = sk_X509_num(ctx->untrusted);
+	if (!cert_list_size)
+		return -EIO;
+
+	p = chain = calloc(cert_list_size, sizeof(struct oc_cert));
+	if (!chain)
+		return -ENOMEM;
+
+	for (i = 0; i < cert_list_size; i++, p++) {
+		X509 *cert = sk_X509_value(ctx->untrusted, i);
+
+		p->der_len = i2d_X509(cert, &p->der_data);
+		if (p->der_len < 0) {
+			openconnect_free_peer_cert_chain(vpninfo, chain);
+			return -ENOMEM;
+		}
+	}
+
+	*chainp = chain;
+	return cert_list_size;
+}
+
+void openconnect_free_peer_cert_chain(struct openconnect_info *vpninfo,
+				      struct oc_cert *chain)
+{
+	int i;
+
+	for (i = 0; i < vpninfo->cert_list_size; i++)
+		OPENSSL_free(chain[i].der_data);
+	free(chain);
+}
+
 static int ssl_app_verify_callback(X509_STORE_CTX *ctx, void *arg)
 {
 	struct openconnect_info *vpninfo = arg;
@@ -1375,9 +1417,16 @@ static int ssl_app_verify_callback(X509_STORE_CTX *ctx, void *arg)
 		     _("Server certificate verify failed: %s\n"),
 		     err_string);
 
-	if (vpninfo->validate_peer_cert &&
-	    !vpninfo->validate_peer_cert(vpninfo->cbdata, err_string))
-		return 1;
+	if (vpninfo->validate_peer_cert) {
+		int ret;
+
+		vpninfo->cert_list_handle = ctx;
+		ret = vpninfo->validate_peer_cert(vpninfo->cbdata, err_string);
+		vpninfo->cert_list_handle = NULL;
+
+		if (!ret)
+			return 1;
+	}
 
 	return 0;
 }
-- 
2.8.1




More information about the openconnect-devel mailing list