Merge pull request #847 from smallstep/herman/allow-deny-next
Refactor allow/deny (WIP)pull/788/head
commit
c45d177d52
@ -0,0 +1,47 @@
|
||||
// Package log implements API-related logging helpers.
|
||||
package log
|
||||
|
||||
import (
|
||||
"log"
|
||||
"net/http"
|
||||
|
||||
"github.com/smallstep/certificates/logging"
|
||||
)
|
||||
|
||||
// Error adds to the response writer the given error if it implements
|
||||
// logging.ResponseLogger. If it does not implement it, then writes the error
|
||||
// using the log package.
|
||||
func Error(rw http.ResponseWriter, err error) {
|
||||
if rl, ok := rw.(logging.ResponseLogger); ok {
|
||||
rl.WithFields(map[string]interface{}{
|
||||
"error": err,
|
||||
})
|
||||
} else {
|
||||
log.Println(err)
|
||||
}
|
||||
}
|
||||
|
||||
// EnabledResponse log the response object if it implements the EnableLogger
|
||||
// interface.
|
||||
func EnabledResponse(rw http.ResponseWriter, v interface{}) {
|
||||
type enableLogger interface {
|
||||
ToLog() (interface{}, error)
|
||||
}
|
||||
|
||||
if el, ok := v.(enableLogger); ok {
|
||||
out, err := el.ToLog()
|
||||
if err != nil {
|
||||
Error(rw, err)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if rl, ok := rw.(logging.ResponseLogger); ok {
|
||||
rl.WithFields(map[string]interface{}{
|
||||
"response": out,
|
||||
})
|
||||
} else {
|
||||
log.Println(out)
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,44 @@
|
||||
package log
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/smallstep/certificates/logging"
|
||||
)
|
||||
|
||||
func TestError(t *testing.T) {
|
||||
theError := errors.New("the error")
|
||||
|
||||
type args struct {
|
||||
rw http.ResponseWriter
|
||||
err error
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
withFields bool
|
||||
}{
|
||||
{"normalLogger", args{httptest.NewRecorder(), theError}, false},
|
||||
{"responseLogger", args{logging.NewResponseLogger(httptest.NewRecorder()), theError}, true},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
Error(tt.args.rw, tt.args.err)
|
||||
if tt.withFields {
|
||||
if rl, ok := tt.args.rw.(logging.ResponseLogger); ok {
|
||||
fields := rl.Fields()
|
||||
if !reflect.DeepEqual(fields["error"], theError) {
|
||||
t.Errorf("ResponseLogger[\"error\"] = %s, wants %s", fields["error"], theError)
|
||||
}
|
||||
} else {
|
||||
t.Error("ResponseWriter does not implement logging.ResponseLogger")
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
@ -0,0 +1,31 @@
|
||||
// Package read implements request object readers.
|
||||
package read
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"io"
|
||||
|
||||
"google.golang.org/protobuf/encoding/protojson"
|
||||
"google.golang.org/protobuf/proto"
|
||||
|
||||
"github.com/smallstep/certificates/errs"
|
||||
)
|
||||
|
||||
// JSON reads JSON from the request body and stores it in the value
|
||||
// pointed by v.
|
||||
func JSON(r io.Reader, v interface{}) error {
|
||||
if err := json.NewDecoder(r).Decode(v); err != nil {
|
||||
return errs.BadRequestErr(err, "error decoding json")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ProtoJSON reads JSON from the request body and stores it in the value
|
||||
// pointed by v.
|
||||
func ProtoJSON(r io.Reader, m proto.Message) error {
|
||||
data, err := io.ReadAll(r)
|
||||
if err != nil {
|
||||
return errs.BadRequestErr(err, "error reading request body")
|
||||
}
|
||||
return protojson.Unmarshal(data, m)
|
||||
}
|
@ -0,0 +1,46 @@
|
||||
package read
|
||||
|
||||
import (
|
||||
"io"
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/smallstep/certificates/errs"
|
||||
)
|
||||
|
||||
func TestJSON(t *testing.T) {
|
||||
type args struct {
|
||||
r io.Reader
|
||||
v interface{}
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
wantErr bool
|
||||
}{
|
||||
{"ok", args{strings.NewReader(`{"foo":"bar"}`), make(map[string]interface{})}, false},
|
||||
{"fail", args{strings.NewReader(`{"foo"}`), make(map[string]interface{})}, true},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
err := JSON(tt.args.r, &tt.args.v)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("JSON() error = %v, wantErr %v", err, tt.wantErr)
|
||||
}
|
||||
|
||||
if tt.wantErr {
|
||||
e, ok := err.(*errs.Error)
|
||||
if ok {
|
||||
if code := e.StatusCode(); code != 400 {
|
||||
t.Errorf("error.StatusCode() = %v, wants 400", code)
|
||||
}
|
||||
} else {
|
||||
t.Errorf("error type = %T, wants *Error", err)
|
||||
}
|
||||
} else if !reflect.DeepEqual(tt.args.v, map[string]interface{}{"foo": "bar"}) {
|
||||
t.Errorf("JSON value = %v, wants %v", tt.args.v, map[string]interface{}{"foo": "bar"})
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
@ -0,0 +1,326 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/go-chi/chi"
|
||||
|
||||
"go.step.sm/linkedca"
|
||||
|
||||
"github.com/smallstep/certificates/api"
|
||||
"github.com/smallstep/certificates/api/read"
|
||||
"github.com/smallstep/certificates/authority/admin"
|
||||
"github.com/smallstep/certificates/authority/provisioner"
|
||||
)
|
||||
|
||||
type policyAdminResponderInterface interface {
|
||||
GetAuthorityPolicy(w http.ResponseWriter, r *http.Request)
|
||||
CreateAuthorityPolicy(w http.ResponseWriter, r *http.Request)
|
||||
UpdateAuthorityPolicy(w http.ResponseWriter, r *http.Request)
|
||||
DeleteAuthorityPolicy(w http.ResponseWriter, r *http.Request)
|
||||
GetProvisionerPolicy(w http.ResponseWriter, r *http.Request)
|
||||
CreateProvisionerPolicy(w http.ResponseWriter, r *http.Request)
|
||||
UpdateProvisionerPolicy(w http.ResponseWriter, r *http.Request)
|
||||
DeleteProvisionerPolicy(w http.ResponseWriter, r *http.Request)
|
||||
GetACMEAccountPolicy(w http.ResponseWriter, r *http.Request)
|
||||
CreateACMEAccountPolicy(w http.ResponseWriter, r *http.Request)
|
||||
UpdateACMEAccountPolicy(w http.ResponseWriter, r *http.Request)
|
||||
DeleteACMEAccountPolicy(w http.ResponseWriter, r *http.Request)
|
||||
}
|
||||
|
||||
// PolicyAdminResponder is responsible for writing ACME admin responses
|
||||
type PolicyAdminResponder struct {
|
||||
auth adminAuthority
|
||||
adminDB admin.DB
|
||||
}
|
||||
|
||||
// NewACMEAdminResponder returns a new ACMEAdminResponder
|
||||
func NewPolicyAdminResponder(auth adminAuthority, adminDB admin.DB) *PolicyAdminResponder {
|
||||
return &PolicyAdminResponder{
|
||||
auth: auth,
|
||||
adminDB: adminDB,
|
||||
}
|
||||
}
|
||||
|
||||
// GetAuthorityPolicy handles the GET /admin/authority/policy request
|
||||
func (par *PolicyAdminResponder) GetAuthorityPolicy(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
policy, err := par.auth.GetAuthorityPolicy(r.Context())
|
||||
if ae, ok := err.(*admin.Error); ok {
|
||||
if !ae.IsType(admin.ErrorNotFoundType) {
|
||||
api.WriteError(w, admin.WrapErrorISE(ae, "error retrieving authority policy"))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if policy == nil {
|
||||
api.JSONNotFound(w)
|
||||
return
|
||||
}
|
||||
|
||||
api.ProtoJSONStatus(w, policy, http.StatusOK)
|
||||
}
|
||||
|
||||
// CreateAuthorityPolicy handles the POST /admin/authority/policy request
|
||||
func (par *PolicyAdminResponder) CreateAuthorityPolicy(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
ctx := r.Context()
|
||||
policy, err := par.auth.GetAuthorityPolicy(ctx)
|
||||
|
||||
shouldWriteError := false
|
||||
if ae, ok := err.(*admin.Error); ok {
|
||||
shouldWriteError = !ae.IsType(admin.ErrorNotFoundType)
|
||||
}
|
||||
|
||||
if shouldWriteError {
|
||||
api.WriteError(w, admin.WrapErrorISE(err, "error retrieving authority policy"))
|
||||
return
|
||||
}
|
||||
|
||||
if policy != nil {
|
||||
adminErr := admin.NewError(admin.ErrorBadRequestType, "authority already has a policy")
|
||||
adminErr.Status = http.StatusConflict
|
||||
api.WriteError(w, adminErr)
|
||||
return
|
||||
}
|
||||
|
||||
var newPolicy = new(linkedca.Policy)
|
||||
if !api.ReadProtoJSONWithCheck(w, r.Body, newPolicy) {
|
||||
return
|
||||
}
|
||||
|
||||
adm := linkedca.AdminFromContext(ctx)
|
||||
|
||||
var createdPolicy *linkedca.Policy
|
||||
if createdPolicy, err = par.auth.CreateAuthorityPolicy(ctx, adm, newPolicy); err != nil {
|
||||
api.WriteError(w, admin.WrapError(admin.ErrorBadRequestType, err, "error storing authority policy"))
|
||||
return
|
||||
}
|
||||
|
||||
api.JSONStatus(w, createdPolicy, http.StatusCreated)
|
||||
}
|
||||
|
||||
// UpdateAuthorityPolicy handles the PUT /admin/authority/policy request
|
||||
func (par *PolicyAdminResponder) UpdateAuthorityPolicy(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
ctx := r.Context()
|
||||
policy, err := par.auth.GetAuthorityPolicy(ctx)
|
||||
|
||||
shouldWriteError := false
|
||||
if ae, ok := err.(*admin.Error); ok {
|
||||
shouldWriteError = !ae.IsType(admin.ErrorNotFoundType)
|
||||
}
|
||||
|
||||
if shouldWriteError {
|
||||
api.WriteError(w, admin.WrapErrorISE(err, "error retrieving authority policy"))
|
||||
return
|
||||
}
|
||||
|
||||
if policy == nil {
|
||||
api.JSONNotFound(w)
|
||||
return
|
||||
}
|
||||
|
||||
var newPolicy = new(linkedca.Policy)
|
||||
if err := read.ProtoJSON(r.Body, newPolicy); err != nil {
|
||||
api.WriteError(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
adm := linkedca.AdminFromContext(ctx)
|
||||
|
||||
var updatedPolicy *linkedca.Policy
|
||||
if updatedPolicy, err = par.auth.UpdateAuthorityPolicy(ctx, adm, newPolicy); err != nil {
|
||||
api.WriteError(w, admin.WrapError(admin.ErrorBadRequestType, err, "error updating authority policy"))
|
||||
return
|
||||
}
|
||||
|
||||
api.ProtoJSONStatus(w, updatedPolicy, http.StatusOK)
|
||||
}
|
||||
|
||||
// DeleteAuthorityPolicy handles the DELETE /admin/authority/policy request
|
||||
func (par *PolicyAdminResponder) DeleteAuthorityPolicy(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
ctx := r.Context()
|
||||
policy, err := par.auth.GetAuthorityPolicy(ctx)
|
||||
|
||||
if ae, ok := err.(*admin.Error); ok {
|
||||
if !ae.IsType(admin.ErrorNotFoundType) {
|
||||
api.WriteError(w, admin.WrapErrorISE(ae, "error retrieving authority policy"))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if policy == nil {
|
||||
api.JSONNotFound(w)
|
||||
return
|
||||
}
|
||||
|
||||
err = par.auth.RemoveAuthorityPolicy(ctx)
|
||||
if err != nil {
|
||||
api.WriteError(w, admin.WrapErrorISE(err, "error deleting authority policy"))
|
||||
return
|
||||
}
|
||||
|
||||
api.JSONStatus(w, DeleteResponse{Status: "ok"}, http.StatusOK)
|
||||
}
|
||||
|
||||
// GetProvisionerPolicy handles the GET /admin/provisioners/{name}/policy request
|
||||
func (par *PolicyAdminResponder) GetProvisionerPolicy(w http.ResponseWriter, r *http.Request) {
|
||||
// TODO: move getting provisioner to middleware?
|
||||
ctx := r.Context()
|
||||
name := chi.URLParam(r, "name")
|
||||
var (
|
||||
p provisioner.Interface
|
||||
err error
|
||||
)
|
||||
if p, err = par.auth.LoadProvisionerByName(name); err != nil {
|
||||
api.WriteError(w, admin.WrapErrorISE(err, "error loading provisioner %s", name))
|
||||
return
|
||||
}
|
||||
|
||||
prov, err := par.adminDB.GetProvisioner(ctx, p.GetID())
|
||||
if err != nil {
|
||||
api.WriteError(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
policy := prov.GetPolicy()
|
||||
if policy == nil {
|
||||
api.JSONNotFound(w)
|
||||
return
|
||||
}
|
||||
|
||||
api.ProtoJSONStatus(w, policy, http.StatusOK)
|
||||
}
|
||||
|
||||
// CreateProvisionerPolicy handles the POST /admin/provisioners/{name}/policy request
|
||||
func (par *PolicyAdminResponder) CreateProvisionerPolicy(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
name := chi.URLParam(r, "name")
|
||||
var (
|
||||
p provisioner.Interface
|
||||
err error
|
||||
)
|
||||
if p, err = par.auth.LoadProvisionerByName(name); err != nil {
|
||||
api.WriteError(w, admin.WrapErrorISE(err, "error loading provisioner %s", name))
|
||||
return
|
||||
}
|
||||
|
||||
prov, err := par.adminDB.GetProvisioner(ctx, p.GetID())
|
||||
if err != nil {
|
||||
api.WriteError(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
policy := prov.GetPolicy()
|
||||
if policy != nil {
|
||||
adminErr := admin.NewError(admin.ErrorBadRequestType, "provisioner %s already has a policy", name)
|
||||
adminErr.Status = http.StatusConflict
|
||||
api.WriteError(w, adminErr)
|
||||
}
|
||||
|
||||
var newPolicy = new(linkedca.Policy)
|
||||
if err := read.ProtoJSON(r.Body, newPolicy); err != nil {
|
||||
api.WriteError(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
prov.Policy = newPolicy
|
||||
|
||||
err = par.auth.UpdateProvisioner(ctx, prov)
|
||||
if err != nil {
|
||||
api.WriteError(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
api.ProtoJSONStatus(w, newPolicy, http.StatusCreated)
|
||||
}
|
||||
|
||||
// UpdateProvisionerPolicy handles the PUT /admin/provisioners/{name}/policy request
|
||||
func (par *PolicyAdminResponder) UpdateProvisionerPolicy(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
name := chi.URLParam(r, "name")
|
||||
var (
|
||||
p provisioner.Interface
|
||||
err error
|
||||
)
|
||||
if p, err = par.auth.LoadProvisionerByName(name); err != nil {
|
||||
api.WriteError(w, admin.WrapErrorISE(err, "error loading provisioner %s", name))
|
||||
return
|
||||
}
|
||||
|
||||
prov, err := par.adminDB.GetProvisioner(ctx, p.GetID())
|
||||
if err != nil {
|
||||
api.WriteError(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
var policy = new(linkedca.Policy)
|
||||
if err := read.ProtoJSON(r.Body, policy); err != nil {
|
||||
api.WriteError(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
prov.Policy = policy
|
||||
err = par.auth.UpdateProvisioner(ctx, prov)
|
||||
if err != nil {
|
||||
api.WriteError(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
api.ProtoJSONStatus(w, policy, http.StatusOK)
|
||||
}
|
||||
|
||||
// DeleteProvisionerPolicy handles the DELETE /admin/provisioners/{name}/policy request
|
||||
func (par *PolicyAdminResponder) DeleteProvisionerPolicy(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
ctx := r.Context()
|
||||
name := chi.URLParam(r, "name")
|
||||
var (
|
||||
p provisioner.Interface
|
||||
err error
|
||||
)
|
||||
if p, err = par.auth.LoadProvisionerByName(name); err != nil {
|
||||
api.WriteError(w, admin.WrapErrorISE(err, "error loading provisioner %s", name))
|
||||
return
|
||||
}
|
||||
|
||||
prov, err := par.adminDB.GetProvisioner(ctx, p.GetID())
|
||||
if err != nil {
|
||||
api.WriteError(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
if prov.Policy == nil {
|
||||
api.JSONNotFound(w)
|
||||
return
|
||||
}
|
||||
|
||||
// remove the policy
|
||||
prov.Policy = nil
|
||||
|
||||
err = par.auth.UpdateProvisioner(ctx, prov)
|
||||
if err != nil {
|
||||
api.WriteError(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
api.JSON(w, &DeleteResponse{Status: "ok"})
|
||||
}
|
||||
|
||||
func (par *PolicyAdminResponder) GetACMEAccountPolicy(w http.ResponseWriter, r *http.Request) {
|
||||
api.JSON(w, "ok")
|
||||
}
|
||||
|
||||
func (par *PolicyAdminResponder) CreateACMEAccountPolicy(w http.ResponseWriter, r *http.Request) {
|
||||
api.JSON(w, "ok")
|
||||
}
|
||||
|
||||
func (par *PolicyAdminResponder) UpdateACMEAccountPolicy(w http.ResponseWriter, r *http.Request) {
|
||||
api.JSON(w, "ok")
|
||||
}
|
||||
|
||||
func (par *PolicyAdminResponder) DeleteACMEAccountPolicy(w http.ResponseWriter, r *http.Request) {
|
||||
api.JSON(w, "ok")
|
||||
}
|
@ -0,0 +1,10 @@
|
||||
package admin
|
||||
|
||||
// ContextKey is the key type for storing and searching for
|
||||
// Admin API objects in request contexts.
|
||||
type ContextKey string
|
||||
|
||||
const (
|
||||
// AdminContextKey account key
|
||||
AdminContextKey = ContextKey("admin")
|
||||
)
|
@ -0,0 +1,144 @@
|
||||
package nosql
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
|
||||
"github.com/smallstep/certificates/authority/admin"
|
||||
"github.com/smallstep/nosql"
|
||||
"go.step.sm/linkedca"
|
||||
)
|
||||
|
||||
type dbAuthorityPolicy struct {
|
||||
ID string `json:"id"`
|
||||
AuthorityID string `json:"authorityID"`
|
||||
Policy *linkedca.Policy `json:"policy"`
|
||||
}
|
||||
|
||||
func (dbap *dbAuthorityPolicy) convert() *linkedca.Policy {
|
||||
return dbap.Policy
|
||||
}
|
||||
|
||||
func (dbap *dbAuthorityPolicy) clone() *dbAuthorityPolicy {
|
||||
u := *dbap
|
||||
return &u
|
||||
}
|
||||
|
||||
func (db *DB) getDBAuthorityPolicyBytes(ctx context.Context, authorityID string) ([]byte, error) {
|
||||
data, err := db.db.Get(authorityPoliciesTable, []byte(authorityID))
|
||||
if nosql.IsErrNotFound(err) {
|
||||
return nil, admin.NewError(admin.ErrorNotFoundType, "policy %s not found", authorityID)
|
||||
} else if err != nil {
|
||||
return nil, errors.Wrapf(err, "error loading admin %s", authorityID)
|
||||
}
|
||||
return data, nil
|
||||
}
|
||||
|
||||
func (db *DB) unmarshalDBAuthorityPolicy(data []byte, authorityID string) (*dbAuthorityPolicy, error) {
|
||||
var dba = new(dbAuthorityPolicy)
|
||||
if err := json.Unmarshal(data, dba); err != nil {
|
||||
return nil, errors.Wrapf(err, "error unmarshaling admin %s into dbAdmin", authorityID)
|
||||
}
|
||||
// if !dba.DeletedAt.IsZero() {
|
||||
// return nil, admin.NewError(admin.ErrorDeletedType, "admin %s is deleted", authorityID)
|
||||
// }
|
||||
if dba.AuthorityID != db.authorityID {
|
||||
return nil, admin.NewError(admin.ErrorAuthorityMismatchType,
|
||||
"admin %s is not owned by authority %s", dba.ID, db.authorityID)
|
||||
}
|
||||
return dba, nil
|
||||
}
|
||||
|
||||
func (db *DB) getDBAuthorityPolicy(ctx context.Context, authorityID string) (*dbAuthorityPolicy, error) {
|
||||
data, err := db.getDBAuthorityPolicyBytes(ctx, authorityID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
dbap, err := db.unmarshalDBAuthorityPolicy(data, authorityID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return dbap, nil
|
||||
}
|
||||
|
||||
// func (db *DB) unmarshalAuthorityPolicy(data []byte, authorityID string) (*linkedca.Policy, error) {
|
||||
// dbap, err := db.unmarshalDBAuthorityPolicy(data, authorityID)
|
||||
// if err != nil {
|
||||
// return nil, err
|
||||
// }
|
||||
// return dbap.convert(), nil
|
||||
// }
|
||||
|
||||
func (db *DB) CreateAuthorityPolicy(ctx context.Context, policy *linkedca.Policy) error {
|
||||
|
||||
dbap := &dbAuthorityPolicy{
|
||||
ID: db.authorityID,
|
||||
AuthorityID: db.authorityID,
|
||||
Policy: policy,
|
||||
}
|
||||
|
||||
old, err := db.getDBAuthorityPolicy(ctx, db.authorityID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return db.save(ctx, dbap.ID, dbap, old, "authority_policy", authorityPoliciesTable)
|
||||
}
|
||||
|
||||
func (db *DB) GetAuthorityPolicy(ctx context.Context) (*linkedca.Policy, error) {
|
||||
// policy := &linkedca.Policy{
|
||||
// X509: &linkedca.X509Policy{
|
||||
// Allow: &linkedca.X509Names{
|
||||
// Dns: []string{".localhost"},
|
||||
// },
|
||||
// Deny: &linkedca.X509Names{
|
||||
// Dns: []string{"denied.localhost"},
|
||||
// },
|
||||
// },
|
||||
// Ssh: &linkedca.SSHPolicy{
|
||||
// User: &linkedca.SSHUserPolicy{
|
||||
// Allow: &linkedca.SSHUserNames{},
|
||||
// Deny: &linkedca.SSHUserNames{},
|
||||
// },
|
||||
// Host: &linkedca.SSHHostPolicy{
|
||||
// Allow: &linkedca.SSHHostNames{},
|
||||
// Deny: &linkedca.SSHHostNames{},
|
||||
// },
|
||||
// },
|
||||
// }
|
||||
|
||||
dbap, err := db.getDBAuthorityPolicy(ctx, db.authorityID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return dbap.convert(), nil
|
||||
}
|
||||
|
||||
func (db *DB) UpdateAuthorityPolicy(ctx context.Context, policy *linkedca.Policy) error {
|
||||
old, err := db.getDBAuthorityPolicy(ctx, db.authorityID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
dbap := &dbAuthorityPolicy{
|
||||
ID: db.authorityID,
|
||||
AuthorityID: db.authorityID,
|
||||
Policy: policy,
|
||||
}
|
||||
|
||||
return db.save(ctx, dbap.ID, dbap, old, "authority_policy", authorityPoliciesTable)
|
||||
}
|
||||
|
||||
func (db *DB) DeleteAuthorityPolicy(ctx context.Context) error {
|
||||
dbap, err := db.getDBAuthorityPolicy(ctx, db.authorityID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
old := dbap.clone()
|
||||
|
||||
dbap.Policy = nil
|
||||
return db.save(ctx, dbap.ID, dbap, old, "authority_policy", authorityPoliciesTable)
|
||||
}
|
@ -0,0 +1,195 @@
|
||||
package authority
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
|
||||
"go.step.sm/linkedca"
|
||||
|
||||
"github.com/smallstep/certificates/authority/admin"
|
||||
authPolicy "github.com/smallstep/certificates/authority/policy"
|
||||
policy "github.com/smallstep/certificates/policy"
|
||||
)
|
||||
|
||||
func (a *Authority) GetAuthorityPolicy(ctx context.Context) (*linkedca.Policy, error) {
|
||||
a.adminMutex.Lock()
|
||||
defer a.adminMutex.Unlock()
|
||||
|
||||
p, err := a.adminDB.GetAuthorityPolicy(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return p, nil
|
||||
}
|
||||
|
||||
func (a *Authority) CreateAuthorityPolicy(ctx context.Context, adm *linkedca.Admin, p *linkedca.Policy) (*linkedca.Policy, error) {
|
||||
a.adminMutex.Lock()
|
||||
defer a.adminMutex.Unlock()
|
||||
|
||||
if err := a.checkPolicy(ctx, adm, p); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := a.adminDB.CreateAuthorityPolicy(ctx, p); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := a.reloadPolicyEngines(ctx); err != nil {
|
||||
return nil, admin.WrapErrorISE(err, "error reloading policy engines when creating authority policy")
|
||||
}
|
||||
|
||||
return p, nil // TODO: return the newly stored policy
|
||||
}
|
||||
|
||||
func (a *Authority) UpdateAuthorityPolicy(ctx context.Context, adm *linkedca.Admin, p *linkedca.Policy) (*linkedca.Policy, error) {
|
||||
a.adminMutex.Lock()
|
||||
defer a.adminMutex.Unlock()
|
||||
|
||||
if err := a.checkPolicy(ctx, adm, p); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := a.adminDB.UpdateAuthorityPolicy(ctx, p); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := a.reloadPolicyEngines(ctx); err != nil {
|
||||
return nil, admin.WrapErrorISE(err, "error reloading policy engines when updating authority policy")
|
||||
}
|
||||
|
||||
return p, nil // TODO: return the updated stored policy
|
||||
}
|
||||
|
||||
func (a *Authority) RemoveAuthorityPolicy(ctx context.Context) error {
|
||||
a.adminMutex.Lock()
|
||||
defer a.adminMutex.Unlock()
|
||||
|
||||
if err := a.adminDB.DeleteAuthorityPolicy(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := a.reloadPolicyEngines(ctx); err != nil {
|
||||
return admin.WrapErrorISE(err, "error reloading policy engines when deleting authority policy")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *Authority) checkPolicy(ctx context.Context, adm *linkedca.Admin, p *linkedca.Policy) error {
|
||||
|
||||
// convert the policy; return early if nil
|
||||
policyOptions := policyToCertificates(p)
|
||||
if policyOptions == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
engine, err := authPolicy.NewX509PolicyEngine(policyOptions.GetX509Options())
|
||||
if err != nil {
|
||||
return admin.WrapErrorISE(err, "error creating temporary policy engine")
|
||||
}
|
||||
|
||||
// TODO(hs): Provide option to force the policy, even when the admin subject would be locked out?
|
||||
|
||||
sans := []string{adm.Subject}
|
||||
if err := isAllowed(engine, sans); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// TODO(hs): perform the check for other admin subjects too?
|
||||
// What logic to use for that: do all admins need access? Only super admins? At least one?
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func isAllowed(engine authPolicy.X509Policy, sans []string) error {
|
||||
var (
|
||||
allowed bool
|
||||
err error
|
||||
)
|
||||
if allowed, err = engine.AreSANsAllowed(sans); err != nil {
|
||||
var policyErr *policy.NamePolicyError
|
||||
if isPolicyErr := errors.As(err, &policyErr); isPolicyErr && policyErr.Reason == policy.NotAuthorizedForThisName {
|
||||
return fmt.Errorf("the provided policy would lock out %s from the CA. Please update your policy to include %s as an allowed name", sans, sans)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
if !allowed {
|
||||
return fmt.Errorf("the provided policy would lock out %s from the CA. Please update your policy to include %s as an allowed name", sans, sans)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func policyToCertificates(p *linkedca.Policy) *authPolicy.Options {
|
||||
|
||||
// return early
|
||||
if p == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
// prepare full policy struct
|
||||
opts := &authPolicy.Options{
|
||||
X509: &authPolicy.X509PolicyOptions{
|
||||
AllowedNames: &authPolicy.X509NameOptions{},
|
||||
DeniedNames: &authPolicy.X509NameOptions{},
|
||||
},
|
||||
SSH: &authPolicy.SSHPolicyOptions{
|
||||
Host: &authPolicy.SSHHostCertificateOptions{
|
||||
AllowedNames: &authPolicy.SSHNameOptions{},
|
||||
DeniedNames: &authPolicy.SSHNameOptions{},
|
||||
},
|
||||
User: &authPolicy.SSHUserCertificateOptions{
|
||||
AllowedNames: &authPolicy.SSHNameOptions{},
|
||||
DeniedNames: &authPolicy.SSHNameOptions{},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// fill x509 policy configuration
|
||||
if p.X509 != nil {
|
||||
if p.X509.Allow != nil {
|
||||
opts.X509.AllowedNames.DNSDomains = p.X509.Allow.Dns
|
||||
opts.X509.AllowedNames.IPRanges = p.X509.Allow.Ips
|
||||
opts.X509.AllowedNames.EmailAddresses = p.X509.Allow.Emails
|
||||
opts.X509.AllowedNames.URIDomains = p.X509.Allow.Uris
|
||||
}
|
||||
if p.X509.Deny != nil {
|
||||
opts.X509.DeniedNames.DNSDomains = p.X509.Deny.Dns
|
||||
opts.X509.DeniedNames.IPRanges = p.X509.Deny.Ips
|
||||
opts.X509.DeniedNames.EmailAddresses = p.X509.Deny.Emails
|
||||
opts.X509.DeniedNames.URIDomains = p.X509.Deny.Uris
|
||||
}
|
||||
}
|
||||
|
||||
// fill ssh policy configuration
|
||||
if p.Ssh != nil {
|
||||
if p.Ssh.Host != nil {
|
||||
if p.Ssh.Host.Allow != nil {
|
||||
opts.SSH.Host.AllowedNames.DNSDomains = p.Ssh.Host.Allow.Dns
|
||||
opts.SSH.Host.AllowedNames.IPRanges = p.Ssh.Host.Allow.Ips
|
||||
opts.SSH.Host.AllowedNames.EmailAddresses = p.Ssh.Host.Allow.Principals
|
||||
}
|
||||
if p.Ssh.Host.Deny != nil {
|
||||
opts.SSH.Host.DeniedNames.DNSDomains = p.Ssh.Host.Deny.Dns
|
||||
opts.SSH.Host.DeniedNames.IPRanges = p.Ssh.Host.Deny.Ips
|
||||
opts.SSH.Host.DeniedNames.Principals = p.Ssh.Host.Deny.Principals
|
||||
}
|
||||
}
|
||||
if p.Ssh.User != nil {
|
||||
if p.Ssh.User.Allow != nil {
|
||||
opts.SSH.User.AllowedNames.EmailAddresses = p.Ssh.User.Allow.Emails
|
||||
opts.SSH.User.AllowedNames.Principals = p.Ssh.User.Allow.Principals
|
||||
}
|
||||
if p.Ssh.User.Deny != nil {
|
||||
opts.SSH.User.DeniedNames.EmailAddresses = p.Ssh.User.Deny.Emails
|
||||
opts.SSH.User.DeniedNames.Principals = p.Ssh.User.Deny.Principals
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return opts
|
||||
}
|
@ -0,0 +1,188 @@
|
||||
package policy
|
||||
|
||||
// Options is a container for authority level x509 and SSH
|
||||
// policy configuration.
|
||||
type Options struct {
|
||||
X509 *X509PolicyOptions `json:"x509,omitempty"`
|
||||
SSH *SSHPolicyOptions `json:"ssh,omitempty"`
|
||||
}
|
||||
|
||||
// GetX509Options returns the x509 authority level policy
|
||||
// configuration
|
||||
func (o *Options) GetX509Options() *X509PolicyOptions {
|
||||
if o == nil {
|
||||
return nil
|
||||
}
|
||||
return o.X509
|
||||
}
|
||||
|
||||
// GetSSHOptions returns the SSH authority level policy
|
||||
// configuration
|
||||
func (o *Options) GetSSHOptions() *SSHPolicyOptions {
|
||||
if o == nil {
|
||||
return nil
|
||||
}
|
||||
return o.SSH
|
||||
}
|
||||
|
||||
// X509PolicyOptionsInterface is an interface for providers
|
||||
// of x509 allowed and denied names.
|
||||
type X509PolicyOptionsInterface interface {
|
||||
GetAllowedNameOptions() *X509NameOptions
|
||||
GetDeniedNameOptions() *X509NameOptions
|
||||
}
|
||||
|
||||
// X509PolicyOptions is a container for x509 allowed and denied
|
||||
// names.
|
||||
type X509PolicyOptions struct {
|
||||
// AllowedNames contains the x509 allowed names
|
||||
AllowedNames *X509NameOptions `json:"allow,omitempty"`
|
||||
// DeniedNames contains the x509 denied names
|
||||
DeniedNames *X509NameOptions `json:"deny,omitempty"`
|
||||
}
|
||||
|
||||
// X509NameOptions models the X509 name policy configuration.
|
||||
type X509NameOptions struct {
|
||||
DNSDomains []string `json:"dns,omitempty"`
|
||||
IPRanges []string `json:"ip,omitempty"`
|
||||
EmailAddresses []string `json:"email,omitempty"`
|
||||
URIDomains []string `json:"uri,omitempty"`
|
||||
}
|
||||
|
||||
// HasNames checks if the AllowedNameOptions has one or more
|
||||
// names configured.
|
||||
func (o *X509NameOptions) HasNames() bool {
|
||||
return len(o.DNSDomains) > 0 ||
|
||||
len(o.IPRanges) > 0 ||
|
||||
len(o.EmailAddresses) > 0 ||
|
||||
len(o.URIDomains) > 0
|
||||
}
|
||||
|
||||
// SSHPolicyOptionsInterface is an interface for providers of
|
||||
// SSH user and host name policy configuration.
|
||||
type SSHPolicyOptionsInterface interface {
|
||||
GetAllowedUserNameOptions() *SSHNameOptions
|
||||
GetDeniedUserNameOptions() *SSHNameOptions
|
||||
GetAllowedHostNameOptions() *SSHNameOptions
|
||||
GetDeniedHostNameOptions() *SSHNameOptions
|
||||
}
|
||||
|
||||
// SSHPolicyOptions is a container for SSH user and host policy
|
||||
// configuration
|
||||
type SSHPolicyOptions struct {
|
||||
// User contains SSH user certificate options.
|
||||
User *SSHUserCertificateOptions `json:"user,omitempty"`
|
||||
// Host contains SSH host certificate options.
|
||||
Host *SSHHostCertificateOptions `json:"host,omitempty"`
|
||||
}
|
||||
|
||||
// GetAllowedNameOptions returns x509 allowed name policy configuration
|
||||
func (o *X509PolicyOptions) GetAllowedNameOptions() *X509NameOptions {
|
||||
if o == nil {
|
||||
return nil
|
||||
}
|
||||
return o.AllowedNames
|
||||
}
|
||||
|
||||
// GetDeniedNameOptions returns the x509 denied name policy configuration
|
||||
func (o *X509PolicyOptions) GetDeniedNameOptions() *X509NameOptions {
|
||||
if o == nil {
|
||||
return nil
|
||||
}
|
||||
return o.DeniedNames
|
||||
}
|
||||
|
||||
// GetAllowedUserNameOptions returns the SSH allowed user name policy
|
||||
// configuration.
|
||||
func (o *SSHPolicyOptions) GetAllowedUserNameOptions() *SSHNameOptions {
|
||||
if o == nil {
|
||||
return nil
|
||||
}
|
||||
if o.User == nil {
|
||||
return nil
|
||||
}
|
||||
return o.User.AllowedNames
|
||||
}
|
||||
|
||||
// GetDeniedUserNameOptions returns the SSH denied user name policy
|
||||
// configuration.
|
||||
func (o *SSHPolicyOptions) GetDeniedUserNameOptions() *SSHNameOptions {
|
||||
if o == nil {
|
||||
return nil
|
||||
}
|
||||
if o.User == nil {
|
||||
return nil
|
||||
}
|
||||
return o.User.DeniedNames
|
||||
}
|
||||
|
||||
// GetAllowedHostNameOptions returns the SSH allowed host name policy
|
||||
// configuration.
|
||||
func (o *SSHPolicyOptions) GetAllowedHostNameOptions() *SSHNameOptions {
|
||||
if o == nil {
|
||||
return nil
|
||||
}
|
||||
if o.Host == nil {
|
||||
return nil
|
||||
}
|
||||
return o.Host.AllowedNames
|
||||
}
|
||||
|
||||
// GetDeniedHostNameOptions returns the SSH denied host name policy
|
||||
// configuration.
|
||||
func (o *SSHPolicyOptions) GetDeniedHostNameOptions() *SSHNameOptions {
|
||||
if o == nil {
|
||||
return nil
|
||||
}
|
||||
if o.Host == nil {
|
||||
return nil
|
||||
}
|
||||
return o.Host.DeniedNames
|
||||
}
|
||||
|
||||
// SSHUserCertificateOptions is a collection of SSH user certificate options.
|
||||
type SSHUserCertificateOptions struct {
|
||||
// AllowedNames contains the names the provisioner is authorized to sign
|
||||
AllowedNames *SSHNameOptions `json:"allow,omitempty"`
|
||||
// DeniedNames contains the names the provisioner is not authorized to sign
|
||||
DeniedNames *SSHNameOptions `json:"deny,omitempty"`
|
||||
}
|
||||
|
||||
// SSHHostCertificateOptions is a collection of SSH host certificate options.
|
||||
// It's an alias of SSHUserCertificateOptions, as the options are the same
|
||||
// for both types of certificates.
|
||||
type SSHHostCertificateOptions SSHUserCertificateOptions
|
||||
|
||||
// SSHNameOptions models the SSH name policy configuration.
|
||||
type SSHNameOptions struct {
|
||||
DNSDomains []string `json:"dns,omitempty"`
|
||||
IPRanges []string `json:"ip,omitempty"`
|
||||
EmailAddresses []string `json:"email,omitempty"`
|
||||
Principals []string `json:"principal,omitempty"`
|
||||
}
|
||||
|
||||
// GetAllowedNameOptions returns the AllowedSSHNameOptions, which models the
|
||||
// names that a provisioner is authorized to sign SSH certificates for.
|
||||
func (o *SSHUserCertificateOptions) GetAllowedNameOptions() *SSHNameOptions {
|
||||
if o == nil {
|
||||
return nil
|
||||
}
|
||||
return o.AllowedNames
|
||||
}
|
||||
|
||||
// GetDeniedNameOptions returns the DeniedSSHNameOptions, which models the
|
||||
// names that a provisioner is NOT authorized to sign SSH certificates for.
|
||||
func (o *SSHUserCertificateOptions) GetDeniedNameOptions() *SSHNameOptions {
|
||||
if o == nil {
|
||||
return nil
|
||||
}
|
||||
return o.DeniedNames
|
||||
}
|
||||
|
||||
// HasNames checks if the SSHNameOptions has one or more
|
||||
// names configured.
|
||||
func (o *SSHNameOptions) HasNames() bool {
|
||||
return len(o.DNSDomains) > 0 ||
|
||||
len(o.EmailAddresses) > 0 ||
|
||||
len(o.Principals) > 0
|
||||
}
|
@ -0,0 +1,134 @@
|
||||
package policy
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/smallstep/certificates/policy"
|
||||
)
|
||||
|
||||
// X509Policy is an alias for policy.X509NamePolicyEngine
|
||||
type X509Policy policy.X509NamePolicyEngine
|
||||
|
||||
// UserPolicy is an alias for policy.SSHNamePolicyEngine
|
||||
type UserPolicy policy.SSHNamePolicyEngine
|
||||
|
||||
// HostPolicy is an alias for policy.SSHNamePolicyEngine
|
||||
type HostPolicy policy.SSHNamePolicyEngine
|
||||
|
||||
// NewX509PolicyEngine creates a new x509 name policy engine
|
||||
func NewX509PolicyEngine(policyOptions X509PolicyOptionsInterface) (X509Policy, error) {
|
||||
|
||||
// return early if no policy engine options to configure
|
||||
if policyOptions == nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
options := []policy.NamePolicyOption{}
|
||||
|
||||
allowed := policyOptions.GetAllowedNameOptions()
|
||||
if allowed != nil && allowed.HasNames() {
|
||||
options = append(options,
|
||||
policy.WithPermittedDNSDomains(allowed.DNSDomains),
|
||||
policy.WithPermittedIPsOrCIDRs(allowed.IPRanges),
|
||||
policy.WithPermittedEmailAddresses(allowed.EmailAddresses),
|
||||
policy.WithPermittedURIDomains(allowed.URIDomains),
|
||||
)
|
||||
}
|
||||
|
||||
denied := policyOptions.GetDeniedNameOptions()
|
||||
if denied != nil && denied.HasNames() {
|
||||
options = append(options,
|
||||
policy.WithExcludedDNSDomains(denied.DNSDomains),
|
||||
policy.WithExcludedIPsOrCIDRs(denied.IPRanges),
|
||||
policy.WithExcludedEmailAddresses(denied.EmailAddresses),
|
||||
policy.WithExcludedURIDomains(denied.URIDomains),
|
||||
)
|
||||
}
|
||||
|
||||
// ensure no policy engine is returned when no name options were provided
|
||||
if len(options) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// enable x509 Subject Common Name validation by default
|
||||
options = append(options, policy.WithSubjectCommonNameVerification())
|
||||
|
||||
return policy.New(options...)
|
||||
}
|
||||
|
||||
type sshPolicyEngineType string
|
||||
|
||||
const (
|
||||
UserPolicyEngineType sshPolicyEngineType = "user"
|
||||
HostPolicyEngineType sshPolicyEngineType = "host"
|
||||
)
|
||||
|
||||
// newSSHUserPolicyEngine creates a new SSH user certificate policy engine
|
||||
func NewSSHUserPolicyEngine(policyOptions SSHPolicyOptionsInterface) (UserPolicy, error) {
|
||||
policyEngine, err := newSSHPolicyEngine(policyOptions, UserPolicyEngineType)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return policyEngine, nil
|
||||
}
|
||||
|
||||
// newSSHHostPolicyEngine create a new SSH host certificate policy engine
|
||||
func NewSSHHostPolicyEngine(policyOptions SSHPolicyOptionsInterface) (HostPolicy, error) {
|
||||
policyEngine, err := newSSHPolicyEngine(policyOptions, HostPolicyEngineType)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return policyEngine, nil
|
||||
}
|
||||
|
||||
// newSSHPolicyEngine creates a new SSH name policy engine
|
||||
func newSSHPolicyEngine(policyOptions SSHPolicyOptionsInterface, typ sshPolicyEngineType) (policy.SSHNamePolicyEngine, error) {
|
||||
|
||||
// return early if no policy engine options to configure
|
||||
if policyOptions == nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
var (
|
||||
allowed *SSHNameOptions
|
||||
denied *SSHNameOptions
|
||||
)
|
||||
|
||||
switch typ {
|
||||
case UserPolicyEngineType:
|
||||
allowed = policyOptions.GetAllowedUserNameOptions()
|
||||
denied = policyOptions.GetDeniedUserNameOptions()
|
||||
case HostPolicyEngineType:
|
||||
allowed = policyOptions.GetAllowedHostNameOptions()
|
||||
denied = policyOptions.GetDeniedHostNameOptions()
|
||||
default:
|
||||
return nil, fmt.Errorf("unknown SSH policy engine type %s provided", typ)
|
||||
}
|
||||
|
||||
options := []policy.NamePolicyOption{}
|
||||
|
||||
if allowed != nil && allowed.HasNames() {
|
||||
options = append(options,
|
||||
policy.WithPermittedDNSDomains(allowed.DNSDomains),
|
||||
policy.WithPermittedIPsOrCIDRs(allowed.IPRanges),
|
||||
policy.WithPermittedEmailAddresses(allowed.EmailAddresses),
|
||||
policy.WithPermittedPrincipals(allowed.Principals),
|
||||
)
|
||||
}
|
||||
|
||||
if denied != nil && denied.HasNames() {
|
||||
options = append(options,
|
||||
policy.WithExcludedDNSDomains(denied.DNSDomains),
|
||||
policy.WithExcludedIPsOrCIDRs(denied.IPRanges),
|
||||
policy.WithExcludedEmailAddresses(denied.EmailAddresses),
|
||||
policy.WithExcludedPrincipals(denied.Principals),
|
||||
)
|
||||
}
|
||||
|
||||
// ensure no policy engine is returned when no name options were provided
|
||||
if len(options) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
return policy.New(options...)
|
||||
}
|
@ -0,0 +1,194 @@
|
||||
package provisioner
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/x509"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/smallstep/certificates/errs"
|
||||
"golang.org/x/crypto/ssh"
|
||||
)
|
||||
|
||||
// Controller wraps a provisioner with other attributes useful in callback
|
||||
// functions.
|
||||
type Controller struct {
|
||||
Interface
|
||||
Audiences *Audiences
|
||||
Claimer *Claimer
|
||||
IdentityFunc GetIdentityFunc
|
||||
AuthorizeRenewFunc AuthorizeRenewFunc
|
||||
AuthorizeSSHRenewFunc AuthorizeSSHRenewFunc
|
||||
}
|
||||
|
||||
// NewController initializes a new provisioner controller.
|
||||
func NewController(p Interface, claims *Claims, config Config) (*Controller, error) {
|
||||
claimer, err := NewClaimer(claims, config.Claims)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &Controller{
|
||||
Interface: p,
|
||||
Audiences: &config.Audiences,
|
||||
Claimer: claimer,
|
||||
IdentityFunc: config.GetIdentityFunc,
|
||||
AuthorizeRenewFunc: config.AuthorizeRenewFunc,
|
||||
AuthorizeSSHRenewFunc: config.AuthorizeSSHRenewFunc,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// GetIdentity returns the identity for a given email.
|
||||
func (c *Controller) GetIdentity(ctx context.Context, email string) (*Identity, error) {
|
||||
if c.IdentityFunc != nil {
|
||||
return c.IdentityFunc(ctx, c.Interface, email)
|
||||
}
|
||||
return DefaultIdentityFunc(ctx, c.Interface, email)
|
||||
}
|
||||
|
||||
// AuthorizeRenew returns nil if the given cert can be renewed, returns an error
|
||||
// otherwise.
|
||||
func (c *Controller) AuthorizeRenew(ctx context.Context, cert *x509.Certificate) error {
|
||||
if c.AuthorizeRenewFunc != nil {
|
||||
return c.AuthorizeRenewFunc(ctx, c, cert)
|
||||
}
|
||||
return DefaultAuthorizeRenew(ctx, c, cert)
|
||||
}
|
||||
|
||||
// AuthorizeSSHRenew returns nil if the given cert can be renewed, returns an
|
||||
// error otherwise.
|
||||
func (c *Controller) AuthorizeSSHRenew(ctx context.Context, cert *ssh.Certificate) error {
|
||||
if c.AuthorizeSSHRenewFunc != nil {
|
||||
return c.AuthorizeSSHRenewFunc(ctx, c, cert)
|
||||
}
|
||||
return DefaultAuthorizeSSHRenew(ctx, c, cert)
|
||||
}
|
||||
|
||||
// Identity is the type representing an externally supplied identity that is used
|
||||
// by provisioners to populate certificate fields.
|
||||
type Identity struct {
|
||||
Usernames []string `json:"usernames"`
|
||||
Permissions `json:"permissions"`
|
||||
}
|
||||
|
||||
// GetIdentityFunc is a function that returns an identity.
|
||||
type GetIdentityFunc func(ctx context.Context, p Interface, email string) (*Identity, error)
|
||||
|
||||
// AuthorizeRenewFunc is a function that returns nil if the renewal of a
|
||||
// certificate is enabled.
|
||||
type AuthorizeRenewFunc func(ctx context.Context, p *Controller, cert *x509.Certificate) error
|
||||
|
||||
// AuthorizeSSHRenewFunc is a function that returns nil if the renewal of the
|
||||
// given SSH certificate is enabled.
|
||||
type AuthorizeSSHRenewFunc func(ctx context.Context, p *Controller, cert *ssh.Certificate) error
|
||||
|
||||
// DefaultIdentityFunc return a default identity depending on the provisioner
|
||||
// type. For OIDC email is always present and the usernames might
|
||||
// contain empty strings.
|
||||
func DefaultIdentityFunc(ctx context.Context, p Interface, email string) (*Identity, error) {
|
||||
switch k := p.(type) {
|
||||
case *OIDC:
|
||||
// OIDC principals would be:
|
||||
// ~~1. Preferred usernames.~~ Note: Under discussion, currently disabled
|
||||
// 2. Sanitized local.
|
||||
// 3. Raw local (if different).
|
||||
// 4. Email address.
|
||||
name := SanitizeSSHUserPrincipal(email)
|
||||
if !sshUserRegex.MatchString(name) {
|
||||
return nil, errors.Errorf("invalid principal '%s' from email '%s'", name, email)
|
||||
}
|
||||
usernames := []string{name}
|
||||
if i := strings.LastIndex(email, "@"); i >= 0 {
|
||||
usernames = append(usernames, email[:i])
|
||||
}
|
||||
usernames = append(usernames, email)
|
||||
return &Identity{
|
||||
Usernames: SanitizeStringSlices(usernames),
|
||||
}, nil
|
||||
default:
|
||||
return nil, errors.Errorf("provisioner type '%T' not supported by identity function", k)
|
||||
}
|
||||
}
|
||||
|
||||
// DefaultAuthorizeRenew is the default implementation of AuthorizeRenew. It
|
||||
// will return an error if the provisioner has the renewal disabled, if the
|
||||
// certificate is not yet valid or if the certificate is expired and renew after
|
||||
// expiry is disabled.
|
||||
func DefaultAuthorizeRenew(ctx context.Context, p *Controller, cert *x509.Certificate) error {
|
||||
if p.Claimer.IsDisableRenewal() {
|
||||
return errs.Unauthorized("renew is disabled for provisioner '%s'", p.GetName())
|
||||
}
|
||||
|
||||
now := time.Now().Truncate(time.Second)
|
||||
if now.Before(cert.NotBefore) {
|
||||
return errs.Unauthorized("certificate is not yet valid" + " " + now.UTC().Format(time.RFC3339Nano) + " vs " + cert.NotBefore.Format(time.RFC3339Nano))
|
||||
}
|
||||
if now.After(cert.NotAfter) && !p.Claimer.AllowRenewAfterExpiry() {
|
||||
return errs.Unauthorized("certificate has expired")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// DefaultAuthorizeSSHRenew is the default implementation of AuthorizeSSHRenew. It
|
||||
// will return an error if the provisioner has the renewal disabled, if the
|
||||
// certificate is not yet valid or if the certificate is expired and renew after
|
||||
// expiry is disabled.
|
||||
func DefaultAuthorizeSSHRenew(ctx context.Context, p *Controller, cert *ssh.Certificate) error {
|
||||
if p.Claimer.IsDisableRenewal() {
|
||||
return errs.Unauthorized("renew is disabled for provisioner '%s'", p.GetName())
|
||||
}
|
||||
|
||||
unixNow := time.Now().Unix()
|
||||
if after := int64(cert.ValidAfter); after < 0 || unixNow < int64(cert.ValidAfter) {
|
||||
return errs.Unauthorized("certificate is not yet valid")
|
||||
}
|
||||
if before := int64(cert.ValidBefore); cert.ValidBefore != uint64(ssh.CertTimeInfinity) && (unixNow >= before || before < 0) && !p.Claimer.AllowRenewAfterExpiry() {
|
||||
return errs.Unauthorized("certificate has expired")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
var sshUserRegex = regexp.MustCompile("^[a-z][-a-z0-9_]*$")
|
||||
|
||||
// SanitizeStringSlices removes duplicated an empty strings.
|
||||
func SanitizeStringSlices(original []string) []string {
|
||||
output := []string{}
|
||||
seen := make(map[string]struct{})
|
||||
for _, entry := range original {
|
||||
if entry == "" {
|
||||
continue
|
||||
}
|
||||
if _, value := seen[entry]; !value {
|
||||
seen[entry] = struct{}{}
|
||||
output = append(output, entry)
|
||||
}
|
||||
}
|
||||
return output
|
||||
}
|
||||
|
||||
// SanitizeSSHUserPrincipal grabs an email or a string with the format
|
||||
// local@domain and returns a sanitized version of the local, valid to be used
|
||||
// as a user name. If the email starts with a letter between a and z, the
|
||||
// resulting string will match the regular expression `^[a-z][-a-z0-9_]*$`.
|
||||
func SanitizeSSHUserPrincipal(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 == '-':
|
||||
return '-'
|
||||
case r == '.': // drop dots
|
||||
return -1
|
||||
default:
|
||||
return '_'
|
||||
}
|
||||
}, strings.ToLower(email))
|
||||
}
|
@ -0,0 +1,391 @@
|
||||
package provisioner
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/x509"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"golang.org/x/crypto/ssh"
|
||||
)
|
||||
|
||||
var trueValue = true
|
||||
|
||||
func mustClaimer(t *testing.T, claims *Claims, global Claims) *Claimer {
|
||||
t.Helper()
|
||||
c, err := NewClaimer(claims, global)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return c
|
||||
}
|
||||
func mustDuration(t *testing.T, s string) *Duration {
|
||||
t.Helper()
|
||||
d, err := NewDuration(s)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return d
|
||||
}
|
||||
|
||||
func TestNewController(t *testing.T) {
|
||||
type args struct {
|
||||
p Interface
|
||||
claims *Claims
|
||||
config Config
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want *Controller
|
||||
wantErr bool
|
||||
}{
|
||||
{"ok", args{&JWK{}, nil, Config{
|
||||
Claims: globalProvisionerClaims,
|
||||
Audiences: testAudiences,
|
||||
}}, &Controller{
|
||||
Interface: &JWK{},
|
||||
Audiences: &testAudiences,
|
||||
Claimer: mustClaimer(t, nil, globalProvisionerClaims),
|
||||
}, false},
|
||||
{"ok with claims", args{&JWK{}, &Claims{
|
||||
DisableRenewal: &defaultDisableRenewal,
|
||||
}, Config{
|
||||
Claims: globalProvisionerClaims,
|
||||
Audiences: testAudiences,
|
||||
}}, &Controller{
|
||||
Interface: &JWK{},
|
||||
Audiences: &testAudiences,
|
||||
Claimer: mustClaimer(t, &Claims{
|
||||
DisableRenewal: &defaultDisableRenewal,
|
||||
}, globalProvisionerClaims),
|
||||
}, false},
|
||||
{"fail claimer", args{&JWK{}, &Claims{
|
||||
MinTLSDur: mustDuration(t, "24h"),
|
||||
MaxTLSDur: mustDuration(t, "2h"),
|
||||
}, Config{
|
||||
Claims: globalProvisionerClaims,
|
||||
Audiences: testAudiences,
|
||||
}}, nil, true},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, err := NewController(tt.args.p, tt.args.claims, tt.args.config)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("NewController() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
if !reflect.DeepEqual(got, tt.want) {
|
||||
t.Errorf("NewController() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestController_GetIdentity(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
type fields struct {
|
||||
Interface Interface
|
||||
IdentityFunc GetIdentityFunc
|
||||
}
|
||||
type args struct {
|
||||
ctx context.Context
|
||||
email string
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
fields fields
|
||||
args args
|
||||
want *Identity
|
||||
wantErr bool
|
||||
}{
|
||||
{"ok", fields{&OIDC{}, nil}, args{ctx, "jane@doe.org"}, &Identity{
|
||||
Usernames: []string{"jane", "jane@doe.org"},
|
||||
}, false},
|
||||
{"ok custom", fields{&OIDC{}, func(ctx context.Context, p Interface, email string) (*Identity, error) {
|
||||
return &Identity{Usernames: []string{"jane"}}, nil
|
||||
}}, args{ctx, "jane@doe.org"}, &Identity{
|
||||
Usernames: []string{"jane"},
|
||||
}, false},
|
||||
{"fail provisioner", fields{&JWK{}, nil}, args{ctx, "jane@doe.org"}, nil, true},
|
||||
{"fail custom", fields{&OIDC{}, func(ctx context.Context, p Interface, email string) (*Identity, error) {
|
||||
return nil, fmt.Errorf("an error")
|
||||
}}, args{ctx, "jane@doe.org"}, nil, true},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
c := &Controller{
|
||||
Interface: tt.fields.Interface,
|
||||
IdentityFunc: tt.fields.IdentityFunc,
|
||||
}
|
||||
got, err := c.GetIdentity(tt.args.ctx, tt.args.email)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("Controller.GetIdentity() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
if !reflect.DeepEqual(got, tt.want) {
|
||||
t.Errorf("Controller.GetIdentity() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestController_AuthorizeRenew(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
now := time.Now().Truncate(time.Second)
|
||||
type fields struct {
|
||||
Interface Interface
|
||||
Claimer *Claimer
|
||||
AuthorizeRenewFunc AuthorizeRenewFunc
|
||||
}
|
||||
type args struct {
|
||||
ctx context.Context
|
||||
cert *x509.Certificate
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
fields fields
|
||||
args args
|
||||
wantErr bool
|
||||
}{
|
||||
{"ok", fields{&JWK{}, mustClaimer(t, nil, globalProvisionerClaims), nil}, args{ctx, &x509.Certificate{
|
||||
NotBefore: now,
|
||||
NotAfter: now.Add(time.Hour),
|
||||
}}, false},
|
||||
{"ok custom", fields{&JWK{}, mustClaimer(t, nil, globalProvisionerClaims), func(ctx context.Context, p *Controller, cert *x509.Certificate) error {
|
||||
return nil
|
||||
}}, args{ctx, &x509.Certificate{
|
||||
NotBefore: now,
|
||||
NotAfter: now.Add(time.Hour),
|
||||
}}, false},
|
||||
{"ok custom disabled", fields{&JWK{}, mustClaimer(t, &Claims{AllowRenewAfterExpiry: &trueValue}, globalProvisionerClaims), func(ctx context.Context, p *Controller, cert *x509.Certificate) error {
|
||||
return nil
|
||||
}}, args{ctx, &x509.Certificate{
|
||||
NotBefore: now,
|
||||
NotAfter: now.Add(time.Hour),
|
||||
}}, false},
|
||||
{"ok renew after expiry", fields{&JWK{}, mustClaimer(t, &Claims{AllowRenewAfterExpiry: &trueValue}, globalProvisionerClaims), nil}, args{ctx, &x509.Certificate{
|
||||
NotBefore: now.Add(-time.Hour),
|
||||
NotAfter: now.Add(-time.Minute),
|
||||
}}, false},
|
||||
{"fail disabled", fields{&JWK{}, mustClaimer(t, &Claims{DisableRenewal: &trueValue}, globalProvisionerClaims), nil}, args{ctx, &x509.Certificate{
|
||||
NotBefore: now,
|
||||
NotAfter: now.Add(time.Hour),
|
||||
}}, true},
|
||||
{"fail not yet valid", fields{&JWK{}, mustClaimer(t, nil, globalProvisionerClaims), nil}, args{ctx, &x509.Certificate{
|
||||
NotBefore: now.Add(time.Hour),
|
||||
NotAfter: now.Add(2 * time.Hour),
|
||||
}}, true},
|
||||
{"fail expired", fields{&JWK{}, mustClaimer(t, nil, globalProvisionerClaims), nil}, args{ctx, &x509.Certificate{
|
||||
NotBefore: now.Add(-time.Hour),
|
||||
NotAfter: now.Add(-time.Minute),
|
||||
}}, true},
|
||||
{"fail custom", fields{&JWK{}, mustClaimer(t, nil, globalProvisionerClaims), func(ctx context.Context, p *Controller, cert *x509.Certificate) error {
|
||||
return fmt.Errorf("an error")
|
||||
}}, args{ctx, &x509.Certificate{
|
||||
NotBefore: now,
|
||||
NotAfter: now.Add(time.Hour),
|
||||
}}, true},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
c := &Controller{
|
||||
Interface: tt.fields.Interface,
|
||||
Claimer: tt.fields.Claimer,
|
||||
AuthorizeRenewFunc: tt.fields.AuthorizeRenewFunc,
|
||||
}
|
||||
if err := c.AuthorizeRenew(tt.args.ctx, tt.args.cert); (err != nil) != tt.wantErr {
|
||||
t.Errorf("Controller.AuthorizeRenew() error = %v, wantErr %v", err, tt.wantErr)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestController_AuthorizeSSHRenew(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
now := time.Now()
|
||||
type fields struct {
|
||||
Interface Interface
|
||||
Claimer *Claimer
|
||||
AuthorizeSSHRenewFunc AuthorizeSSHRenewFunc
|
||||
}
|
||||
type args struct {
|
||||
ctx context.Context
|
||||
cert *ssh.Certificate
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
fields fields
|
||||
args args
|
||||
wantErr bool
|
||||
}{
|
||||
{"ok", fields{&JWK{}, mustClaimer(t, nil, globalProvisionerClaims), nil}, args{ctx, &ssh.Certificate{
|
||||
ValidAfter: uint64(now.Unix()),
|
||||
ValidBefore: uint64(now.Add(time.Hour).Unix()),
|
||||
}}, false},
|
||||
{"ok custom", fields{&JWK{}, mustClaimer(t, nil, globalProvisionerClaims), func(ctx context.Context, p *Controller, cert *ssh.Certificate) error {
|
||||
return nil
|
||||
}}, args{ctx, &ssh.Certificate{
|
||||
ValidAfter: uint64(now.Unix()),
|
||||
ValidBefore: uint64(now.Add(time.Hour).Unix()),
|
||||
}}, false},
|
||||
{"ok custom disabled", fields{&JWK{}, mustClaimer(t, &Claims{AllowRenewAfterExpiry: &trueValue}, globalProvisionerClaims), func(ctx context.Context, p *Controller, cert *ssh.Certificate) error {
|
||||
return nil
|
||||
}}, args{ctx, &ssh.Certificate{
|
||||
ValidAfter: uint64(now.Unix()),
|
||||
ValidBefore: uint64(now.Add(time.Hour).Unix()),
|
||||
}}, false},
|
||||
{"ok renew after expiry", fields{&JWK{}, mustClaimer(t, &Claims{AllowRenewAfterExpiry: &trueValue}, globalProvisionerClaims), nil}, args{ctx, &ssh.Certificate{
|
||||
ValidAfter: uint64(now.Add(-time.Hour).Unix()),
|
||||
ValidBefore: uint64(now.Add(-time.Minute).Unix()),
|
||||
}}, false},
|
||||
{"fail disabled", fields{&JWK{}, mustClaimer(t, &Claims{DisableRenewal: &trueValue}, globalProvisionerClaims), nil}, args{ctx, &ssh.Certificate{
|
||||
ValidAfter: uint64(now.Unix()),
|
||||
ValidBefore: uint64(now.Add(time.Hour).Unix()),
|
||||
}}, true},
|
||||
{"fail not yet valid", fields{&JWK{}, mustClaimer(t, nil, globalProvisionerClaims), nil}, args{ctx, &ssh.Certificate{
|
||||
ValidAfter: uint64(now.Add(time.Hour).Unix()),
|
||||
ValidBefore: uint64(now.Add(2 * time.Hour).Unix()),
|
||||
}}, true},
|
||||
{"fail expired", fields{&JWK{}, mustClaimer(t, nil, globalProvisionerClaims), nil}, args{ctx, &ssh.Certificate{
|
||||
ValidAfter: uint64(now.Add(-time.Hour).Unix()),
|
||||
ValidBefore: uint64(now.Add(-time.Minute).Unix()),
|
||||
}}, true},
|
||||
{"fail custom", fields{&JWK{}, mustClaimer(t, nil, globalProvisionerClaims), func(ctx context.Context, p *Controller, cert *ssh.Certificate) error {
|
||||
return fmt.Errorf("an error")
|
||||
}}, args{ctx, &ssh.Certificate{
|
||||
ValidAfter: uint64(now.Unix()),
|
||||
ValidBefore: uint64(now.Add(time.Hour).Unix()),
|
||||
}}, true},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
c := &Controller{
|
||||
Interface: tt.fields.Interface,
|
||||
Claimer: tt.fields.Claimer,
|
||||
AuthorizeSSHRenewFunc: tt.fields.AuthorizeSSHRenewFunc,
|
||||
}
|
||||
if err := c.AuthorizeSSHRenew(tt.args.ctx, tt.args.cert); (err != nil) != tt.wantErr {
|
||||
t.Errorf("Controller.AuthorizeSSHRenew() error = %v, wantErr %v", err, tt.wantErr)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestDefaultAuthorizeRenew(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
now := time.Now().Truncate(time.Second)
|
||||
type args struct {
|
||||
ctx context.Context
|
||||
p *Controller
|
||||
cert *x509.Certificate
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
wantErr bool
|
||||
}{
|
||||
{"ok", args{ctx, &Controller{
|
||||
Interface: &JWK{},
|
||||
Claimer: mustClaimer(t, nil, globalProvisionerClaims),
|
||||
}, &x509.Certificate{
|
||||
NotBefore: now,
|
||||
NotAfter: now.Add(time.Hour),
|
||||
}}, false},
|
||||
{"ok renew after expiry", args{ctx, &Controller{
|
||||
Interface: &JWK{},
|
||||
Claimer: mustClaimer(t, &Claims{AllowRenewAfterExpiry: &trueValue}, globalProvisionerClaims),
|
||||
}, &x509.Certificate{
|
||||
NotBefore: now.Add(-time.Hour),
|
||||
NotAfter: now.Add(-time.Minute),
|
||||
}}, false},
|
||||
{"fail disabled", args{ctx, &Controller{
|
||||
Interface: &JWK{},
|
||||
Claimer: mustClaimer(t, &Claims{DisableRenewal: &trueValue}, globalProvisionerClaims),
|
||||
}, &x509.Certificate{
|
||||
NotBefore: now,
|
||||
NotAfter: now.Add(time.Hour),
|
||||
}}, true},
|
||||
{"fail not yet valid", args{ctx, &Controller{
|
||||
Interface: &JWK{},
|
||||
Claimer: mustClaimer(t, &Claims{DisableRenewal: &trueValue}, globalProvisionerClaims),
|
||||
}, &x509.Certificate{
|
||||
NotBefore: now.Add(time.Hour),
|
||||
NotAfter: now.Add(2 * time.Hour),
|
||||
}}, true},
|
||||
{"fail expired", args{ctx, &Controller{
|
||||
Interface: &JWK{},
|
||||
Claimer: mustClaimer(t, &Claims{DisableRenewal: &trueValue}, globalProvisionerClaims),
|
||||
}, &x509.Certificate{
|
||||
NotBefore: now.Add(-time.Hour),
|
||||
NotAfter: now.Add(-time.Minute),
|
||||
}}, true},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if err := DefaultAuthorizeRenew(tt.args.ctx, tt.args.p, tt.args.cert); (err != nil) != tt.wantErr {
|
||||
t.Errorf("DefaultAuthorizeRenew() error = %v, wantErr %v", err, tt.wantErr)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestDefaultAuthorizeSSHRenew(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
now := time.Now()
|
||||
type args struct {
|
||||
ctx context.Context
|
||||
p *Controller
|
||||
cert *ssh.Certificate
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
wantErr bool
|
||||
}{
|
||||
{"ok", args{ctx, &Controller{
|
||||
Interface: &JWK{},
|
||||
Claimer: mustClaimer(t, nil, globalProvisionerClaims),
|
||||
}, &ssh.Certificate{
|
||||
ValidAfter: uint64(now.Unix()),
|
||||
ValidBefore: uint64(now.Add(time.Hour).Unix()),
|
||||
}}, false},
|
||||
{"ok renew after expiry", args{ctx, &Controller{
|
||||
Interface: &JWK{},
|
||||
Claimer: mustClaimer(t, &Claims{AllowRenewAfterExpiry: &trueValue}, globalProvisionerClaims),
|
||||
}, &ssh.Certificate{
|
||||
ValidAfter: uint64(now.Add(-time.Hour).Unix()),
|
||||
ValidBefore: uint64(now.Add(-time.Minute).Unix()),
|
||||
}}, false},
|
||||
{"fail disabled", args{ctx, &Controller{
|
||||
Interface: &JWK{},
|
||||
Claimer: mustClaimer(t, &Claims{DisableRenewal: &trueValue}, globalProvisionerClaims),
|
||||
}, &ssh.Certificate{
|
||||
ValidAfter: uint64(now.Unix()),
|
||||
ValidBefore: uint64(now.Add(time.Hour).Unix()),
|
||||
}}, true},
|
||||
{"fail not yet valid", args{ctx, &Controller{
|
||||
Interface: &JWK{},
|
||||
Claimer: mustClaimer(t, &Claims{DisableRenewal: &trueValue}, globalProvisionerClaims),
|
||||
}, &ssh.Certificate{
|
||||
ValidAfter: uint64(now.Add(time.Hour).Unix()),
|
||||
ValidBefore: uint64(now.Add(2 * time.Hour).Unix()),
|
||||
}}, true},
|
||||
{"fail expired", args{ctx, &Controller{
|
||||
Interface: &JWK{},
|
||||
Claimer: mustClaimer(t, &Claims{DisableRenewal: &trueValue}, globalProvisionerClaims),
|
||||
}, &ssh.Certificate{
|
||||
ValidAfter: uint64(now.Add(-time.Hour).Unix()),
|
||||
ValidBefore: uint64(now.Add(-time.Minute).Unix()),
|
||||
}}, true},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if err := DefaultAuthorizeSSHRenew(tt.args.ctx, tt.args.p, tt.args.cert); (err != nil) != tt.wantErr {
|
||||
t.Errorf("DefaultAuthorizeSSHRenew() error = %v, wantErr %v", err, tt.wantErr)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
@ -0,0 +1,73 @@
|
||||
package provisioner
|
||||
|
||||
import (
|
||||
"crypto/x509"
|
||||
"crypto/x509/pkix"
|
||||
"encoding/asn1"
|
||||
)
|
||||
|
||||
var (
|
||||
// StepOIDRoot is the root OID for smallstep.
|
||||
StepOIDRoot = asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 37476, 9000, 64}
|
||||
|
||||
// StepOIDProvisioner is the OID for the provisioner extension.
|
||||
StepOIDProvisioner = append(asn1.ObjectIdentifier(nil), append(StepOIDRoot, 1)...)
|
||||
)
|
||||
|
||||
// Extension is the Go representation of the provisioner extension.
|
||||
type Extension struct {
|
||||
Type Type
|
||||
Name string
|
||||
CredentialID string
|
||||
KeyValuePairs []string
|
||||
}
|
||||
|
||||
type extensionASN1 struct {
|
||||
Type int
|
||||
Name []byte
|
||||
CredentialID []byte
|
||||
KeyValuePairs []string `asn1:"optional,omitempty"`
|
||||
}
|
||||
|
||||
// Marshal marshals the extension using encoding/asn1.
|
||||
func (e *Extension) Marshal() ([]byte, error) {
|
||||
return asn1.Marshal(extensionASN1{
|
||||
Type: int(e.Type),
|
||||
Name: []byte(e.Name),
|
||||
CredentialID: []byte(e.CredentialID),
|
||||
KeyValuePairs: e.KeyValuePairs,
|
||||
})
|
||||
}
|
||||
|
||||
// ToExtension returns the pkix.Extension representation of the provisioner
|
||||
// extension.
|
||||
func (e *Extension) ToExtension() (pkix.Extension, error) {
|
||||
b, err := e.Marshal()
|
||||
if err != nil {
|
||||
return pkix.Extension{}, err
|
||||
}
|
||||
return pkix.Extension{
|
||||
Id: StepOIDProvisioner,
|
||||
Value: b,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// GetProvisionerExtension goes through all the certificate extensions and
|
||||
// returns the provisioner extension (1.3.6.1.4.1.37476.9000.64.1).
|
||||
func GetProvisionerExtension(cert *x509.Certificate) (*Extension, bool) {
|
||||
for _, e := range cert.Extensions {
|
||||
if e.Id.Equal(StepOIDProvisioner) {
|
||||
var provisioner extensionASN1
|
||||
if _, err := asn1.Unmarshal(e.Value, &provisioner); err != nil {
|
||||
return nil, false
|
||||
}
|
||||
return &Extension{
|
||||
Type: Type(provisioner.Type),
|
||||
Name: string(provisioner.Name),
|
||||
CredentialID: string(provisioner.CredentialID),
|
||||
KeyValuePairs: provisioner.KeyValuePairs,
|
||||
}, true
|
||||
}
|
||||
}
|
||||
return nil, false
|
||||
}
|
@ -0,0 +1,158 @@
|
||||
package provisioner
|
||||
|
||||
import (
|
||||
"crypto/x509"
|
||||
"crypto/x509/pkix"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"go.step.sm/crypto/pemutil"
|
||||
)
|
||||
|
||||
func TestExtension_Marshal(t *testing.T) {
|
||||
type fields struct {
|
||||
Type Type
|
||||
Name string
|
||||
CredentialID string
|
||||
KeyValuePairs []string
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
fields fields
|
||||
want []byte
|
||||
wantErr bool
|
||||
}{
|
||||
{"ok", fields{TypeJWK, "name", "credentialID", nil}, []byte{
|
||||
0x30, 0x17, 0x02, 0x01, 0x01, 0x04, 0x04, 0x6e,
|
||||
0x61, 0x6d, 0x65, 0x04, 0x0c, 0x63, 0x72, 0x65,
|
||||
0x64, 0x65, 0x6e, 0x74, 0x69, 0x61, 0x6c, 0x49,
|
||||
0x44,
|
||||
}, false},
|
||||
{"ok with pairs", fields{TypeJWK, "name", "credentialID", []string{"foo", "bar"}}, []byte{
|
||||
0x30, 0x23, 0x02, 0x01, 0x01, 0x04, 0x04, 0x6e,
|
||||
0x61, 0x6d, 0x65, 0x04, 0x0c, 0x63, 0x72, 0x65,
|
||||
0x64, 0x65, 0x6e, 0x74, 0x69, 0x61, 0x6c, 0x49,
|
||||
0x44, 0x30, 0x0a, 0x13, 0x03, 0x66, 0x6f, 0x6f,
|
||||
0x13, 0x03, 0x62, 0x61, 0x72,
|
||||
}, false},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
e := &Extension{
|
||||
Type: tt.fields.Type,
|
||||
Name: tt.fields.Name,
|
||||
CredentialID: tt.fields.CredentialID,
|
||||
KeyValuePairs: tt.fields.KeyValuePairs,
|
||||
}
|
||||
got, err := e.Marshal()
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("Extension.Marshal() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
if !reflect.DeepEqual(got, tt.want) {
|
||||
t.Errorf("Extension.Marshal() = %x, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestExtension_ToExtension(t *testing.T) {
|
||||
type fields struct {
|
||||
Type Type
|
||||
Name string
|
||||
CredentialID string
|
||||
KeyValuePairs []string
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
fields fields
|
||||
want pkix.Extension
|
||||
wantErr bool
|
||||
}{
|
||||
{"ok", fields{TypeJWK, "name", "credentialID", nil}, pkix.Extension{
|
||||
Id: StepOIDProvisioner,
|
||||
Value: []byte{
|
||||
0x30, 0x17, 0x02, 0x01, 0x01, 0x04, 0x04, 0x6e,
|
||||
0x61, 0x6d, 0x65, 0x04, 0x0c, 0x63, 0x72, 0x65,
|
||||
0x64, 0x65, 0x6e, 0x74, 0x69, 0x61, 0x6c, 0x49,
|
||||
0x44,
|
||||
},
|
||||
}, false},
|
||||
{"ok empty pairs", fields{TypeJWK, "name", "credentialID", []string{}}, pkix.Extension{
|
||||
Id: StepOIDProvisioner,
|
||||
Value: []byte{
|
||||
0x30, 0x17, 0x02, 0x01, 0x01, 0x04, 0x04, 0x6e,
|
||||
0x61, 0x6d, 0x65, 0x04, 0x0c, 0x63, 0x72, 0x65,
|
||||
0x64, 0x65, 0x6e, 0x74, 0x69, 0x61, 0x6c, 0x49,
|
||||
0x44,
|
||||
},
|
||||
}, false},
|
||||
{"ok with pairs", fields{TypeJWK, "name", "credentialID", []string{"foo", "bar"}}, pkix.Extension{
|
||||
Id: StepOIDProvisioner,
|
||||
Value: []byte{
|
||||
0x30, 0x23, 0x02, 0x01, 0x01, 0x04, 0x04, 0x6e,
|
||||
0x61, 0x6d, 0x65, 0x04, 0x0c, 0x63, 0x72, 0x65,
|
||||
0x64, 0x65, 0x6e, 0x74, 0x69, 0x61, 0x6c, 0x49,
|
||||
0x44, 0x30, 0x0a, 0x13, 0x03, 0x66, 0x6f, 0x6f,
|
||||
0x13, 0x03, 0x62, 0x61, 0x72,
|
||||
},
|
||||
}, false},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
e := &Extension{
|
||||
Type: tt.fields.Type,
|
||||
Name: tt.fields.Name,
|
||||
CredentialID: tt.fields.CredentialID,
|
||||
KeyValuePairs: tt.fields.KeyValuePairs,
|
||||
}
|
||||
got, err := e.ToExtension()
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("Extension.ToExtension() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
if !reflect.DeepEqual(got, tt.want) {
|
||||
t.Errorf("Extension.ToExtension() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetProvisionerExtension(t *testing.T) {
|
||||
mustCertificate := func(fn string) *x509.Certificate {
|
||||
cert, err := pemutil.ReadCertificate(fn)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return cert
|
||||
}
|
||||
|
||||
type args struct {
|
||||
cert *x509.Certificate
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want *Extension
|
||||
want1 bool
|
||||
}{
|
||||
{"ok", args{mustCertificate("testdata/certs/good-extension.crt")}, &Extension{
|
||||
Type: TypeJWK,
|
||||
Name: "mariano@smallstep.com",
|
||||
CredentialID: "nvgnR8wSzpUlrt_tC3mvrhwhBx9Y7T1WL_JjcFVWYBQ",
|
||||
}, true},
|
||||
{"fail unmarshal", args{mustCertificate("testdata/certs/bad-extension.crt")}, nil, false},
|
||||
{"missing extension", args{mustCertificate("testdata/certs/aws.crt")}, nil, false},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, got1 := GetProvisionerExtension(tt.args.cert)
|
||||
if !reflect.DeepEqual(got, tt.want) {
|
||||
t.Errorf("GetProvisionerExtension() got = %v, want %v", got, tt.want)
|
||||
}
|
||||
if got1 != tt.want1 {
|
||||
t.Errorf("GetProvisionerExtension() got1 = %v, want %v", got1, tt.want1)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
@ -1,156 +0,0 @@
|
||||
package provisioner
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/smallstep/certificates/policy"
|
||||
"golang.org/x/crypto/ssh"
|
||||
)
|
||||
|
||||
type sshPolicyEngineType string
|
||||
|
||||
const (
|
||||
userPolicyEngineType sshPolicyEngineType = "user"
|
||||
hostPolicyEngineType sshPolicyEngineType = "host"
|
||||
)
|
||||
|
||||
var certTypeToPolicyEngineType = map[uint32]sshPolicyEngineType{
|
||||
uint32(ssh.UserCert): userPolicyEngineType,
|
||||
uint32(ssh.HostCert): hostPolicyEngineType,
|
||||
}
|
||||
|
||||
type x509PolicyEngine interface {
|
||||
policy.X509NamePolicyEngine
|
||||
}
|
||||
|
||||
type userPolicyEngine struct {
|
||||
policy.SSHNamePolicyEngine
|
||||
}
|
||||
|
||||
type hostPolicyEngine struct {
|
||||
policy.SSHNamePolicyEngine
|
||||
}
|
||||
|
||||
// newX509PolicyEngine creates a new x509 name policy engine
|
||||
func newX509PolicyEngine(x509Opts *X509Options) (x509PolicyEngine, error) {
|
||||
|
||||
if x509Opts == nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
options := []policy.NamePolicyOption{
|
||||
policy.WithSubjectCommonNameVerification(), // enable x509 Subject Common Name validation by default
|
||||
}
|
||||
|
||||
allowed := x509Opts.GetAllowedNameOptions()
|
||||
if allowed != nil && allowed.HasNames() {
|
||||
options = append(options,
|
||||
policy.WithPermittedDNSDomains(allowed.DNSDomains),
|
||||
policy.WithPermittedIPsOrCIDRs(allowed.IPRanges),
|
||||
policy.WithPermittedEmailAddresses(allowed.EmailAddresses),
|
||||
policy.WithPermittedURIDomains(allowed.URIDomains),
|
||||
)
|
||||
}
|
||||
|
||||
denied := x509Opts.GetDeniedNameOptions()
|
||||
if denied != nil && denied.HasNames() {
|
||||
options = append(options,
|
||||
policy.WithExcludedDNSDomains(denied.DNSDomains),
|
||||
policy.WithExcludedIPsOrCIDRs(denied.IPRanges),
|
||||
policy.WithExcludedEmailAddresses(denied.EmailAddresses),
|
||||
policy.WithExcludedURIDomains(denied.URIDomains),
|
||||
)
|
||||
}
|
||||
|
||||
return policy.New(options...)
|
||||
}
|
||||
|
||||
// newSSHUserPolicyEngine creates a new SSH user certificate policy engine
|
||||
func newSSHUserPolicyEngine(sshOpts *SSHOptions) (*userPolicyEngine, error) {
|
||||
policyEngine, err := newSSHPolicyEngine(sshOpts, userPolicyEngineType)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// ensure we're not wrapping a nil engine
|
||||
if policyEngine == nil {
|
||||
return nil, nil
|
||||
}
|
||||
return &userPolicyEngine{
|
||||
SSHNamePolicyEngine: policyEngine,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// newSSHHostPolicyEngine create a new SSH host certificate policy engine
|
||||
func newSSHHostPolicyEngine(sshOpts *SSHOptions) (*hostPolicyEngine, error) {
|
||||
policyEngine, err := newSSHPolicyEngine(sshOpts, hostPolicyEngineType)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// ensure we're not wrapping a nil engine
|
||||
if policyEngine == nil {
|
||||
return nil, nil
|
||||
}
|
||||
return &hostPolicyEngine{
|
||||
SSHNamePolicyEngine: policyEngine,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// newSSHPolicyEngine creates a new SSH name policy engine
|
||||
func newSSHPolicyEngine(sshOpts *SSHOptions, typ sshPolicyEngineType) (policy.SSHNamePolicyEngine, error) {
|
||||
|
||||
if sshOpts == nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
var (
|
||||
allowed *SSHNameOptions
|
||||
denied *SSHNameOptions
|
||||
)
|
||||
|
||||
// TODO: embed the type in the policy engine itself for reference?
|
||||
switch typ {
|
||||
case userPolicyEngineType:
|
||||
if sshOpts.User != nil {
|
||||
allowed = sshOpts.User.GetAllowedNameOptions()
|
||||
denied = sshOpts.User.GetDeniedNameOptions()
|
||||
}
|
||||
case hostPolicyEngineType:
|
||||
if sshOpts.Host != nil {
|
||||
allowed = sshOpts.Host.AllowedNames
|
||||
denied = sshOpts.Host.DeniedNames
|
||||
}
|
||||
default:
|
||||
return nil, fmt.Errorf("unknown SSH policy engine type %s provided", typ)
|
||||
}
|
||||
|
||||
options := []policy.NamePolicyOption{}
|
||||
|
||||
if allowed != nil && allowed.HasNames() {
|
||||
options = append(options,
|
||||
policy.WithPermittedDNSDomains(allowed.DNSDomains),
|
||||
policy.WithPermittedIPsOrCIDRs(allowed.IPRanges),
|
||||
policy.WithPermittedEmailAddresses(allowed.EmailAddresses),
|
||||
policy.WithPermittedPrincipals(allowed.Principals),
|
||||
)
|
||||
}
|
||||
|
||||
if denied != nil && denied.HasNames() {
|
||||
options = append(options,
|
||||
policy.WithExcludedDNSDomains(denied.DNSDomains),
|
||||
policy.WithExcludedIPsOrCIDRs(denied.IPRanges),
|
||||
policy.WithExcludedEmailAddresses(denied.EmailAddresses),
|
||||
policy.WithExcludedPrincipals(denied.Principals),
|
||||
)
|
||||
}
|
||||
|
||||
// Return nil, because there's no policy to execute. This is
|
||||
// important, because the logic that determines user vs. host certs
|
||||
// are allowed depends on this fact. The two policy engines are
|
||||
// not aware of eachother, so this check is performed in the
|
||||
// SSH name validator, instead.
|
||||
if len(options) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
return policy.New(options...)
|
||||
}
|
@ -0,0 +1,21 @@
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIDeTCCAx+gAwIBAgIRAOTItW2pYuSU+PkmLW090iUwCgYIKoZIzj0EAwIwJDEi
|
||||
MCAGA1UEAxMZU21hbGxzdGVwIEludGVybWVkaWF0ZSBDQTAeFw0yMjAzMTEyMjUy
|
||||
MjBaFw0yMjAzMTIyMjUzMjBaMIGcMQswCQYDVQQGEwJDSDETMBEGA1UECBMKQ2Fs
|
||||
aWZvcm5pYTEWMBQGA1UEBxMNU2FuIEZyYW5jaXNjbzEYMBYGA1UECRMPMSBUaGUg
|
||||
U3RyZWV0IFN0MRMwEQYDVQQKDAo8bm8gdmFsdWU+MRYwFAYDVQQLEw1TbWFsbHN0
|
||||
ZXAgRW5nMRkwFwYDVQQDDBB0ZXN0QGV4YW1wbGUuY29tMFkwEwYHKoZIzj0CAQYI
|
||||
KoZIzj0DAQcDQgAE/9vvOZ1Zzysnf3VeGyotMJEMZdAborB36Ah5QL/3yQNMRWIc
|
||||
pv9Dwx19pHw7SquVE8jIaPPJSjaeWnfMPDYDxaOCAbcwggGzMA4GA1UdDwEB/wQE
|
||||
AwIHgDAdBgNVHSUEFjAUBggrBgEFBQcDAQYIKwYBBQUHAwIwDAYDVR0TAQH/BAIw
|
||||
ADAdBgNVHQ4EFgQUkJUg6AsqWlqTZt6BHidRMwh1vKYwHwYDVR0jBBgwFoAUDpTg
|
||||
d3VFCn6e71wXcwbDCURBomUwgZoGCCsGAQUFBwEBBIGNMIGKMBcGCCsGAQUFBzAB
|
||||
hgtodHRwczovL2ZvbzBvBggrBgEFBQcwAoZjaHR0cHM6Ly9jYS5zbWFsbHN0ZXAu
|
||||
Y29tOjkwMDAvcm9vdC9hNzhhODUwMDI1YzBjMjM0Mzg1ZWRhMjNkNzE5Mjk2NGNh
|
||||
NTZhYTlkNzI3ZjUzNTY1M2IwYWZiODFjMWUwNTU5MBsGA1UdEQQUMBKBEHRlc3RA
|
||||
ZXhhbXBsZS5jb20wIAYDVR0gBBkwFzALBglghkgBhv1sAQEwCAYGZ4EMAQICMD8G
|
||||
A1UdHwQ4MDYwNKAyoDCGLmh0dHA6Ly9jcmwzLmRpZ2ljZXJ0LmNvbS9zaGEyLWV2
|
||||
LXNlcnZlci1nMy5jcmwwFwYMKwYBBAGCpGTGKEABBAdmb29vYmFyMAoGCCqGSM49
|
||||
BAMCA0gAMEUCIQCWYqOuk4bLkVVeHvo3P8TlJJ3fw6ijDDLstvdrQqAl5wIgEjSY
|
||||
wVcR649Oc8PJGh/43Kpx0+4OTYPQrD/JqphVF7g=
|
||||
-----END CERTIFICATE-----
|
@ -0,0 +1,22 @@
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIDujCCA2GgAwIBAgIRAM5celDKTTqAGycljO7FZdEwCgYIKoZIzj0EAwIwJDEi
|
||||
MCAGA1UEAxMZU21hbGxzdGVwIEludGVybWVkaWF0ZSBDQTAeFw0yMjAzMTEyMjQx
|
||||
MDRaFw0yMjAzMTIyMjQyMDRaMIGcMQswCQYDVQQGEwJDSDETMBEGA1UECBMKQ2Fs
|
||||
aWZvcm5pYTEWMBQGA1UEBxMNU2FuIEZyYW5jaXNjbzEYMBYGA1UECRMPMSBUaGUg
|
||||
U3RyZWV0IFN0MRMwEQYDVQQKDAo8bm8gdmFsdWU+MRYwFAYDVQQLEw1TbWFsbHN0
|
||||
ZXAgRW5nMRkwFwYDVQQDDBB0ZXN0QGV4YW1wbGUuY29tMFkwEwYHKoZIzj0CAQYI
|
||||
KoZIzj0DAQcDQgAEkXffZYlSJRMxJrZHmUpEMC4jQYCkF86mLJY0iLZ8k00N/xF0
|
||||
4rAGwzTU/l9tfRpNl+z/XfMMWPXS0Q8NU/o4S6OCAfkwggH1MA4GA1UdDwEB/wQE
|
||||
AwIHgDAdBgNVHSUEFjAUBggrBgEFBQcDAQYIKwYBBQUHAwIwDAYDVR0TAQH/BAIw
|
||||
ADAdBgNVHQ4EFgQUL3sSlYW8Tf2l2P+gFTdn5wsUjfgwHwYDVR0jBBgwFoAUDpTg
|
||||
d3VFCn6e71wXcwbDCURBomUwgZoGCCsGAQUFBwEBBIGNMIGKMBcGCCsGAQUFBzAB
|
||||
hgtodHRwczovL2ZvbzBvBggrBgEFBQcwAoZjaHR0cHM6Ly9jYS5zbWFsbHN0ZXAu
|
||||
Y29tOjkwMDAvcm9vdC9hNzhhODUwMDI1YzBjMjM0Mzg1ZWRhMjNkNzE5Mjk2NGNh
|
||||
NTZhYTlkNzI3ZjUzNTY1M2IwYWZiODFjMWUwNTU5MBsGA1UdEQQUMBKBEHRlc3RA
|
||||
ZXhhbXBsZS5jb20wIAYDVR0gBBkwFzALBglghkgBhv1sAQEwCAYGZ4EMAQICMD8G
|
||||
A1UdHwQ4MDYwNKAyoDCGLmh0dHA6Ly9jcmwzLmRpZ2ljZXJ0LmNvbS9zaGEyLWV2
|
||||
LXNlcnZlci1nMy5jcmwwWQYMKwYBBAGCpGTGKEABBEkwRwIBAQQVbWFyaWFub0Bz
|
||||
bWFsbHN0ZXAuY29tBCtudmduUjh3U3pwVWxydF90QzNtdnJod2hCeDlZN1QxV0xf
|
||||
SmpjRlZXWUJRMAoGCCqGSM49BAMCA0cAMEQCIE6umrhSbeQWWVK5cWBvXj5c0cGB
|
||||
bUF0rNw/dsaCaWcwAiAKSkmjhsC63DVPXPCNUki90YgVovO69foO1ZaB43lx5w==
|
||||
-----END CERTIFICATE-----
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue