From 51222e0ea4edcd15a04ac8abf034ac655b3ab9af Mon Sep 17 00:00:00 2001 From: johnabass Date: Wed, 20 Nov 2024 22:33:13 -0800 Subject: [PATCH] custom trust levels based on the disposition of client certificates --- token/claimBuilder.go | 87 ++++++++++++++++++++++++++++++++------ token/claimBuilder_test.go | 5 +-- token/options.go | 44 +++++++++++++++++++ 3 files changed, 120 insertions(+), 16 deletions(-) diff --git a/token/claimBuilder.go b/token/claimBuilder.go index 3aeec21..84157bc 100644 --- a/token/claimBuilder.go +++ b/token/claimBuilder.go @@ -4,6 +4,7 @@ package token import ( "context" + "crypto/x509" "errors" "fmt" "net/http" @@ -12,6 +13,7 @@ import ( "github.com/xmidt-org/themis/random" "github.com/xmidt-org/themis/xhttp/xhttpclient" + "github.com/xmidt-org/themis/xhttp/xhttpserver" "github.com/go-kit/kit/endpoint" kithttp "github.com/go-kit/kit/transport/http" @@ -178,16 +180,75 @@ func newRemoteClaimBuilder(client xhttpclient.Interface, metadata map[string]int return &remoteClaimBuilder{endpoint: c.Endpoint(), url: r.URL, extra: metadata}, nil } -// enforcePeerCertificate sets a trust of 1000 if and only if at least (1) peer certificate -// was supplied. -func enforcePeerCertificate(_ context.Context, r *Request, target map[string]interface{}) error { - if r.TLS != nil && len(r.TLS.PeerCertificates) > 0 { - target[ClaimTrust] = 1000 - } else { - target[ClaimTrust] = 0 +func newClientCertificateClaimBuiler(cc *ClientCertificates) (cb *clientCertificateClaimBuilder, err error) { + if cc == nil { + return } - return nil + cb = &clientCertificateClaimBuilder{ + trust: cc.Trust, + } + + if len(cc.RootCAFile) > 0 { + cb.roots, err = xhttpserver.ReadCertPool(cc.RootCAFile) + } + + if err == nil && len(cc.IntermediatesFile) > 0 { + cb.intermediates, err = xhttpserver.ReadCertPool(cc.IntermediatesFile) + } + + return +} + +type clientCertificateClaimBuilder struct { + roots *x509.CertPool + intermediates *x509.CertPool + trust Trust +} + +func (cb *clientCertificateClaimBuilder) AddClaims(_ context.Context, r *Request, target map[string]interface{}) (err error) { + // first case: this didn't come from a TLS connection, or it did but the client gave no certificates + if r.TLS == nil || len(r.TLS.PeerCertificates) == 0 { + target[ClaimTrust] = cb.trust.NoCertificates + return + } + + now := time.Now() + for i, pc := range r.TLS.PeerCertificates { + if i < len(r.TLS.VerifiedChains) && len(r.TLS.VerifiedChains[i]) > 0 { + // the TLS layer already verified this certificate, so we're done + target[ClaimTrust] = cb.trust.Trusted + return + } + + // special logic around expired certificates + expired := now.After(pc.NotAfter) + vo := x509.VerifyOptions{ + // always set the current time so that we disambiguate expired + // from untrusted. + CurrentTime: pc.NotAfter.Add(-time.Second), + Roots: cb.roots, + Intermediates: cb.intermediates, + } + + _, verifyErr := pc.Verify(vo) + + switch { + case expired && verifyErr != nil: + target[ClaimTrust] = cb.trust.ExpiredUntrusted + + case !expired && verifyErr != nil: + target[ClaimTrust] = cb.trust.Untrusted + + case expired && verifyErr == nil: + target[ClaimTrust] = cb.trust.ExpiredTrusted + + case !expired && verifyErr == nil: + target[ClaimTrust] = cb.trust.Trusted + } + } + + return } // NewClaimBuilders constructs a ClaimBuilders from configuration. The returned instance is typically @@ -268,10 +329,12 @@ func NewClaimBuilders(n random.Noncer, client xhttpclient.Interface, o Options) }) } - builders = append( - builders, - ClaimBuilderFunc(enforcePeerCertificate), - ) + if cb, err := newClientCertificateClaimBuiler(o.ClientCertificates); cb != nil && err == nil { + builders = append( + builders, + cb, + ) + } return builders, nil } diff --git a/token/claimBuilder_test.go b/token/claimBuilder_test.go index 93a4fa1..eb363b8 100644 --- a/token/claimBuilder_test.go +++ b/token/claimBuilder_test.go @@ -545,7 +545,7 @@ func (suite *NewClaimBuildersTestSuite) TestMinimum() { ) suite.Equal( - map[string]interface{}{"request": 123, "trust": 0}, + map[string]interface{}{"request": 123}, actual, ) } @@ -691,7 +691,6 @@ func (suite *NewClaimBuildersTestSuite) TestStatic() { "static1": suite.rawMessage(-72.5), "static2": suite.rawMessage([]string{"a", "b"}), "request": 123, - "trust": 0, }, actual, ) @@ -738,7 +737,6 @@ func (suite *NewClaimBuildersTestSuite) TestNoRemote() { "iat": suite.expectedNow.UTC().Unix(), "nbf": suite.expectedNow.Add(15 * time.Second).UTC().Unix(), "exp": suite.expectedNow.Add(24 * time.Hour).UTC().Unix(), - "trust": 0, }, actual, ) @@ -823,7 +821,6 @@ func (suite *NewClaimBuildersTestSuite) TestFull() { "iat": suite.expectedNow.UTC().Unix(), "nbf": suite.expectedNow.Add(15 * time.Second).UTC().Unix(), "exp": suite.expectedNow.Add(24 * time.Hour).UTC().Unix(), - "trust": 0, }, actual, ) diff --git a/token/options.go b/token/options.go index 01ac2d0..70eed16 100644 --- a/token/options.go +++ b/token/options.go @@ -93,11 +93,55 @@ type PartnerID struct { Default string } +// Trust describes the various levels of trust based upon client +// certificate state. +type Trust struct { + // NoCertificates is the trust level to set when no client certificates are present. + // This value has no default. If unset, the trust value is zero (0). + NoCertificates int + + // ExpiredUntrusted is the trust level to set when a certificate has both expired + // and is within an CA chain that we do not trust. + ExpiredUntrusted int + + // ExpiredTrusted is the trust level to set when a certificate has both expired + // and IS within a trusted CA chain. + ExpiredTrusted int + + // Untrusted is the trust level to set when a client has an otherwise valid + // certificate, but that certificate is part of an untrusted chain. + Untrusted int + + // Trusted is the trust level to set when a client certificate is part of + // a trusted CA chain. + Trusted int +} + +// ClientCertificates describes how peer certificates are to be handled when +// it comes to issuing tokens. +type ClientCertificates struct { + // RootCAFile is the PEM bundle of certificates used for client certificate verification. + // If unset, the system verifier and/or bundle is used. + RootCAFile string + + // IntermediatesFile is the PEM bundle of certificates used for client certificate verification. + // If unset, no intermediary certificates are considered. + IntermediatesFile string + + // Trust defines the trust levels to set for various situations involving + // client certificates. + Trust Trust +} + // Options holds the configurable information for a token Factory type Options struct { // Alg is the required JWT signing algorithm to use Alg string + // ClientCertificates describes how peer certificates affect the issued tokens. + // If unset, client certificates are not considered when issuing tokens. + ClientCertificates *ClientCertificates + // Key describes the signing key to use Key key.Descriptor