Add support for SSH certificates to OIDC.

Update the interface for all the provisioners.
This commit is contained in:
Mariano Cano 2019-07-29 15:54:07 -07:00
parent a44b0a1d52
commit f01286bb48
9 changed files with 147 additions and 13 deletions

View file

@ -1,6 +1,7 @@
package provisioner package provisioner
import ( import (
"context"
"crypto/sha256" "crypto/sha256"
"crypto/x509" "crypto/x509"
"encoding/base64" "encoding/base64"
@ -266,13 +267,18 @@ func (p *AWS) Init(config Config) (err error) {
// AuthorizeSign validates the given token and returns the sign options that // AuthorizeSign validates the given token and returns the sign options that
// will be used on certificate creation. // will be used on certificate creation.
func (p *AWS) AuthorizeSign(token string) ([]SignOption, error) { func (p *AWS) AuthorizeSign(ctx context.Context, token string) ([]SignOption, error) {
payload, err := p.authorizeToken(token) payload, err := p.authorizeToken(token)
if err != nil { if err != nil {
return nil, err return nil, err
} }
doc := payload.document
// Check for the sign ssh method, default to sign X.509
if m := MethodFromContext(ctx); m == SignSSHMethod {
return p.authorizeSSHSign(payload)
}
doc := payload.document
// Enforce known CN and default DNS and IP if configured. // Enforce known CN and default DNS and IP if configured.
// By default we'll accept the CN and SANs in the CSR. // By default we'll accept the CN and SANs in the CSR.
// There's no way to trust them other than TOFU. // There's no way to trust them other than TOFU.
@ -432,3 +438,8 @@ func (p *AWS) authorizeToken(token string) (*awsPayload, error) {
payload.document = doc payload.document = doc
return &payload, nil return &payload, nil
} }
// authorizeSSHSign returns the list of SignOption for a SignSSH request.
func (p *AWS) authorizeSSHSign(claims *awsPayload) ([]SignOption, error) {
return nil, nil
}

View file

@ -1,6 +1,7 @@
package provisioner package provisioner
import ( import (
"context"
"crypto/sha256" "crypto/sha256"
"crypto/x509" "crypto/x509"
"encoding/hex" "encoding/hex"
@ -209,7 +210,7 @@ func (p *Azure) Init(config Config) (err error) {
// AuthorizeSign validates the given token and returns the sign options that // AuthorizeSign validates the given token and returns the sign options that
// will be used on certificate creation. // will be used on certificate creation.
func (p *Azure) AuthorizeSign(token string) ([]SignOption, error) { func (p *Azure) AuthorizeSign(ctx context.Context, token string) ([]SignOption, error) {
jwt, err := jose.ParseSigned(token) jwt, err := jose.ParseSigned(token)
if err != nil { if err != nil {
return nil, errors.Wrapf(err, "error parsing token") return nil, errors.Wrapf(err, "error parsing token")
@ -264,6 +265,11 @@ func (p *Azure) AuthorizeSign(token string) ([]SignOption, error) {
} }
} }
// Check for the sign ssh method, default to sign X.509
if m := MethodFromContext(ctx); m == SignSSHMethod {
return p.authorizeSSHSign(claims)
}
// Enforce known common name and default DNS if configured. // Enforce known common name and default DNS if configured.
// By default we'll accept the CN and SANs in the CSR. // By default we'll accept the CN and SANs in the CSR.
// There's no way to trust them other than TOFU. // There's no way to trust them other than TOFU.
@ -295,6 +301,11 @@ func (p *Azure) AuthorizeRevoke(token string) error {
return errors.New("revoke is not supported on a Azure provisioner") return errors.New("revoke is not supported on a Azure provisioner")
} }
// authorizeSSHSign returns the list of SignOption for a SignSSH request.
func (p *Azure) authorizeSSHSign(claims azurePayload) ([]SignOption, error) {
return nil, nil
}
// assertConfig initializes the config if it has not been initialized // assertConfig initializes the config if it has not been initialized
func (p *Azure) assertConfig() { func (p *Azure) assertConfig() {
if p.config == nil { if p.config == nil {

View file

@ -2,6 +2,7 @@ package provisioner
import ( import (
"bytes" "bytes"
"context"
"crypto/sha256" "crypto/sha256"
"crypto/x509" "crypto/x509"
"encoding/hex" "encoding/hex"
@ -205,13 +206,18 @@ func (p *GCP) Init(config Config) error {
// AuthorizeSign validates the given token and returns the sign options that // AuthorizeSign validates the given token and returns the sign options that
// will be used on certificate creation. // will be used on certificate creation.
func (p *GCP) AuthorizeSign(token string) ([]SignOption, error) { func (p *GCP) AuthorizeSign(ctx context.Context, token string) ([]SignOption, error) {
claims, err := p.authorizeToken(token) claims, err := p.authorizeToken(token)
if err != nil { if err != nil {
return nil, err return nil, err
} }
ce := claims.Google.ComputeEngine
// Check for the sign ssh method, default to sign X.509
if m := MethodFromContext(ctx); m == SignSSHMethod {
return p.authorizeSSHSign(claims)
}
ce := claims.Google.ComputeEngine
// Enforce known common name and default DNS if configured. // Enforce known common name and default DNS if configured.
// By default we we'll accept the CN and SANs in the CSR. // By default we we'll accept the CN and SANs in the CSR.
// There's no way to trust them other than TOFU. // There's no way to trust them other than TOFU.
@ -344,3 +350,8 @@ func (p *GCP) authorizeToken(token string) (*gcpPayload, error) {
return &claims, nil return &claims, nil
} }
// authorizeSSHSign returns the list of SignOption for a SignSSH request.
func (p *GCP) authorizeSSHSign(claims *gcpPayload) ([]SignOption, error) {
return nil, nil
}

View file

@ -1,6 +1,7 @@
package provisioner package provisioner
import ( import (
"context"
"crypto/x509" "crypto/x509"
"time" "time"
@ -134,7 +135,7 @@ func (p *JWK) AuthorizeRevoke(token string) error {
} }
// AuthorizeSign validates the given token. // AuthorizeSign validates the given token.
func (p *JWK) AuthorizeSign(token string) ([]SignOption, error) { func (p *JWK) AuthorizeSign(ctx context.Context, token string) ([]SignOption, error) {
claims, err := p.authorizeToken(token, p.audiences.Sign) claims, err := p.authorizeToken(token, p.audiences.Sign)
if err != nil { if err != nil {
return nil, err return nil, err
@ -171,6 +172,7 @@ func (p *JWK) AuthorizeRenewal(cert *x509.Certificate) error {
return nil return nil
} }
// authorizeSSHSign returns the list of SignOption for a SignSSH request.
func (p *JWK) authorizeSSHSign(claims *jwtPayload) ([]SignOption, error) { func (p *JWK) authorizeSSHSign(claims *jwtPayload) ([]SignOption, error) {
t := now() t := now()
opts := claims.Step.SSH opts := claims.Step.SSH

View file

@ -0,0 +1,34 @@
package provisioner
import (
"context"
)
// Method indicates the action to action that we will perform, it's used as part
// of the context in the call to authorize. It defaults to Sing.
type Method int
// The key to save the Method in the context.
type methodKey struct{}
const (
// SignMethod is the method used to sign X.509 certificates.
SignMethod Method = iota
// SignSSHMethod is the method used to sign SSH certificate.
SignSSHMethod
// RevokeMethod is the method used to revoke X.509 certificates.
RevokeMethod
)
// NewContextWithMethod creates a new context from ctx and attaches method to
// it.
func NewContextWithMethod(ctx context.Context, method Method) context.Context {
return context.WithValue(ctx, methodKey{}, method)
}
// MethodFromContext returns the Method saved in ctx. Returns Sign if the given
// context has no Method associated with it.
func MethodFromContext(ctx context.Context) Method {
m, _ := ctx.Value(methodKey{}).(Method)
return m
}

View file

@ -1,6 +1,9 @@
package provisioner package provisioner
import "crypto/x509" import (
"context"
"crypto/x509"
)
// noop provisioners is a provisioner that accepts anything. // noop provisioners is a provisioner that accepts anything.
type noop struct{} type noop struct{}
@ -28,7 +31,7 @@ func (p *noop) Init(config Config) error {
return nil return nil
} }
func (p *noop) AuthorizeSign(token string) ([]SignOption, error) { func (p *noop) AuthorizeSign(ctx context.Context, token string) ([]SignOption, error) {
return []SignOption{}, nil return []SignOption{}, nil
} }

View file

@ -1,6 +1,7 @@
package provisioner package provisioner
import ( import (
"context"
"crypto/x509" "crypto/x509"
"encoding/json" "encoding/json"
"net/http" "net/http"
@ -259,11 +260,17 @@ func (o *OIDC) AuthorizeRevoke(token string) error {
} }
// AuthorizeSign validates the given token. // AuthorizeSign validates the given token.
func (o *OIDC) AuthorizeSign(token string) ([]SignOption, error) { func (o *OIDC) AuthorizeSign(ctx context.Context, token string) ([]SignOption, error) {
claims, err := o.authorizeToken(token) claims, err := o.authorizeToken(token)
if err != nil { if err != nil {
return nil, err return nil, err
} }
// Check for the sign ssh method, default to sign X.509
if m := MethodFromContext(ctx); m == SignSSHMethod {
return o.authorizeSSHSign(claims)
}
// Admins should be able to authorize any SAN // Admins should be able to authorize any SAN
if o.IsAdmin(claims.Email) { if o.IsAdmin(claims.Email) {
return []SignOption{ return []SignOption{
@ -289,6 +296,37 @@ func (o *OIDC) AuthorizeRenewal(cert *x509.Certificate) error {
return nil return nil
} }
// authorizeSSHSign returns the list of SignOption for a SignSSH request.
func (o *OIDC) authorizeSSHSign(claims *openIDPayload) ([]SignOption, error) {
signOptions := []SignOption{
// set the default extensions
&sshDefaultExtensionModifier{},
// set the key id to the token subject
sshCertificateKeyIDModifier(claims.Email),
}
// Non-admins are only able to sign user certificates
if o.IsAdmin(claims.Email) {
signOptions = append(signOptions, &sshCertificateOptionsValidator{})
} else {
name := principalFromEmail(claims.Email)
if !sshUserRegex.MatchString(name) {
return nil, errors.Errorf("invalid principal '%s' from email address '%s'", name, claims.Email)
}
signOptions = append(signOptions, &sshCertificateOptionsValidator{&SSHOptions{
CertType: SSHUserCert,
Principals: []string{name},
}})
}
return append(signOptions,
// checks the validity bounds, and set the validity if has not been set
&sshCertificateValidityModifier{o.claimer},
// require all the fields in the SSH certificate
&sshCertificateDefaultValidator{},
), nil
}
func getAndDecode(uri string, v interface{}) error { func getAndDecode(uri string, v interface{}) error {
resp, err := http.Get(uri) resp, err := http.Get(uri)
if err != nil { if err != nil {
@ -300,3 +338,21 @@ func getAndDecode(uri string, v interface{}) error {
} }
return nil return nil
} }
func principalFromEmail(email string) string {
if i := strings.LastIndex(email, "@"); i >= 0 {
email = email[:i]
}
return strings.Map(func(r rune) rune {
switch {
case r >= 'a' && r <= 'z':
return r
case r >= '0' && r <= '9':
return r
case r == '.': // drop dots
return -1
default:
return '_'
}
}, strings.ToLower(email))
}

View file

@ -1,14 +1,18 @@
package provisioner package provisioner
import ( import (
"context"
"crypto/x509" "crypto/x509"
"encoding/json" "encoding/json"
"net/url" "net/url"
"regexp"
"strings" "strings"
"github.com/pkg/errors" "github.com/pkg/errors"
) )
var sshUserRegex = regexp.MustCompile("^[a-z][-a-z0-9_]*$")
// Interface is the interface that all provisioner types must implement. // Interface is the interface that all provisioner types must implement.
type Interface interface { type Interface interface {
GetID() string GetID() string
@ -17,7 +21,7 @@ type Interface interface {
GetType() Type GetType() Type
GetEncryptedKey() (kid string, key string, ok bool) GetEncryptedKey() (kid string, key string, ok bool)
Init(config Config) error Init(config Config) error
AuthorizeSign(token string) ([]SignOption, error) AuthorizeSign(ctx context.Context, token string) ([]SignOption, error)
AuthorizeRenewal(cert *x509.Certificate) error AuthorizeRenewal(cert *x509.Certificate) error
AuthorizeRevoke(token string) error AuthorizeRevoke(token string) error
} }

View file

@ -205,11 +205,13 @@ func (m *sshCertificateValidityModifier) Modify(cert *ssh.Certificate) error {
// sshCertificateOptionsValidator validates the user SSHOptions with the ones // sshCertificateOptionsValidator validates the user SSHOptions with the ones
// usually present in the token. // usually present in the token.
type sshCertificateOptionsValidator struct { type sshCertificateOptionsValidator struct {
*SSHOptions Want *SSHOptions
} }
func (want *sshCertificateOptionsValidator) Valid(got SSHOptions) error { // Valid implements SSHCertificateOptionsValidator and returns nil if both
return want.match(got) // SSHOptions match.
func (v *sshCertificateOptionsValidator) Valid(got SSHOptions) error {
return v.Want.match(got)
} }
// sshCertificateDefaultValidator implements a simple validator for all the // sshCertificateDefaultValidator implements a simple validator for all the