Skip to content

Commit

Permalink
custom trust levels based on the disposition of client certificates
Browse files Browse the repository at this point in the history
  • Loading branch information
johnabass committed Nov 21, 2024
1 parent b7ebbc1 commit 51222e0
Show file tree
Hide file tree
Showing 3 changed files with 120 additions and 16 deletions.
87 changes: 75 additions & 12 deletions token/claimBuilder.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ package token

import (
"context"
"crypto/x509"
"errors"
"fmt"
"net/http"
Expand All @@ -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"
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
}
5 changes: 1 addition & 4 deletions token/claimBuilder_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -545,7 +545,7 @@ func (suite *NewClaimBuildersTestSuite) TestMinimum() {
)

suite.Equal(
map[string]interface{}{"request": 123, "trust": 0},
map[string]interface{}{"request": 123},
actual,
)
}
Expand Down Expand Up @@ -691,7 +691,6 @@ func (suite *NewClaimBuildersTestSuite) TestStatic() {
"static1": suite.rawMessage(-72.5),
"static2": suite.rawMessage([]string{"a", "b"}),
"request": 123,
"trust": 0,
},
actual,
)
Expand Down Expand Up @@ -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,
)
Expand Down Expand Up @@ -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,
)
Expand Down
44 changes: 44 additions & 0 deletions token/options.go
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down

0 comments on commit 51222e0

Please sign in to comment.