mirror of
https://github.com/smallstep/certificates.git
synced 2024-10-31 03:20:16 +00:00
10f6a901ec
When the RA mode with StepCAS is used, let the CA decide which lifetime the RA should get instead of requiring always 24h. This commit also fixes linter warnings. Related to #1094
827 lines
27 KiB
Go
827 lines
27 KiB
Go
package provisioner
|
|
|
|
import (
|
|
"context"
|
|
"crypto/x509"
|
|
"errors"
|
|
"fmt"
|
|
"net/http"
|
|
"testing"
|
|
"time"
|
|
|
|
"go.step.sm/crypto/jose"
|
|
"go.step.sm/crypto/pemutil"
|
|
"go.step.sm/crypto/randutil"
|
|
"go.step.sm/linkedca"
|
|
|
|
"github.com/smallstep/assert"
|
|
"github.com/smallstep/certificates/api/render"
|
|
)
|
|
|
|
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.New("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 {
|
|
//nolint:staticcheck // We don't have a different way to
|
|
// check the number of certificates in the pool.
|
|
numCerts := len(p.rootPool.Subjects())
|
|
if numCerts != 2 {
|
|
return fmt.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) {
|
|
var sc render.StatusCodedError
|
|
if assert.True(t, errors.As(err, &sc), "error does not implement StatusCodedError 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)
|
|
ctx := NewContextWithMethod(context.Background(), SignIdentityMethod)
|
|
if opts, err := tc.p.AuthorizeSign(ctx, tc.token); err != nil {
|
|
if assert.NotNil(t, tc.err) {
|
|
var sc render.StatusCodedError
|
|
if assert.True(t, errors.As(err, &sc), "error does not implement StatusCodedError 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, 10, len(opts))
|
|
for _, o := range opts {
|
|
switch v := o.(type) {
|
|
case *X5C:
|
|
case certificateOptionsFunc:
|
|
case *provisionerExtensionOption:
|
|
assert.Equals(t, v.Type, 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, v.sans, tc.sans)
|
|
assert.Equals(t, MethodFromContext(v.ctx), SignIdentityMethod)
|
|
case *validityValidator:
|
|
assert.Equals(t, v.min, tc.p.ctl.Claimer.MinTLSCertDuration())
|
|
assert.Equals(t, v.max, tc.p.ctl.Claimer.MaxTLSCertDuration())
|
|
case *x509NamePolicyValidator:
|
|
assert.Equals(t, nil, v.policyEngine)
|
|
case *WebhookController:
|
|
assert.Len(t, 0, v.webhooks)
|
|
assert.Equals(t, linkedca.Webhook_X509, v.certType)
|
|
assert.Len(t, 2, v.options)
|
|
default:
|
|
assert.FatalError(t, fmt.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) {
|
|
var sc render.StatusCodedError
|
|
if assert.True(t, errors.As(err, &sc), "error does not implement StatusCodedError 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().Truncate(time.Second)
|
|
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: fmt.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) {
|
|
var sc render.StatusCodedError
|
|
if assert.True(t, errors.As(err, &sc), "error does not implement StatusCodedError 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: fmt.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) {
|
|
var sc render.StatusCodedError
|
|
if assert.True(t, errors.As(err, &sc), "error does not implement StatusCodedError 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 Interface:
|
|
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 *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 *sshNamePolicyValidator:
|
|
assert.Equals(t, nil, v.userPolicyEngine)
|
|
assert.Equals(t, nil, v.hostPolicyEngine)
|
|
case *sshDefaultPublicKeyValidator, *sshCertDefaultValidator, sshCertificateOptionsFunc:
|
|
case *WebhookController:
|
|
assert.Len(t, 0, v.webhooks)
|
|
assert.Equals(t, linkedca.Webhook_SSH, v.certType)
|
|
assert.Len(t, 2, v.options)
|
|
default:
|
|
assert.FatalError(t, fmt.Errorf("unexpected sign option of type %T", v))
|
|
}
|
|
tot++
|
|
}
|
|
if tc.claims.Step.SSH.CertType != "" {
|
|
assert.Equals(t, tot, 12)
|
|
} else {
|
|
assert.Equals(t, tot, 10)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|