diff --git a/authority/authority.go b/authority/authority.go index fcb01a03..53a3ee72 100644 --- a/authority/authority.go +++ b/authority/authority.go @@ -146,17 +146,19 @@ func (a *Authority) init() error { // Pull AuthConfig from DB. if true { - mgmtDB, err := authMgmtNosql.New(a.db.(nosql.DB), mgmt.DefaultAuthorityID) - if err != nil { - return err - } - _ac, err := mgmtDB.GetAuthConfig(context.Background(), mgmt.DefaultAuthorityID) - if err != nil { - return err - } - a.config.AuthorityConfig, err = _ac.ToCertificates() - if err != nil { - return err + if len(a.config.AuthConfig.AuthorityID)_== 0 { + mgmtDB, err := authMgmtNosql.New(a.db.(nosql.DB), mgmt.DefaultAuthorityID) + if err != nil { + return err + } + mgmtAuthConfig, err := mgmt.CreateAuthority(context.Background, mgmtDB, WithDefaultAuthorityID) + if err != nil { + return err + } + a.config.AuthConfig, err := mgmtAuthConfig.ToCertificates() + if err != nil { + return err + } } } diff --git a/authority/config/config.go b/authority/config/config.go index 1f6b7619..852641c2 100644 --- a/authority/config/config.go +++ b/authority/config/config.go @@ -32,23 +32,23 @@ var ( MaxVersion: 1.2, Renegotiation: false, } - defaultBackdate = time.Minute - defaultDisableRenewal = false - defaultEnableSSHCA = false + DefaultBackdate = time.Minute + DefaultDisableRenewal = false + DefaultEnableSSHCA = false // GlobalProvisionerClaims default claims for the Authority. Can be overriden // by provisioner specific claims. GlobalProvisionerClaims = provisioner.Claims{ MinTLSDur: &provisioner.Duration{Duration: 5 * time.Minute}, // TLS certs MaxTLSDur: &provisioner.Duration{Duration: 24 * time.Hour}, DefaultTLSDur: &provisioner.Duration{Duration: 24 * time.Hour}, - DisableRenewal: &defaultDisableRenewal, + DisableRenewal: &DefaultDisableRenewal, MinUserSSHDur: &provisioner.Duration{Duration: 5 * time.Minute}, // User SSH certs MaxUserSSHDur: &provisioner.Duration{Duration: 24 * time.Hour}, DefaultUserSSHDur: &provisioner.Duration{Duration: 16 * time.Hour}, MinHostSSHDur: &provisioner.Duration{Duration: 5 * time.Minute}, // Host SSH certs MaxHostSSHDur: &provisioner.Duration{Duration: 30 * 24 * time.Hour}, DefaultHostSSHDur: &provisioner.Duration{Duration: 30 * 24 * time.Hour}, - EnableSSHCA: &defaultEnableSSHCA, + EnableSSHCA: &DefaultEnableSSHCA, } ) @@ -88,6 +88,7 @@ type ASN1DN struct { // cas.Options. type AuthConfig struct { *cas.Options + AuthorityID string `json:"authorityID,omitempty"` Provisioners provisioner.List `json:"provisioners"` Template *ASN1DN `json:"template,omitempty"` Claims *provisioner.Claims `json:"claims,omitempty"` @@ -106,7 +107,7 @@ func (c *AuthConfig) init() { } if c.Backdate == nil { c.Backdate = &provisioner.Duration{ - Duration: defaultBackdate, + Duration: DefaultBackdate, } } } diff --git a/authority/mgmt/admin.go b/authority/mgmt/admin.go new file mode 100644 index 00000000..8a4104a5 --- /dev/null +++ b/authority/mgmt/admin.go @@ -0,0 +1,31 @@ +package mgmt + +import "context" + +// Admin type. +type Admin struct { + ID string `json:"-"` + AuthorityID string `json:"-"` + ProvisionerID string `json:"provisionerID"` + Name string `json:"name"` + ProvisionerName string `json:"provisionerName"` + ProvisionerType string `json:"provisionerType"` + IsSuperAdmin bool `json:"isSuperAdmin"` + Status StatusType `json:"status"` +} + +// CreateAdmin builds and stores an admin type in the DB. +func CreateAdmin(ctx context.Context, db DB, name string, prov *Provisioner, isSuperAdmin bool) (*Admin, error) { + adm := &Admin{ + Name: name, + ProvisionerID: prov.ID, + ProvisionerName: prov.Name, + ProvisionerType: prov.Type, + IsSuperAdmin: isSuperAdmin, + Status: StatusActive, + } + if err := db.CreateAdmin(ctx, adm); err != nil { + return nil, WrapErrorISE(err, "error creating admin") + } + return adm, nil +} diff --git a/authority/mgmt/authConfig.go b/authority/mgmt/authConfig.go new file mode 100644 index 00000000..595b2ed0 --- /dev/null +++ b/authority/mgmt/authConfig.go @@ -0,0 +1,77 @@ +package mgmt + +import ( + "github.com/smallstep/certificates/authority/config" + "github.com/smallstep/certificates/authority/provisioner" +) + +// AuthConfig represents the Authority Configuration. +type AuthConfig struct { + //*cas.Options `json:"cas"` + ID string `json:"id"` + ASN1DN *config.ASN1DN `json:"template,omitempty"` + Provisioners []*Provisioner `json:"-"` + Admins []*Admin `json:"-"` + Claims *Claims `json:"claims,omitempty"` + Backdate string `json:"backdate,omitempty"` + Status StatusType `json:"status,omitempty"` +} + +func NewDefaultAuthConfig() *AuthConfig { + return &AuthConfig{ + Claims: &Claims{ + X509: &X509Claims{ + Durations: &Durations{ + Min: config.GlobalProvisionerClaims.MinTLSDur.String(), + Max: config.GlobalProvisionerClaims.MaxTLSDur.String(), + Default: config.GlobalProvisionerClaims.DefaultTLSDur.String(), + }, + }, + SSH: &SSHClaims{ + UserDurations: &Durations{ + Min: config.GlobalProvisionerClaims.MinUserSSHDur.String(), + Max: config.GlobalProvisionerClaims.MaxUserSSHDur.String(), + Default: config.GlobalProvisionerClaims.DefaultUserSSHDur.String(), + }, + HostDurations: &Durations{ + Min: config.GlobalProvisionerClaims.MinHostSSHDur.String(), + Max: config.GlobalProvisionerClaims.MaxHostSSHDur.String(), + Default: config.GlobalProvisionerClaims.DefaultHostSSHDur.String(), + }, + }, + DisableRenewal: config.DefaultDisableRenewal, + }, + Backdate: config.DefaultBackdate.String(), + Status: StatusActive, + } +} + +// ToCertificates converts a mgmt AuthConfig to configuration that can be +// directly used by the `step-ca` process. Resources are normalized and +// initialized. +func (ac *AuthConfig) ToCertificates() (*config.AuthConfig, error) { + claims, err := ac.Claims.ToCertificates() + if err != nil { + return nil, err + } + backdate, err := provisioner.NewDuration(ac.Backdate) + if err != nil { + return nil, WrapErrorISE(err, "error converting backdate %s to duration", ac.Backdate) + } + var provs []provisioner.Interface + for _, p := range ac.Provisioners { + authProv, err := p.ToCertificates() + if err != nil { + return nil, err + } + provs = append(provs, authProv) + } + return &config.AuthConfig{ + AuthorityID: ac.ID, + Provisioners: provs, + Template: ac.ASN1DN, + Claims: claims, + DisableIssuedAtCheck: false, + Backdate: backdate, + }, nil +} diff --git a/authority/mgmt/config.go b/authority/mgmt/config.go index b5bebceb..db70e6e5 100644 --- a/authority/mgmt/config.go +++ b/authority/mgmt/config.go @@ -1,11 +1,15 @@ package mgmt import ( - "github.com/smallstep/certificates/authority/config" - authority "github.com/smallstep/certificates/authority/config" + "context" + + "github.com/pkg/errors" ) const ( + // DefaultAuthorityID is the default AuthorityID. This will be the ID + // of the first Authority created, as well as the default AuthorityID + // if one is not specified in the configuration. DefaultAuthorityID = "00000000-0000-0000-0000-000000000000" ) @@ -19,339 +23,76 @@ const ( StatusDeleted ) +// Claims encapsulates all x509 and ssh claims applied to the authority +// configuration. E.g. maxTLSCertDuration, defaultSSHCertDuration, etc. type Claims struct { - *X509Claims `json:"x509Claims"` - *SSHClaims `json:"sshClaims"` - DisableRenewal *bool `json:"disableRenewal"` + X509 *X509Claims `json:"x509Claims"` + SSH *SSHClaims `json:"sshClaims"` + DisableRenewal bool `json:"disableRenewal"` } +// X509Claims are the x509 claims applied to the authority. type X509Claims struct { Durations *Durations `json:"durations"` } +// SSHClaims are the ssh claims applied to the authority. type SSHClaims struct { - UserDuration *Durations `json:"userDurations"` - HostDuration *Durations `json:"hostDuration"` + Enabled bool `json:"enabled"` + UserDurations *Durations `json:"userDurations"` + HostDurations *Durations `json:"hostDurations"` } +// Durations represents min, max, default, duration. type Durations struct { Min string `json:"min"` Max string `json:"max"` Default string `json:"default"` } -// Admin type. -type Admin struct { - ID string `json:"-"` - AuthorityID string `json:"-"` - Name string `json:"name"` - Provisioner string `json:"provisioner"` - IsSuperAdmin bool `json:"isSuperAdmin"` - Status StatusType `json:"status"` +type AuthorityOption func(*AuthConfig) error + +func WithDefaultAuthorityID(ac *AuthConfig) error { + ac.ID = DefaultAuthorityID + return nil } -// Provisioner type. -type Provisioner struct { - ID string `json:"-"` - AuthorityID string `json:"-"` - Type string `json:"type"` - Name string `json:"name"` - Claims *Claims `json:"claims"` - Details interface{} `json:"details"` - X509Template string `json:"x509Template"` - SSHTemplate string `json:"sshTemplate"` - Status StatusType `json:"status"` +func CreateDefaultAuthority(ctx context.Context, db DB) (*AuthConfig, error) { + options := []AuthorityOption{WithDefaultAuthorityID} + + return CreateAuthority(ctx, db, options...) } -// AuthConfig represents the Authority Configuration. -type AuthConfig struct { - //*cas.Options `json:"cas"` - ID string `json:"id"` - ASN1DN *config.ASN1DN `json:"template,omitempty"` - Provisioners []*Provisioner `json:"-"` - Claims *Claims `json:"claims,omitempty"` - DisableIssuedAtCheck bool `json:"disableIssuedAtCheck,omitempty"` - Backdate string `json:"backdate,omitempty"` - Status StatusType `json:"status,omitempty"` -} +func CreateAuthority(ctx context.Context, db DB, options ...AuthorityOption) (*AuthConfig, error) { + ac := NewDefaultAuthConfig() -func (ac *AuthConfig) ToCertificates() (*config.AuthConfig, error) { - return &authority.AuthConfig{}, nil -} - -/* -// ToCertificates converts the landlord provisioner type to the open source -// provisioner type. -func (p *Provisioner) ToCertificates(ctx context.Context, db database.DB) (provisioner.Interface, error) { - claims, err := p.Claims.ToCertificates() - if err != nil { - return nil, err - } - - details := p.Details.GetData() - if details == nil { - return nil, fmt.Errorf("provisioner does not have any details") - } - - options, err := p.getOptions(ctx, db) - if err != nil { - return nil, err - } - - switch d := details.(type) { - case *ProvisionerDetails_JWK: - k := d.JWK.GetKey() - jwk := new(jose.JSONWebKey) - if err := json.Unmarshal(k.Key.Public, &jwk); err != nil { + for _, o := range options { + if err := o(ac); err != nil { return nil, err } - return &provisioner.JWK{ - Type: p.Type.String(), - Name: p.Name, - Key: jwk, - EncryptedKey: string(k.Key.Private), - Claims: claims, - Options: options, - }, nil - case *ProvisionerDetails_OIDC: - cfg := d.OIDC - return &provisioner.OIDC{ - Type: p.Type.String(), - Name: p.Name, - TenantID: cfg.TenantId, - ClientID: cfg.ClientId, - ClientSecret: cfg.ClientSecret, - ConfigurationEndpoint: cfg.ConfigurationEndpoint, - Admins: cfg.Admins, - Domains: cfg.Domains, - Groups: cfg.Groups, - ListenAddress: cfg.ListenAddress, - Claims: claims, - Options: options, - }, nil - case *ProvisionerDetails_GCP: - cfg := d.GCP - return &provisioner.GCP{ - Type: p.Type.String(), - Name: p.Name, - ServiceAccounts: cfg.ServiceAccounts, - ProjectIDs: cfg.ProjectIds, - DisableCustomSANs: cfg.DisableCustomSans, - DisableTrustOnFirstUse: cfg.DisableTrustOnFirstUse, - InstanceAge: durationValue(cfg.InstanceAge), - Claims: claims, - Options: options, - }, nil - case *ProvisionerDetails_AWS: - cfg := d.AWS - return &provisioner.AWS{ - Type: p.Type.String(), - Name: p.Name, - Accounts: cfg.Accounts, - DisableCustomSANs: cfg.DisableCustomSans, - DisableTrustOnFirstUse: cfg.DisableTrustOnFirstUse, - InstanceAge: durationValue(cfg.InstanceAge), - Claims: claims, - Options: options, - }, nil - case *ProvisionerDetails_Azure: - cfg := d.Azure - return &provisioner.Azure{ - Type: p.Type.String(), - Name: p.Name, - TenantID: cfg.TenantId, - ResourceGroups: cfg.ResourceGroups, - Audience: cfg.Audience, - DisableCustomSANs: cfg.DisableCustomSans, - DisableTrustOnFirstUse: cfg.DisableTrustOnFirstUse, - Claims: claims, - Options: options, - }, nil - case *ProvisionerDetails_X5C: - var roots []byte - for i, k := range d.X5C.GetRoots() { - if b := k.GetKey().GetPublic(); b != nil { - if i > 0 { - roots = append(roots, '\n') - } - roots = append(roots, b...) - } - } - return &provisioner.X5C{ - Type: p.Type.String(), - Name: p.Name, - Roots: roots, - Claims: claims, - Options: options, - }, nil - case *ProvisionerDetails_K8SSA: - var publicKeys []byte - for i, k := range d.K8SSA.GetPublicKeys() { - if b := k.GetKey().GetPublic(); b != nil { - if i > 0 { - publicKeys = append(publicKeys, '\n') - } - publicKeys = append(publicKeys, k.Key.Public...) - } - } - return &provisioner.K8sSA{ - Type: p.Type.String(), - Name: p.Name, - PubKeys: publicKeys, - Claims: claims, - Options: options, - }, nil - case *ProvisionerDetails_SSHPOP: - return &provisioner.SSHPOP{ - Type: p.Type.String(), - Name: p.Name, - Claims: claims, - }, nil - case *ProvisionerDetails_ACME: - cfg := d.ACME - return &provisioner.ACME{ - Type: p.Type.String(), - Name: p.Name, - ForceCN: cfg.ForceCn, - Claims: claims, - Options: options, - }, nil - default: - return nil, fmt.Errorf("provisioner %s not implemented", p.Type.String()) } -} -// ToCertificates converts the landlord provisioner claims type to the open source -// (step-ca) claims type. -func (c *Claims) ToCertificates() (*provisioner.Claims, error) { - x509, ssh := c.GetX509(), c.GetSsh() - x509Durations := x509.GetDurations() - hostDurations := ssh.GetHostDurations() - userDurations := ssh.GetUserDurations() - enableSSHCA := ssh.GetEnabled() - return &provisioner.Claims{ - MinTLSDur: durationPtr(x509Durations.GetMin()), - MaxTLSDur: durationPtr(x509Durations.GetMax()), - DefaultTLSDur: durationPtr(x509Durations.GetDefault()), - DisableRenewal: &c.DisableRenewal, - MinUserSSHDur: durationPtr(userDurations.GetMin()), - MaxUserSSHDur: durationPtr(userDurations.GetMax()), - DefaultUserSSHDur: durationPtr(userDurations.GetDefault()), - MinHostSSHDur: durationPtr(hostDurations.GetMin()), - MaxHostSSHDur: durationPtr(hostDurations.GetMax()), - DefaultHostSSHDur: durationPtr(hostDurations.GetDefault()), - EnableSSHCA: &enableSSHCA, - }, nil -} + if err := db.CreateAuthConfig(ctx, ac); err != nil { + return nil, errors.Wrap(err, "error creating authConfig") + } -func durationPtr(d *duration.Duration) *provisioner.Duration { - if d == nil { - return nil - } - return &provisioner.Duration{ - Duration: time.Duration(d.Seconds)*time.Second + time.Duration(d.Nanos)*time.Nanosecond, - } -} + // Generate default JWK provisioner. -func durationValue(d *duration.Duration) provisioner.Duration { - if d == nil { - return provisioner.Duration{} - } - return provisioner.Duration{ - Duration: time.Duration(d.Seconds)*time.Second + time.Duration(d.Nanos)*time.Nanosecond, - } -} - -func marshalDetails(d *ProvisionerDetails) (sql.NullString, error) { - b, err := json.Marshal(d.GetData()) + provOpts := []ProvisionerOption{WithPassword("pass")} + prov, err := CreateProvisioner(ctx, db, "JWK", "changeme", provOpts...) if err != nil { - return sql.NullString{}, nil - } - return sql.NullString{ - String: string(b), - Valid: len(b) > 0, - }, nil -} - -func unmarshalDetails(ctx context.Context, db database.DB, typ ProvisionerType, s sql.NullString) (*ProvisionerDetails, error) { - if !s.Valid { - return nil, nil - } - var v isProvisionerDetails_Data - switch typ { - case ProvisionerType_JWK: - p := new(ProvisionerDetails_JWK) - if err := json.Unmarshal([]byte(s.String), p); err != nil { - return nil, err - } - if p.JWK.Key.Key == nil { - key, err := LoadKey(ctx, db, p.JWK.Key.Id.Id) - if err != nil { - return nil, err - } - p.JWK.Key = key - } - return &ProvisionerDetails{Data: p}, nil - case ProvisionerType_OIDC: - v = new(ProvisionerDetails_OIDC) - case ProvisionerType_GCP: - v = new(ProvisionerDetails_GCP) - case ProvisionerType_AWS: - v = new(ProvisionerDetails_AWS) - case ProvisionerType_AZURE: - v = new(ProvisionerDetails_Azure) - case ProvisionerType_ACME: - v = new(ProvisionerDetails_ACME) - case ProvisionerType_X5C: - p := new(ProvisionerDetails_X5C) - if err := json.Unmarshal([]byte(s.String), p); err != nil { - return nil, err - } - for _, k := range p.X5C.GetRoots() { - if err := k.Select(ctx, db, k.Id.Id); err != nil { - return nil, err - } - } - return &ProvisionerDetails{Data: p}, nil - case ProvisionerType_K8SSA: - p := new(ProvisionerDetails_K8SSA) - if err := json.Unmarshal([]byte(s.String), p); err != nil { - return nil, err - } - for _, k := range p.K8SSA.GetPublicKeys() { - if err := k.Select(ctx, db, k.Id.Id); err != nil { - return nil, err - } - } - return &ProvisionerDetails{Data: p}, nil - case ProvisionerType_SSHPOP: - v = new(ProvisionerDetails_SSHPOP) - default: - return nil, fmt.Errorf("unsupported provisioner type %s", typ) + // TODO should we try to clean up? + return nil, WrapErrorISE(err, "error creating first provisioner") } - if err := json.Unmarshal([]byte(s.String), v); err != nil { - return nil, err - } - return &ProvisionerDetails{Data: v}, nil -} - -func marshalClaims(c *Claims) (sql.NullString, error) { - b, err := json.Marshal(c) + admin, err := CreateAdmin(ctx, db, "Change Me", prov, true) if err != nil { - return sql.NullString{}, nil + // TODO should we try to clean up? + return nil, WrapErrorISE(err, "error creating first provisioner") } - return sql.NullString{ - String: string(b), - Valid: len(b) > 0, - }, nil -} -func unmarshalClaims(s sql.NullString) (*Claims, error) { - if !s.Valid { - return nil, nil - } - v := new(Claims) - return v, json.Unmarshal([]byte(s.String), v) + ac.Provisioners = []*Provisioner{prov} + ac.Admins = []*Admin{admin} + + return ac, nil } -*/ diff --git a/authority/mgmt/db/nosql/admin.go b/authority/mgmt/db/nosql/admin.go index 8b33110c..cba8c59d 100644 --- a/authority/mgmt/db/nosql/admin.go +++ b/authority/mgmt/db/nosql/admin.go @@ -13,13 +13,13 @@ import ( // dbAdmin is the database representation of the Admin type. type dbAdmin struct { - ID string `json:"id"` - AuthorityID string `json:"authorityID"` - Name string `json:"name"` - Provisioner string `json:"provisioner"` - IsSuperAdmin bool `json:"isSuperAdmin"` - CreatedAt time.Time `json:"createdAt"` - DeletedAt time.Time `json:"deletedAt"` + ID string `json:"id"` + AuthorityID string `json:"authorityID"` + ProvisionerID string `json:"provisionerID"` + Name string `json:"name"` + IsSuperAdmin bool `json:"isSuperAdmin"` + CreatedAt time.Time `json:"createdAt"` + DeletedAt time.Time `json:"deletedAt"` } func (dbp *dbAdmin) clone() *dbAdmin { @@ -70,6 +70,13 @@ func (db *DB) GetAdmin(ctx context.Context, id string) (*mgmt.Admin, error) { return nil, mgmt.NewError(mgmt.ErrorAuthorityMismatchType, "admin %s is not owned by authority %s", adm.ID, db.authorityID) } + + prov, err := db.GetProvisioner(ctx, adm.ProvisionerID) + if err != nil { + return nil, err + } + adm.ProvisionerName = prov.Name + adm.ProvisionerType = prov.Type return adm, nil } @@ -87,10 +94,11 @@ func unmarshalAdmin(data []byte, id string) (*mgmt.Admin, error) { return nil, errors.Wrapf(err, "error unmarshaling admin %s into dbAdmin", id) } adm := &mgmt.Admin{ - ID: dba.ID, - Name: dba.Name, - Provisioner: dba.Provisioner, - IsSuperAdmin: dba.IsSuperAdmin, + ID: dba.ID, + AuthorityID: dba.AuthorityID, + ProvisionerID: dba.ProvisionerID, + Name: dba.Name, + IsSuperAdmin: dba.IsSuperAdmin, } if !dba.DeletedAt.IsZero() { adm.Status = mgmt.StatusDeleted @@ -132,12 +140,12 @@ func (db *DB) CreateAdmin(ctx context.Context, adm *mgmt.Admin) error { } dba := &dbAdmin{ - ID: adm.ID, - AuthorityID: db.authorityID, - Name: adm.Name, - Provisioner: adm.Provisioner, - IsSuperAdmin: adm.IsSuperAdmin, - CreatedAt: clock.Now(), + ID: adm.ID, + AuthorityID: db.authorityID, + ProvisionerID: adm.ProvisionerID, + Name: adm.Name, + IsSuperAdmin: adm.IsSuperAdmin, + CreatedAt: clock.Now(), } return db.save(ctx, dba.ID, dba, nil, "admin", authorityAdminsTable) @@ -156,7 +164,7 @@ func (db *DB) UpdateAdmin(ctx context.Context, adm *mgmt.Admin) error { if old.DeletedAt.IsZero() && adm.Status == mgmt.StatusDeleted { nu.DeletedAt = clock.Now() } - nu.Provisioner = adm.Provisioner + nu.ProvisionerID = adm.ProvisionerID nu.IsSuperAdmin = adm.IsSuperAdmin return db.save(ctx, old.ID, nu, old, "admin", authorityAdminsTable) diff --git a/authority/mgmt/provisioner.go b/authority/mgmt/provisioner.go new file mode 100644 index 00000000..2cb21403 --- /dev/null +++ b/authority/mgmt/provisioner.go @@ -0,0 +1,401 @@ +package mgmt + +import ( + "context" + "encoding/json" + "fmt" + + "github.com/smallstep/certificates/authority/provisioner" + "go.step.sm/crypto/jose" +) + +type ProvisionerOption func(*ProvisionerCtx) + +type ProvisionerCtx struct { + JWK *jose.JSONWebKey + JWE *jose.JSONWebEncryption + X509Template, SSHTemplate string + X509TemplateData, SSHTemplateData []byte + Claims *Claims + Password string +} + +func WithJWK(jwk *jose.JSONWebKey, jwe *jose.JSONWebEncryption) func(*ProvisionerCtx) { + return func(ctx *ProvisionerCtx) { + ctx.JWK = jwk + ctx.JWE = jwe + } +} + +func WithPassword(pass string) func(*ProvisionerCtx) { + return func(ctx *ProvisionerCtx) { + ctx.Password = pass + } +} + +// Provisioner type. +type Provisioner struct { + ID string `json:"-"` + AuthorityID string `json:"-"` + Type string `json:"type"` + Name string `json:"name"` + Claims *Claims `json:"claims"` + Details interface{} `json:"details"` + X509Template string `json:"x509Template"` + X509TemplateData []byte `json:"x509TemplateData"` + SSHTemplate string `json:"sshTemplate"` + SSHTemplateData []byte `json:"sshTemplateData"` + Status StatusType `json:"status"` +} + +func (p *Provisioner) GetOptions() *provisioner.Options { + return &provisioner.Options{ + X509: &provisioner.X509Options{ + Template: p.X509Template, + TemplateData: p.X509TemplateData, + }, + SSH: &provisioner.SSHOptions{ + Template: p.SSHTemplate, + TemplateData: p.SSHTemplateData, + }, + } +} + +func CreateProvisioner(ctx context.Context, db DB, typ, name string, opts ...ProvisionerOption) (*Provisioner, error) { + pc := new(ProvisionerCtx) + for _, o := range opts { + o(pc) + } + + details, err := createJWKDetails(pc) + if err != nil { + return nil, err + } + + p := &Provisioner{ + Type: typ, + Name: name, + Claims: pc.Claims, + Details: details, + X509Template: pc.X509Template, + X509TemplateData: pc.X509TemplateData, + SSHTemplate: pc.SSHTemplate, + SSHTemplateData: pc.SSHTemplateData, + Status: StatusActive, + } + + if err := db.CreateProvisioner(ctx, p); err != nil { + return nil, WrapErrorISE(err, "error creating provisioner") + } + return p, nil +} + +type ProvisionerDetails_JWK struct { + PubKey []byte `json:"pubKey"` + PrivKey string `json:"privKey"` +} + +func createJWKDetails(pc *ProvisionerCtx) (*ProvisionerDetails_JWK, error) { + var err error + + if pc.JWK != nil && pc.JWE == nil { + return nil, NewErrorISE("JWE is required with JWK for createJWKProvisioner") + } + if pc.JWE != nil && pc.JWK == nil { + return nil, NewErrorISE("JWK is required with JWE for createJWKProvisioner") + } + if pc.JWK == nil && pc.JWE == nil { + // Create a new JWK w/ encrypted private key. + if pc.Password == "" { + return nil, NewErrorISE("password is required to provisioner with new keys") + } + pc.JWK, pc.JWE, err = jose.GenerateDefaultKeyPair([]byte(pc.Password)) + if err != nil { + return nil, WrapErrorISE(err, "error generating JWK key pair") + } + } + + jwkPubBytes, err := pc.JWK.MarshalJSON() + if err != nil { + return nil, WrapErrorISE(err, "error marshaling JWK") + } + jwePrivStr, err := pc.JWE.CompactSerialize() + if err != nil { + return nil, WrapErrorISE(err, "error serializing JWE") + } + + return &ProvisionerDetails_JWK{ + PubKey: jwkPubBytes, + PrivKey: jwePrivStr, + }, nil +} + +// ToCertificates converts the landlord provisioner type to the open source +// provisioner type. +func (p *Provisioner) ToCertificates() (provisioner.Interface, error) { + claims, err := p.Claims.ToCertificates() + if err != nil { + return nil, err + } + + if err != nil { + return nil, err + } + + switch details := p.Details.(type) { + case *ProvisionerDetails_JWK: + jwk := new(jose.JSONWebKey) + if err := json.Unmarshal(details.PubKey, &jwk); err != nil { + return nil, err + } + return &provisioner.JWK{ + Type: p.Type, + Name: p.Name, + Key: jwk, + EncryptedKey: details.PrivKey, + Claims: claims, + Options: p.GetOptions(), + }, nil + /* + case *ProvisionerDetails_OIDC: + cfg := d.OIDC + return &provisioner.OIDC{ + Type: p.Type.String(), + Name: p.Name, + TenantID: cfg.TenantId, + ClientID: cfg.ClientId, + ClientSecret: cfg.ClientSecret, + ConfigurationEndpoint: cfg.ConfigurationEndpoint, + Admins: cfg.Admins, + Domains: cfg.Domains, + Groups: cfg.Groups, + ListenAddress: cfg.ListenAddress, + Claims: claims, + Options: options, + }, nil + case *ProvisionerDetails_GCP: + cfg := d.GCP + return &provisioner.GCP{ + Type: p.Type.String(), + Name: p.Name, + ServiceAccounts: cfg.ServiceAccounts, + ProjectIDs: cfg.ProjectIds, + DisableCustomSANs: cfg.DisableCustomSans, + DisableTrustOnFirstUse: cfg.DisableTrustOnFirstUse, + InstanceAge: durationValue(cfg.InstanceAge), + Claims: claims, + Options: options, + }, nil + case *ProvisionerDetails_AWS: + cfg := d.AWS + return &provisioner.AWS{ + Type: p.Type.String(), + Name: p.Name, + Accounts: cfg.Accounts, + DisableCustomSANs: cfg.DisableCustomSans, + DisableTrustOnFirstUse: cfg.DisableTrustOnFirstUse, + InstanceAge: durationValue(cfg.InstanceAge), + Claims: claims, + Options: options, + }, nil + case *ProvisionerDetails_Azure: + cfg := d.Azure + return &provisioner.Azure{ + Type: p.Type.String(), + Name: p.Name, + TenantID: cfg.TenantId, + ResourceGroups: cfg.ResourceGroups, + Audience: cfg.Audience, + DisableCustomSANs: cfg.DisableCustomSans, + DisableTrustOnFirstUse: cfg.DisableTrustOnFirstUse, + Claims: claims, + Options: options, + }, nil + case *ProvisionerDetails_X5C: + var roots []byte + for i, k := range d.X5C.GetRoots() { + if b := k.GetKey().GetPublic(); b != nil { + if i > 0 { + roots = append(roots, '\n') + } + roots = append(roots, b...) + } + } + return &provisioner.X5C{ + Type: p.Type.String(), + Name: p.Name, + Roots: roots, + Claims: claims, + Options: options, + }, nil + case *ProvisionerDetails_K8SSA: + var publicKeys []byte + for i, k := range d.K8SSA.GetPublicKeys() { + if b := k.GetKey().GetPublic(); b != nil { + if i > 0 { + publicKeys = append(publicKeys, '\n') + } + publicKeys = append(publicKeys, k.Key.Public...) + } + } + return &provisioner.K8sSA{ + Type: p.Type.String(), + Name: p.Name, + PubKeys: publicKeys, + Claims: claims, + Options: options, + }, nil + case *ProvisionerDetails_SSHPOP: + return &provisioner.SSHPOP{ + Type: p.Type.String(), + Name: p.Name, + Claims: claims, + }, nil + case *ProvisionerDetails_ACME: + cfg := d.ACME + return &provisioner.ACME{ + Type: p.Type.String(), + Name: p.Name, + ForceCN: cfg.ForceCn, + Claims: claims, + Options: options, + }, nil + */ + default: + return nil, fmt.Errorf("provisioner %s not implemented", p.Type) + } +} + +// ToCertificates converts the landlord provisioner claims type to the open source +// (step-ca) claims type. +func (c *Claims) ToCertificates() (*provisioner.Claims, error) { + var durs = map[string]struct { + durStr string + dur *provisioner.Duration + }{ + "minTLSDur": {durStr: c.X509.Durations.Min}, + "maxTLSDur": {durStr: c.X509.Durations.Max}, + "defaultTLSDur": {durStr: c.X509.Durations.Default}, + "minSSHUserDur": {durStr: c.SSH.UserDurations.Min}, + "maxSSHUserDur": {durStr: c.SSH.UserDurations.Max}, + "defaultSSHUserDur": {durStr: c.SSH.UserDurations.Default}, + "minSSHHostDur": {durStr: c.SSH.HostDurations.Min}, + "maxSSHHostDur": {durStr: c.SSH.HostDurations.Max}, + "defaultSSHHostDur": {durStr: c.SSH.HostDurations.Default}, + } + var err error + for k, v := range durs { + v.dur, err = provisioner.NewDuration(v.durStr) + if err != nil { + return nil, WrapErrorISE(err, "error parsing %s %s from claims", k, v.durStr) + } + } + return &provisioner.Claims{ + MinTLSDur: durs["minTLSDur"].dur, + MaxTLSDur: durs["maxTLSDur"].dur, + DefaultTLSDur: durs["defaultTLSDur"].dur, + DisableRenewal: &c.DisableRenewal, + MinUserSSHDur: durs["minSSHUserDur"].dur, + MaxUserSSHDur: durs["maxSSHUserDur"].dur, + DefaultUserSSHDur: durs["defaultSSHUserDur"].dur, + MinHostSSHDur: durs["minSSHHostDur"].dur, + MaxHostSSHDur: durs["maxSSHHostDur"].dur, + DefaultHostSSHDur: durs["defaultSSHHostDur"].dur, + EnableSSHCA: &c.SSH.Enabled, + }, nil +} + +/* +func marshalDetails(d *ProvisionerDetails) (sql.NullString, error) { + b, err := json.Marshal(d.GetData()) + if err != nil { + return sql.NullString{}, nil + } + return sql.NullString{ + String: string(b), + Valid: len(b) > 0, + }, nil +} + +func unmarshalDetails(ctx context.Context, db database.DB, typ ProvisionerType, s sql.NullString) (*ProvisionerDetails, error) { + if !s.Valid { + return nil, nil + } + var v isProvisionerDetails_Data + switch typ { + case ProvisionerType_JWK: + p := new(ProvisionerDetails_JWK) + if err := json.Unmarshal([]byte(s.String), p); err != nil { + return nil, err + } + if p.JWK.Key.Key == nil { + key, err := LoadKey(ctx, db, p.JWK.Key.Id.Id) + if err != nil { + return nil, err + } + p.JWK.Key = key + } + return &ProvisionerDetails{Data: p}, nil + case ProvisionerType_OIDC: + v = new(ProvisionerDetails_OIDC) + case ProvisionerType_GCP: + v = new(ProvisionerDetails_GCP) + case ProvisionerType_AWS: + v = new(ProvisionerDetails_AWS) + case ProvisionerType_AZURE: + v = new(ProvisionerDetails_Azure) + case ProvisionerType_ACME: + v = new(ProvisionerDetails_ACME) + case ProvisionerType_X5C: + p := new(ProvisionerDetails_X5C) + if err := json.Unmarshal([]byte(s.String), p); err != nil { + return nil, err + } + for _, k := range p.X5C.GetRoots() { + if err := k.Select(ctx, db, k.Id.Id); err != nil { + return nil, err + } + } + return &ProvisionerDetails{Data: p}, nil + case ProvisionerType_K8SSA: + p := new(ProvisionerDetails_K8SSA) + if err := json.Unmarshal([]byte(s.String), p); err != nil { + return nil, err + } + for _, k := range p.K8SSA.GetPublicKeys() { + if err := k.Select(ctx, db, k.Id.Id); err != nil { + return nil, err + } + } + return &ProvisionerDetails{Data: p}, nil + case ProvisionerType_SSHPOP: + v = new(ProvisionerDetails_SSHPOP) + default: + return nil, fmt.Errorf("unsupported provisioner type %s", typ) + } + + if err := json.Unmarshal([]byte(s.String), v); err != nil { + return nil, err + } + return &ProvisionerDetails{Data: v}, nil +} + +func marshalClaims(c *Claims) (sql.NullString, error) { + b, err := json.Marshal(c) + if err != nil { + return sql.NullString{}, nil + } + return sql.NullString{ + String: string(b), + Valid: len(b) > 0, + }, nil +} + +func unmarshalClaims(s sql.NullString) (*Claims, error) { + if !s.Valid { + return nil, nil + } + v := new(Claims) + return v, json.Unmarshal([]byte(s.String), v) +} +*/