Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

PKI Valdiator always fails on unknown CAs #3529

Merged
merged 5 commits into from
Nov 4, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion docs/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -31,14 +31,15 @@ Nuts documentation
pages/deployment/configuration.rst
pages/deployment/migration.rst
pages/deployment/recommended-deployment.rst
pages/deployment/certificates.rst
pages/deployment/docker.rst
pages/deployment/storage.rst
pages/deployment/verifiable-credentials.rst
pages/deployment/logging.rst
pages/deployment/monitoring.rst
pages/deployment/cli-reference.rst
pages/deployment/discovery.rst
pages/deployment/pex.rst
pages/deployment/policy.rst
pages/deployment/key-rotation.rst
pages/deployment/audit-logging.rst
pages/deployment/oauth.rst
Expand Down
34 changes: 34 additions & 0 deletions docs/pages/deployment/certificates.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
.. _certificates:

Certificates
############

client authentication
*********************

Nuts-node versions before v6 only use TLS certificates for client authentication on the ``/n2n`` endpoints and in the ``gRPC Nuts network``.
The Nuts-node also validates the client certificates used by its peers on the ``gRPC network`` when a new connection is established, and periodically after that as long as the connection exists.
To do this, all trusted certificate chains must be configured in ``tls.truststorefile``.
The Certificate Revocation List (CRL) of the CAs in the truststore are periodically downloaded to confirm a peer's client certificate is not revoked.
To prevent a CA with downtime on its CRL endpoint from bringing down the network, the Nuts-node uses a soft-fail strategy that does not reject certificates if it cannot download the CRL.
This behavior can be changed to hard-fail (fail if certificate is invalid, expired, of revoked, or if any of the previous cannot be determined) using the ``pki.softfail`` config flag.
The ``gRPC Nuts network`` and ``/n2n`` endpoints are deprecated and will be removed in the future.

did:x509
********

In ``did:x509`` a certificate is converted to a DID Document (that includes its entire certificate chain) so it can be used in the Verifiable Credentials ecosystem.
This DID Method provides a temporary bridge between the 'old' world of CAs/Certificates and the 'new' Verifiable Credential world.
With other DID Methods, certificates are only used to create an secure channel for communication and optionally for client authentication.
In ``did:x509`` the certificates are also used in the cryptographic proofs to obtain access-tokens.
This means the certificate chain now provides the root of trust and has stricter requirements than connection certificates.

Trust in specific certificate CAs is configured per use-case in a :ref:`Discovery <discovery>` and :ref:`Policy <policy>` definition file.
In addition, all trusted CA chains must also be added to the ``tls.truststorefile``.
For certificate chains used in ``did:x509`` the Nuts-node always uses a hard-fail strategy, i.e., the ``pki.softfail`` config value is ignored during certificate validation for ``did:x509``.
This means that the Nuts-node will not be able to verify a ``did:x509`` DID or Verifiable Credential signed by this DID Method if the CRL cannot be downloaded and the CRL in the cache is older than ``pki.maxupdatefailhours``.

.. note::

Since the configured truststore file is now used for multiple purposes, it is no longer possible for the Nuts-node to determine what certificate chain is accepted/trusted for what purpose.
This means that all incoming TLS connections (including gRPC) must be offloaded in a proxy and validated against the expected certificate chain.
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
.. _pex:
.. _policy:

Access Token Policy
###################

Presentation Definition mapping
###############################
*******************************

Wallet functionality uses `Presentation Definitions <https://identity.foundation/presentation-exchange/>`_ to determine the required credentials for a given presentation request.
An OAuth2 authorization request uses scopes to determine the required permissions for a given request.
The mapping between scopes and presentation definitions is defined in a configuration file.
The mapping between scopes and presentation definitions is defined in a policy definition file.

Configuration
*************
Expand All @@ -14,7 +17,7 @@ The Nuts config supports the mapping between OAuth2 scopes and presentation defi
The file-based configuration is a simple way to define the mapping between scopes and presentation definitions.
It can be used for simple use cases where the mapping is static and does not change often.

To use file-based configuration, you need to define the path to a directory that contains policy configuration files:
To use file-based configuration, you need to define the path to a directory that contains policy definition files:

.. code-block:: yaml

Expand Down
3 changes: 2 additions & 1 deletion pki/interface.go
Original file line number Diff line number Diff line change
Expand Up @@ -62,8 +62,8 @@ type Validator interface {
// ErrCertRevoked and ErrCertUntrusted indicate that at least one of the certificates is revoked, or signed by a CA that is not in the truststore.
// ErrCRLMissing and ErrCRLExpired signal that at least one of the certificates cannot be validated reliably.
// If the certificate was revoked on an expired CRL, it wil return ErrCertRevoked.
// Ignoring all errors except ErrCertRevoked changes the behavior from hard-fail to soft-fail. Without a truststore, the Validator is a noop if set to soft-fail
// Validate uses the configured soft-/hard-fail strategy
// If set to soft-fail it ignores ErrCRLMissing and ErrCRLExpired errors.
// The certificate chain is expected to be sorted leaf to root.
Validate(chain []*x509.Certificate) error

Expand All @@ -75,6 +75,7 @@ type Validator interface {

// AddTruststore adds all CAs to the truststore for validation of CRL signatures. It also adds all CRL Distribution Endpoints found in the chain.
// CRL Distribution Points encountered during operation, such as on end user certificates, are only added to the monitored CRLs if their issuer is in the truststore.
// This fails if any of the issuers mentioned in the chain is not also in the chain or already in the truststore
AddTruststore(chain []*x509.Certificate) error

// SubscribeDenied registers a callback that is triggered everytime the denylist is updated.
Expand Down
12 changes: 3 additions & 9 deletions pki/validator.go
Original file line number Diff line number Diff line change
Expand Up @@ -143,8 +143,8 @@ func (v *validator) validate(chain []*x509.Certificate, softfail bool) error {
// check in reverse order to prevent CRL expiration errors due to revoked CAs no longer issuing CRLs
if err = v.validateCert(cert); err != nil {
errOut := fmt.Errorf("%w: subject=%s, S/N=%s, issuer=%s", err, cert.Subject.String(), cert.SerialNumber.String(), cert.Issuer.String())
if softfail && !(errors.Is(err, ErrCertRevoked) || errors.Is(err, ErrCertBanned)) {
// Accept the certificate even if it cannot be properly validated
if softfail && (errors.Is(err, ErrCRLExpired) || errors.Is(err, ErrCRLMissing) || errors.Is(err, ErrDenylistMissing)) {
// Accept the certificate even if it cannot be properly validated against the CRL or denylist
logger().WithError(errOut).Error("Certificate CRL check softfail bypass. Might be unsafe, find cause of failure!")
continue
}
Expand Down Expand Up @@ -249,13 +249,7 @@ func (v *validator) AddTruststore(chain []*x509.Certificate) error {
for _, certificate = range chain {
issuer, ok := v.getCert(certificate.Issuer.String())
if !ok {
err = fmt.Errorf("certificate's issuer is not in the trust store: subject=%s, issuer=%s", certificate.Subject.String(), certificate.Issuer.String())
if !v.softfail {
return fmt.Errorf("pki: %w", err)
}
// Can happen if the intermediate CA issuing end entity (EE) certificates is added, but not its issuer. EE wil be checked for revocation, CA revocation is not.
logger().WithError(err).Warn("Did not add CRL Distribution Points")
continue
return fmt.Errorf("pki: certificate's issuer is not in the trust store: subject=%s, issuer=%s", certificate.Subject.String(), certificate.Issuer.String())
}
err = v.addEndpoints(issuer, certificate.CRLDistributionPoints)
if err != nil {
Expand Down
18 changes: 5 additions & 13 deletions pki/validator_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,7 @@ func TestValidator_Validate(t *testing.T) {
})
t.Run("unknown issuer", func(t *testing.T) {
val := &validator{}
testSoftHard(t, val, validCertA, nil, ErrCertUntrusted)
testSoftHard(t, val, validCertA, ErrCertUntrusted, ErrCertUntrusted)
})
t.Run("missing crl", func(t *testing.T) {
testSoftHard(t, val, validCertBWithRevokedCA, nil, ErrCRLMissing)
Expand Down Expand Up @@ -215,20 +215,12 @@ func TestValidator_AddTruststore(t *testing.T) {

t.Run("missing CA", func(t *testing.T) {
noRootStore := store.Certificates()[:2]
t.Run("softfail", func(t *testing.T) {
val, err := newValidator(Config{Softfail: true})
require.NoError(t, err)

assert.NoError(t, val.AddTruststore(noRootStore))
})
t.Run("hardfail", func(t *testing.T) {
val, err := newValidator(Config{Softfail: false})
require.NoError(t, err)
val, err := newValidator(Config{Softfail: true})
require.NoError(t, err)

err = val.AddTruststore(noRootStore)
err = val.AddTruststore(noRootStore)

assert.ErrorContains(t, err, "certificate's issuer is not in the trust store")
})
assert.ErrorContains(t, err, "certificate's issuer is not in the trust store")
})
}

Expand Down
Loading