forked from TrueCloudLab/certificates
Merge pull request #1485 from smallstep/webhooks-x5c
Send X5C leaf certificate to webhooks
This commit is contained in:
commit
31533c4a15
9 changed files with 240 additions and 20 deletions
|
@ -10,6 +10,7 @@ import (
|
||||||
|
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
"github.com/smallstep/certificates/errs"
|
"github.com/smallstep/certificates/errs"
|
||||||
|
"github.com/smallstep/certificates/webhook"
|
||||||
"go.step.sm/linkedca"
|
"go.step.sm/linkedca"
|
||||||
"golang.org/x/crypto/ssh"
|
"golang.org/x/crypto/ssh"
|
||||||
)
|
)
|
||||||
|
@ -77,7 +78,7 @@ func (c *Controller) AuthorizeSSHRenew(ctx context.Context, cert *ssh.Certificat
|
||||||
return DefaultAuthorizeSSHRenew(ctx, c, cert)
|
return DefaultAuthorizeSSHRenew(ctx, c, cert)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Controller) newWebhookController(templateData WebhookSetter, certType linkedca.Webhook_CertType) *WebhookController {
|
func (c *Controller) newWebhookController(templateData WebhookSetter, certType linkedca.Webhook_CertType, opts ...webhook.RequestBodyOption) *WebhookController {
|
||||||
client := c.webhookClient
|
client := c.webhookClient
|
||||||
if client == nil {
|
if client == nil {
|
||||||
client = http.DefaultClient
|
client = http.DefaultClient
|
||||||
|
@ -87,6 +88,7 @@ func (c *Controller) newWebhookController(templateData WebhookSetter, certType l
|
||||||
client: client,
|
client: client,
|
||||||
webhooks: c.webhooks,
|
webhooks: c.webhooks,
|
||||||
certType: certType,
|
certType: certType,
|
||||||
|
options: opts,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -4,15 +4,18 @@ import (
|
||||||
"context"
|
"context"
|
||||||
"crypto/x509"
|
"crypto/x509"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"net/http"
|
||||||
"reflect"
|
"reflect"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"go.step.sm/crypto/pemutil"
|
||||||
"go.step.sm/crypto/x509util"
|
"go.step.sm/crypto/x509util"
|
||||||
"go.step.sm/linkedca"
|
"go.step.sm/linkedca"
|
||||||
"golang.org/x/crypto/ssh"
|
"golang.org/x/crypto/ssh"
|
||||||
|
|
||||||
"github.com/smallstep/certificates/authority/policy"
|
"github.com/smallstep/certificates/authority/policy"
|
||||||
|
"github.com/smallstep/certificates/webhook"
|
||||||
)
|
)
|
||||||
|
|
||||||
var trueValue = true
|
var trueValue = true
|
||||||
|
@ -449,16 +452,39 @@ func TestDefaultAuthorizeSSHRenew(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func Test_newWebhookController(t *testing.T) {
|
func Test_newWebhookController(t *testing.T) {
|
||||||
|
cert, err := pemutil.ReadCertificate("testdata/certs/x5c-leaf.crt", pemutil.WithFirstBlock())
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
opts := []webhook.RequestBodyOption{webhook.WithX5CCertificate(cert)}
|
||||||
|
|
||||||
|
type args struct {
|
||||||
|
templateData WebhookSetter
|
||||||
|
certType linkedca.Webhook_CertType
|
||||||
|
opts []webhook.RequestBodyOption
|
||||||
|
}
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
args args
|
||||||
|
want *WebhookController
|
||||||
|
}{
|
||||||
|
{"ok", args{x509util.TemplateData{"foo": "bar"}, linkedca.Webhook_X509, nil}, &WebhookController{
|
||||||
|
TemplateData: x509util.TemplateData{"foo": "bar"},
|
||||||
|
certType: linkedca.Webhook_X509,
|
||||||
|
client: http.DefaultClient,
|
||||||
|
}},
|
||||||
|
{"ok with options", args{x509util.TemplateData{"foo": "bar"}, linkedca.Webhook_SSH, opts}, &WebhookController{
|
||||||
|
TemplateData: x509util.TemplateData{"foo": "bar"},
|
||||||
|
certType: linkedca.Webhook_SSH,
|
||||||
|
client: http.DefaultClient,
|
||||||
|
options: opts,
|
||||||
|
}},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
c := &Controller{}
|
c := &Controller{}
|
||||||
data := x509util.TemplateData{"foo": "bar"}
|
got := c.newWebhookController(tt.args.templateData, tt.args.certType, tt.args.opts...)
|
||||||
ctl := c.newWebhookController(data, linkedca.Webhook_X509)
|
if !reflect.DeepEqual(got, tt.want) {
|
||||||
if !reflect.DeepEqual(ctl.TemplateData, data) {
|
t.Errorf("newWebhookController() = %v, want %v", got, tt.want)
|
||||||
t.Error("Failed to set templateData")
|
}
|
||||||
}
|
|
||||||
if ctl.certType != linkedca.Webhook_X509 {
|
|
||||||
t.Error("Failed to set certType")
|
|
||||||
}
|
|
||||||
if ctl.client == nil {
|
|
||||||
t.Error("Failed to set client")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -30,6 +30,7 @@ type WebhookController struct {
|
||||||
client *http.Client
|
client *http.Client
|
||||||
webhooks []*Webhook
|
webhooks []*Webhook
|
||||||
certType linkedca.Webhook_CertType
|
certType linkedca.Webhook_CertType
|
||||||
|
options []webhook.RequestBodyOption
|
||||||
TemplateData WebhookSetter
|
TemplateData WebhookSetter
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -39,6 +40,14 @@ func (wc *WebhookController) Enrich(req *webhook.RequestBody) error {
|
||||||
if wc == nil {
|
if wc == nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Apply extra options in the webhook controller
|
||||||
|
for _, fn := range wc.options {
|
||||||
|
if err := fn(req); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
for _, wh := range wc.webhooks {
|
for _, wh := range wc.webhooks {
|
||||||
if wh.Kind != linkedca.Webhook_ENRICHING.String() {
|
if wh.Kind != linkedca.Webhook_ENRICHING.String() {
|
||||||
continue
|
continue
|
||||||
|
@ -63,6 +72,14 @@ func (wc *WebhookController) Authorize(req *webhook.RequestBody) error {
|
||||||
if wc == nil {
|
if wc == nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Apply extra options in the webhook controller
|
||||||
|
for _, fn := range wc.options {
|
||||||
|
if err := fn(req); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
for _, wh := range wc.webhooks {
|
for _, wh := range wc.webhooks {
|
||||||
if wh.Kind != linkedca.Webhook_AUTHORIZING.String() {
|
if wh.Kind != linkedca.Webhook_AUTHORIZING.String() {
|
||||||
continue
|
continue
|
||||||
|
|
|
@ -4,6 +4,7 @@ import (
|
||||||
"crypto/hmac"
|
"crypto/hmac"
|
||||||
"crypto/sha256"
|
"crypto/sha256"
|
||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
|
"crypto/x509"
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
@ -16,6 +17,7 @@ import (
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
"github.com/smallstep/assert"
|
"github.com/smallstep/assert"
|
||||||
"github.com/smallstep/certificates/webhook"
|
"github.com/smallstep/certificates/webhook"
|
||||||
|
"go.step.sm/crypto/pemutil"
|
||||||
"go.step.sm/crypto/x509util"
|
"go.step.sm/crypto/x509util"
|
||||||
"go.step.sm/linkedca"
|
"go.step.sm/linkedca"
|
||||||
)
|
)
|
||||||
|
@ -96,12 +98,18 @@ func TestWebhookController_isCertTypeOK(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestWebhookController_Enrich(t *testing.T) {
|
func TestWebhookController_Enrich(t *testing.T) {
|
||||||
|
cert, err := pemutil.ReadCertificate("testdata/certs/x5c-leaf.crt", pemutil.WithFirstBlock())
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
type test struct {
|
type test struct {
|
||||||
ctl *WebhookController
|
ctl *WebhookController
|
||||||
req *webhook.RequestBody
|
req *webhook.RequestBody
|
||||||
responses []*webhook.ResponseBody
|
responses []*webhook.ResponseBody
|
||||||
expectErr bool
|
expectErr bool
|
||||||
expectTemplateData any
|
expectTemplateData any
|
||||||
|
assertRequest func(t *testing.T, req *webhook.RequestBody)
|
||||||
}
|
}
|
||||||
tests := map[string]test{
|
tests := map[string]test{
|
||||||
"ok/no enriching webhooks": {
|
"ok/no enriching webhooks": {
|
||||||
|
@ -170,6 +178,29 @@ func TestWebhookController_Enrich(t *testing.T) {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
"ok/with options": {
|
||||||
|
ctl: &WebhookController{
|
||||||
|
client: http.DefaultClient,
|
||||||
|
webhooks: []*Webhook{{Name: "people", Kind: "ENRICHING"}},
|
||||||
|
TemplateData: x509util.TemplateData{},
|
||||||
|
options: []webhook.RequestBodyOption{webhook.WithX5CCertificate(cert)},
|
||||||
|
},
|
||||||
|
req: &webhook.RequestBody{},
|
||||||
|
responses: []*webhook.ResponseBody{{Allow: true, Data: map[string]any{"role": "bar"}}},
|
||||||
|
expectErr: false,
|
||||||
|
expectTemplateData: x509util.TemplateData{"Webhooks": map[string]any{"people": map[string]any{"role": "bar"}}},
|
||||||
|
assertRequest: func(t *testing.T, req *webhook.RequestBody) {
|
||||||
|
key, err := x509.MarshalPKIXPublicKey(cert.PublicKey)
|
||||||
|
assert.FatalError(t, err)
|
||||||
|
assert.Equals(t, &webhook.X5CCertificate{
|
||||||
|
Raw: cert.Raw,
|
||||||
|
PublicKey: key,
|
||||||
|
PublicKeyAlgorithm: cert.PublicKeyAlgorithm.String(),
|
||||||
|
NotBefore: cert.NotBefore,
|
||||||
|
NotAfter: cert.NotAfter,
|
||||||
|
}, req.X5CCertificate)
|
||||||
|
},
|
||||||
|
},
|
||||||
"deny": {
|
"deny": {
|
||||||
ctl: &WebhookController{
|
ctl: &WebhookController{
|
||||||
client: http.DefaultClient,
|
client: http.DefaultClient,
|
||||||
|
@ -181,6 +212,20 @@ func TestWebhookController_Enrich(t *testing.T) {
|
||||||
expectErr: true,
|
expectErr: true,
|
||||||
expectTemplateData: x509util.TemplateData{},
|
expectTemplateData: x509util.TemplateData{},
|
||||||
},
|
},
|
||||||
|
"fail/with options": {
|
||||||
|
ctl: &WebhookController{
|
||||||
|
client: http.DefaultClient,
|
||||||
|
webhooks: []*Webhook{{Name: "people", Kind: "ENRICHING"}},
|
||||||
|
TemplateData: x509util.TemplateData{},
|
||||||
|
options: []webhook.RequestBodyOption{webhook.WithX5CCertificate(&x509.Certificate{
|
||||||
|
PublicKey: []byte("bad"),
|
||||||
|
})},
|
||||||
|
},
|
||||||
|
req: &webhook.RequestBody{},
|
||||||
|
responses: []*webhook.ResponseBody{{Allow: false}},
|
||||||
|
expectErr: true,
|
||||||
|
expectTemplateData: x509util.TemplateData{},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
for name, test := range tests {
|
for name, test := range tests {
|
||||||
t.Run(name, func(t *testing.T) {
|
t.Run(name, func(t *testing.T) {
|
||||||
|
@ -200,16 +245,25 @@ func TestWebhookController_Enrich(t *testing.T) {
|
||||||
t.Fatalf("Got err %v, want %v", err, test.expectErr)
|
t.Fatalf("Got err %v, want %v", err, test.expectErr)
|
||||||
}
|
}
|
||||||
assert.Equals(t, test.expectTemplateData, test.ctl.TemplateData)
|
assert.Equals(t, test.expectTemplateData, test.ctl.TemplateData)
|
||||||
|
if test.assertRequest != nil {
|
||||||
|
test.assertRequest(t, test.req)
|
||||||
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestWebhookController_Authorize(t *testing.T) {
|
func TestWebhookController_Authorize(t *testing.T) {
|
||||||
|
cert, err := pemutil.ReadCertificate("testdata/certs/x5c-leaf.crt", pemutil.WithFirstBlock())
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
type test struct {
|
type test struct {
|
||||||
ctl *WebhookController
|
ctl *WebhookController
|
||||||
req *webhook.RequestBody
|
req *webhook.RequestBody
|
||||||
responses []*webhook.ResponseBody
|
responses []*webhook.ResponseBody
|
||||||
expectErr bool
|
expectErr bool
|
||||||
|
assertRequest func(t *testing.T, req *webhook.RequestBody)
|
||||||
}
|
}
|
||||||
tests := map[string]test{
|
tests := map[string]test{
|
||||||
"ok/no enriching webhooks": {
|
"ok/no enriching webhooks": {
|
||||||
|
@ -240,6 +294,27 @@ func TestWebhookController_Authorize(t *testing.T) {
|
||||||
responses: []*webhook.ResponseBody{{Allow: false}},
|
responses: []*webhook.ResponseBody{{Allow: false}},
|
||||||
expectErr: false,
|
expectErr: false,
|
||||||
},
|
},
|
||||||
|
"ok/with options": {
|
||||||
|
ctl: &WebhookController{
|
||||||
|
client: http.DefaultClient,
|
||||||
|
webhooks: []*Webhook{{Name: "people", Kind: "AUTHORIZING"}},
|
||||||
|
options: []webhook.RequestBodyOption{webhook.WithX5CCertificate(cert)},
|
||||||
|
},
|
||||||
|
req: &webhook.RequestBody{},
|
||||||
|
responses: []*webhook.ResponseBody{{Allow: true}},
|
||||||
|
expectErr: false,
|
||||||
|
assertRequest: func(t *testing.T, req *webhook.RequestBody) {
|
||||||
|
key, err := x509.MarshalPKIXPublicKey(cert.PublicKey)
|
||||||
|
assert.FatalError(t, err)
|
||||||
|
assert.Equals(t, &webhook.X5CCertificate{
|
||||||
|
Raw: cert.Raw,
|
||||||
|
PublicKey: key,
|
||||||
|
PublicKeyAlgorithm: cert.PublicKeyAlgorithm.String(),
|
||||||
|
NotBefore: cert.NotBefore,
|
||||||
|
NotAfter: cert.NotAfter,
|
||||||
|
}, req.X5CCertificate)
|
||||||
|
},
|
||||||
|
},
|
||||||
"deny": {
|
"deny": {
|
||||||
ctl: &WebhookController{
|
ctl: &WebhookController{
|
||||||
client: http.DefaultClient,
|
client: http.DefaultClient,
|
||||||
|
@ -249,6 +324,18 @@ func TestWebhookController_Authorize(t *testing.T) {
|
||||||
responses: []*webhook.ResponseBody{{Allow: false}},
|
responses: []*webhook.ResponseBody{{Allow: false}},
|
||||||
expectErr: true,
|
expectErr: true,
|
||||||
},
|
},
|
||||||
|
"fail/with options": {
|
||||||
|
ctl: &WebhookController{
|
||||||
|
client: http.DefaultClient,
|
||||||
|
webhooks: []*Webhook{{Name: "people", Kind: "AUTHORIZING"}},
|
||||||
|
options: []webhook.RequestBodyOption{webhook.WithX5CCertificate(&x509.Certificate{
|
||||||
|
PublicKey: []byte("bad"),
|
||||||
|
})},
|
||||||
|
},
|
||||||
|
req: &webhook.RequestBody{},
|
||||||
|
responses: []*webhook.ResponseBody{{Allow: false}},
|
||||||
|
expectErr: true,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
for name, test := range tests {
|
for name, test := range tests {
|
||||||
t.Run(name, func(t *testing.T) {
|
t.Run(name, func(t *testing.T) {
|
||||||
|
@ -267,6 +354,9 @@ func TestWebhookController_Authorize(t *testing.T) {
|
||||||
if (err != nil) != test.expectErr {
|
if (err != nil) != test.expectErr {
|
||||||
t.Fatalf("Got err %v, want %v", err, test.expectErr)
|
t.Fatalf("Got err %v, want %v", err, test.expectErr)
|
||||||
}
|
}
|
||||||
|
if test.assertRequest != nil {
|
||||||
|
test.assertRequest(t, test.req)
|
||||||
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,6 +15,7 @@ import (
|
||||||
"go.step.sm/linkedca"
|
"go.step.sm/linkedca"
|
||||||
|
|
||||||
"github.com/smallstep/certificates/errs"
|
"github.com/smallstep/certificates/errs"
|
||||||
|
"github.com/smallstep/certificates/webhook"
|
||||||
)
|
)
|
||||||
|
|
||||||
// x5cPayload extends jwt.Claims with step attributes.
|
// x5cPayload extends jwt.Claims with step attributes.
|
||||||
|
@ -215,7 +216,8 @@ func (p *X5C) AuthorizeSign(_ context.Context, token string) ([]SignOption, erro
|
||||||
// The X509 certificate will be available using the template variable
|
// The X509 certificate will be available using the template variable
|
||||||
// AuthorizationCrt. For example {{ .AuthorizationCrt.DNSNames }} can be
|
// AuthorizationCrt. For example {{ .AuthorizationCrt.DNSNames }} can be
|
||||||
// used to get all the domains.
|
// used to get all the domains.
|
||||||
data.SetAuthorizationCertificate(claims.chains[0][0])
|
x5cLeaf := claims.chains[0][0]
|
||||||
|
data.SetAuthorizationCertificate(x5cLeaf)
|
||||||
|
|
||||||
templateOptions, err := TemplateOptions(p.Options, data)
|
templateOptions, err := TemplateOptions(p.Options, data)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -238,7 +240,7 @@ func (p *X5C) AuthorizeSign(_ context.Context, token string) ([]SignOption, erro
|
||||||
newProvisionerExtensionOption(TypeX5C, p.Name, ""),
|
newProvisionerExtensionOption(TypeX5C, p.Name, ""),
|
||||||
profileLimitDuration{
|
profileLimitDuration{
|
||||||
p.ctl.Claimer.DefaultTLSCertDuration(),
|
p.ctl.Claimer.DefaultTLSCertDuration(),
|
||||||
claims.chains[0][0].NotBefore, claims.chains[0][0].NotAfter,
|
x5cLeaf.NotBefore, x5cLeaf.NotAfter,
|
||||||
},
|
},
|
||||||
// validators
|
// validators
|
||||||
commonNameValidator(claims.Subject),
|
commonNameValidator(claims.Subject),
|
||||||
|
@ -246,7 +248,7 @@ func (p *X5C) AuthorizeSign(_ context.Context, token string) ([]SignOption, erro
|
||||||
defaultPublicKeyValidator{},
|
defaultPublicKeyValidator{},
|
||||||
newValidityValidator(p.ctl.Claimer.MinTLSCertDuration(), p.ctl.Claimer.MaxTLSCertDuration()),
|
newValidityValidator(p.ctl.Claimer.MinTLSCertDuration(), p.ctl.Claimer.MaxTLSCertDuration()),
|
||||||
newX509NamePolicyValidator(p.ctl.getPolicy().getX509()),
|
newX509NamePolicyValidator(p.ctl.getPolicy().getX509()),
|
||||||
p.ctl.newWebhookController(data, linkedca.Webhook_X509),
|
p.ctl.newWebhookController(data, linkedca.Webhook_X509, webhook.WithX5CCertificate(x5cLeaf)),
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -305,7 +307,8 @@ func (p *X5C) AuthorizeSSHSign(_ context.Context, token string) ([]SignOption, e
|
||||||
// The X509 certificate will be available using the template variable
|
// The X509 certificate will be available using the template variable
|
||||||
// AuthorizationCrt. For example {{ .AuthorizationCrt.DNSNames }} can be
|
// AuthorizationCrt. For example {{ .AuthorizationCrt.DNSNames }} can be
|
||||||
// used to get all the domains.
|
// used to get all the domains.
|
||||||
data.SetAuthorizationCertificate(claims.chains[0][0])
|
x5cLeaf := claims.chains[0][0]
|
||||||
|
data.SetAuthorizationCertificate(x5cLeaf)
|
||||||
|
|
||||||
templateOptions, err := TemplateSSHOptions(p.Options, data)
|
templateOptions, err := TemplateSSHOptions(p.Options, data)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -325,7 +328,7 @@ func (p *X5C) AuthorizeSSHSign(_ context.Context, token string) ([]SignOption, e
|
||||||
return append(signOptions,
|
return append(signOptions,
|
||||||
p,
|
p,
|
||||||
// Checks the validity bounds, and set the validity if has not been set.
|
// Checks the validity bounds, and set the validity if has not been set.
|
||||||
&sshLimitDuration{p.ctl.Claimer, claims.chains[0][0].NotAfter},
|
&sshLimitDuration{p.ctl.Claimer, x5cLeaf.NotAfter},
|
||||||
// Validate public key.
|
// Validate public key.
|
||||||
&sshDefaultPublicKeyValidator{},
|
&sshDefaultPublicKeyValidator{},
|
||||||
// Validate the validity period.
|
// Validate the validity period.
|
||||||
|
@ -335,6 +338,6 @@ func (p *X5C) AuthorizeSSHSign(_ context.Context, token string) ([]SignOption, e
|
||||||
// Ensure that all principal names are allowed
|
// Ensure that all principal names are allowed
|
||||||
newSSHNamePolicyValidator(p.ctl.getPolicy().getSSHHost(), p.ctl.getPolicy().getSSHUser()),
|
newSSHNamePolicyValidator(p.ctl.getPolicy().getSSHHost(), p.ctl.getPolicy().getSSHUser()),
|
||||||
// Call webhooks
|
// Call webhooks
|
||||||
p.ctl.newWebhookController(data, linkedca.Webhook_SSH),
|
p.ctl.newWebhookController(data, linkedca.Webhook_SSH, webhook.WithX5CCertificate(x5cLeaf)),
|
||||||
), nil
|
), nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,6 +12,7 @@ import (
|
||||||
"go.step.sm/crypto/jose"
|
"go.step.sm/crypto/jose"
|
||||||
"go.step.sm/crypto/pemutil"
|
"go.step.sm/crypto/pemutil"
|
||||||
"go.step.sm/crypto/randutil"
|
"go.step.sm/crypto/randutil"
|
||||||
|
"go.step.sm/linkedca"
|
||||||
|
|
||||||
"github.com/smallstep/assert"
|
"github.com/smallstep/assert"
|
||||||
"github.com/smallstep/certificates/api/render"
|
"github.com/smallstep/certificates/api/render"
|
||||||
|
@ -497,6 +498,8 @@ func TestX5C_AuthorizeSign(t *testing.T) {
|
||||||
assert.Equals(t, nil, v.policyEngine)
|
assert.Equals(t, nil, v.policyEngine)
|
||||||
case *WebhookController:
|
case *WebhookController:
|
||||||
assert.Len(t, 0, v.webhooks)
|
assert.Len(t, 0, v.webhooks)
|
||||||
|
assert.Equals(t, linkedca.Webhook_X509, v.certType)
|
||||||
|
assert.Len(t, 1, v.options)
|
||||||
default:
|
default:
|
||||||
assert.FatalError(t, fmt.Errorf("unexpected sign option of type %T", v))
|
assert.FatalError(t, fmt.Errorf("unexpected sign option of type %T", v))
|
||||||
}
|
}
|
||||||
|
@ -801,6 +804,8 @@ func TestX5C_AuthorizeSSHSign(t *testing.T) {
|
||||||
case *sshDefaultPublicKeyValidator, *sshCertDefaultValidator, sshCertificateOptionsFunc:
|
case *sshDefaultPublicKeyValidator, *sshCertDefaultValidator, sshCertificateOptionsFunc:
|
||||||
case *WebhookController:
|
case *WebhookController:
|
||||||
assert.Len(t, 0, v.webhooks)
|
assert.Len(t, 0, v.webhooks)
|
||||||
|
assert.Equals(t, linkedca.Webhook_SSH, v.certType)
|
||||||
|
assert.Len(t, 1, v.options)
|
||||||
default:
|
default:
|
||||||
assert.FatalError(t, fmt.Errorf("unexpected sign option of type %T", v))
|
assert.FatalError(t, fmt.Errorf("unexpected sign option of type %T", v))
|
||||||
}
|
}
|
||||||
|
|
|
@ -95,3 +95,23 @@ func WithSSHCertificate(cert *sshutil.Certificate, certTpl *ssh.Certificate) Req
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func WithX5CCertificate(leaf *x509.Certificate) RequestBodyOption {
|
||||||
|
return func(rb *RequestBody) error {
|
||||||
|
rb.X5CCertificate = &X5CCertificate{
|
||||||
|
Raw: leaf.Raw,
|
||||||
|
PublicKeyAlgorithm: leaf.PublicKeyAlgorithm.String(),
|
||||||
|
NotBefore: leaf.NotBefore,
|
||||||
|
NotAfter: leaf.NotAfter,
|
||||||
|
}
|
||||||
|
if leaf.PublicKey != nil {
|
||||||
|
key, err := x509.MarshalPKIXPublicKey(leaf.PublicKey)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
rb.X5CCertificate.PublicKey = key
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -7,6 +7,7 @@ import (
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/smallstep/assert"
|
"github.com/smallstep/assert"
|
||||||
|
"go.step.sm/crypto/keyutil"
|
||||||
"go.step.sm/crypto/sshutil"
|
"go.step.sm/crypto/sshutil"
|
||||||
"go.step.sm/crypto/x509util"
|
"go.step.sm/crypto/x509util"
|
||||||
"golang.org/x/crypto/ssh"
|
"golang.org/x/crypto/ssh"
|
||||||
|
@ -16,6 +17,15 @@ func TestNewRequestBody(t *testing.T) {
|
||||||
t1 := time.Now()
|
t1 := time.Now()
|
||||||
t2 := t1.Add(time.Hour)
|
t2 := t1.Add(time.Hour)
|
||||||
|
|
||||||
|
key, err := keyutil.GenerateDefaultSigner()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
keyBytes, err := x509.MarshalPKIXPublicKey(key.Public())
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
type test struct {
|
type test struct {
|
||||||
options []RequestBodyOption
|
options []RequestBodyOption
|
||||||
want *RequestBody
|
want *RequestBody
|
||||||
|
@ -103,6 +113,40 @@ func TestNewRequestBody(t *testing.T) {
|
||||||
},
|
},
|
||||||
wantErr: false,
|
wantErr: false,
|
||||||
},
|
},
|
||||||
|
"X5C Certificate": {
|
||||||
|
options: []RequestBodyOption{
|
||||||
|
WithX5CCertificate(&x509.Certificate{
|
||||||
|
Raw: []byte("some raw data"),
|
||||||
|
NotBefore: t1,
|
||||||
|
NotAfter: t2,
|
||||||
|
PublicKeyAlgorithm: x509.ECDSA,
|
||||||
|
PublicKey: key.Public(),
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
want: &RequestBody{
|
||||||
|
X5CCertificate: &X5CCertificate{
|
||||||
|
Raw: []byte("some raw data"),
|
||||||
|
PublicKeyAlgorithm: "ECDSA",
|
||||||
|
NotBefore: t1,
|
||||||
|
NotAfter: t2,
|
||||||
|
PublicKey: keyBytes,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
"fail/X5C Certificate": {
|
||||||
|
options: []RequestBodyOption{
|
||||||
|
WithX5CCertificate(&x509.Certificate{
|
||||||
|
Raw: []byte("some raw data"),
|
||||||
|
NotBefore: t1,
|
||||||
|
NotAfter: t2,
|
||||||
|
PublicKeyAlgorithm: x509.ECDSA,
|
||||||
|
PublicKey: []byte("fail"),
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
want: nil,
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
for name, test := range tests {
|
for name, test := range tests {
|
||||||
t.Run(name, func(t *testing.T) {
|
t.Run(name, func(t *testing.T) {
|
||||||
|
|
|
@ -56,6 +56,17 @@ type AttestationData struct {
|
||||||
PermanentIdentifier string `json:"permanentIdentifier"`
|
PermanentIdentifier string `json:"permanentIdentifier"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// X5CCertificate is the authorization certificate sent to webhook servers for
|
||||||
|
// enriching or authorizing webhooks when signing X509 or SSH certificates using
|
||||||
|
// the X5C provisioner.
|
||||||
|
type X5CCertificate struct {
|
||||||
|
Raw []byte `json:"raw"`
|
||||||
|
PublicKey []byte `json:"publicKey"`
|
||||||
|
PublicKeyAlgorithm string `json:"publicKeyAlgorithm"`
|
||||||
|
NotBefore time.Time `json:"notBefore"`
|
||||||
|
NotAfter time.Time `json:"notAfter"`
|
||||||
|
}
|
||||||
|
|
||||||
// RequestBody is the body sent to webhook servers.
|
// RequestBody is the body sent to webhook servers.
|
||||||
type RequestBody struct {
|
type RequestBody struct {
|
||||||
Timestamp time.Time `json:"timestamp"`
|
Timestamp time.Time `json:"timestamp"`
|
||||||
|
@ -71,4 +82,6 @@ type RequestBody struct {
|
||||||
// Only set for SCEP challenge validation requests
|
// Only set for SCEP challenge validation requests
|
||||||
SCEPChallenge string `json:"scepChallenge,omitempty"`
|
SCEPChallenge string `json:"scepChallenge,omitempty"`
|
||||||
SCEPTransactionID string `json:"scepTransactionID,omitempty"`
|
SCEPTransactionID string `json:"scepTransactionID,omitempty"`
|
||||||
|
// Only set for X5C provisioners
|
||||||
|
X5CCertificate *X5CCertificate `json:"x5cCertificate,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue