forked from TrueCloudLab/certificates
Add support for SSH certificates to OIDC.
Update the interface for all the provisioners.
This commit is contained in:
parent
a44b0a1d52
commit
f01286bb48
9 changed files with 147 additions and 13 deletions
|
@ -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
|
||||||
|
}
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
34
authority/provisioner/method.go
Normal file
34
authority/provisioner/method.go
Normal 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
|
||||||
|
}
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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))
|
||||||
|
}
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
Loading…
Reference in a new issue