1
0
mirror of https://github.com/smallstep/cli.git synced 2025-04-19 10:42:15 +03:00
step-ca-cli/token/options_test.go
2024-08-06 15:54:03 +02:00

278 lines
10 KiB
Go

package token
import (
"crypto/ecdh"
"crypto/ecdsa"
"crypto/ed25519"
"crypto/elliptic"
"crypto/rand"
"net"
"os"
"path/filepath"
"testing"
"time"
nebula "github.com/slackhq/nebula/cert"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"go.step.sm/crypto/jose"
"go.step.sm/crypto/pemutil"
"go.step.sm/crypto/x25519"
"golang.org/x/crypto/ssh"
)
func TestOptions(t *testing.T) {
empty := new(Claims)
now := time.Now()
c25519CACert, c25519CAKey := mustNebulaCurve25519CA(t)
p256CACert, p256CAKey := mustNebulaP256CA(t)
c25519Cert, c25519Signer := mustNebulaCurve25519Cert(t, "test.lan", mustNebulaIPNet(t, "10.1.0.1/16"), []string{"test"}, c25519CACert, c25519CAKey)
p256Cert, p256Signer := mustNebulaP256Cert(t, "test.lan", mustNebulaIPNet(t, "10.1.0.1/16"), []string{"test"}, p256CACert, p256CAKey)
tempDir := t.TempDir()
c25519CACertFilename, c25519CACertData := serializeAndWriteNebulaCert(t, tempDir, c25519CACert)
c25519CertFilename, c25519CertData := serializeAndWriteNebulaCert(t, tempDir, c25519Cert)
p256CertFilename, p256CertData := serializeAndWriteNebulaCert(t, tempDir, p256Cert)
p256ECDHSigner, err := p256Signer.ECDH()
require.NoError(t, err)
testCSR, err := pemutil.ReadCertificateRequest("testdata/test.csr")
require.NoError(t, err)
testSSH := mustReadSSHPublicKey(t, "testdata/ssh-key.pub")
wrongNebulaContentsFilename := "testdata/ca.crt"
emptyFile, err := os.CreateTemp(tempDir, "empty-file")
require.NoError(t, err)
emptyFile.Close()
tests := []struct {
name string
option Options
want *Claims
wantErr bool
}{
{"WithClaim ok", WithClaim("name", "foo"), &Claims{ExtraClaims: map[string]interface{}{"name": "foo"}}, false},
{"WithClaim fail", WithClaim("", "foo"), empty, true},
{"WithRootCA ok", WithRootCA("testdata/ca.crt"), &Claims{ExtraClaims: map[string]interface{}{"sha": "6908751f68290d4573ae0be39a98c8b9b7b7d4e8b2a6694b7509946626adfe98"}}, false},
{"WithRootCA fail", WithRootCA("not-exists"), empty, true},
{"WithValidity ok", WithValidity(now, now.Add(5*time.Minute)), &Claims{Claims: jose.Claims{NotBefore: jose.NewNumericDate(now), Expiry: jose.NewNumericDate(now.Add(5 * time.Minute))}}, false},
{"WithRootCA expired", WithValidity(now, now.Add(-1*time.Second)), empty, true},
{"WithRootCA long delay", WithValidity(now.Add(MaxValidityDelay+time.Minute), now.Add(MaxValidityDelay+10*time.Minute)), empty, true},
{"WithRootCA min validity ok", WithValidity(now, now.Add(MinValidity)), &Claims{Claims: jose.Claims{NotBefore: jose.NewNumericDate(now), Expiry: jose.NewNumericDate(now.Add(MinValidity))}}, false},
{"WithRootCA min validity fail", WithValidity(now, now.Add(MinValidity-time.Second)), empty, true},
{"WithRootCA max validity ok", WithValidity(now, now.Add(MaxValidity)), &Claims{Claims: jose.Claims{NotBefore: jose.NewNumericDate(now), Expiry: jose.NewNumericDate(now.Add(MaxValidity))}}, false},
{"WithRootCA max validity fail", WithValidity(now, now.Add(MaxValidity+time.Second)), empty, true},
{"WithIssuer ok", WithIssuer("value"), &Claims{Claims: jose.Claims{Issuer: "value"}}, false},
{"WithIssuer fail", WithIssuer(""), empty, true},
{"WithSubject ok", WithSubject("value"), &Claims{Claims: jose.Claims{Subject: "value"}}, false},
{"WithSubject fail", WithSubject(""), empty, true},
{"WithAudience ok", WithAudience("value"), &Claims{Claims: jose.Claims{Audience: jose.Audience{"value"}}}, false},
{"WithAudience fail", WithAudience(""), empty, true},
{"WithJWTID ok", WithJWTID("value"), &Claims{Claims: jose.Claims{ID: "value"}}, false},
{"WithJWTID fail", WithJWTID(""), empty, true},
{"WithKid ok", WithKid("value"), &Claims{ExtraHeaders: map[string]interface{}{"kid": "value"}}, false},
{"WithKid fail", WithKid(""), empty, true},
{"WithSHA ok", WithSHA("6908751f68290d4573ae0be39a98c8b9b7b7d4e8b2a6694b7509946626adfe98"), &Claims{ExtraClaims: map[string]interface{}{"sha": "6908751f68290d4573ae0be39a98c8b9b7b7d4e8b2a6694b7509946626adfe98"}}, false},
{"WithNebulaCurve25519Cert ok", WithNebulaCert(c25519CertFilename, c25519Signer), &Claims{ExtraHeaders: map[string]interface{}{"nebula": c25519CertData}}, false},
{"WithNebulaCurve25519CACert ok", WithNebulaCert(c25519CACertFilename, c25519CAKey), &Claims{ExtraHeaders: map[string]interface{}{"nebula": c25519CACertData}}, false},
{"WithNebulaCurve25519Cert and key as bytes ok", WithNebulaCert(c25519CertFilename, []byte(c25519Signer)), &Claims{ExtraHeaders: map[string]interface{}{"nebula": c25519CertData}}, false},
{"WithNebulaP256Cert ok", WithNebulaCert(p256CertFilename, p256Signer), &Claims{ExtraHeaders: map[string]interface{}{"nebula": p256CertData}}, false},
{"WithNebulaP256Cert as ECDH signer ok", WithNebulaCert(p256CertFilename, p256ECDHSigner), &Claims{ExtraHeaders: map[string]interface{}{"nebula": p256CertData}}, false},
{"WithNebulaCurve25519Cert non existing file fail", WithNebulaCert(filepath.Join(tempDir, "does-not-exist"), nil), empty, true},
{"WithNebulaCurve25519Cert wrong contents fail", WithNebulaCert(wrongNebulaContentsFilename, nil), empty, true},
{"WithNebulaCurve25519Cert empty file fail", WithNebulaCert(emptyFile.Name(), nil), empty, true},
{"WithNebulaCurve25519Cert invalid content fail", WithNebulaCert(c25519CertFilename, nil), empty, true},
{"WithNebulaCurve25519Cert mismatching key fail", WithNebulaCert(c25519CertFilename, p256Signer), empty, true},
{"WithConfirmationFingerprint ok", WithConfirmationFingerprint("my-kid"), &Claims{ExtraClaims: map[string]any{"cnf": map[string]string{"x5rt#S256": "my-kid"}}}, false},
{"WithFingerprint csr ok", WithFingerprint(testCSR), &Claims{ExtraClaims: map[string]any{"cnf": map[string]string{"x5rt#S256": "ak6j6CwuZbd_mOQ-pNOUwhpmtSN0mY0xrLvaQL4J5l8"}}}, false},
{"WithFingerprint ssh ok", WithFingerprint(testSSH), &Claims{ExtraClaims: map[string]any{"cnf": map[string]string{"x5rt#S256": "hpTQOoB7fIRxTp-FhXCIm94mGBv7_dzr_5SxLn1Pnwk"}}}, false},
{"WithFingerprint fail", WithFingerprint("unexpected type"), empty, true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
claim := new(Claims)
err := tt.option(claim)
if tt.wantErr {
assert.Error(t, err)
return
}
assert.NoError(t, err)
assert.Equal(t, tt.want, claim)
})
}
}
func mustReadSSHPublicKey(t *testing.T, filename string) ssh.PublicKey {
t.Helper()
b, err := os.ReadFile(filename)
require.NoError(t, err)
pub, _, _, _, err := ssh.ParseAuthorizedKey(b)
require.NoError(t, err)
return pub
}
func serializeAndWriteNebulaCert(t *testing.T, tempDir string, cert *nebula.NebulaCertificate) (string, []byte) {
file, err := os.CreateTemp(tempDir, "nebula-test-cert-*")
require.NoError(t, err)
defer file.Close()
pem, err := cert.MarshalToPEM()
require.NoError(t, err)
data, err := cert.Marshal()
require.NoError(t, err)
_, err = file.Write(pem)
require.NoError(t, err)
return file.Name(), data
}
func mustNebulaIPNet(t *testing.T, s string) *net.IPNet {
t.Helper()
ip, ipNet, err := net.ParseCIDR(s)
require.NoError(t, err)
if ip = ip.To4(); ip == nil {
require.Failf(t, "nebula only supports ipv4, have %s", s)
}
ipNet.IP = ip
return ipNet
}
func mustNebulaCurve25519CA(t *testing.T) (*nebula.NebulaCertificate, ed25519.PrivateKey) {
t.Helper()
pub, priv, err := ed25519.GenerateKey(rand.Reader)
require.NoError(t, err)
nc := &nebula.NebulaCertificate{
Details: nebula.NebulaCertificateDetails{
Name: "TestCA",
Groups: []string{"test"},
Ips: []*net.IPNet{
mustNebulaIPNet(t, "10.1.0.0/16"),
},
Subnets: []*net.IPNet{},
NotBefore: time.Now(),
NotAfter: time.Now().Add(10 * time.Minute),
PublicKey: pub,
IsCA: true,
Curve: nebula.Curve_CURVE25519,
},
}
require.NoError(t, nc.Sign(nebula.Curve_CURVE25519, priv))
return nc, priv
}
func mustNebulaP256CA(t *testing.T) (*nebula.NebulaCertificate, *ecdh.PrivateKey) {
t.Helper()
priv, err := ecdh.P256().GenerateKey(rand.Reader)
require.NoError(t, err)
nc := &nebula.NebulaCertificate{
Details: nebula.NebulaCertificateDetails{
Name: "TestCA",
Groups: []string{"test"},
Ips: []*net.IPNet{
mustNebulaIPNet(t, "10.1.0.0/16"),
},
Subnets: []*net.IPNet{},
NotBefore: time.Now(),
NotAfter: time.Now().Add(10 * time.Minute),
PublicKey: priv.PublicKey().Bytes(),
IsCA: true,
Curve: nebula.Curve_P256,
},
}
require.NoError(t, nc.Sign(nebula.Curve_P256, priv.Bytes()))
return nc, priv
}
func mustNebulaCurve25519Cert(t *testing.T, name string, ipNet *net.IPNet, groups []string, ca *nebula.NebulaCertificate, signer ed25519.PrivateKey) (*nebula.NebulaCertificate, x25519.PrivateKey) {
t.Helper()
pub, priv, err := x25519.GenerateKey(rand.Reader)
require.NoError(t, err)
issuer, err := ca.Sha256Sum()
require.NoError(t, err)
invertedGroups := make(map[string]struct{}, len(groups))
for _, name := range groups {
invertedGroups[name] = struct{}{}
}
key := []byte(signer)
curve := nebula.Curve_CURVE25519
t1 := time.Now().Truncate(time.Second)
nc := &nebula.NebulaCertificate{
Details: nebula.NebulaCertificateDetails{
Name: name,
Ips: []*net.IPNet{ipNet},
Subnets: []*net.IPNet{},
Groups: groups,
NotBefore: t1,
NotAfter: t1.Add(5 * time.Minute),
PublicKey: pub,
IsCA: false,
Issuer: issuer,
InvertedGroups: invertedGroups,
Curve: curve,
},
}
require.NoError(t, nc.Sign(curve, key))
return nc, priv
}
func mustNebulaP256Cert(t *testing.T, name string, ipNet *net.IPNet, groups []string, ca *nebula.NebulaCertificate, signer *ecdh.PrivateKey) (*nebula.NebulaCertificate, *ecdsa.PrivateKey) {
t.Helper()
priv, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
require.NoError(t, err)
issuer, err := ca.Sha256Sum()
require.NoError(t, err)
invertedGroups := make(map[string]struct{}, len(groups))
for _, name := range groups {
invertedGroups[name] = struct{}{}
}
key := signer.Bytes()
curve := nebula.Curve_P256
pk, err := priv.ECDH()
require.NoError(t, err)
t1 := time.Now().Truncate(time.Second)
nc := &nebula.NebulaCertificate{
Details: nebula.NebulaCertificateDetails{
Name: name,
Ips: []*net.IPNet{ipNet},
Subnets: []*net.IPNet{},
Groups: groups,
NotBefore: t1,
NotAfter: t1.Add(5 * time.Minute),
PublicKey: pk.PublicKey().Bytes(),
IsCA: false,
Issuer: issuer,
InvertedGroups: invertedGroups,
Curve: curve,
},
}
require.NoError(t, nc.Sign(curve, key))
return nc, priv
}