Skip to content

Commit

Permalink
Merge pull request #262 from xmidt-org/feature/certificate-claims
Browse files Browse the repository at this point in the history
Feature/certificate claims
  • Loading branch information
johnabass authored Nov 21, 2024
2 parents b7ebbc1 + eb6a4f9 commit 66e9ec0
Show file tree
Hide file tree
Showing 5 changed files with 177 additions and 20 deletions.
2 changes: 0 additions & 2 deletions devMode.go
Original file line number Diff line number Diff line change
Expand Up @@ -78,8 +78,6 @@ token:
parameter: uuid
- key: iss
value: "development"
- key: trust
value: 1000
- key: sub
value: "client-supplied"
- key: aud
Expand Down
2 changes: 0 additions & 2 deletions themis.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -72,8 +72,6 @@ token:
parameter: uuid
- key: iss
value: "development"
- key: trust
value: 1000
- key: sub
value: "client-supplied"
- key: aud
Expand Down
99 changes: 87 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,87 @@ 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.enforceDefaults(),
}

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) {
// simplest 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()
var trust int
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
// we assume Trusted is the highest trust level
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:
if trust < cb.trust.ExpiredUntrusted {
trust = cb.trust.ExpiredUntrusted
}

case !expired && verifyErr != nil:
if trust < cb.trust.Untrusted {
trust = cb.trust.Untrusted
}

case expired && verifyErr == nil:
if trust < cb.trust.ExpiredTrusted {
trust = cb.trust.ExpiredTrusted
}

case !expired && verifyErr == nil:
// we assume Trusted is the highest trust level
target[ClaimTrust] = cb.trust.Trusted
return
}
}

// take the highest, non-Trusted level
target[ClaimTrust] = trust
return
}

// NewClaimBuilders constructs a ClaimBuilders from configuration. The returned instance is typically
Expand Down Expand Up @@ -268,10 +341,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
89 changes: 89 additions & 0 deletions token/options.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,14 @@ import (
"github.com/xmidt-org/themis/key"
)

const (
DefaultTrustLevelNoCertificates = 0
DefaultTrustLevelExpiredUntrusted = 100
DefaultTrustLevelExpiredTrusted = 1000
DefaultTrustLevelUntrusted = 1000
DefaultTrustLevelTrusted = 1000
)

// RemoteClaims describes a remote HTTP endpoint that can produce claims given the
// metadata from a token request.
type RemoteClaims struct {
Expand Down Expand Up @@ -93,11 +101,92 @@ 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.
// If unset, DefaultTrustLevelNoCertificates is used.
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.
//
// If unset, DefaultTrustLevelExpiredTrusted is used.
ExpiredUntrusted int

// ExpiredTrusted is the trust level to set when a certificate has both expired
// and IS within a trusted CA chain.
//
// If unset, DefaultTrustLevelExpiredTrusted is used.
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.
//
// If unset, DefaultTrustLevelUntrusted is used.
Untrusted int

// Trusted is the trust level to set when a client certificate is part of
//
// If unset, DefaultTrustLevelTrusted is used.
// a trusted CA chain.
Trusted int
}

// enforceDefaults returns a Trust that has ensures any unset values are
// set to their defaults.
func (t Trust) enforceDefaults() (other Trust) {
other = t
if other.NoCertificates <= 0 {
other.NoCertificates = DefaultTrustLevelNoCertificates
}

if other.ExpiredUntrusted <= 0 {
other.ExpiredUntrusted = DefaultTrustLevelExpiredUntrusted
}

if other.ExpiredTrusted <= 0 {
other.ExpiredTrusted = DefaultTrustLevelExpiredTrusted
}

if other.Untrusted <= 0 {
other.Untrusted = DefaultTrustLevelUntrusted
}

if other.Trusted <= 0 {
other.Trusted = DefaultTrustLevelTrusted
}

return
}

// 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.
//
// Generally, this value should be the same as the the mtls.clientCACertificateFile.
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 66e9ec0

Please sign in to comment.