forked from TrueCloudLab/certificates
259e95947c
The claimer, audiences and custom callback methods are now managed by the provisioner controller in an uniform way.
802 lines
26 KiB
Go
802 lines
26 KiB
Go
package provisioner
|
|
|
|
import (
|
|
"context"
|
|
"crypto/x509"
|
|
"net/http"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/pkg/errors"
|
|
"github.com/smallstep/assert"
|
|
"github.com/smallstep/certificates/errs"
|
|
"go.step.sm/crypto/jose"
|
|
"go.step.sm/crypto/pemutil"
|
|
"go.step.sm/crypto/randutil"
|
|
)
|
|
|
|
func TestX5C_Getters(t *testing.T) {
|
|
p, err := generateX5C(nil)
|
|
assert.FatalError(t, err)
|
|
id := "x5c/" + p.Name
|
|
if got := p.GetID(); got != id {
|
|
t.Errorf("X5C.GetID() = %v, want %v:%v", got, p.Name, id)
|
|
}
|
|
if got := p.GetName(); got != p.Name {
|
|
t.Errorf("X5C.GetName() = %v, want %v", got, p.Name)
|
|
}
|
|
if got := p.GetType(); got != TypeX5C {
|
|
t.Errorf("X5C.GetType() = %v, want %v", got, TypeX5C)
|
|
}
|
|
kid, key, ok := p.GetEncryptedKey()
|
|
if kid != "" || key != "" || ok == true {
|
|
t.Errorf("X5C.GetEncryptedKey() = (%v, %v, %v), want (%v, %v, %v)",
|
|
kid, key, ok, "", "", false)
|
|
}
|
|
}
|
|
|
|
func TestX5C_Init(t *testing.T) {
|
|
type ProvisionerValidateTest struct {
|
|
p *X5C
|
|
err error
|
|
extraValid func(*X5C) error
|
|
}
|
|
tests := map[string]func(*testing.T) ProvisionerValidateTest{
|
|
"fail/empty": func(t *testing.T) ProvisionerValidateTest {
|
|
return ProvisionerValidateTest{
|
|
p: &X5C{},
|
|
err: errors.New("provisioner type cannot be empty"),
|
|
}
|
|
},
|
|
"fail/empty-name": func(t *testing.T) ProvisionerValidateTest {
|
|
return ProvisionerValidateTest{
|
|
p: &X5C{
|
|
Type: "X5C",
|
|
},
|
|
err: errors.New("provisioner name cannot be empty"),
|
|
}
|
|
},
|
|
"fail/empty-type": func(t *testing.T) ProvisionerValidateTest {
|
|
return ProvisionerValidateTest{
|
|
p: &X5C{Name: "foo"},
|
|
err: errors.New("provisioner type cannot be empty"),
|
|
}
|
|
},
|
|
"fail/empty-key": func(t *testing.T) ProvisionerValidateTest {
|
|
return ProvisionerValidateTest{
|
|
p: &X5C{Name: "foo", Type: "bar"},
|
|
err: errors.New("provisioner root(s) cannot be empty"),
|
|
}
|
|
},
|
|
"fail/no-valid-root-certs": func(t *testing.T) ProvisionerValidateTest {
|
|
return ProvisionerValidateTest{
|
|
p: &X5C{Name: "foo", Type: "bar", Roots: []byte("foo")},
|
|
err: errors.Errorf("no x509 certificates found in roots attribute for provisioner 'foo'"),
|
|
}
|
|
},
|
|
"fail/invalid-duration": func(t *testing.T) ProvisionerValidateTest {
|
|
p, err := generateX5C(nil)
|
|
assert.FatalError(t, err)
|
|
p.Claims = &Claims{DefaultTLSDur: &Duration{0}}
|
|
return ProvisionerValidateTest{
|
|
p: p,
|
|
err: errors.New("claims: MinTLSCertDuration must be greater than 0"),
|
|
}
|
|
},
|
|
"ok": func(t *testing.T) ProvisionerValidateTest {
|
|
p, err := generateX5C(nil)
|
|
assert.FatalError(t, err)
|
|
return ProvisionerValidateTest{
|
|
p: p,
|
|
}
|
|
},
|
|
"ok/root-chain": func(t *testing.T) ProvisionerValidateTest {
|
|
p, err := generateX5C([]byte(`-----BEGIN CERTIFICATE-----
|
|
MIIBtjCCAVygAwIBAgIQNr+f4IkABY2n4wx4sLOMrTAKBggqhkjOPQQDAjAUMRIw
|
|
EAYDVQQDEwlyb290LXRlc3QwIBcNMTkxMDAyMDI0MDM0WhgPMjExOTA5MDgwMjQw
|
|
MzJaMBwxGjAYBgNVBAMTEWludGVybWVkaWF0ZS10ZXN0MFkwEwYHKoZIzj0CAQYI
|
|
KoZIzj0DAQcDQgAEflfRhPjgJXv4zsPWahXjM2UU61aRFErN0iw88ZPyxea22fxl
|
|
qN9ezntTXxzsS+mZiWapl8B40ACJgvP+WLQBHKOBhTCBgjAOBgNVHQ8BAf8EBAMC
|
|
AQYwEgYDVR0TAQH/BAgwBgEB/wIBADAdBgNVHQ4EFgQUnJAxiZcy2ibHcuvfFx99
|
|
oDwzKXMwHwYDVR0jBBgwFoAUpHS7FfaQ5bCrTxUeu6R2ZC3VGOowHAYDVR0RBBUw
|
|
E4IRaW50ZXJtZWRpYXRlLXRlc3QwCgYIKoZIzj0EAwIDSAAwRQIgII8XpQ8ezDO1
|
|
2xdq3hShf155C5X/5jO8qr0VyEJgzlkCIQCTqph1Gwu/dmuf6dYLCfQqJyb371LC
|
|
lgsqsR63is+0YQ==
|
|
-----END CERTIFICATE-----
|
|
-----BEGIN CERTIFICATE-----
|
|
MIIBhTCCASqgAwIBAgIRAMalM7pKi0GCdKjO6u88OyowCgYIKoZIzj0EAwIwFDES
|
|
MBAGA1UEAxMJcm9vdC10ZXN0MCAXDTE5MTAwMjAyMzk0OFoYDzIxMTkwOTA4MDIz
|
|
OTQ4WjAUMRIwEAYDVQQDEwlyb290LXRlc3QwWTATBgcqhkjOPQIBBggqhkjOPQMB
|
|
BwNCAAS29QTCXUu7cx9sa9wZPpRSFq/zXaw8Ai3EIygayrBsKnX42U2atBUjcBZO
|
|
BWL6A+PpLzU9ja867U5SYNHERS+Oo1swWTAOBgNVHQ8BAf8EBAMCAQYwEgYDVR0T
|
|
AQH/BAgwBgEB/wIBATAdBgNVHQ4EFgQUpHS7FfaQ5bCrTxUeu6R2ZC3VGOowFAYD
|
|
VR0RBA0wC4IJcm9vdC10ZXN0MAoGCCqGSM49BAMCA0kAMEYCIQC2vgqwla0u8LHH
|
|
1MHob14qvS5o76HautbIBW7fcHzz5gIhAIx5A2+wkJYX4026kqaZCk/1sAwTxSGY
|
|
M46l92gdOozT
|
|
-----END CERTIFICATE-----`))
|
|
assert.FatalError(t, err)
|
|
return ProvisionerValidateTest{
|
|
p: p,
|
|
extraValid: func(p *X5C) error {
|
|
numCerts := len(p.rootPool.Subjects())
|
|
if numCerts != 2 {
|
|
return errors.Errorf("unexpected number of certs: want 2, but got %d", numCerts)
|
|
}
|
|
return nil
|
|
},
|
|
}
|
|
},
|
|
}
|
|
|
|
config := Config{
|
|
Claims: globalProvisionerClaims,
|
|
Audiences: testAudiences,
|
|
}
|
|
for name, get := range tests {
|
|
t.Run(name, func(t *testing.T) {
|
|
tc := get(t)
|
|
err := tc.p.Init(config)
|
|
if err != nil {
|
|
if assert.NotNil(t, tc.err) {
|
|
assert.Equals(t, tc.err.Error(), err.Error())
|
|
}
|
|
} else {
|
|
if assert.Nil(t, tc.err) {
|
|
assert.Equals(t, *tc.p.ctl.Audiences, config.Audiences.WithFragment(tc.p.GetID()))
|
|
if tc.extraValid != nil {
|
|
assert.Nil(t, tc.extraValid(tc.p))
|
|
}
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestX5C_authorizeToken(t *testing.T) {
|
|
x5cCerts, err := pemutil.ReadCertificateBundle("./testdata/certs/x5c-leaf.crt")
|
|
assert.FatalError(t, err)
|
|
x5cJWK, err := jose.ReadKey("./testdata/secrets/x5c-leaf.key")
|
|
assert.FatalError(t, err)
|
|
|
|
type test struct {
|
|
p *X5C
|
|
token string
|
|
code int
|
|
err error
|
|
}
|
|
tests := map[string]func(*testing.T) test{
|
|
"fail/bad-token": func(t *testing.T) test {
|
|
p, err := generateX5C(nil)
|
|
assert.FatalError(t, err)
|
|
return test{
|
|
p: p,
|
|
token: "foo",
|
|
code: http.StatusUnauthorized,
|
|
err: errors.New("x5c.authorizeToken; error parsing x5c token"),
|
|
}
|
|
},
|
|
"fail/invalid-cert-chain": func(t *testing.T) test {
|
|
certs, err := parseCerts([]byte(`-----BEGIN CERTIFICATE-----
|
|
MIIBpTCCAUugAwIBAgIRAOn2LHXjYyTXQ7PNjDTSKiIwCgYIKoZIzj0EAwIwHDEa
|
|
MBgGA1UEAxMRU21hbGxzdGVwIFJvb3QgQ0EwHhcNMTkwOTE0MDk1NTM2WhcNMjkw
|
|
OTExMDk1NTM2WjAkMSIwIAYDVQQDExlTbWFsbHN0ZXAgSW50ZXJtZWRpYXRlIENB
|
|
MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE2Cs0TY0dLM4b2s+z8+cc3JJp/W5H
|
|
zQRvICX/1aJ4MuObNLcvoSguJwJEkYpGB5fhb0KvoL+ebHfEOywGNwrWkaNmMGQw
|
|
DgYDVR0PAQH/BAQDAgEGMBIGA1UdEwEB/wQIMAYBAf8CAQAwHQYDVR0OBBYEFNLJ
|
|
4ZXoX9cI6YkGPxgs2US3ssVzMB8GA1UdIwQYMBaAFGIwpqz85wL29aF47Vj9XSVM
|
|
P9K7MAoGCCqGSM49BAMCA0gAMEUCIQC5c1ldDcesDb31GlO5cEJvOcRrIrNtkk8m
|
|
a5wpg+9s6QIgHIW6L60F8klQX+EO3o0SBqLeNcaskA4oSZsKjEdpSGo=
|
|
-----END CERTIFICATE-----`))
|
|
assert.FatalError(t, err)
|
|
jwk, err := jose.GenerateJWK("EC", "P-256", "ES256", "sig", "", 0)
|
|
assert.FatalError(t, err)
|
|
p, err := generateX5C(nil)
|
|
assert.FatalError(t, err)
|
|
tok, err := generateToken("", p.Name, testAudiences.Sign[0], "",
|
|
[]string{"test.smallstep.com"}, time.Now(), jwk,
|
|
withX5CHdr(certs))
|
|
assert.FatalError(t, err)
|
|
return test{
|
|
p: p,
|
|
token: tok,
|
|
code: http.StatusUnauthorized,
|
|
err: errors.New("x5c.authorizeToken; error verifying x5c certificate chain in token"),
|
|
}
|
|
},
|
|
"fail/doubled-up-self-signed-cert": func(t *testing.T) test {
|
|
certs, err := parseCerts([]byte(`-----BEGIN CERTIFICATE-----
|
|
MIIBgjCCASigAwIBAgIQIZiE9wpmSj6SMMDfHD17qjAKBggqhkjOPQQDAjAQMQ4w
|
|
DAYDVQQDEwVsZWFmMjAgFw0xOTEwMDIwMzEzNTlaGA8yMTE5MDkwODAzMTM1OVow
|
|
EDEOMAwGA1UEAxMFbGVhZjIwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAATuajJI
|
|
3YgDaj+jorioJzGJc2+V1hUM7XzN9tIHoUeItgny9GW08TrTc23h1cCZteNZvayG
|
|
M0wGpGeXOnE4IlH9o2IwYDAOBgNVHQ8BAf8EBAMCBSAwHQYDVR0lBBYwFAYIKwYB
|
|
BQUHAwEGCCsGAQUFBwMCMB0GA1UdDgQWBBT99+JChTh3LWOHaqlSwNiwND18/zAQ
|
|
BgNVHREECTAHggVsZWFmMjAKBggqhkjOPQQDAgNIADBFAiB7gMRy3t81HpcnoRAS
|
|
ELZmDFaEnoLCsVfbmanFykazQQIhAI0sZjoE9t6gvzQp7XQp6CoxzCc3Jv3FwZ8G
|
|
EXAHTA9L
|
|
-----END CERTIFICATE-----
|
|
-----BEGIN CERTIFICATE-----
|
|
MIIBgjCCASigAwIBAgIQIZiE9wpmSj6SMMDfHD17qjAKBggqhkjOPQQDAjAQMQ4w
|
|
DAYDVQQDEwVsZWFmMjAgFw0xOTEwMDIwMzEzNTlaGA8yMTE5MDkwODAzMTM1OVow
|
|
EDEOMAwGA1UEAxMFbGVhZjIwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAATuajJI
|
|
3YgDaj+jorioJzGJc2+V1hUM7XzN9tIHoUeItgny9GW08TrTc23h1cCZteNZvayG
|
|
M0wGpGeXOnE4IlH9o2IwYDAOBgNVHQ8BAf8EBAMCBSAwHQYDVR0lBBYwFAYIKwYB
|
|
BQUHAwEGCCsGAQUFBwMCMB0GA1UdDgQWBBT99+JChTh3LWOHaqlSwNiwND18/zAQ
|
|
BgNVHREECTAHggVsZWFmMjAKBggqhkjOPQQDAgNIADBFAiB7gMRy3t81HpcnoRAS
|
|
ELZmDFaEnoLCsVfbmanFykazQQIhAI0sZjoE9t6gvzQp7XQp6CoxzCc3Jv3FwZ8G
|
|
EXAHTA9L
|
|
-----END CERTIFICATE-----`))
|
|
assert.FatalError(t, err)
|
|
jwk, err := jose.GenerateJWK("EC", "P-256", "ES256", "sig", "", 0)
|
|
assert.FatalError(t, err)
|
|
p, err := generateX5C(nil)
|
|
assert.FatalError(t, err)
|
|
tok, err := generateToken("", p.Name, testAudiences.Sign[0], "",
|
|
[]string{"test.smallstep.com"}, time.Now(), jwk,
|
|
withX5CHdr(certs))
|
|
assert.FatalError(t, err)
|
|
return test{
|
|
p: p,
|
|
token: tok,
|
|
code: http.StatusUnauthorized,
|
|
err: errors.New("x5c.authorizeToken; error verifying x5c certificate chain in token"),
|
|
}
|
|
},
|
|
"fail/digital-signature-ext-required": func(t *testing.T) test {
|
|
certs, err := parseCerts([]byte(`-----BEGIN CERTIFICATE-----
|
|
MIIBuTCCAV+gAwIBAgIQeRJLdDMIdn/T2ORKxYABezAKBggqhkjOPQQDAjAcMRow
|
|
GAYDVQQDExFpbnRlcm1lZGlhdGUtdGVzdDAgFw0xOTEwMDIwMjQxMTRaGA8yMTE5
|
|
MDkwODAyNDExMlowFDESMBAGA1UEAxMJbGVhZi10ZXN0MFkwEwYHKoZIzj0CAQYI
|
|
KoZIzj0DAQcDQgAEDA1nGTOujobkcBWklyvymhWE5gQlvNLarVzhhhvPDw+MK2LX
|
|
yqkXrYZM10GrwQZuQ7ykHnjz00U/KXpPRQ7+0qOBiDCBhTAOBgNVHQ8BAf8EBAMC
|
|
BSAwHQYDVR0lBBYwFAYIKwYBBQUHAwEGCCsGAQUFBwMCMB0GA1UdDgQWBBQYv0AK
|
|
3GUOvC+m8ZTfyhn7tKQOazAfBgNVHSMEGDAWgBSckDGJlzLaJsdy698XH32gPDMp
|
|
czAUBgNVHREEDTALgglsZWFmLXRlc3QwCgYIKoZIzj0EAwIDSAAwRQIhAPmertx0
|
|
lchRU3kAu647exvlhEr1xosPOu6P8kVYbtTEAiAA51w9EYIT/Zb26M3eQV817T2g
|
|
Dnhl0ElPQsA92pkqbA==
|
|
-----END CERTIFICATE-----
|
|
-----BEGIN CERTIFICATE-----
|
|
MIIBtjCCAVygAwIBAgIQNr+f4IkABY2n4wx4sLOMrTAKBggqhkjOPQQDAjAUMRIw
|
|
EAYDVQQDEwlyb290LXRlc3QwIBcNMTkxMDAyMDI0MDM0WhgPMjExOTA5MDgwMjQw
|
|
MzJaMBwxGjAYBgNVBAMTEWludGVybWVkaWF0ZS10ZXN0MFkwEwYHKoZIzj0CAQYI
|
|
KoZIzj0DAQcDQgAEflfRhPjgJXv4zsPWahXjM2UU61aRFErN0iw88ZPyxea22fxl
|
|
qN9ezntTXxzsS+mZiWapl8B40ACJgvP+WLQBHKOBhTCBgjAOBgNVHQ8BAf8EBAMC
|
|
AQYwEgYDVR0TAQH/BAgwBgEB/wIBADAdBgNVHQ4EFgQUnJAxiZcy2ibHcuvfFx99
|
|
oDwzKXMwHwYDVR0jBBgwFoAUpHS7FfaQ5bCrTxUeu6R2ZC3VGOowHAYDVR0RBBUw
|
|
E4IRaW50ZXJtZWRpYXRlLXRlc3QwCgYIKoZIzj0EAwIDSAAwRQIgII8XpQ8ezDO1
|
|
2xdq3hShf155C5X/5jO8qr0VyEJgzlkCIQCTqph1Gwu/dmuf6dYLCfQqJyb371LC
|
|
lgsqsR63is+0YQ==
|
|
-----END CERTIFICATE-----`))
|
|
assert.FatalError(t, err)
|
|
jwk, err := jose.GenerateJWK("EC", "P-256", "ES256", "sig", "", 0)
|
|
assert.FatalError(t, err)
|
|
p, err := generateX5C(nil)
|
|
assert.FatalError(t, err)
|
|
|
|
tok, err := generateToken("", p.Name, testAudiences.Sign[0], "",
|
|
[]string{"test.smallstep.com"}, time.Now(), jwk,
|
|
withX5CHdr(certs))
|
|
assert.FatalError(t, err)
|
|
return test{
|
|
p: p,
|
|
token: tok,
|
|
code: http.StatusUnauthorized,
|
|
err: errors.New("x5c.authorizeToken; certificate used to sign x5c token cannot be used for digital signature"),
|
|
}
|
|
},
|
|
"fail/signature-does-not-match-x5c-pub-key": func(t *testing.T) test {
|
|
certs, err := parseCerts([]byte(`-----BEGIN CERTIFICATE-----
|
|
MIIBuDCCAV+gAwIBAgIQFdu723gqgGaTaqjf6ny88zAKBggqhkjOPQQDAjAcMRow
|
|
GAYDVQQDExFpbnRlcm1lZGlhdGUtdGVzdDAgFw0xOTEwMDIwMzE4NTNaGA8yMTE5
|
|
MDkwODAzMTg1MVowFDESMBAGA1UEAxMJbGVhZi10ZXN0MFkwEwYHKoZIzj0CAQYI
|
|
KoZIzj0DAQcDQgAEaV6807GhWEtMxA39zjuMVHAiN2/Ri5B1R1s+Y/8mlrKIvuvr
|
|
VpgSPXYruNRFduPWX564Abz/TDmb276JbKGeQqOBiDCBhTAOBgNVHQ8BAf8EBAMC
|
|
BaAwHQYDVR0lBBYwFAYIKwYBBQUHAwEGCCsGAQUFBwMCMB0GA1UdDgQWBBReMkPW
|
|
f4MNWdg7KN4xI4ZLJd0IJDAfBgNVHSMEGDAWgBSckDGJlzLaJsdy698XH32gPDMp
|
|
czAUBgNVHREEDTALgglsZWFmLXRlc3QwCgYIKoZIzj0EAwIDRwAwRAIgKYLKXpTN
|
|
wtvZZaIvDzq1p8MO/SZ8yI42Ot69dNk/QtkCIBSvg5PozYcfbvwkgX5SwsjfYu0Z
|
|
AvUgkUQ2G25NBRmX
|
|
-----END CERTIFICATE-----
|
|
-----BEGIN CERTIFICATE-----
|
|
MIIBtjCCAVygAwIBAgIQNr+f4IkABY2n4wx4sLOMrTAKBggqhkjOPQQDAjAUMRIw
|
|
EAYDVQQDEwlyb290LXRlc3QwIBcNMTkxMDAyMDI0MDM0WhgPMjExOTA5MDgwMjQw
|
|
MzJaMBwxGjAYBgNVBAMTEWludGVybWVkaWF0ZS10ZXN0MFkwEwYHKoZIzj0CAQYI
|
|
KoZIzj0DAQcDQgAEflfRhPjgJXv4zsPWahXjM2UU61aRFErN0iw88ZPyxea22fxl
|
|
qN9ezntTXxzsS+mZiWapl8B40ACJgvP+WLQBHKOBhTCBgjAOBgNVHQ8BAf8EBAMC
|
|
AQYwEgYDVR0TAQH/BAgwBgEB/wIBADAdBgNVHQ4EFgQUnJAxiZcy2ibHcuvfFx99
|
|
oDwzKXMwHwYDVR0jBBgwFoAUpHS7FfaQ5bCrTxUeu6R2ZC3VGOowHAYDVR0RBBUw
|
|
E4IRaW50ZXJtZWRpYXRlLXRlc3QwCgYIKoZIzj0EAwIDSAAwRQIgII8XpQ8ezDO1
|
|
2xdq3hShf155C5X/5jO8qr0VyEJgzlkCIQCTqph1Gwu/dmuf6dYLCfQqJyb371LC
|
|
lgsqsR63is+0YQ==
|
|
-----END CERTIFICATE-----`))
|
|
assert.FatalError(t, err)
|
|
jwk, err := jose.GenerateJWK("EC", "P-256", "ES256", "sig", "", 0)
|
|
assert.FatalError(t, err)
|
|
p, err := generateX5C(nil)
|
|
assert.FatalError(t, err)
|
|
tok, err := generateToken("", "foobar", testAudiences.Sign[0], "",
|
|
[]string{"test.smallstep.com"}, time.Now(), jwk,
|
|
withX5CHdr(certs))
|
|
assert.FatalError(t, err)
|
|
return test{
|
|
p: p,
|
|
token: tok,
|
|
code: http.StatusUnauthorized,
|
|
err: errors.New("x5c.authorizeToken; error parsing x5c claims"),
|
|
}
|
|
},
|
|
"fail/invalid-issuer": func(t *testing.T) test {
|
|
p, err := generateX5C(nil)
|
|
assert.FatalError(t, err)
|
|
tok, err := generateToken("", "foobar", testAudiences.Sign[0], "",
|
|
[]string{"test.smallstep.com"}, time.Now(), x5cJWK,
|
|
withX5CHdr(x5cCerts))
|
|
assert.FatalError(t, err)
|
|
return test{
|
|
p: p,
|
|
token: tok,
|
|
code: http.StatusUnauthorized,
|
|
err: errors.New("x5c.authorizeToken; invalid x5c claims"),
|
|
}
|
|
},
|
|
"fail/invalid-audience": func(t *testing.T) test {
|
|
p, err := generateX5C(nil)
|
|
assert.FatalError(t, err)
|
|
tok, err := generateToken("", p.GetName(), "foobar", "",
|
|
[]string{"test.smallstep.com"}, time.Now(), x5cJWK,
|
|
withX5CHdr(x5cCerts))
|
|
assert.FatalError(t, err)
|
|
return test{
|
|
p: p,
|
|
token: tok,
|
|
code: http.StatusUnauthorized,
|
|
err: errors.New("x5c.authorizeToken; x5c token has invalid audience claim (aud)"),
|
|
}
|
|
},
|
|
"fail/empty-subject": func(t *testing.T) test {
|
|
p, err := generateX5C(nil)
|
|
assert.FatalError(t, err)
|
|
tok, err := generateToken("", p.GetName(), testAudiences.Sign[0], "",
|
|
[]string{"test.smallstep.com"}, time.Now(), x5cJWK,
|
|
withX5CHdr(x5cCerts))
|
|
assert.FatalError(t, err)
|
|
return test{
|
|
p: p,
|
|
token: tok,
|
|
code: http.StatusUnauthorized,
|
|
err: errors.New("x5c.authorizeToken; x5c token subject cannot be empty"),
|
|
}
|
|
},
|
|
"ok": func(t *testing.T) test {
|
|
p, err := generateX5C(nil)
|
|
assert.FatalError(t, err)
|
|
tok, err := generateToken("foo", p.GetName(), testAudiences.Sign[0], "",
|
|
[]string{"test.smallstep.com"}, time.Now(), x5cJWK,
|
|
withX5CHdr(x5cCerts))
|
|
assert.FatalError(t, err)
|
|
return test{
|
|
p: p,
|
|
token: tok,
|
|
}
|
|
},
|
|
}
|
|
for name, tt := range tests {
|
|
t.Run(name, func(t *testing.T) {
|
|
tc := tt(t)
|
|
if claims, err := tc.p.authorizeToken(tc.token, testAudiences.Sign); err != nil {
|
|
if assert.NotNil(t, tc.err) {
|
|
sc, ok := err.(errs.StatusCoder)
|
|
assert.Fatal(t, ok, "error does not implement StatusCoder interface")
|
|
assert.Equals(t, sc.StatusCode(), tc.code)
|
|
assert.HasPrefix(t, err.Error(), tc.err.Error())
|
|
}
|
|
} else {
|
|
if assert.Nil(t, tc.err) {
|
|
assert.NotNil(t, claims)
|
|
assert.NotNil(t, claims.chains)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestX5C_AuthorizeSign(t *testing.T) {
|
|
certs, err := pemutil.ReadCertificateBundle("./testdata/certs/x5c-leaf.crt")
|
|
assert.FatalError(t, err)
|
|
jwk, err := jose.ReadKey("./testdata/secrets/x5c-leaf.key")
|
|
assert.FatalError(t, err)
|
|
|
|
type test struct {
|
|
p *X5C
|
|
token string
|
|
code int
|
|
err error
|
|
sans []string
|
|
}
|
|
tests := map[string]func(*testing.T) test{
|
|
"fail/invalid-token": func(t *testing.T) test {
|
|
p, err := generateX5C(nil)
|
|
assert.FatalError(t, err)
|
|
return test{
|
|
p: p,
|
|
token: "foo",
|
|
code: http.StatusUnauthorized,
|
|
err: errors.New("x5c.AuthorizeSign: x5c.authorizeToken; error parsing x5c token"),
|
|
}
|
|
},
|
|
"ok/empty-sans": func(t *testing.T) test {
|
|
p, err := generateX5C(nil)
|
|
assert.FatalError(t, err)
|
|
tok, err := generateToken("foo", p.GetName(), testAudiences.Sign[0], "",
|
|
[]string{}, time.Now(), jwk,
|
|
withX5CHdr(certs))
|
|
assert.FatalError(t, err)
|
|
return test{
|
|
p: p,
|
|
token: tok,
|
|
sans: []string{"foo"},
|
|
}
|
|
},
|
|
"ok/multi-sans": func(t *testing.T) test {
|
|
p, err := generateX5C(nil)
|
|
assert.FatalError(t, err)
|
|
tok, err := generateToken("foo", p.GetName(), testAudiences.Sign[0], "",
|
|
[]string{"127.0.0.1", "foo", "max@smallstep.com"}, time.Now(), jwk,
|
|
withX5CHdr(certs))
|
|
assert.FatalError(t, err)
|
|
return test{
|
|
p: p,
|
|
token: tok,
|
|
sans: []string{"127.0.0.1", "foo", "max@smallstep.com"},
|
|
}
|
|
},
|
|
}
|
|
for name, tt := range tests {
|
|
t.Run(name, func(t *testing.T) {
|
|
tc := tt(t)
|
|
if opts, err := tc.p.AuthorizeSign(context.Background(), tc.token); err != nil {
|
|
if assert.NotNil(t, tc.err) {
|
|
sc, ok := err.(errs.StatusCoder)
|
|
assert.Fatal(t, ok, "error does not implement StatusCoder interface")
|
|
assert.Equals(t, sc.StatusCode(), tc.code)
|
|
assert.HasPrefix(t, err.Error(), tc.err.Error())
|
|
}
|
|
} else {
|
|
if assert.Nil(t, tc.err) {
|
|
if assert.NotNil(t, opts) {
|
|
assert.Equals(t, len(opts), 7)
|
|
for _, o := range opts {
|
|
switch v := o.(type) {
|
|
case certificateOptionsFunc:
|
|
case *provisionerExtensionOption:
|
|
assert.Equals(t, v.Type, int(TypeX5C))
|
|
assert.Equals(t, v.Name, tc.p.GetName())
|
|
assert.Equals(t, v.CredentialID, "")
|
|
assert.Len(t, 0, v.KeyValuePairs)
|
|
case profileLimitDuration:
|
|
assert.Equals(t, v.def, tc.p.ctl.Claimer.DefaultTLSCertDuration())
|
|
|
|
claims, err := tc.p.authorizeToken(tc.token, tc.p.ctl.Audiences.Sign)
|
|
assert.FatalError(t, err)
|
|
assert.Equals(t, v.notAfter, claims.chains[0][0].NotAfter)
|
|
case commonNameValidator:
|
|
assert.Equals(t, string(v), "foo")
|
|
case defaultPublicKeyValidator:
|
|
case defaultSANsValidator:
|
|
assert.Equals(t, []string(v), tc.sans)
|
|
case *validityValidator:
|
|
assert.Equals(t, v.min, tc.p.ctl.Claimer.MinTLSCertDuration())
|
|
assert.Equals(t, v.max, tc.p.ctl.Claimer.MaxTLSCertDuration())
|
|
default:
|
|
assert.FatalError(t, errors.Errorf("unexpected sign option of type %T", v))
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestX5C_AuthorizeRevoke(t *testing.T) {
|
|
type test struct {
|
|
p *X5C
|
|
token string
|
|
code int
|
|
err error
|
|
}
|
|
tests := map[string]func(*testing.T) test{
|
|
"fail/invalid-token": func(t *testing.T) test {
|
|
p, err := generateX5C(nil)
|
|
assert.FatalError(t, err)
|
|
return test{
|
|
p: p,
|
|
token: "foo",
|
|
code: http.StatusUnauthorized,
|
|
err: errors.New("x5c.AuthorizeRevoke: x5c.authorizeToken; error parsing x5c token"),
|
|
}
|
|
},
|
|
"ok": func(t *testing.T) test {
|
|
certs, err := pemutil.ReadCertificateBundle("./testdata/certs/x5c-leaf.crt")
|
|
assert.FatalError(t, err)
|
|
jwk, err := jose.ReadKey("./testdata/secrets/x5c-leaf.key")
|
|
assert.FatalError(t, err)
|
|
|
|
p, err := generateX5C(nil)
|
|
assert.FatalError(t, err)
|
|
tok, err := generateToken("foo", p.GetName(), testAudiences.Revoke[0], "",
|
|
[]string{"test.smallstep.com"}, time.Now(), jwk,
|
|
withX5CHdr(certs))
|
|
assert.FatalError(t, err)
|
|
return test{
|
|
p: p,
|
|
token: tok,
|
|
}
|
|
},
|
|
}
|
|
for name, tt := range tests {
|
|
t.Run(name, func(t *testing.T) {
|
|
tc := tt(t)
|
|
if err := tc.p.AuthorizeRevoke(context.Background(), tc.token); err != nil {
|
|
if assert.NotNil(t, tc.err) {
|
|
sc, ok := err.(errs.StatusCoder)
|
|
assert.Fatal(t, ok, "error does not implement StatusCoder interface")
|
|
assert.Equals(t, sc.StatusCode(), tc.code)
|
|
assert.HasPrefix(t, err.Error(), tc.err.Error())
|
|
}
|
|
} else {
|
|
assert.Nil(t, tc.err)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestX5C_AuthorizeRenew(t *testing.T) {
|
|
now := time.Now()
|
|
type test struct {
|
|
p *X5C
|
|
code int
|
|
err error
|
|
}
|
|
tests := map[string]func(*testing.T) test{
|
|
"fail/renew-disabled": func(t *testing.T) test {
|
|
p, err := generateX5C(nil)
|
|
assert.FatalError(t, err)
|
|
// disable renewal
|
|
disable := true
|
|
p.Claims = &Claims{DisableRenewal: &disable}
|
|
p.ctl.Claimer, err = NewClaimer(p.Claims, globalProvisionerClaims)
|
|
assert.FatalError(t, err)
|
|
return test{
|
|
p: p,
|
|
code: http.StatusUnauthorized,
|
|
err: errors.Errorf("renew is disabled for provisioner '%s'", p.GetName()),
|
|
}
|
|
},
|
|
"ok": func(t *testing.T) test {
|
|
p, err := generateX5C(nil)
|
|
assert.FatalError(t, err)
|
|
return test{
|
|
p: p,
|
|
}
|
|
},
|
|
}
|
|
for name, tt := range tests {
|
|
t.Run(name, func(t *testing.T) {
|
|
tc := tt(t)
|
|
if err := tc.p.AuthorizeRenew(context.Background(), &x509.Certificate{
|
|
NotBefore: now,
|
|
NotAfter: now.Add(time.Hour),
|
|
}); err != nil {
|
|
if assert.NotNil(t, tc.err) {
|
|
sc, ok := err.(errs.StatusCoder)
|
|
assert.Fatal(t, ok, "error does not implement StatusCoder interface")
|
|
assert.Equals(t, sc.StatusCode(), tc.code)
|
|
assert.HasPrefix(t, err.Error(), tc.err.Error())
|
|
}
|
|
} else {
|
|
assert.Nil(t, tc.err)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestX5C_AuthorizeSSHSign(t *testing.T) {
|
|
x5cCerts, err := pemutil.ReadCertificateBundle("./testdata/certs/x5c-leaf.crt")
|
|
assert.FatalError(t, err)
|
|
x5cJWK, err := jose.ReadKey("./testdata/secrets/x5c-leaf.key")
|
|
assert.FatalError(t, err)
|
|
|
|
_, fn := mockNow()
|
|
defer fn()
|
|
type test struct {
|
|
p *X5C
|
|
token string
|
|
claims *x5cPayload
|
|
code int
|
|
err error
|
|
}
|
|
tests := map[string]func(*testing.T) test{
|
|
"fail/sshCA-disabled": func(t *testing.T) test {
|
|
p, err := generateX5C(nil)
|
|
assert.FatalError(t, err)
|
|
// disable sshCA
|
|
enable := false
|
|
p.Claims = &Claims{EnableSSHCA: &enable}
|
|
p.ctl.Claimer, err = NewClaimer(p.Claims, globalProvisionerClaims)
|
|
assert.FatalError(t, err)
|
|
return test{
|
|
p: p,
|
|
token: "foo",
|
|
code: http.StatusUnauthorized,
|
|
err: errors.Errorf("x5c.AuthorizeSSHSign; sshCA is disabled for x5c provisioner '%s'", p.GetName()),
|
|
}
|
|
},
|
|
"fail/invalid-token": func(t *testing.T) test {
|
|
p, err := generateX5C(nil)
|
|
assert.FatalError(t, err)
|
|
return test{
|
|
p: p,
|
|
token: "foo",
|
|
code: http.StatusUnauthorized,
|
|
err: errors.New("x5c.AuthorizeSSHSign: x5c.authorizeToken; error parsing x5c token"),
|
|
}
|
|
},
|
|
"fail/no-Step-claim": func(t *testing.T) test {
|
|
p, err := generateX5C(nil)
|
|
assert.FatalError(t, err)
|
|
tok, err := generateToken("foo", p.GetName(), testAudiences.SSHSign[0], "",
|
|
[]string{"test.smallstep.com"}, time.Now(), x5cJWK,
|
|
withX5CHdr(x5cCerts))
|
|
assert.FatalError(t, err)
|
|
return test{
|
|
p: p,
|
|
token: tok,
|
|
code: http.StatusUnauthorized,
|
|
err: errors.New("x5c.AuthorizeSSHSign; x5c token must be an SSH provisioning token"),
|
|
}
|
|
},
|
|
"fail/no-SSH-subattribute-in-claims": func(t *testing.T) test {
|
|
p, err := generateX5C(nil)
|
|
assert.FatalError(t, err)
|
|
|
|
id, err := randutil.ASCII(64)
|
|
assert.FatalError(t, err)
|
|
now := time.Now()
|
|
claims := &x5cPayload{
|
|
Claims: jose.Claims{
|
|
ID: id,
|
|
Subject: "foo",
|
|
Issuer: p.GetName(),
|
|
IssuedAt: jose.NewNumericDate(now),
|
|
NotBefore: jose.NewNumericDate(now),
|
|
Expiry: jose.NewNumericDate(now.Add(5 * time.Minute)),
|
|
Audience: []string{testAudiences.SSHSign[0]},
|
|
},
|
|
Step: &stepPayload{},
|
|
}
|
|
tok, err := generateX5CSSHToken(x5cJWK, claims, withX5CHdr(x5cCerts))
|
|
assert.FatalError(t, err)
|
|
return test{
|
|
p: p,
|
|
token: tok,
|
|
code: http.StatusUnauthorized,
|
|
err: errors.New("x5c.AuthorizeSSHSign; x5c token must be an SSH provisioning token"),
|
|
}
|
|
},
|
|
"ok/with-claims": func(t *testing.T) test {
|
|
p, err := generateX5C(nil)
|
|
assert.FatalError(t, err)
|
|
|
|
id, err := randutil.ASCII(64)
|
|
assert.FatalError(t, err)
|
|
now := time.Now()
|
|
claims := &x5cPayload{
|
|
Claims: jose.Claims{
|
|
ID: id,
|
|
Subject: "foo",
|
|
Issuer: p.GetName(),
|
|
IssuedAt: jose.NewNumericDate(now),
|
|
NotBefore: jose.NewNumericDate(now),
|
|
Expiry: jose.NewNumericDate(now.Add(5 * time.Minute)),
|
|
Audience: []string{testAudiences.SSHSign[0]},
|
|
},
|
|
Step: &stepPayload{SSH: &SignSSHOptions{
|
|
CertType: SSHHostCert,
|
|
KeyID: "foo",
|
|
Principals: []string{"max", "mariano", "alan"},
|
|
ValidAfter: TimeDuration{d: 5 * time.Minute},
|
|
ValidBefore: TimeDuration{d: 10 * time.Minute},
|
|
}},
|
|
}
|
|
tok, err := generateX5CSSHToken(x5cJWK, claims, withX5CHdr(x5cCerts))
|
|
assert.FatalError(t, err)
|
|
return test{
|
|
p: p,
|
|
claims: claims,
|
|
token: tok,
|
|
}
|
|
},
|
|
"ok/without-claims": func(t *testing.T) test {
|
|
p, err := generateX5C(nil)
|
|
assert.FatalError(t, err)
|
|
|
|
id, err := randutil.ASCII(64)
|
|
assert.FatalError(t, err)
|
|
now := time.Now()
|
|
claims := &x5cPayload{
|
|
Claims: jose.Claims{
|
|
ID: id,
|
|
Subject: "foo",
|
|
Issuer: p.GetName(),
|
|
IssuedAt: jose.NewNumericDate(now),
|
|
NotBefore: jose.NewNumericDate(now),
|
|
Expiry: jose.NewNumericDate(now.Add(5 * time.Minute)),
|
|
Audience: []string{testAudiences.SSHSign[0]},
|
|
},
|
|
Step: &stepPayload{SSH: &SignSSHOptions{}},
|
|
}
|
|
tok, err := generateX5CSSHToken(x5cJWK, claims, withX5CHdr(x5cCerts))
|
|
assert.FatalError(t, err)
|
|
return test{
|
|
p: p,
|
|
claims: claims,
|
|
token: tok,
|
|
}
|
|
},
|
|
}
|
|
for name, tt := range tests {
|
|
t.Run(name, func(t *testing.T) {
|
|
tc := tt(t)
|
|
if opts, err := tc.p.AuthorizeSSHSign(context.Background(), tc.token); err != nil {
|
|
if assert.NotNil(t, tc.err) {
|
|
sc, ok := err.(errs.StatusCoder)
|
|
assert.Fatal(t, ok, "error does not implement StatusCoder interface")
|
|
assert.Equals(t, sc.StatusCode(), tc.code)
|
|
assert.HasPrefix(t, err.Error(), tc.err.Error())
|
|
}
|
|
} else {
|
|
if assert.Nil(t, tc.err) {
|
|
if assert.NotNil(t, opts) {
|
|
tot := 0
|
|
firstValidator := true
|
|
nw := now()
|
|
for _, o := range opts {
|
|
switch v := o.(type) {
|
|
case sshCertOptionsValidator:
|
|
tc.claims.Step.SSH.ValidAfter.t = time.Time{}
|
|
tc.claims.Step.SSH.ValidBefore.t = time.Time{}
|
|
if firstValidator {
|
|
assert.Equals(t, SignSSHOptions(v), *tc.claims.Step.SSH)
|
|
} else {
|
|
assert.Equals(t, SignSSHOptions(v), SignSSHOptions{KeyID: tc.claims.Subject})
|
|
}
|
|
firstValidator = false
|
|
case sshCertValidAfterModifier:
|
|
assert.Equals(t, int64(v), tc.claims.Step.SSH.ValidAfter.RelativeTime(nw).Unix())
|
|
case sshCertValidBeforeModifier:
|
|
assert.Equals(t, int64(v), tc.claims.Step.SSH.ValidBefore.RelativeTime(nw).Unix())
|
|
case sshCertDefaultsModifier:
|
|
assert.Equals(t, SignSSHOptions(v), SignSSHOptions{CertType: SSHUserCert})
|
|
case *sshLimitDuration:
|
|
assert.Equals(t, v.Claimer, tc.p.ctl.Claimer)
|
|
assert.Equals(t, v.NotAfter, x5cCerts[0].NotAfter)
|
|
case *sshCertValidityValidator:
|
|
assert.Equals(t, v.Claimer, tc.p.ctl.Claimer)
|
|
case *sshDefaultPublicKeyValidator, *sshCertDefaultValidator, sshCertificateOptionsFunc:
|
|
default:
|
|
assert.FatalError(t, errors.Errorf("unexpected sign option of type %T", v))
|
|
}
|
|
tot++
|
|
}
|
|
if len(tc.claims.Step.SSH.CertType) > 0 {
|
|
assert.Equals(t, tot, 9)
|
|
} else {
|
|
assert.Equals(t, tot, 7)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|