github.com/slackhq/nebula@v1.9.0/cmd/nebula-cert/ca_test.go (about)

     1  //go:build !windows
     2  // +build !windows
     3  
     4  package main
     5  
     6  import (
     7  	"bytes"
     8  	"encoding/pem"
     9  	"errors"
    10  	"os"
    11  	"strings"
    12  	"testing"
    13  	"time"
    14  
    15  	"github.com/slackhq/nebula/cert"
    16  	"github.com/stretchr/testify/assert"
    17  )
    18  
    19  //TODO: test file permissions
    20  
    21  func Test_caSummary(t *testing.T) {
    22  	assert.Equal(t, "ca <flags>: create a self signed certificate authority", caSummary())
    23  }
    24  
    25  func Test_caHelp(t *testing.T) {
    26  	ob := &bytes.Buffer{}
    27  	caHelp(ob)
    28  	assert.Equal(
    29  		t,
    30  		"Usage of "+os.Args[0]+" ca <flags>: create a self signed certificate authority\n"+
    31  			"  -argon-iterations uint\n"+
    32  			"    \tOptional: Argon2 iterations parameter used for encrypted private key passphrase (default 1)\n"+
    33  			"  -argon-memory uint\n"+
    34  			"    \tOptional: Argon2 memory parameter (in KiB) used for encrypted private key passphrase (default 2097152)\n"+
    35  			"  -argon-parallelism uint\n"+
    36  			"    \tOptional: Argon2 parallelism parameter used for encrypted private key passphrase (default 4)\n"+
    37  			"  -curve string\n"+
    38  			"    \tEdDSA/ECDSA Curve (25519, P256) (default \"25519\")\n"+
    39  			"  -duration duration\n"+
    40  			"    \tOptional: amount of time the certificate should be valid for. Valid time units are seconds: \"s\", minutes: \"m\", hours: \"h\" (default 8760h0m0s)\n"+
    41  			"  -encrypt\n"+
    42  			"    \tOptional: prompt for passphrase and write out-key in an encrypted format\n"+
    43  			"  -groups string\n"+
    44  			"    \tOptional: comma separated list of groups. This will limit which groups subordinate certs can use\n"+
    45  			"  -ips string\n"+
    46  			"    \tOptional: comma separated list of ipv4 address and network in CIDR notation. This will limit which ipv4 addresses and networks subordinate certs can use for ip addresses\n"+
    47  			"  -name string\n"+
    48  			"    \tRequired: name of the certificate authority\n"+
    49  			"  -out-crt string\n"+
    50  			"    \tOptional: path to write the certificate to (default \"ca.crt\")\n"+
    51  			"  -out-key string\n"+
    52  			"    \tOptional: path to write the private key to (default \"ca.key\")\n"+
    53  			"  -out-qr string\n"+
    54  			"    \tOptional: output a qr code image (png) of the certificate\n"+
    55  			"  -subnets string\n"+
    56  			"    \tOptional: comma separated list of ipv4 address and network in CIDR notation. This will limit which ipv4 addresses and networks subordinate certs can use in subnets\n",
    57  		ob.String(),
    58  	)
    59  }
    60  
    61  func Test_ca(t *testing.T) {
    62  	ob := &bytes.Buffer{}
    63  	eb := &bytes.Buffer{}
    64  
    65  	nopw := &StubPasswordReader{
    66  		password: []byte(""),
    67  		err:      nil,
    68  	}
    69  
    70  	errpw := &StubPasswordReader{
    71  		password: []byte(""),
    72  		err:      errors.New("stub error"),
    73  	}
    74  
    75  	passphrase := []byte("DO NOT USE THIS KEY")
    76  	testpw := &StubPasswordReader{
    77  		password: passphrase,
    78  		err:      nil,
    79  	}
    80  
    81  	pwPromptOb := "Enter passphrase: "
    82  
    83  	// required args
    84  	assertHelpError(t, ca(
    85  		[]string{"-out-key", "nope", "-out-crt", "nope", "duration", "100m"}, ob, eb, nopw,
    86  	), "-name is required")
    87  	assert.Equal(t, "", ob.String())
    88  	assert.Equal(t, "", eb.String())
    89  
    90  	// ipv4 only ips
    91  	assertHelpError(t, ca([]string{"-name", "ipv6", "-ips", "100::100/100"}, ob, eb, nopw), "invalid ip definition: can only be ipv4, have 100::100/100")
    92  	assert.Equal(t, "", ob.String())
    93  	assert.Equal(t, "", eb.String())
    94  
    95  	// ipv4 only subnets
    96  	assertHelpError(t, ca([]string{"-name", "ipv6", "-subnets", "100::100/100"}, ob, eb, nopw), "invalid subnet definition: can only be ipv4, have 100::100/100")
    97  	assert.Equal(t, "", ob.String())
    98  	assert.Equal(t, "", eb.String())
    99  
   100  	// failed key write
   101  	ob.Reset()
   102  	eb.Reset()
   103  	args := []string{"-name", "test", "-duration", "100m", "-out-crt", "/do/not/write/pleasecrt", "-out-key", "/do/not/write/pleasekey"}
   104  	assert.EqualError(t, ca(args, ob, eb, nopw), "error while writing out-key: open /do/not/write/pleasekey: "+NoSuchDirError)
   105  	assert.Equal(t, "", ob.String())
   106  	assert.Equal(t, "", eb.String())
   107  
   108  	// create temp key file
   109  	keyF, err := os.CreateTemp("", "test.key")
   110  	assert.Nil(t, err)
   111  	os.Remove(keyF.Name())
   112  
   113  	// failed cert write
   114  	ob.Reset()
   115  	eb.Reset()
   116  	args = []string{"-name", "test", "-duration", "100m", "-out-crt", "/do/not/write/pleasecrt", "-out-key", keyF.Name()}
   117  	assert.EqualError(t, ca(args, ob, eb, nopw), "error while writing out-crt: open /do/not/write/pleasecrt: "+NoSuchDirError)
   118  	assert.Equal(t, "", ob.String())
   119  	assert.Equal(t, "", eb.String())
   120  
   121  	// create temp cert file
   122  	crtF, err := os.CreateTemp("", "test.crt")
   123  	assert.Nil(t, err)
   124  	os.Remove(crtF.Name())
   125  	os.Remove(keyF.Name())
   126  
   127  	// test proper cert with removed empty groups and subnets
   128  	ob.Reset()
   129  	eb.Reset()
   130  	args = []string{"-name", "test", "-duration", "100m", "-groups", "1,,   2    ,        ,,,3,4,5", "-out-crt", crtF.Name(), "-out-key", keyF.Name()}
   131  	assert.Nil(t, ca(args, ob, eb, nopw))
   132  	assert.Equal(t, "", ob.String())
   133  	assert.Equal(t, "", eb.String())
   134  
   135  	// read cert and key files
   136  	rb, _ := os.ReadFile(keyF.Name())
   137  	lKey, b, err := cert.UnmarshalEd25519PrivateKey(rb)
   138  	assert.Len(t, b, 0)
   139  	assert.Nil(t, err)
   140  	assert.Len(t, lKey, 64)
   141  
   142  	rb, _ = os.ReadFile(crtF.Name())
   143  	lCrt, b, err := cert.UnmarshalNebulaCertificateFromPEM(rb)
   144  	assert.Len(t, b, 0)
   145  	assert.Nil(t, err)
   146  
   147  	assert.Equal(t, "test", lCrt.Details.Name)
   148  	assert.Len(t, lCrt.Details.Ips, 0)
   149  	assert.True(t, lCrt.Details.IsCA)
   150  	assert.Equal(t, []string{"1", "2", "3", "4", "5"}, lCrt.Details.Groups)
   151  	assert.Len(t, lCrt.Details.Subnets, 0)
   152  	assert.Len(t, lCrt.Details.PublicKey, 32)
   153  	assert.Equal(t, time.Duration(time.Minute*100), lCrt.Details.NotAfter.Sub(lCrt.Details.NotBefore))
   154  	assert.Equal(t, "", lCrt.Details.Issuer)
   155  	assert.True(t, lCrt.CheckSignature(lCrt.Details.PublicKey))
   156  
   157  	// test encrypted key
   158  	os.Remove(keyF.Name())
   159  	os.Remove(crtF.Name())
   160  	ob.Reset()
   161  	eb.Reset()
   162  	args = []string{"-encrypt", "-name", "test", "-duration", "100m", "-groups", "1,2,3,4,5", "-out-crt", crtF.Name(), "-out-key", keyF.Name()}
   163  	assert.Nil(t, ca(args, ob, eb, testpw))
   164  	assert.Equal(t, pwPromptOb, ob.String())
   165  	assert.Equal(t, "", eb.String())
   166  
   167  	// read encrypted key file and verify default params
   168  	rb, _ = os.ReadFile(keyF.Name())
   169  	k, _ := pem.Decode(rb)
   170  	ned, err := cert.UnmarshalNebulaEncryptedData(k.Bytes)
   171  	assert.Nil(t, err)
   172  	// we won't know salt in advance, so just check start of string
   173  	assert.Equal(t, uint32(2*1024*1024), ned.EncryptionMetadata.Argon2Parameters.Memory)
   174  	assert.Equal(t, uint8(4), ned.EncryptionMetadata.Argon2Parameters.Parallelism)
   175  	assert.Equal(t, uint32(1), ned.EncryptionMetadata.Argon2Parameters.Iterations)
   176  
   177  	// verify the key is valid and decrypt-able
   178  	var curve cert.Curve
   179  	curve, lKey, b, err = cert.DecryptAndUnmarshalSigningPrivateKey(passphrase, rb)
   180  	assert.Equal(t, cert.Curve_CURVE25519, curve)
   181  	assert.Nil(t, err)
   182  	assert.Len(t, b, 0)
   183  	assert.Len(t, lKey, 64)
   184  
   185  	// test when reading passsword results in an error
   186  	os.Remove(keyF.Name())
   187  	os.Remove(crtF.Name())
   188  	ob.Reset()
   189  	eb.Reset()
   190  	args = []string{"-encrypt", "-name", "test", "-duration", "100m", "-groups", "1,2,3,4,5", "-out-crt", crtF.Name(), "-out-key", keyF.Name()}
   191  	assert.Error(t, ca(args, ob, eb, errpw))
   192  	assert.Equal(t, pwPromptOb, ob.String())
   193  	assert.Equal(t, "", eb.String())
   194  
   195  	// test when user fails to enter a password
   196  	os.Remove(keyF.Name())
   197  	os.Remove(crtF.Name())
   198  	ob.Reset()
   199  	eb.Reset()
   200  	args = []string{"-encrypt", "-name", "test", "-duration", "100m", "-groups", "1,2,3,4,5", "-out-crt", crtF.Name(), "-out-key", keyF.Name()}
   201  	assert.EqualError(t, ca(args, ob, eb, nopw), "no passphrase specified, remove -encrypt flag to write out-key in plaintext")
   202  	assert.Equal(t, strings.Repeat(pwPromptOb, 5), ob.String()) // prompts 5 times before giving up
   203  	assert.Equal(t, "", eb.String())
   204  
   205  	// create valid cert/key for overwrite tests
   206  	os.Remove(keyF.Name())
   207  	os.Remove(crtF.Name())
   208  	ob.Reset()
   209  	eb.Reset()
   210  	args = []string{"-name", "test", "-duration", "100m", "-groups", "1,,   2    ,        ,,,3,4,5", "-out-crt", crtF.Name(), "-out-key", keyF.Name()}
   211  	assert.Nil(t, ca(args, ob, eb, nopw))
   212  
   213  	// test that we won't overwrite existing certificate file
   214  	ob.Reset()
   215  	eb.Reset()
   216  	args = []string{"-name", "test", "-duration", "100m", "-groups", "1,,   2    ,        ,,,3,4,5", "-out-crt", crtF.Name(), "-out-key", keyF.Name()}
   217  	assert.EqualError(t, ca(args, ob, eb, nopw), "refusing to overwrite existing CA key: "+keyF.Name())
   218  	assert.Equal(t, "", ob.String())
   219  	assert.Equal(t, "", eb.String())
   220  
   221  	// test that we won't overwrite existing key file
   222  	os.Remove(keyF.Name())
   223  	ob.Reset()
   224  	eb.Reset()
   225  	args = []string{"-name", "test", "-duration", "100m", "-groups", "1,,   2    ,        ,,,3,4,5", "-out-crt", crtF.Name(), "-out-key", keyF.Name()}
   226  	assert.EqualError(t, ca(args, ob, eb, nopw), "refusing to overwrite existing CA cert: "+crtF.Name())
   227  	assert.Equal(t, "", ob.String())
   228  	assert.Equal(t, "", eb.String())
   229  	os.Remove(keyF.Name())
   230  
   231  }