Repository: duo-labs/py_webauthn

Issue:
Update certificate chain could not be validated log to include raised error
Right now we have

```
    try:
        context.verify_certificate()
    except X509StoreContextError:
        raise InvalidCertificateChain("Certificate chain could not be validated")
```

This makes it so callers lose out on the context of the original `X509StoreContextError` that was raised (which can be useful in debugging).

We can/should fix this by maintaining the original error message in the newly raised error as we do elsewhere in the file.

This issue is to do so!

Complete when the new error raised has context on the original error message/failure as we do elsewhere in the file.

Discussion:
@MasterKale: Thanks for the report @robbie-duo, this has been fixed as of `webauthn==2.5.4` 🎉

Solution patch:
diff --git a/webauthn/helpers/validate_certificate_chain.py b/webauthn/helpers/validate_certificate_chain.py
index 2620eeb..ace2904 100644
--- a/webauthn/helpers/validate_certificate_chain.py
+++ b/webauthn/helpers/validate_certificate_chain.py
@@ -35,8 +35,8 @@ def validate_certificate_chain(
         leaf_cert_bytes = x5c[0]
         leaf_cert_crypto = load_der_x509_certificate(leaf_cert_bytes)
         leaf_cert = X509().from_cryptography(leaf_cert_crypto)
-    except Exception as err:
-        raise InvalidCertificateChain(f"Could not prepare leaf cert: {err}")
+    except Exception as exc:
+        raise InvalidCertificateChain("Could not prepare leaf cert") from exc
 
     # Prepare any intermediate certs
     try:
@@ -46,16 +46,16 @@ def validate_certificate_chain(
             load_der_x509_certificate(cert) for cert in intermediate_certs_bytes
         ]
         intermediate_certs = [X509().from_cryptography(cert) for cert in intermediate_certs_crypto]
-    except Exception as err:
-        raise InvalidCertificateChain(f"Could not prepare intermediate certs: {err}")
+    except Exception as exc:
+        raise InvalidCertificateChain("Could not prepare intermediate certs") from exc
 
     # Prepare a collection of possible root certificates
     cert_store = _generate_new_cert_store()
     try:
         for cert in pem_root_certs_bytes:
             cert_store.add_cert(pem_cert_bytes_to_open_ssl_x509(cert))
-    except Exception as err:
-        raise InvalidCertificateChain(f"Could not prepare root certs: {err}")
+    except Exception as exc:
+        raise InvalidCertificateChain("Could not prepare root certs") from exc
 
     # Load certs into a "context" for validation
     context = X509StoreContext(
@@ -67,8 +67,8 @@ def validate_certificate_chain(
     # Validate the chain (will raise if it can't)
     try:
         context.verify_certificate()
-    except X509StoreContextError:
-        raise InvalidCertificateChain("Certificate chain could not be validated")
+    except X509StoreContextError as exc:
+        raise InvalidCertificateChain("Certificate chain could not be validated") from exc
 
     return True
 


Test patch:
diff --git a/tests/helpers/x509store.py b/tests/helpers/x509store.py
index f489542..6c3f912 100644
--- a/tests/helpers/x509store.py
+++ b/tests/helpers/x509store.py
@@ -22,6 +22,8 @@ def patch_validate_certificate_chain_x509store_getter(func):
     from datetime import datetime
     from OpenSSL.crypto import X509Store
 
+    from .helpers.x509store import patch_validate_certificate_chain_x509store_getter
+
     class TestX509Validation(TestCase):
         @patch_validate_certificate_chain_x509store_getter
         def test_validate_x509_chain(self, patched_x509store: X509Store):
diff --git a/tests/test_validate_certificate_chain.py b/tests/test_validate_certificate_chain.py
index 0bb9834..75a3d90 100644
--- a/tests/test_validate_certificate_chain.py
+++ b/tests/test_validate_certificate_chain.py
@@ -1,6 +1,7 @@
 from unittest import TestCase
+from unittest.mock import patch
 from datetime import datetime
-from OpenSSL.crypto import X509Store
+from OpenSSL.crypto import X509, X509Store, X509StoreContextError
 
 from webauthn.helpers.exceptions import InvalidCertificateChain
 from webauthn.helpers.known_root_certs import (
@@ -75,3 +76,23 @@ class TestValidateCertificateChain(TestCase):
         except Exception as err:
             print(err)
             self.fail("validate_certificate_chain failed when it should have succeeded")
+
+    def test_includes_original_exception_when_raising(self):
+        """
+        A low-effort attempt at ensuring that the attempt to validate a certificate chain will
+        include the original exception raised by the X509 validation library, instead of just its
+        message.
+        """
+        custom_exception = X509StoreContextError(message="Oops", certificate=X509(), errors=[])
+
+        with patch(
+            "OpenSSL.crypto.X509StoreContext.verify_certificate", side_effect=custom_exception
+        ):
+            with self.assertRaises(InvalidCertificateChain) as ctx:
+                validate_certificate_chain(
+                    x5c=apple_x5c_certs,
+                    pem_root_certs_bytes=[apple_webauthn_root_ca],
+                )
+
+            cause = ctx.exception.__cause__
+            self.assertEqual(cause, custom_exception)