Merge pull request #785 from smallstep/nebulous
Add initial implementation of a nebula provisionerpull/794/head
commit
57f9e54151
@ -0,0 +1,460 @@
|
|||||||
|
package provisioner
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"crypto/ed25519"
|
||||||
|
"crypto/x509"
|
||||||
|
"encoding/base64"
|
||||||
|
"net"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
nebula "github.com/slackhq/nebula/cert"
|
||||||
|
"github.com/smallstep/certificates/errs"
|
||||||
|
"go.step.sm/crypto/jose"
|
||||||
|
"go.step.sm/crypto/sshutil"
|
||||||
|
"go.step.sm/crypto/x25519"
|
||||||
|
"go.step.sm/crypto/x509util"
|
||||||
|
"golang.org/x/crypto/ssh"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
// NebulaCertHeader is the token header that contains a Nebula certificate.
|
||||||
|
NebulaCertHeader jose.HeaderKey = "nebula"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Nebula is a provisioner that verifies tokens signed using Nebula private
|
||||||
|
// keys. The tokens contain a Nebula certificate in the header, which can be
|
||||||
|
// used to verify the token signature. The certificates are themselves verified
|
||||||
|
// using the Nebula CA certificates encoded in Roots. The verification process
|
||||||
|
// is similar to the process for X5C tokens.
|
||||||
|
//
|
||||||
|
// Because Nebula "leaf" certificates use X25519 keys, the tokens are signed
|
||||||
|
// using XEd25519 defined at
|
||||||
|
// https://signal.org/docs/specifications/xeddsa/#xeddsa and implemented by
|
||||||
|
// go.step.sm/crypto/x25519.
|
||||||
|
type Nebula struct {
|
||||||
|
ID string `json:"-"`
|
||||||
|
Type string `json:"type"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Roots []byte `json:"roots"`
|
||||||
|
Claims *Claims `json:"claims,omitempty"`
|
||||||
|
Options *Options `json:"options,omitempty"`
|
||||||
|
claimer *Claimer
|
||||||
|
caPool *nebula.NebulaCAPool
|
||||||
|
audiences Audiences
|
||||||
|
}
|
||||||
|
|
||||||
|
// Init verifies and initializes the Nebula provisioner.
|
||||||
|
func (p *Nebula) Init(config Config) error {
|
||||||
|
switch {
|
||||||
|
case p.Type == "":
|
||||||
|
return errors.New("provisioner type cannot be empty")
|
||||||
|
case p.Name == "":
|
||||||
|
return errors.New("provisioner name cannot be empty")
|
||||||
|
case len(p.Roots) == 0:
|
||||||
|
return errors.New("provisioner root(s) cannot be empty")
|
||||||
|
}
|
||||||
|
|
||||||
|
var err error
|
||||||
|
if p.claimer, err = NewClaimer(p.Claims, config.Claims); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
p.caPool, err = nebula.NewCAPoolFromBytes(p.Roots)
|
||||||
|
if err != nil {
|
||||||
|
return errs.InternalServer("failed to create ca pool: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
p.audiences = config.Audiences.WithFragment(p.GetIDForToken())
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetID returns the provisioner id.
|
||||||
|
func (p *Nebula) GetID() string {
|
||||||
|
if p.ID != "" {
|
||||||
|
return p.ID
|
||||||
|
}
|
||||||
|
return p.GetIDForToken()
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetIDForToken returns an identifier that will be used to load the provisioner
|
||||||
|
// from a token.
|
||||||
|
func (p *Nebula) GetIDForToken() string {
|
||||||
|
return "nebula/" + p.Name
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetTokenID returns the identifier of the token.
|
||||||
|
func (p *Nebula) GetTokenID(token string) (string, error) {
|
||||||
|
// Validate payload
|
||||||
|
t, err := jose.ParseSigned(token)
|
||||||
|
if err != nil {
|
||||||
|
return "", errors.Wrap(err, "error parsing token")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get claims w/out verification. We need to look up the provisioner
|
||||||
|
// key in order to verify the claims and we need the issuer from the claims
|
||||||
|
// before we can look up the provisioner.
|
||||||
|
var claims jose.Claims
|
||||||
|
if err = t.UnsafeClaimsWithoutVerification(&claims); err != nil {
|
||||||
|
return "", errors.Wrap(err, "error verifying claims")
|
||||||
|
}
|
||||||
|
return claims.ID, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetName returns the name of the provisioner.
|
||||||
|
func (p *Nebula) GetName() string {
|
||||||
|
return p.Name
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetType returns the type of provisioner.
|
||||||
|
func (p *Nebula) GetType() Type {
|
||||||
|
return TypeNebula
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetEncryptedKey returns the base provisioner encrypted key if it's defined.
|
||||||
|
func (p *Nebula) GetEncryptedKey() (kid, key string, ok bool) {
|
||||||
|
return "", "", false
|
||||||
|
}
|
||||||
|
|
||||||
|
// AuthorizeSign returns the list of SignOption for a Sign request.
|
||||||
|
func (p *Nebula) AuthorizeSign(ctx context.Context, token string) ([]SignOption, error) {
|
||||||
|
crt, claims, err := p.authorizeToken(token, p.audiences.Sign)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
sans := claims.SANs
|
||||||
|
if len(sans) == 0 {
|
||||||
|
sans = make([]string, len(crt.Details.Ips)+1)
|
||||||
|
sans[0] = crt.Details.Name
|
||||||
|
for i, ipnet := range crt.Details.Ips {
|
||||||
|
sans[i+1] = ipnet.IP.String()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
data := x509util.CreateTemplateData(claims.Subject, sans)
|
||||||
|
if v, err := unsafeParseSigned(token); err == nil {
|
||||||
|
data.SetToken(v)
|
||||||
|
}
|
||||||
|
|
||||||
|
// The Nebula certificate will be available using the template variable Crt.
|
||||||
|
// For example {{ .Crt.Details.Groups }} can be used to get all the groups.
|
||||||
|
data.SetAuthorizationCertificate(crt)
|
||||||
|
|
||||||
|
templateOptions, err := TemplateOptions(p.Options, data)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return []SignOption{
|
||||||
|
templateOptions,
|
||||||
|
// modifiers / withOptions
|
||||||
|
newProvisionerExtensionOption(TypeNebula, p.Name, ""),
|
||||||
|
profileLimitDuration{
|
||||||
|
def: p.claimer.DefaultTLSCertDuration(),
|
||||||
|
notBefore: crt.Details.NotBefore,
|
||||||
|
notAfter: crt.Details.NotAfter,
|
||||||
|
},
|
||||||
|
// validators
|
||||||
|
commonNameValidator(claims.Subject),
|
||||||
|
nebulaSANsValidator{
|
||||||
|
Name: crt.Details.Name,
|
||||||
|
IPs: crt.Details.Ips,
|
||||||
|
},
|
||||||
|
defaultPublicKeyValidator{},
|
||||||
|
newValidityValidator(p.claimer.MinTLSCertDuration(), p.claimer.MaxTLSCertDuration()),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// AuthorizeSSHSign returns the list of SignOption for a SignSSH request.
|
||||||
|
// Currently the Nebula provisioner only grants host SSH certificates.
|
||||||
|
func (p *Nebula) AuthorizeSSHSign(ctx context.Context, token string) ([]SignOption, error) {
|
||||||
|
if !p.claimer.IsSSHCAEnabled() {
|
||||||
|
return nil, errs.Unauthorized("ssh is disabled for nebula provisioner '%s'", p.Name)
|
||||||
|
}
|
||||||
|
|
||||||
|
crt, claims, err := p.authorizeToken(token, p.audiences.SSHSign)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Default template attributes.
|
||||||
|
keyID := claims.Subject
|
||||||
|
principals := make([]string, len(crt.Details.Ips)+1)
|
||||||
|
principals[0] = crt.Details.Name
|
||||||
|
for i, ipnet := range crt.Details.Ips {
|
||||||
|
principals[i+1] = ipnet.IP.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
var signOptions []SignOption
|
||||||
|
// If step ssh options are given, validate them and set key id, principals
|
||||||
|
// and validity.
|
||||||
|
if claims.Step != nil && claims.Step.SSH != nil {
|
||||||
|
opts := claims.Step.SSH
|
||||||
|
|
||||||
|
// Check that the token only contains valid principals.
|
||||||
|
v := nebulaPrincipalsValidator{
|
||||||
|
Name: crt.Details.Name,
|
||||||
|
IPs: crt.Details.Ips,
|
||||||
|
}
|
||||||
|
if err := v.Valid(*opts); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
// Check that the cert type is a valid one.
|
||||||
|
if opts.CertType != "" && opts.CertType != SSHHostCert {
|
||||||
|
return nil, errs.Forbidden("ssh certificate type does not match - got %v, want %v", opts.CertType, SSHHostCert)
|
||||||
|
}
|
||||||
|
|
||||||
|
signOptions = []SignOption{
|
||||||
|
// validate is a host certificate and users's KeyID is the subject.
|
||||||
|
sshCertOptionsValidator(SignSSHOptions{
|
||||||
|
CertType: SSHHostCert,
|
||||||
|
KeyID: claims.Subject,
|
||||||
|
}),
|
||||||
|
// validates user's SSHOptions with the ones in the token
|
||||||
|
sshCertOptionsValidator(*opts),
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use options in the token.
|
||||||
|
if opts.KeyID != "" {
|
||||||
|
keyID = opts.KeyID
|
||||||
|
}
|
||||||
|
if len(opts.Principals) > 0 {
|
||||||
|
principals = opts.Principals
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add modifiers from custom claims
|
||||||
|
t := now()
|
||||||
|
if !opts.ValidAfter.IsZero() {
|
||||||
|
signOptions = append(signOptions, sshCertValidAfterModifier(opts.ValidAfter.RelativeTime(t).Unix()))
|
||||||
|
}
|
||||||
|
if !opts.ValidBefore.IsZero() {
|
||||||
|
signOptions = append(signOptions, sshCertValidBeforeModifier(opts.ValidBefore.RelativeTime(t).Unix()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Certificate templates.
|
||||||
|
data := sshutil.CreateTemplateData(sshutil.HostCert, keyID, principals)
|
||||||
|
if v, err := unsafeParseSigned(token); err == nil {
|
||||||
|
data.SetToken(v)
|
||||||
|
}
|
||||||
|
|
||||||
|
// The Nebula certificate will be available using the template variable Crt.
|
||||||
|
// For example {{ .AuthorizationCrt.Details.Groups }} can be used to get all the groups.
|
||||||
|
data.SetAuthorizationCertificate(crt)
|
||||||
|
|
||||||
|
templateOptions, err := TemplateSSHOptions(p.Options, data)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return append(signOptions,
|
||||||
|
templateOptions,
|
||||||
|
// Checks the validity bounds, and set the validity if has not been set.
|
||||||
|
&sshLimitDuration{p.claimer, crt.Details.NotAfter},
|
||||||
|
// Validate public key.
|
||||||
|
&sshDefaultPublicKeyValidator{},
|
||||||
|
// Validate the validity period.
|
||||||
|
&sshCertValidityValidator{p.claimer},
|
||||||
|
// Require all the fields in the SSH certificate
|
||||||
|
&sshCertDefaultValidator{},
|
||||||
|
), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// AuthorizeRenew returns an error if the renewal is disabled.
|
||||||
|
func (p *Nebula) AuthorizeRenew(ctx context.Context, crt *x509.Certificate) error {
|
||||||
|
if p.claimer.IsDisableRenewal() {
|
||||||
|
return errs.Unauthorized("renew is disabled for nebula provisioner '%s'", p.GetName())
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// AuthorizeRevoke returns an error if the token is not valid.
|
||||||
|
func (p *Nebula) AuthorizeRevoke(ctx context.Context, token string) error {
|
||||||
|
return p.validateToken(token, p.audiences.Revoke)
|
||||||
|
}
|
||||||
|
|
||||||
|
// AuthorizeSSHRevoke returns an error if SSH is disabled or the token is invalid.
|
||||||
|
func (p *Nebula) AuthorizeSSHRevoke(ctx context.Context, token string) error {
|
||||||
|
if !p.claimer.IsSSHCAEnabled() {
|
||||||
|
return errs.Unauthorized("ssh is disabled for nebula provisioner '%s'", p.Name)
|
||||||
|
}
|
||||||
|
if _, _, err := p.authorizeToken(token, p.audiences.SSHRevoke); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// AuthorizeSSHRenew returns an unauthorized error.
|
||||||
|
func (p *Nebula) AuthorizeSSHRenew(ctx context.Context, token string) (*ssh.Certificate, error) {
|
||||||
|
return nil, errs.Unauthorized("nebula provisioner does not support SSH renew")
|
||||||
|
}
|
||||||
|
|
||||||
|
// AuthorizeSSHRekey returns an unauthorized error.
|
||||||
|
func (p *Nebula) AuthorizeSSHRekey(ctx context.Context, token string) (*ssh.Certificate, []SignOption, error) {
|
||||||
|
return nil, nil, errs.Unauthorized("nebula provisioner does not support SSH rekey")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Nebula) validateToken(token string, audiences []string) error {
|
||||||
|
_, _, err := p.authorizeToken(token, audiences)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Nebula) authorizeToken(token string, audiences []string) (*nebula.NebulaCertificate, *jwtPayload, error) {
|
||||||
|
jwt, err := jose.ParseSigned(token)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, errs.UnauthorizedErr(err, errs.WithMessage("failed to parse token"))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract Nebula certificate
|
||||||
|
h, ok := jwt.Headers[0].ExtraHeaders[NebulaCertHeader]
|
||||||
|
if !ok {
|
||||||
|
return nil, nil, errs.Unauthorized("failed to parse token: nebula header is missing")
|
||||||
|
}
|
||||||
|
s, ok := h.(string)
|
||||||
|
if !ok {
|
||||||
|
return nil, nil, errs.Unauthorized("failed to parse token: nebula header is not valid")
|
||||||
|
}
|
||||||
|
b, err := base64.StdEncoding.DecodeString(s)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, errs.UnauthorizedErr(err, errs.WithMessage("failed to parse token: nebula header is not valid"))
|
||||||
|
}
|
||||||
|
c, err := nebula.UnmarshalNebulaCertificate(b)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, errs.UnauthorizedErr(err, errs.WithMessage("failed to parse nebula certificate: nebula header is not valid"))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate nebula certificate against CAs
|
||||||
|
if valid, err := c.Verify(now(), p.caPool); !valid {
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, errs.UnauthorizedErr(err, errs.WithMessage("token is not valid: failed to verify certificate against configured CA"))
|
||||||
|
}
|
||||||
|
return nil, nil, errs.Unauthorized("token is not valid: failed to verify certificate against configured CA")
|
||||||
|
}
|
||||||
|
|
||||||
|
var pub interface{}
|
||||||
|
if c.Details.IsCA {
|
||||||
|
pub = ed25519.PublicKey(c.Details.PublicKey)
|
||||||
|
} else {
|
||||||
|
pub = x25519.PublicKey(c.Details.PublicKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate token with public key
|
||||||
|
var claims jwtPayload
|
||||||
|
if err := jose.Verify(jwt, pub, &claims); err != nil {
|
||||||
|
return nil, nil, errs.UnauthorizedErr(err, errs.WithMessage("token is not valid: signature does not match"))
|
||||||
|
}
|
||||||
|
|
||||||
|
// According to "rfc7519 JSON Web Token" acceptable skew should be no
|
||||||
|
// more than a few minutes.
|
||||||
|
if err = claims.ValidateWithLeeway(jose.Expected{
|
||||||
|
Issuer: p.Name,
|
||||||
|
Time: now(),
|
||||||
|
}, time.Minute); err != nil {
|
||||||
|
return nil, nil, errs.UnauthorizedErr(err, errs.WithMessage("token is not valid: invalid claims"))
|
||||||
|
}
|
||||||
|
// Validate token and subject too.
|
||||||
|
if !matchesAudience(claims.Audience, audiences) {
|
||||||
|
return nil, nil, errs.Unauthorized("token is not valid: invalid claims")
|
||||||
|
}
|
||||||
|
if claims.Subject == "" {
|
||||||
|
return nil, nil, errs.Unauthorized("token is not valid: subject cannot be empty")
|
||||||
|
}
|
||||||
|
|
||||||
|
return c, &claims, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type nebulaSANsValidator struct {
|
||||||
|
Name string
|
||||||
|
IPs []*net.IPNet
|
||||||
|
}
|
||||||
|
|
||||||
|
// Valid verifies that the SANs stored in the validator are contained with those
|
||||||
|
// requested in the x509 certificate request.
|
||||||
|
func (v nebulaSANsValidator) Valid(req *x509.CertificateRequest) error {
|
||||||
|
dnsNames, ips, emails, uris := x509util.SplitSANs([]string{v.Name})
|
||||||
|
if len(req.DNSNames) > 0 {
|
||||||
|
if err := dnsNamesValidator(dnsNames).Valid(req); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(req.EmailAddresses) > 0 {
|
||||||
|
if err := emailAddressesValidator(emails).Valid(req); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(req.URIs) > 0 {
|
||||||
|
if err := urisValidator(uris).Valid(req); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(req.IPAddresses) > 0 {
|
||||||
|
for _, ip := range req.IPAddresses {
|
||||||
|
var valid bool
|
||||||
|
// Check ip in name
|
||||||
|
for _, ipInName := range ips {
|
||||||
|
if ip.Equal(ipInName) {
|
||||||
|
valid = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Check ip network
|
||||||
|
if !valid {
|
||||||
|
for _, ipNet := range v.IPs {
|
||||||
|
if ip.Equal(ipNet.IP) {
|
||||||
|
valid = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !valid {
|
||||||
|
for _, ipNet := range v.IPs {
|
||||||
|
ips = append(ips, ipNet.IP)
|
||||||
|
}
|
||||||
|
return errs.Forbidden("certificate request contains invalid IP addresses - got %v, want %v", req.IPAddresses, ips)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type nebulaPrincipalsValidator struct {
|
||||||
|
Name string
|
||||||
|
IPs []*net.IPNet
|
||||||
|
}
|
||||||
|
|
||||||
|
// Valid checks that the SignSSHOptions principals contains only names in the
|
||||||
|
// Nebula certificate.
|
||||||
|
func (v nebulaPrincipalsValidator) Valid(got SignSSHOptions) error {
|
||||||
|
for _, p := range got.Principals {
|
||||||
|
var valid bool
|
||||||
|
if p == v.Name {
|
||||||
|
valid = true
|
||||||
|
}
|
||||||
|
if !valid {
|
||||||
|
if ip := net.ParseIP(p); ip != nil {
|
||||||
|
for _, ipnet := range v.IPs {
|
||||||
|
if ip.Equal(ipnet.IP) {
|
||||||
|
valid = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !valid {
|
||||||
|
ips := make([]net.IP, len(v.IPs))
|
||||||
|
for i, ipNet := range v.IPs {
|
||||||
|
ips[i] = ipNet.IP
|
||||||
|
}
|
||||||
|
return errs.Forbidden(
|
||||||
|
"ssh certificate principals contains invalid name or IP addresses - got %v, want %s or %v",
|
||||||
|
got.Principals, v.Name, ips,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
@ -0,0 +1,955 @@
|
|||||||
|
package provisioner
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"crypto"
|
||||||
|
"crypto/ed25519"
|
||||||
|
"crypto/rand"
|
||||||
|
"crypto/x509"
|
||||||
|
"net"
|
||||||
|
"net/url"
|
||||||
|
"reflect"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/google/go-cmp/cmp"
|
||||||
|
"github.com/slackhq/nebula/cert"
|
||||||
|
"go.step.sm/crypto/jose"
|
||||||
|
"go.step.sm/crypto/randutil"
|
||||||
|
"go.step.sm/crypto/x25519"
|
||||||
|
"go.step.sm/crypto/x509util"
|
||||||
|
"golang.org/x/crypto/ssh"
|
||||||
|
)
|
||||||
|
|
||||||
|
func mustNebulaIPNet(t *testing.T, s string) *net.IPNet {
|
||||||
|
t.Helper()
|
||||||
|
ip, ipNet, err := net.ParseCIDR(s)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if ip = ip.To4(); ip == nil {
|
||||||
|
t.Fatalf("nebula only supports ipv4, have %s", s)
|
||||||
|
}
|
||||||
|
ipNet.IP = ip
|
||||||
|
return ipNet
|
||||||
|
}
|
||||||
|
|
||||||
|
func mustNebulaCA(t *testing.T) (*cert.NebulaCertificate, ed25519.PrivateKey) {
|
||||||
|
t.Helper()
|
||||||
|
pub, priv, err := ed25519.GenerateKey(rand.Reader)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
nc := &cert.NebulaCertificate{
|
||||||
|
Details: cert.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,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
if err := nc.Sign(priv); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
return nc, priv
|
||||||
|
}
|
||||||
|
|
||||||
|
func mustNebulaCert(t *testing.T, name string, ipNet *net.IPNet, groups []string, ca *cert.NebulaCertificate, signer ed25519.PrivateKey) (*cert.NebulaCertificate, crypto.Signer) {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
pub, priv, err := x25519.GenerateKey(rand.Reader)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
issuer, err := ca.Sha256Sum()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
invertedGroups := make(map[string]struct{}, len(groups))
|
||||||
|
for _, name := range groups {
|
||||||
|
invertedGroups[name] = struct{}{}
|
||||||
|
}
|
||||||
|
|
||||||
|
t1 := time.Now().Truncate(time.Second)
|
||||||
|
nc := &cert.NebulaCertificate{
|
||||||
|
Details: cert.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,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := nc.Sign(signer); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nc, priv
|
||||||
|
}
|
||||||
|
|
||||||
|
func mustNebulaProvisioner(t *testing.T) (*Nebula, *cert.NebulaCertificate, ed25519.PrivateKey) {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
nc, signer := mustNebulaCA(t)
|
||||||
|
ncPem, err := nc.MarshalToPEM()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
bTrue := true
|
||||||
|
p := &Nebula{
|
||||||
|
Type: TypeNebula.String(),
|
||||||
|
Name: "nebulous",
|
||||||
|
Roots: ncPem,
|
||||||
|
Claims: &Claims{
|
||||||
|
EnableSSHCA: &bTrue,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
if err := p.Init(Config{
|
||||||
|
Claims: globalProvisionerClaims,
|
||||||
|
Audiences: testAudiences,
|
||||||
|
}); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return p, nc, signer
|
||||||
|
}
|
||||||
|
|
||||||
|
func mustNebulaToken(t *testing.T, sub, iss, aud string, iat time.Time, sans []string, nc *cert.NebulaCertificate, key crypto.Signer) string {
|
||||||
|
t.Helper()
|
||||||
|
ncDer, err := nc.Marshal()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
so := new(jose.SignerOptions)
|
||||||
|
so.WithType("JWT")
|
||||||
|
so.WithHeader(NebulaCertHeader, ncDer)
|
||||||
|
|
||||||
|
sig, err := jose.NewSigner(jose.SigningKey{Algorithm: jose.XEdDSA, Key: key}, so)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
id, err := randutil.ASCII(64)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
claims := struct {
|
||||||
|
jose.Claims
|
||||||
|
SANS []string `json:"sans"`
|
||||||
|
}{
|
||||||
|
Claims: jose.Claims{
|
||||||
|
ID: id,
|
||||||
|
Subject: sub,
|
||||||
|
Issuer: iss,
|
||||||
|
IssuedAt: jose.NewNumericDate(iat),
|
||||||
|
NotBefore: jose.NewNumericDate(iat),
|
||||||
|
Expiry: jose.NewNumericDate(iat.Add(5 * time.Minute)),
|
||||||
|
Audience: []string{aud},
|
||||||
|
},
|
||||||
|
SANS: sans,
|
||||||
|
}
|
||||||
|
tok, err := jose.Signed(sig).Claims(claims).CompactSerialize()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
return tok
|
||||||
|
}
|
||||||
|
|
||||||
|
func mustNebulaSSHToken(t *testing.T, sub, iss, aud string, iat time.Time, opts *SignSSHOptions, nc *cert.NebulaCertificate, key crypto.Signer) string {
|
||||||
|
t.Helper()
|
||||||
|
ncDer, err := nc.Marshal()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
so := new(jose.SignerOptions)
|
||||||
|
so.WithType("JWT")
|
||||||
|
so.WithHeader(NebulaCertHeader, ncDer)
|
||||||
|
|
||||||
|
sig, err := jose.NewSigner(jose.SigningKey{Algorithm: jose.XEdDSA, Key: key}, so)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
id, err := randutil.ASCII(64)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
claims := struct {
|
||||||
|
jose.Claims
|
||||||
|
Step *stepPayload `json:"step,omitempty"`
|
||||||
|
}{
|
||||||
|
Claims: jose.Claims{
|
||||||
|
ID: id,
|
||||||
|
Subject: sub,
|
||||||
|
Issuer: iss,
|
||||||
|
IssuedAt: jose.NewNumericDate(iat),
|
||||||
|
NotBefore: jose.NewNumericDate(iat),
|
||||||
|
Expiry: jose.NewNumericDate(iat.Add(5 * time.Minute)),
|
||||||
|
Audience: []string{aud},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
if opts != nil {
|
||||||
|
claims.Step = &stepPayload{
|
||||||
|
SSH: opts,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
tok, err := jose.Signed(sig).Claims(claims).CompactSerialize()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
return tok
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNebula_Init(t *testing.T) {
|
||||||
|
nc, _ := mustNebulaCA(t)
|
||||||
|
ncPem, err := nc.MarshalToPEM()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
cfg := Config{
|
||||||
|
Claims: globalProvisionerClaims,
|
||||||
|
Audiences: testAudiences,
|
||||||
|
}
|
||||||
|
|
||||||
|
type fields struct {
|
||||||
|
Type string
|
||||||
|
Name string
|
||||||
|
Roots []byte
|
||||||
|
Claims *Claims
|
||||||
|
Options *Options
|
||||||
|
}
|
||||||
|
type args struct {
|
||||||
|
config Config
|
||||||
|
}
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
fields fields
|
||||||
|
args args
|
||||||
|
wantErr bool
|
||||||
|
}{
|
||||||
|
{"ok", fields{"Nebula", "Nebulous", ncPem, nil, nil}, args{cfg}, false},
|
||||||
|
{"ok with claims", fields{"Nebula", "Nebulous", ncPem, &Claims{DefaultTLSDur: &Duration{Duration: time.Hour}}, nil}, args{cfg}, false},
|
||||||
|
{"ok with options", fields{"Nebula", "Nebulous", ncPem, nil, &Options{X509: &X509Options{Template: x509util.DefaultLeafTemplate}}}, args{cfg}, false},
|
||||||
|
{"fail type", fields{"", "Nebulous", ncPem, nil, nil}, args{cfg}, true},
|
||||||
|
{"fail name", fields{"Nebula", "", ncPem, nil, nil}, args{cfg}, true},
|
||||||
|
{"fail root", fields{"Nebula", "Nebulous", nil, nil, nil}, args{cfg}, true},
|
||||||
|
{"fail bad root", fields{"Nebula", "Nebulous", ncPem[:16], nil, nil}, args{cfg}, true},
|
||||||
|
{"fail bad claims", fields{"Nebula", "Nebulous", ncPem, &Claims{
|
||||||
|
MinTLSDur: &Duration{Duration: 0},
|
||||||
|
}, nil}, args{cfg}, true},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
p := &Nebula{
|
||||||
|
Type: tt.fields.Type,
|
||||||
|
Name: tt.fields.Name,
|
||||||
|
Roots: tt.fields.Roots,
|
||||||
|
Claims: tt.fields.Claims,
|
||||||
|
Options: tt.fields.Options,
|
||||||
|
}
|
||||||
|
if err := p.Init(tt.args.config); (err != nil) != tt.wantErr {
|
||||||
|
t.Errorf("Nebula.Init() error = %v, wantErr %v", err, tt.wantErr)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNebula_GetID(t *testing.T) {
|
||||||
|
type fields struct {
|
||||||
|
ID string
|
||||||
|
Name string
|
||||||
|
}
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
fields fields
|
||||||
|
want string
|
||||||
|
}{
|
||||||
|
{"ok with id", fields{"1234", "nebulous"}, "1234"},
|
||||||
|
{"ok with name", fields{"", "nebulous"}, "nebula/nebulous"},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
p := &Nebula{
|
||||||
|
ID: tt.fields.ID,
|
||||||
|
Name: tt.fields.Name,
|
||||||
|
}
|
||||||
|
if got := p.GetID(); got != tt.want {
|
||||||
|
t.Errorf("Nebula.GetID() = %v, want %v", got, tt.want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNebula_GetIDForToken(t *testing.T) {
|
||||||
|
type fields struct {
|
||||||
|
Name string
|
||||||
|
}
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
fields fields
|
||||||
|
want string
|
||||||
|
}{
|
||||||
|
{"ok", fields{"nebulous"}, "nebula/nebulous"},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
p := &Nebula{
|
||||||
|
Name: tt.fields.Name,
|
||||||
|
}
|
||||||
|
if got := p.GetIDForToken(); got != tt.want {
|
||||||
|
t.Errorf("Nebula.GetIDForToken() = %v, want %v", got, tt.want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNebula_GetTokenID(t *testing.T) {
|
||||||
|
p, ca, signer := mustNebulaProvisioner(t)
|
||||||
|
c1, priv := mustNebulaCert(t, "test.lan", mustNebulaIPNet(t, "10.1.0.1/16"), []string{"group"}, ca, signer)
|
||||||
|
t1 := mustNebulaToken(t, "test.lan", p.Name, p.audiences.Sign[0], now(), []string{"test.lan", "10.1.0.1"}, c1, priv)
|
||||||
|
_, claims, err := parseToken(t1)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
type args struct {
|
||||||
|
token string
|
||||||
|
}
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
p *Nebula
|
||||||
|
args args
|
||||||
|
want string
|
||||||
|
wantErr bool
|
||||||
|
}{
|
||||||
|
{"ok", p, args{t1}, claims.ID, false},
|
||||||
|
{"fail parse", p, args{"token"}, "", true},
|
||||||
|
{"fail claims", p, args{func() string {
|
||||||
|
parts := strings.Split(t1, ".")
|
||||||
|
|
||||||
|
return parts[0] + ".eyIifQ." + parts[1]
|
||||||
|
}()}, "", true},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
got, err := tt.p.GetTokenID(tt.args.token)
|
||||||
|
if (err != nil) != tt.wantErr {
|
||||||
|
t.Errorf("Nebula.GetTokenID() error = %v, wantErr %v", err, tt.wantErr)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if got != tt.want {
|
||||||
|
t.Errorf("Nebula.GetTokenID() = %v, want %v", got, tt.want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNebula_GetName(t *testing.T) {
|
||||||
|
type fields struct {
|
||||||
|
Name string
|
||||||
|
}
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
fields fields
|
||||||
|
want string
|
||||||
|
}{
|
||||||
|
{"ok", fields{"nebulous"}, "nebulous"},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
p := &Nebula{
|
||||||
|
Name: tt.fields.Name,
|
||||||
|
}
|
||||||
|
if got := p.GetName(); got != tt.want {
|
||||||
|
t.Errorf("Nebula.GetName() = %v, want %v", got, tt.want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNebula_GetType(t *testing.T) {
|
||||||
|
type fields struct {
|
||||||
|
Type string
|
||||||
|
}
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
fields fields
|
||||||
|
want Type
|
||||||
|
}{
|
||||||
|
{"ok", fields{"Nebula"}, TypeNebula},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
p := &Nebula{
|
||||||
|
Type: tt.fields.Type,
|
||||||
|
}
|
||||||
|
if got := p.GetType(); got != tt.want {
|
||||||
|
t.Errorf("Nebula.GetType() = %v, want %v", got, tt.want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNebula_GetEncryptedKey(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
p *Nebula
|
||||||
|
wantKid string
|
||||||
|
wantKey string
|
||||||
|
wantOk bool
|
||||||
|
}{
|
||||||
|
{"ok", &Nebula{}, "", "", false},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
gotKid, gotKey, gotOk := tt.p.GetEncryptedKey()
|
||||||
|
if gotKid != tt.wantKid {
|
||||||
|
t.Errorf("Nebula.GetEncryptedKey() gotKid = %v, want %v", gotKid, tt.wantKid)
|
||||||
|
}
|
||||||
|
if gotKey != tt.wantKey {
|
||||||
|
t.Errorf("Nebula.GetEncryptedKey() gotKey = %v, want %v", gotKey, tt.wantKey)
|
||||||
|
}
|
||||||
|
if gotOk != tt.wantOk {
|
||||||
|
t.Errorf("Nebula.GetEncryptedKey() gotOk = %v, want %v", gotOk, tt.wantOk)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNebula_AuthorizeSign(t *testing.T) {
|
||||||
|
ctx := context.TODO()
|
||||||
|
p, ca, signer := mustNebulaProvisioner(t)
|
||||||
|
crt, priv := mustNebulaCert(t, "test.lan", mustNebulaIPNet(t, "10.1.0.1/16"), []string{"test"}, ca, signer)
|
||||||
|
ok := mustNebulaToken(t, "test.lan", p.Name, p.audiences.Sign[0], now(), []string{"test.lan", "10.1.0.1"}, crt, priv)
|
||||||
|
okNoSANs := mustNebulaToken(t, "test.lan", p.Name, p.audiences.Sign[0], now(), nil, crt, priv)
|
||||||
|
|
||||||
|
pBadOptions, _, _ := mustNebulaProvisioner(t)
|
||||||
|
pBadOptions.caPool = p.caPool
|
||||||
|
pBadOptions.Options = &Options{
|
||||||
|
X509: &X509Options{
|
||||||
|
TemplateData: []byte(`{""}`),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
type args struct {
|
||||||
|
ctx context.Context
|
||||||
|
token string
|
||||||
|
}
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
p *Nebula
|
||||||
|
args args
|
||||||
|
wantErr bool
|
||||||
|
}{
|
||||||
|
{"ok", p, args{ctx, ok}, false},
|
||||||
|
{"ok no sans", p, args{ctx, okNoSANs}, false},
|
||||||
|
{"fail token", p, args{ctx, "token"}, true},
|
||||||
|
{"fail template", pBadOptions, args{ctx, ok}, true},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
_, err := tt.p.AuthorizeSign(tt.args.ctx, tt.args.token)
|
||||||
|
if (err != nil) != tt.wantErr {
|
||||||
|
t.Errorf("Nebula.AuthorizeSign() error = %v, wantErr %v", err, tt.wantErr)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNebula_AuthorizeSSHSign(t *testing.T) {
|
||||||
|
ctx := context.TODO()
|
||||||
|
// Ok provisioner
|
||||||
|
p, ca, signer := mustNebulaProvisioner(t)
|
||||||
|
crt, priv := mustNebulaCert(t, "test.lan", mustNebulaIPNet(t, "10.1.0.1/16"), []string{"test"}, ca, signer)
|
||||||
|
ok := mustNebulaSSHToken(t, "test.lan", p.Name, p.audiences.SSHSign[0], now(), &SignSSHOptions{
|
||||||
|
CertType: "host",
|
||||||
|
KeyID: "test.lan",
|
||||||
|
Principals: []string{"test.lan", "10.1.0.1"},
|
||||||
|
}, crt, priv)
|
||||||
|
okNoOptions := mustNebulaSSHToken(t, "test.lan", p.Name, p.audiences.SSHSign[0], now(), nil, crt, priv)
|
||||||
|
okWithValidity := mustNebulaSSHToken(t, "test.lan", p.Name, p.audiences.SSHSign[0], now(), &SignSSHOptions{
|
||||||
|
ValidAfter: NewTimeDuration(now().Add(1 * time.Hour)),
|
||||||
|
ValidBefore: NewTimeDuration(now().Add(10 * time.Hour)),
|
||||||
|
}, crt, priv)
|
||||||
|
failUserCert := mustNebulaSSHToken(t, "test.lan", p.Name, p.audiences.SSHSign[0], now(), &SignSSHOptions{
|
||||||
|
CertType: "user",
|
||||||
|
}, crt, priv)
|
||||||
|
failPrincipals := mustNebulaSSHToken(t, "test.lan", p.Name, p.audiences.SSHSign[0], now(), &SignSSHOptions{
|
||||||
|
CertType: "host",
|
||||||
|
KeyID: "test.lan",
|
||||||
|
Principals: []string{"test.lan", "10.1.0.1", "foo.bar"},
|
||||||
|
}, crt, priv)
|
||||||
|
|
||||||
|
// Provisioner with SSH disabled
|
||||||
|
var bFalse bool
|
||||||
|
pDisabled, _, _ := mustNebulaProvisioner(t)
|
||||||
|
pDisabled.caPool = p.caPool
|
||||||
|
pDisabled.Claims.EnableSSHCA = &bFalse
|
||||||
|
|
||||||
|
// Provisioner with bad templates
|
||||||
|
pBadOptions, _, _ := mustNebulaProvisioner(t)
|
||||||
|
pBadOptions.caPool = p.caPool
|
||||||
|
pBadOptions.Options = &Options{
|
||||||
|
SSH: &SSHOptions{
|
||||||
|
TemplateData: []byte(`{""}`),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
type args struct {
|
||||||
|
ctx context.Context
|
||||||
|
token string
|
||||||
|
}
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
p *Nebula
|
||||||
|
args args
|
||||||
|
wantErr bool
|
||||||
|
}{
|
||||||
|
{"ok", p, args{ctx, ok}, false},
|
||||||
|
{"ok no options", p, args{ctx, okNoOptions}, false},
|
||||||
|
{"ok with validity", p, args{ctx, okWithValidity}, false},
|
||||||
|
{"fail token", p, args{ctx, "token"}, true},
|
||||||
|
{"fail user", p, args{ctx, failUserCert}, true},
|
||||||
|
{"fail principals", p, args{ctx, failPrincipals}, true},
|
||||||
|
{"fail disabled", pDisabled, args{ctx, ok}, true},
|
||||||
|
{"fail template", pBadOptions, args{ctx, ok}, true},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
_, err := tt.p.AuthorizeSSHSign(tt.args.ctx, tt.args.token)
|
||||||
|
if (err != nil) != tt.wantErr {
|
||||||
|
t.Errorf("Nebula.AuthorizeSSHSign() error = %v, wantErr %v", err, tt.wantErr)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNebula_AuthorizeRenew(t *testing.T) {
|
||||||
|
ctx := context.TODO()
|
||||||
|
// Ok provisioner
|
||||||
|
p, _, _ := mustNebulaProvisioner(t)
|
||||||
|
|
||||||
|
// Provisioner with renewal disabled
|
||||||
|
bTrue := true
|
||||||
|
pDisabled, _, _ := mustNebulaProvisioner(t)
|
||||||
|
pDisabled.Claims.DisableRenewal = &bTrue
|
||||||
|
|
||||||
|
type args struct {
|
||||||
|
ctx context.Context
|
||||||
|
crt *x509.Certificate
|
||||||
|
}
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
p *Nebula
|
||||||
|
args args
|
||||||
|
wantErr bool
|
||||||
|
}{
|
||||||
|
{"ok", p, args{ctx, &x509.Certificate{}}, false},
|
||||||
|
{"fail disabled", pDisabled, args{ctx, &x509.Certificate{}}, true},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
if err := tt.p.AuthorizeRenew(tt.args.ctx, tt.args.crt); (err != nil) != tt.wantErr {
|
||||||
|
t.Errorf("Nebula.AuthorizeRenew() error = %v, wantErr %v", err, tt.wantErr)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNebula_AuthorizeRevoke(t *testing.T) {
|
||||||
|
ctx := context.TODO()
|
||||||
|
// Ok provisioner
|
||||||
|
p, ca, signer := mustNebulaProvisioner(t)
|
||||||
|
crt, priv := mustNebulaCert(t, "test.lan", mustNebulaIPNet(t, "10.1.0.1/16"), []string{"test"}, ca, signer)
|
||||||
|
ok := mustNebulaToken(t, "test.lan", p.Name, p.audiences.Revoke[0], now(), nil, crt, priv)
|
||||||
|
|
||||||
|
// Fail different CA
|
||||||
|
nc, signer := mustNebulaCA(t)
|
||||||
|
crt, priv = mustNebulaCert(t, "test.lan", mustNebulaIPNet(t, "10.1.0.1/16"), []string{"test"}, nc, signer)
|
||||||
|
failToken := mustNebulaToken(t, "test.lan", p.Name, p.audiences.Revoke[0], now(), nil, crt, priv)
|
||||||
|
|
||||||
|
type args struct {
|
||||||
|
ctx context.Context
|
||||||
|
token string
|
||||||
|
}
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
p *Nebula
|
||||||
|
args args
|
||||||
|
wantErr bool
|
||||||
|
}{
|
||||||
|
{"ok", p, args{ctx, ok}, false},
|
||||||
|
{"fail token", p, args{ctx, failToken}, true},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
if err := tt.p.AuthorizeRevoke(tt.args.ctx, tt.args.token); (err != nil) != tt.wantErr {
|
||||||
|
t.Errorf("Nebula.AuthorizeRevoke() error = %v, wantErr %v", err, tt.wantErr)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNebula_AuthorizeSSHRevoke(t *testing.T) {
|
||||||
|
ctx := context.TODO()
|
||||||
|
// Ok provisioner
|
||||||
|
p, ca, signer := mustNebulaProvisioner(t)
|
||||||
|
crt, priv := mustNebulaCert(t, "test.lan", mustNebulaIPNet(t, "10.1.0.1/16"), []string{"test"}, ca, signer)
|
||||||
|
ok := mustNebulaSSHToken(t, "test.lan", p.Name, p.audiences.SSHRevoke[0], now(), nil, crt, priv)
|
||||||
|
|
||||||
|
// Fail different CA
|
||||||
|
nc, signer := mustNebulaCA(t)
|
||||||
|
crt, priv = mustNebulaCert(t, "test.lan", mustNebulaIPNet(t, "10.1.0.1/16"), []string{"test"}, nc, signer)
|
||||||
|
failToken := mustNebulaSSHToken(t, "test.lan", p.Name, p.audiences.SSHRevoke[0], now(), nil, crt, priv)
|
||||||
|
|
||||||
|
// Provisioner with SSH disabled
|
||||||
|
var bFalse bool
|
||||||
|
pDisabled, _, _ := mustNebulaProvisioner(t)
|
||||||
|
pDisabled.caPool = p.caPool
|
||||||
|
pDisabled.Claims.EnableSSHCA = &bFalse
|
||||||
|
|
||||||
|
type args struct {
|
||||||
|
ctx context.Context
|
||||||
|
token string
|
||||||
|
}
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
p *Nebula
|
||||||
|
args args
|
||||||
|
wantErr bool
|
||||||
|
}{
|
||||||
|
{"ok", p, args{ctx, ok}, false},
|
||||||
|
{"fail token", p, args{ctx, failToken}, true},
|
||||||
|
{"fail disabled", pDisabled, args{ctx, ok}, true},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
if err := tt.p.AuthorizeSSHRevoke(tt.args.ctx, tt.args.token); (err != nil) != tt.wantErr {
|
||||||
|
t.Errorf("Nebula.AuthorizeSSHRevoke() error = %v, wantErr %v", err, tt.wantErr)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNebula_AuthorizeSSHRenew(t *testing.T) {
|
||||||
|
p, ca, signer := mustNebulaProvisioner(t)
|
||||||
|
crt, priv := mustNebulaCert(t, "test.lan", mustNebulaIPNet(t, "10.1.0.1/16"), []string{"test"}, ca, signer)
|
||||||
|
t1 := mustNebulaSSHToken(t, "test.lan", p.Name, p.audiences.SSHRenew[0], now(), nil, crt, priv)
|
||||||
|
|
||||||
|
type args struct {
|
||||||
|
ctx context.Context
|
||||||
|
token string
|
||||||
|
}
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
p *Nebula
|
||||||
|
args args
|
||||||
|
want *ssh.Certificate
|
||||||
|
wantErr bool
|
||||||
|
}{
|
||||||
|
{"fail", p, args{context.TODO(), t1}, nil, true},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
got, err := tt.p.AuthorizeSSHRenew(tt.args.ctx, tt.args.token)
|
||||||
|
if (err != nil) != tt.wantErr {
|
||||||
|
t.Errorf("Nebula.AuthorizeSSHRenew() error = %v, wantErr %v", err, tt.wantErr)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if !reflect.DeepEqual(got, tt.want) {
|
||||||
|
t.Errorf("Nebula.AuthorizeSSHRenew() = %v, want %v", got, tt.want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNebula_AuthorizeSSHRekey(t *testing.T) {
|
||||||
|
p, ca, signer := mustNebulaProvisioner(t)
|
||||||
|
crt, priv := mustNebulaCert(t, "test.lan", mustNebulaIPNet(t, "10.1.0.1/16"), []string{"test"}, ca, signer)
|
||||||
|
t1 := mustNebulaSSHToken(t, "test.lan", p.Name, p.audiences.SSHRekey[0], now(), nil, crt, priv)
|
||||||
|
|
||||||
|
type args struct {
|
||||||
|
ctx context.Context
|
||||||
|
token string
|
||||||
|
}
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
p *Nebula
|
||||||
|
args args
|
||||||
|
want *ssh.Certificate
|
||||||
|
want1 []SignOption
|
||||||
|
wantErr bool
|
||||||
|
}{
|
||||||
|
{"fail", p, args{context.TODO(), t1}, nil, nil, true},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
got, got1, err := tt.p.AuthorizeSSHRekey(tt.args.ctx, tt.args.token)
|
||||||
|
if (err != nil) != tt.wantErr {
|
||||||
|
t.Errorf("Nebula.AuthorizeSSHRekey() error = %v, wantErr %v", err, tt.wantErr)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if !reflect.DeepEqual(got, tt.want) {
|
||||||
|
t.Errorf("Nebula.AuthorizeSSHRekey() got = %v, want %v", got, tt.want)
|
||||||
|
}
|
||||||
|
if !reflect.DeepEqual(got1, tt.want1) {
|
||||||
|
t.Errorf("Nebula.AuthorizeSSHRekey() got1 = %v, want %v", got1, tt.want1)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNebula_authorizeToken(t *testing.T) {
|
||||||
|
t1 := now()
|
||||||
|
p, ca, signer := mustNebulaProvisioner(t)
|
||||||
|
crt, priv := mustNebulaCert(t, "test.lan", mustNebulaIPNet(t, "10.1.0.1/16"), []string{"test"}, ca, signer)
|
||||||
|
ok := mustNebulaToken(t, "test.lan", p.Name, p.audiences.Sign[0], t1, []string{"10.1.0.1"}, crt, priv)
|
||||||
|
okNoSANs := mustNebulaToken(t, "test.lan", p.Name, p.audiences.Sign[0], t1, nil, crt, priv)
|
||||||
|
okSSH := mustNebulaSSHToken(t, "test.lan", p.Name, p.audiences.SSHSign[0], t1, &SignSSHOptions{
|
||||||
|
CertType: "host",
|
||||||
|
KeyID: "test.lan",
|
||||||
|
Principals: []string{"test.lan"},
|
||||||
|
}, crt, priv)
|
||||||
|
okSSHNoOptions := mustNebulaSSHToken(t, "test.lan", p.Name, p.audiences.SSHSign[0], t1, nil, crt, priv)
|
||||||
|
|
||||||
|
// Token with errors
|
||||||
|
failNotBefore := mustNebulaToken(t, "test.lan", p.Name, p.audiences.Sign[0], t1.Add(1*time.Hour), []string{"10.1.0.1"}, crt, priv)
|
||||||
|
failIssuer := mustNebulaToken(t, "test.lan", "foo", p.audiences.Sign[0], t1, []string{"10.1.0.1"}, crt, priv)
|
||||||
|
failAudience := mustNebulaToken(t, "test.lan", p.Name, "foo", t1, []string{"10.1.0.1"}, crt, priv)
|
||||||
|
failSubject := mustNebulaToken(t, "", p.Name, p.audiences.Sign[0], t1, []string{"10.1.0.1"}, crt, priv)
|
||||||
|
|
||||||
|
// Not a nebula token
|
||||||
|
jwk, err := generateJSONWebKey()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
simpleToken, err := generateSimpleToken("iss", "aud", jwk)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Provisioner with a different CA
|
||||||
|
p2, _, _ := mustNebulaProvisioner(t)
|
||||||
|
|
||||||
|
x509Claims := jose.Claims{
|
||||||
|
ID: "[REPLACEME]",
|
||||||
|
Subject: "test.lan",
|
||||||
|
Issuer: p.Name,
|
||||||
|
IssuedAt: jose.NewNumericDate(t1),
|
||||||
|
NotBefore: jose.NewNumericDate(t1),
|
||||||
|
Expiry: jose.NewNumericDate(t1.Add(5 * time.Minute)),
|
||||||
|
Audience: []string{p.audiences.Sign[0]},
|
||||||
|
}
|
||||||
|
sshClaims := jose.Claims{
|
||||||
|
ID: "[REPLACEME]",
|
||||||
|
Subject: "test.lan",
|
||||||
|
Issuer: p.Name,
|
||||||
|
IssuedAt: jose.NewNumericDate(t1),
|
||||||
|
NotBefore: jose.NewNumericDate(t1),
|
||||||
|
Expiry: jose.NewNumericDate(t1.Add(5 * time.Minute)),
|
||||||
|
Audience: []string{p.audiences.SSHSign[0]},
|
||||||
|
}
|
||||||
|
|
||||||
|
type args struct {
|
||||||
|
token string
|
||||||
|
audiences []string
|
||||||
|
}
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
p *Nebula
|
||||||
|
args args
|
||||||
|
want *cert.NebulaCertificate
|
||||||
|
want1 *jwtPayload
|
||||||
|
wantErr bool
|
||||||
|
}{
|
||||||
|
{"ok x509", p, args{ok, p.audiences.Sign}, crt, &jwtPayload{
|
||||||
|
Claims: x509Claims,
|
||||||
|
SANs: []string{"10.1.0.1"},
|
||||||
|
}, false},
|
||||||
|
{"ok x509 no sans", p, args{okNoSANs, p.audiences.Sign}, crt, &jwtPayload{
|
||||||
|
Claims: x509Claims,
|
||||||
|
}, false},
|
||||||
|
{"ok ssh", p, args{okSSH, p.audiences.SSHSign}, crt, &jwtPayload{
|
||||||
|
Claims: sshClaims,
|
||||||
|
Step: &stepPayload{
|
||||||
|
SSH: &SignSSHOptions{
|
||||||
|
CertType: "host",
|
||||||
|
KeyID: "test.lan",
|
||||||
|
Principals: []string{"test.lan"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}, false},
|
||||||
|
{"ok ssh no principals", p, args{okSSHNoOptions, p.audiences.SSHSign}, crt, &jwtPayload{
|
||||||
|
Claims: sshClaims,
|
||||||
|
}, false},
|
||||||
|
{"fail parse", p, args{"bad.token", p.audiences.Sign}, nil, nil, true},
|
||||||
|
{"fail header", p, args{simpleToken, p.audiences.Sign}, nil, nil, true},
|
||||||
|
{"fail verify", p2, args{ok, p.audiences.Sign}, nil, nil, true},
|
||||||
|
{"fail claims nbf", p, args{failNotBefore, p.audiences.Sign}, nil, nil, true},
|
||||||
|
{"fail claims iss", p, args{failIssuer, p.audiences.Sign}, nil, nil, true},
|
||||||
|
{"fail claims aud", p, args{failAudience, p.audiences.Sign}, nil, nil, true},
|
||||||
|
{"fail claims sub", p, args{failSubject, p.audiences.Sign}, nil, nil, true},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
got, got1, err := tt.p.authorizeToken(tt.args.token, tt.args.audiences)
|
||||||
|
if (err != nil) != tt.wantErr {
|
||||||
|
t.Errorf("Nebula.authorizeToken() error = %v, wantErr %v", err, tt.wantErr)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if !reflect.DeepEqual(got, tt.want) {
|
||||||
|
t.Errorf("Nebula.authorizeToken() got = %#v, want %#v", got, tt.want)
|
||||||
|
t.Error(cmp.Equal(got, tt.want))
|
||||||
|
}
|
||||||
|
|
||||||
|
if got1 != nil && tt.want1 != nil {
|
||||||
|
tt.want1.ID = got1.ID
|
||||||
|
}
|
||||||
|
|
||||||
|
if !reflect.DeepEqual(got1, tt.want1) {
|
||||||
|
t.Errorf("Nebula.authorizeToken() got1 = %v, want %v", got1, tt.want1)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_nebulaSANsValidator_Valid(t *testing.T) {
|
||||||
|
ipNet := mustNebulaIPNet(t, "10.1.2.3/16")
|
||||||
|
type fields struct {
|
||||||
|
Name string
|
||||||
|
IPs []*net.IPNet
|
||||||
|
}
|
||||||
|
type args struct {
|
||||||
|
req *x509.CertificateRequest
|
||||||
|
}
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
fields fields
|
||||||
|
args args
|
||||||
|
wantErr bool
|
||||||
|
}{
|
||||||
|
{"ok", fields{"dns.name", []*net.IPNet{ipNet}}, args{&x509.CertificateRequest{
|
||||||
|
DNSNames: []string{"dns.name"},
|
||||||
|
IPAddresses: []net.IP{net.IPv4(10, 1, 2, 3)},
|
||||||
|
}}, false},
|
||||||
|
{"ok name only", fields{"dns.name", []*net.IPNet{ipNet}}, args{&x509.CertificateRequest{
|
||||||
|
DNSNames: []string{"dns.name"},
|
||||||
|
}}, false},
|
||||||
|
{"ok ip only", fields{"dns.name", []*net.IPNet{ipNet}}, args{&x509.CertificateRequest{
|
||||||
|
IPAddresses: []net.IP{net.IPv4(10, 1, 2, 3)},
|
||||||
|
}}, false},
|
||||||
|
{"ok email name", fields{"jane@doe.org", []*net.IPNet{ipNet}}, args{&x509.CertificateRequest{
|
||||||
|
EmailAddresses: []string{"jane@doe.org"},
|
||||||
|
IPAddresses: []net.IP{net.IPv4(10, 1, 2, 3)},
|
||||||
|
}}, false},
|
||||||
|
{"ok uri name", fields{"urn:foobar", []*net.IPNet{ipNet}}, args{&x509.CertificateRequest{
|
||||||
|
URIs: []*url.URL{{Scheme: "urn", Opaque: "foobar"}},
|
||||||
|
IPAddresses: []net.IP{net.IPv4(10, 1, 2, 3)},
|
||||||
|
}}, false},
|
||||||
|
{"ok ip name", fields{"127.0.0.1", []*net.IPNet{ipNet}}, args{&x509.CertificateRequest{
|
||||||
|
IPAddresses: []net.IP{net.IPv4(127, 0, 0, 1), net.IPv4(10, 1, 2, 3)},
|
||||||
|
}}, false},
|
||||||
|
{"ok multiple ips", fields{"dns.name", []*net.IPNet{ipNet, mustNebulaIPNet(t, "10.2.2.3/8")}}, args{&x509.CertificateRequest{
|
||||||
|
DNSNames: []string{"dns.name"},
|
||||||
|
IPAddresses: []net.IP{net.IPv4(10, 1, 2, 3), net.IPv4(10, 2, 2, 3)},
|
||||||
|
}}, false},
|
||||||
|
{"fail dns", fields{"fail.name", []*net.IPNet{ipNet}}, args{&x509.CertificateRequest{
|
||||||
|
DNSNames: []string{"dns.name"},
|
||||||
|
IPAddresses: []net.IP{net.IPv4(10, 1, 2, 3)},
|
||||||
|
}}, true},
|
||||||
|
{"fail email", fields{"fail@doe.org", []*net.IPNet{ipNet}}, args{&x509.CertificateRequest{
|
||||||
|
EmailAddresses: []string{"jane@doe.org"},
|
||||||
|
IPAddresses: []net.IP{net.IPv4(10, 1, 2, 3)},
|
||||||
|
}}, true},
|
||||||
|
{"fail uri", fields{"urn:barfoo", []*net.IPNet{ipNet}}, args{&x509.CertificateRequest{
|
||||||
|
URIs: []*url.URL{{Scheme: "urn", Opaque: "foobar"}},
|
||||||
|
IPAddresses: []net.IP{net.IPv4(10, 1, 2, 3)},
|
||||||
|
}}, true},
|
||||||
|
{"fail ip", fields{"127.0.0.1", []*net.IPNet{ipNet}}, args{&x509.CertificateRequest{
|
||||||
|
IPAddresses: []net.IP{net.IPv4(10, 1, 2, 1), net.IPv4(10, 1, 2, 3)},
|
||||||
|
}}, true},
|
||||||
|
{"fail nebula ip", fields{"dns.name", []*net.IPNet{ipNet}}, args{&x509.CertificateRequest{
|
||||||
|
DNSNames: []string{"dns.name"},
|
||||||
|
IPAddresses: []net.IP{net.IPv4(10, 2, 2, 3)},
|
||||||
|
}}, true},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
v := nebulaSANsValidator{
|
||||||
|
Name: tt.fields.Name,
|
||||||
|
IPs: tt.fields.IPs,
|
||||||
|
}
|
||||||
|
if err := v.Valid(tt.args.req); (err != nil) != tt.wantErr {
|
||||||
|
t.Errorf("nebulaSANsValidator.Valid() error = %v, wantErr %v", err, tt.wantErr)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_nebulaPrincipalsValidator_Valid(t *testing.T) {
|
||||||
|
ipNet := mustNebulaIPNet(t, "10.1.2.3/16")
|
||||||
|
|
||||||
|
type fields struct {
|
||||||
|
Name string
|
||||||
|
IPs []*net.IPNet
|
||||||
|
}
|
||||||
|
type args struct {
|
||||||
|
got SignSSHOptions
|
||||||
|
}
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
fields fields
|
||||||
|
args args
|
||||||
|
wantErr bool
|
||||||
|
}{
|
||||||
|
{"ok", fields{"dns.name", []*net.IPNet{ipNet}}, args{SignSSHOptions{
|
||||||
|
Principals: []string{"dns.name", "10.1.2.3"},
|
||||||
|
}}, false},
|
||||||
|
{"ok name", fields{"dns.name", []*net.IPNet{ipNet}}, args{SignSSHOptions{
|
||||||
|
Principals: []string{"dns.name"},
|
||||||
|
}}, false},
|
||||||
|
{"ok ip", fields{"dns.name", []*net.IPNet{ipNet}}, args{SignSSHOptions{
|
||||||
|
Principals: []string{"10.1.2.3"},
|
||||||
|
}}, false},
|
||||||
|
{"fail name", fields{"dns.name", []*net.IPNet{ipNet}}, args{SignSSHOptions{
|
||||||
|
Principals: []string{"foo.name", "10.1.2.3"},
|
||||||
|
}}, true},
|
||||||
|
{"fail ip", fields{"dns.name", []*net.IPNet{ipNet}}, args{SignSSHOptions{
|
||||||
|
Principals: []string{"dns.name", "10.2.2.3"},
|
||||||
|
}}, true},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
v := nebulaPrincipalsValidator{
|
||||||
|
Name: tt.fields.Name,
|
||||||
|
IPs: tt.fields.IPs,
|
||||||
|
}
|
||||||
|
if err := v.Valid(tt.args.got); (err != nil) != tt.wantErr {
|
||||||
|
t.Errorf("nebulaPrincipalsValidator.Valid() error = %v, wantErr %v", err, tt.wantErr)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue