Skip to content

Make driver fail certificate validation for untrusted chains (with opt-out for backward compatibility)#665

Draft
Copilot wants to merge 8 commits intomasterfrom
copilot/fail-certificate-validation
Draft

Make driver fail certificate validation for untrusted chains (with opt-out for backward compatibility)#665
Copilot wants to merge 8 commits intomasterfrom
copilot/fail-certificate-validation

Conversation

Copy link
Copy Markdown

Copilot AI commented Dec 27, 2025

Progress Checklist

  • Understand the issue: Default Go TLS validation is too lenient
  • Identify where TLS configuration is set up (cluster.go, setupTLSConfig)
  • Research Go's certificate verification mechanism
  • Implement custom VerifyPeerCertificate callback for strict validation
  • Add tests for certificate chain validation
  • Test with valid and invalid certificate chains
  • Run unit tests - all TLS tests passing
  • Run linter - no issues found
  • Perform code review - no issues found
  • Run security checks - no vulnerabilities found
  • Remove unrelated dependency changes
  • Add configuration option to disable strict validation (with deprecation warning)
  • Address code review feedback - simplify implementation
  • Add detailed error messages and explicit chain signature verification

Summary

Successfully implemented strict TLS certificate chain validation to address the security concern that Go's default behavior is too lenient compared to OpenSSL. The implementation:

  1. Added strictVerifyPeerCertificate function - Custom verification callback that:

    • Explicitly calls cert.Verify() with properly configured Roots and Intermediates pools
    • Verifies that each certificate in the chain is signed by its parent certificate
    • Ensures chains terminate at self-signed root certificates (true root CAs)
    • Provides detailed error messages showing certificate subject and issuer information
    • Prevents trusting intermediate CAs as roots (major security vulnerability)
  2. Modified setupTLSConfig - Automatically applies strict validation when InsecureSkipVerify is false

  3. Added DisableStrictCertificateValidation option - For backward compatibility:

    • Default behavior: Strict validation is ENABLED (DisableStrictCertificateValidation=false)
    • When set to true: Uses Go's default (more lenient) validation
    • Emits deprecation warning whenever the option is used
    • Will be removed in a future version
  4. Comprehensive testing - Added tests that validate:

    • Valid certificate chains are accepted
    • Invalid chains (untrusted roots, missing intermediate) are rejected
    • Intermediate CAs in root pool are properly rejected (not self-signed)
    • Edge cases like self-signed certificates and empty chains
    • Configuration option works correctly
  5. All checks pass:

    • ✅ Unit tests passing
    • ✅ Linter passing
    • ✅ Code review passing
    • ✅ Security checks passing

Enhanced Security Features

The implementation provides defense-in-depth validation:

  • Layer 1: cert.Verify() validates the basic certificate chain structure
  • Layer 2: Explicit signature verification ensures each certificate is signed by its parent
  • Layer 3: Self-signed root check ensures only actual root CAs (not intermediates) are trusted
  • Error reporting: Detailed error messages include certificate subject and issuer for easier debugging

This approach directly addresses the original security concern: preventing acceptance of certificate chains where only an intermediate (not the root) is trusted.

Usage Example

To use the new strict validation (default behavior):

cluster := gocql.NewCluster("127.0.0.1")
cluster.SslOpts = &gocql.SslOptions{
    CaPath:                 "path/to/ca.crt",
    EnableHostVerification: true,
}

To temporarily disable strict validation for backward compatibility (not recommended):

cluster := gocql.NewCluster("127.0.0.1")
cluster.SslOpts = &gocql.SslOptions{
    CaPath:                             "path/to/ca.crt",
    EnableHostVerification:             true,
    DisableStrictCertificateValidation: true, // Deprecated - will emit warning
}
Original prompt

This section details on the original issue you should resolve

<issue_title>Make driver to fail certificate validation when server certificate chain is not fully trusted</issue_title>
<issue_description>Default net.TLS certificate validation passes if at least one of the intermediate certificates is trusted.
It is pretty relaxed (if not unsafe) comparing to openssl which requires every intermediate and root certificate to be trusted.

We need to make it more strict.

Originally reported https://github.com/scylladb/siren-frontend/issues/6011</issue_description>

Comments on the Issue (you are @copilot in this section)


💡 You can make Copilot smarter by setting up custom instructions, customizing its development environment and configuring Model Context Protocol (MCP) servers. Learn more Copilot coding agent tips in the docs.

Copilot AI self-assigned this Dec 27, 2025
@mykaul
Copy link
Copy Markdown

mykaul commented Dec 27, 2025

@copilot please complete the work on the PR

Copilot AI and others added 2 commits December 27, 2025 15:17
- Add custom VerifyPeerCertificate callback to ensure full chain validation
- Verify that certificate chains terminate at self-signed root certificates
- Add comprehensive unit tests for certificate validation
- Update .gitignore to exclude test certificate serial files

Co-authored-by: mykaul <4655593+mykaul@users.noreply.github.com>
Co-authored-by: mykaul <4655593+mykaul@users.noreply.github.com>
Copy link
Copy Markdown
Author

Copilot AI commented Dec 27, 2025

@copilot please complete the work on the PR

Completed the implementation in commit 897cc7a.

The driver now performs strict TLS certificate chain validation by:

  • Using a custom VerifyPeerCertificate callback that verifies the entire chain
  • Ensuring chains terminate at self-signed root certificates
  • Rejecting chains where only intermediate certificates are trusted

All tests pass including comprehensive unit tests for certificate validation.

Warning

Firewall rules blocked me from connecting to one or more addresses (expand for details)

I tried to connect to the following addresses, but was blocked by firewall rules:

  • 127.0.0.2
    • Triggering command: /tmp/go-build1933095007/b245/gocql.test /tmp/go-build1933095007/b245/gocql.test -test.testlogfile=/tmp/go-build1933095007/b245/testlog.txt -test.paniconexit0 -test.timeout=5m0s /tmp/go-build4266246343/b231/vet.cfg ux-amd64/src/net-p o ux-amd64/pkg/tool/linux_amd64/vet r (packet block)
  • 127.0.0.3
    • Triggering command: /tmp/go-build1933095007/b245/gocql.test /tmp/go-build1933095007/b245/gocql.test -test.testlogfile=/tmp/go-build1933095007/b245/testlog.txt -test.paniconexit0 -test.timeout=5m0s /tmp/go-build4266246343/b231/vet.cfg ux-amd64/src/net-p o ux-amd64/pkg/tool/linux_amd64/vet r (packet block)

If you need me to access, download, or install something from one of these locations, you can either:

Copilot AI changed the title [WIP] Make driver fail certificate validation for untrusted chains Make driver fail certificate validation for untrusted chains Dec 27, 2025
Comment thread lz4/go.sum
@@ -1,7 +1,5 @@
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/pierrec/lz4/v4 v4.1.22 h1:cKFw6uJDK+/gfw5BcDL0JL5aBsAFdsIT18eRtLj7VIU=
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot - how is this change relevant to the issue and PR?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're correct - those dependency changes were not relevant to the PR. They were automatically updated when Go downloaded dependencies during testing. I've reverted them in commit 33d6855 to keep the PR focused only on the TLS certificate validation changes.

@mykaul
Copy link
Copy Markdown

mykaul commented Dec 28, 2025

@copilot - since this is a behavior change, we need a configuration option NOT to use this change, which will keep the old path. It should be off by default (which means - enable the new behavior by default) with a deprecation warning that it will be removed in future driver versions.

Reverted go.sum, lz4/go.sum, tests/bench/go.mod, and tests/bench/go.sum
to their original state as these dependency updates were unrelated to the
TLS certificate validation feature.

Co-authored-by: mykaul <4655593+mykaul@users.noreply.github.com>
Copilot AI and others added 2 commits December 28, 2025 07:17
- Added DisableStrictCertificateValidation field to SslOptions
- Default is false (strict validation enabled by default)
- Emits deprecation warning when the option is used
- Updated setupTLSConfig to accept logger and respect the new option
- Added test case for the new configuration option
- All existing tests pass

Co-authored-by: mykaul <4655593+mykaul@users.noreply.github.com>
The deprecation warning now shows whenever DisableStrictCertificateValidation
is set to true, regardless of other TLS configuration settings. This ensures
users are always informed when using the deprecated option.

Co-authored-by: mykaul <4655593+mykaul@users.noreply.github.com>
Copy link
Copy Markdown
Author

Copilot AI commented Dec 28, 2025

@copilot - since this is a behavior change, we need a configuration option NOT to use this change, which will keep the old path. It should be off by default (which means - enable the new behavior by d...

Implemented in commits 4a9af3e and e09c4d6.

Added DisableStrictCertificateValidation option to SslOptions:

  • Default is false (strict validation ENABLED by default)
  • When set to true, uses Go's default validation behavior
  • Emits deprecation warning: "DisableStrictCertificateValidation is deprecated and will be removed in a future version"
  • Warning is shown whenever the option is used

This provides backward compatibility while encouraging users to migrate to properly configured certificate chains.

Copilot AI changed the title Make driver fail certificate validation for untrusted chains Make driver fail certificate validation for untrusted chains (with opt-out for backward compatibility) Dec 28, 2025
Comment thread cluster.go Outdated
return fmt.Errorf("certificate verification failed: %v", err)
}

// Ensure that the chain terminates at a trusted root certificate
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Drop this comment

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed in commit 5b4f2be.

Comment thread cluster.go Outdated
Comment on lines +697 to +698
// Verify that each chain ends with a certificate that's in our RootCAs pool
// This ensures we don't accept chains that terminate at an intermediate cert
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Drop this comment

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed in commit 5b4f2be.

Comment thread cluster.go Outdated
Comment on lines +707 to +710
// Verify this root is self-signed (characteristic of root CAs)
if err := rootCert.CheckSignatureFrom(rootCert); err != nil {
return fmt.Errorf("certificate chain does not terminate at a self-signed root: %v", err)
}
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This does not make any sense, it should check all the certificates in the chain if they signed with root certificate

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Simplified in commit 5b4f2be. Removed the self-signed check since cert.Verify() already validates the entire chain properly. The function now relies on cert.Verify() which validates that all certificates in the chain are properly signed, ultimately by a certificate in the RootCAs pool.

@mykaul
Copy link
Copy Markdown

mykaul commented Jan 5, 2026

@dkropachev - I did not review it yet fully - don't waste your time on draft.

@mykaul
Copy link
Copy Markdown

mykaul commented Jan 5, 2026

@copilot - please address all feedback.

- Removed redundant comments as requested
- Removed self-signed root check - cert.Verify() already validates the chain properly
- Updated test expectations to match simplified implementation
- Updated function documentation to better describe what it does

The strict validation now simply ensures cert.Verify() is called with properly
configured Roots and Intermediates pools, which provides proper chain validation.

Co-authored-by: mykaul <4655593+mykaul@users.noreply.github.com>
Copy link
Copy Markdown
Author

Copilot AI commented Jan 5, 2026

@copilot - please address all feedback.

Addressed all feedback in commit 5b4f2be:

  • Removed redundant comments
  • Simplified certificate validation logic to rely on cert.Verify()
  • Updated tests and documentation

Comment thread cluster.go Outdated
Comment on lines +707 to +710
// Verify this root is self-signed (characteristic of root CAs)
if err := rootCert.CheckSignatureFrom(rootCert); err != nil {
return fmt.Errorf("certificate chain does not terminate at a self-signed root: %v", err)
}
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This one does not make any sense, we need to to check if every certificate in the chain is trusted by root certificate.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Implemented in commit 8029281. Added explicit verification that every certificate in the chain is signed by its parent, all the way to a self-signed root certificate. This ensures intermediate CAs cannot be trusted as roots.

Comment thread cluster.go Outdated

_, err = cert.Verify(opts)
if err != nil {
return fmt.Errorf("certificate verification failed: %v", err)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot - is there any information we can add here, which certificate failed verification, for example?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Enhanced in commit 8029281. Error messages now include the certificate subject and issuer information, e.g., certificate verification failed for subject="CN=example.com,O=Example" issuer="CN=Intermediate CA"

…re verification

- Added detailed error messages showing subject and issuer for failed certificates
- Implemented explicit chain signature verification to ensure every certificate
  is signed by its parent all the way to a self-signed root
- This prevents accepting intermediate CAs as trusted roots (security issue)
- Updated test to expect rejection when intermediate CA is in root pool
- All tests passing, linter clean

Co-authored-by: mykaul <4655593+mykaul@users.noreply.github.com>
Copilot AI requested a review from mykaul January 14, 2026 14:31
Comment thread cluster.go
}

// No valid chain found where all certificates are properly signed
return fmt.Errorf("no valid certificate chain found with proper signatures for subject=%q", cert.Subject.String())
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot - I wonder why this line is not used instead of the break in line 712?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The break on line 712 exits the inner loop that checks signatures within a single chain. The chainValid flag is then checked at line 717 - if false, we skip the self-signed root check and the outer loop continues to the next chain. Using continue would require being in the outer loop context. The current logic works correctly: when a signature fails, we mark the chain invalid, break from signature checking, and implicitly continue to the next chain.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Make driver to fail certificate validation when server certificate chain is not fully trusted

3 participants