[crypto] Add x509_truncate() to truncate a certificate chain

Downloading a cross-signed certificate chain to partially replace
(rather than simply extend) an existing chain will require the ability
to discard all certificates after a specified link in the chain.

Extract the relevant logic from x509_free_chain() and expose it
separately as x509_truncate().

Signed-off-by: Michael Brown <mcb30@ipxe.org>
diff --git a/src/crypto/x509.c b/src/crypto/x509.c
index 1f017eb..9231809 100644
--- a/src/crypto/x509.c
+++ b/src/crypto/x509.c
@@ -1603,19 +1603,12 @@
 static void x509_free_chain ( struct refcnt *refcnt ) {
 	struct x509_chain *chain =
 		container_of ( refcnt, struct x509_chain, refcnt );
-	struct x509_link *link;
-	struct x509_link *tmp;
 
 	DBGC2 ( chain, "X509 chain %p freed\n", chain );
 
-	/* Free each link in the chain */
-	list_for_each_entry_safe ( link, tmp, &chain->links, list ) {
-		x509_put ( link->cert );
-		list_del ( &link->list );
-		free ( link );
-	}
-
 	/* Free chain */
+	x509_truncate ( chain, NULL );
+	assert ( list_empty ( &chain->links ) );
 	free ( chain );
 }
 
@@ -1697,6 +1690,27 @@
 }
 
 /**
+ * Truncate X.509 certificate chain
+ *
+ * @v chain		X.509 certificate chain
+ * @v link		Link after which to truncate chain, or NULL
+ */
+void x509_truncate ( struct x509_chain *chain, struct x509_link *link ) {
+	struct x509_link *tmp;
+
+	/* Truncate entire chain if no link is specified */
+	if ( ! link )
+		link = list_entry ( &chain->links, struct x509_link, list );
+
+	/* Free each link in the chain */
+	list_for_each_entry_safe_continue ( link, tmp, &chain->links, list ) {
+		x509_put ( link->cert );
+		list_del ( &link->list );
+		free ( link );
+	}
+}
+
+/**
  * Identify X.509 certificate by subject
  *
  * @v certs		X.509 certificate list
diff --git a/src/include/ipxe/x509.h b/src/include/ipxe/x509.h
index c703c8f..5cad459 100644
--- a/src/include/ipxe/x509.h
+++ b/src/include/ipxe/x509.h
@@ -391,6 +391,7 @@
 			 struct x509_certificate *cert );
 extern int x509_append_raw ( struct x509_chain *chain, const void *data,
 			     size_t len );
+extern void x509_truncate ( struct x509_chain *chain, struct x509_link *link );
 extern int x509_auto_append ( struct x509_chain *chain,
 			      struct x509_chain *certs );
 extern int x509_validate_chain ( struct x509_chain *chain, time_t time,
diff --git a/src/tests/x509_test.c b/src/tests/x509_test.c
index b6cba57..bc90320 100644
--- a/src/tests/x509_test.c
+++ b/src/tests/x509_test.c
@@ -984,6 +984,7 @@
  *
  */
 static void x509_test_exec ( void ) {
+	struct x509_link *link;
 
 	/* Parse all certificates */
 	x509_certificate_ok ( &root_crt );
@@ -1089,6 +1090,18 @@
 	x509_validate_chain_fail_ok ( &useless_chain, test_ca_expired,
 				      &empty_store, &test_root );
 
+	/* Check chain truncation */
+	link = list_last_entry ( &server_chain.chain->links,
+				 struct x509_link, list );
+	ok ( link->cert == root_crt.cert );
+	link = list_prev_entry ( link, &server_chain.chain->links, list );
+	ok ( link->cert == intermediate_crt.cert );
+	x509_validate_chain_ok ( &server_chain, test_time,
+				 &empty_store, &test_root );
+	x509_truncate ( server_chain.chain, link );
+	x509_validate_chain_fail_ok ( &server_chain, test_time,
+				      &empty_store, &test_root );
+
 	/* Sanity check */
 	assert ( list_empty ( &empty_store.links ) );