github.com/slackhq/nebula@v1.9.0/cmd/nebula-cert/sign_test.go (about) 1 //go:build !windows 2 // +build !windows 3 4 package main 5 6 import ( 7 "bytes" 8 "crypto/rand" 9 "errors" 10 "os" 11 "testing" 12 "time" 13 14 "github.com/slackhq/nebula/cert" 15 "github.com/stretchr/testify/assert" 16 "golang.org/x/crypto/ed25519" 17 ) 18 19 //TODO: test file permissions 20 21 func Test_signSummary(t *testing.T) { 22 assert.Equal(t, "sign <flags>: create and sign a certificate", signSummary()) 23 } 24 25 func Test_signHelp(t *testing.T) { 26 ob := &bytes.Buffer{} 27 signHelp(ob) 28 assert.Equal( 29 t, 30 "Usage of "+os.Args[0]+" sign <flags>: create and sign a certificate\n"+ 31 " -ca-crt string\n"+ 32 " \tOptional: path to the signing CA cert (default \"ca.crt\")\n"+ 33 " -ca-key string\n"+ 34 " \tOptional: path to the signing CA key (default \"ca.key\")\n"+ 35 " -duration duration\n"+ 36 " \tOptional: how long the cert should be valid for. The default is 1 second before the signing cert expires. Valid time units are seconds: \"s\", minutes: \"m\", hours: \"h\"\n"+ 37 " -groups string\n"+ 38 " \tOptional: comma separated list of groups\n"+ 39 " -in-pub string\n"+ 40 " \tOptional (if out-key not set): path to read a previously generated public key\n"+ 41 " -ip string\n"+ 42 " \tRequired: ipv4 address and network in CIDR notation to assign the cert\n"+ 43 " -name string\n"+ 44 " \tRequired: name of the cert, usually a hostname\n"+ 45 " -out-crt string\n"+ 46 " \tOptional: path to write the certificate to\n"+ 47 " -out-key string\n"+ 48 " \tOptional (if in-pub not set): path to write the private key to\n"+ 49 " -out-qr string\n"+ 50 " \tOptional: output a qr code image (png) of the certificate\n"+ 51 " -subnets string\n"+ 52 " \tOptional: comma separated list of ipv4 address and network in CIDR notation. Subnets this cert can serve for\n", 53 ob.String(), 54 ) 55 } 56 57 func Test_signCert(t *testing.T) { 58 ob := &bytes.Buffer{} 59 eb := &bytes.Buffer{} 60 61 nopw := &StubPasswordReader{ 62 password: []byte(""), 63 err: nil, 64 } 65 66 errpw := &StubPasswordReader{ 67 password: []byte(""), 68 err: errors.New("stub error"), 69 } 70 71 passphrase := []byte("DO NOT USE THIS KEY") 72 testpw := &StubPasswordReader{ 73 password: passphrase, 74 err: nil, 75 } 76 77 // required args 78 assertHelpError(t, signCert( 79 []string{"-ca-crt", "./nope", "-ca-key", "./nope", "-ip", "1.1.1.1/24", "-out-key", "nope", "-out-crt", "nope"}, ob, eb, nopw, 80 ), "-name is required") 81 assert.Empty(t, ob.String()) 82 assert.Empty(t, eb.String()) 83 84 assertHelpError(t, signCert( 85 []string{"-ca-crt", "./nope", "-ca-key", "./nope", "-name", "test", "-out-key", "nope", "-out-crt", "nope"}, ob, eb, nopw, 86 ), "-ip is required") 87 assert.Empty(t, ob.String()) 88 assert.Empty(t, eb.String()) 89 90 // cannot set -in-pub and -out-key 91 assertHelpError(t, signCert( 92 []string{"-ca-crt", "./nope", "-ca-key", "./nope", "-name", "test", "-in-pub", "nope", "-ip", "1.1.1.1/24", "-out-crt", "nope", "-out-key", "nope"}, ob, eb, nopw, 93 ), "cannot set both -in-pub and -out-key") 94 assert.Empty(t, ob.String()) 95 assert.Empty(t, eb.String()) 96 97 // failed to read key 98 ob.Reset() 99 eb.Reset() 100 args := []string{"-ca-crt", "./nope", "-ca-key", "./nope", "-name", "test", "-ip", "1.1.1.1/24", "-out-crt", "nope", "-out-key", "nope", "-duration", "100m"} 101 assert.EqualError(t, signCert(args, ob, eb, nopw), "error while reading ca-key: open ./nope: "+NoSuchFileError) 102 103 // failed to unmarshal key 104 ob.Reset() 105 eb.Reset() 106 caKeyF, err := os.CreateTemp("", "sign-cert.key") 107 assert.Nil(t, err) 108 defer os.Remove(caKeyF.Name()) 109 110 args = []string{"-ca-crt", "./nope", "-ca-key", caKeyF.Name(), "-name", "test", "-ip", "1.1.1.1/24", "-out-crt", "nope", "-out-key", "nope", "-duration", "100m"} 111 assert.EqualError(t, signCert(args, ob, eb, nopw), "error while parsing ca-key: input did not contain a valid PEM encoded block") 112 assert.Empty(t, ob.String()) 113 assert.Empty(t, eb.String()) 114 115 // Write a proper ca key for later 116 ob.Reset() 117 eb.Reset() 118 caPub, caPriv, _ := ed25519.GenerateKey(rand.Reader) 119 caKeyF.Write(cert.MarshalEd25519PrivateKey(caPriv)) 120 121 // failed to read cert 122 args = []string{"-ca-crt", "./nope", "-ca-key", caKeyF.Name(), "-name", "test", "-ip", "1.1.1.1/24", "-out-crt", "nope", "-out-key", "nope", "-duration", "100m"} 123 assert.EqualError(t, signCert(args, ob, eb, nopw), "error while reading ca-crt: open ./nope: "+NoSuchFileError) 124 assert.Empty(t, ob.String()) 125 assert.Empty(t, eb.String()) 126 127 // failed to unmarshal cert 128 ob.Reset() 129 eb.Reset() 130 caCrtF, err := os.CreateTemp("", "sign-cert.crt") 131 assert.Nil(t, err) 132 defer os.Remove(caCrtF.Name()) 133 134 args = []string{"-ca-crt", caCrtF.Name(), "-ca-key", caKeyF.Name(), "-name", "test", "-ip", "1.1.1.1/24", "-out-crt", "nope", "-out-key", "nope", "-duration", "100m"} 135 assert.EqualError(t, signCert(args, ob, eb, nopw), "error while parsing ca-crt: input did not contain a valid PEM encoded block") 136 assert.Empty(t, ob.String()) 137 assert.Empty(t, eb.String()) 138 139 // write a proper ca cert for later 140 ca := cert.NebulaCertificate{ 141 Details: cert.NebulaCertificateDetails{ 142 Name: "ca", 143 NotBefore: time.Now(), 144 NotAfter: time.Now().Add(time.Minute * 200), 145 PublicKey: caPub, 146 IsCA: true, 147 }, 148 } 149 b, _ := ca.MarshalToPEM() 150 caCrtF.Write(b) 151 152 // failed to read pub 153 args = []string{"-ca-crt", caCrtF.Name(), "-ca-key", caKeyF.Name(), "-name", "test", "-ip", "1.1.1.1/24", "-out-crt", "nope", "-in-pub", "./nope", "-duration", "100m"} 154 assert.EqualError(t, signCert(args, ob, eb, nopw), "error while reading in-pub: open ./nope: "+NoSuchFileError) 155 assert.Empty(t, ob.String()) 156 assert.Empty(t, eb.String()) 157 158 // failed to unmarshal pub 159 ob.Reset() 160 eb.Reset() 161 inPubF, err := os.CreateTemp("", "in.pub") 162 assert.Nil(t, err) 163 defer os.Remove(inPubF.Name()) 164 165 args = []string{"-ca-crt", caCrtF.Name(), "-ca-key", caKeyF.Name(), "-name", "test", "-ip", "1.1.1.1/24", "-out-crt", "nope", "-in-pub", inPubF.Name(), "-duration", "100m"} 166 assert.EqualError(t, signCert(args, ob, eb, nopw), "error while parsing in-pub: input did not contain a valid PEM encoded block") 167 assert.Empty(t, ob.String()) 168 assert.Empty(t, eb.String()) 169 170 // write a proper pub for later 171 ob.Reset() 172 eb.Reset() 173 inPub, _ := x25519Keypair() 174 inPubF.Write(cert.MarshalX25519PublicKey(inPub)) 175 176 // bad ip cidr 177 ob.Reset() 178 eb.Reset() 179 args = []string{"-ca-crt", caCrtF.Name(), "-ca-key", caKeyF.Name(), "-name", "test", "-ip", "a1.1.1.1/24", "-out-crt", "nope", "-out-key", "nope", "-duration", "100m"} 180 assertHelpError(t, signCert(args, ob, eb, nopw), "invalid ip definition: invalid CIDR address: a1.1.1.1/24") 181 assert.Empty(t, ob.String()) 182 assert.Empty(t, eb.String()) 183 184 ob.Reset() 185 eb.Reset() 186 args = []string{"-ca-crt", caCrtF.Name(), "-ca-key", caKeyF.Name(), "-name", "test", "-ip", "100::100/100", "-out-crt", "nope", "-out-key", "nope", "-duration", "100m"} 187 assertHelpError(t, signCert(args, ob, eb, nopw), "invalid ip definition: can only be ipv4, have 100::100/100") 188 assert.Empty(t, ob.String()) 189 assert.Empty(t, eb.String()) 190 191 // bad subnet cidr 192 ob.Reset() 193 eb.Reset() 194 args = []string{"-ca-crt", caCrtF.Name(), "-ca-key", caKeyF.Name(), "-name", "test", "-ip", "1.1.1.1/24", "-out-crt", "nope", "-out-key", "nope", "-duration", "100m", "-subnets", "a"} 195 assertHelpError(t, signCert(args, ob, eb, nopw), "invalid subnet definition: invalid CIDR address: a") 196 assert.Empty(t, ob.String()) 197 assert.Empty(t, eb.String()) 198 199 ob.Reset() 200 eb.Reset() 201 args = []string{"-ca-crt", caCrtF.Name(), "-ca-key", caKeyF.Name(), "-name", "test", "-ip", "1.1.1.1/24", "-out-crt", "nope", "-out-key", "nope", "-duration", "100m", "-subnets", "100::100/100"} 202 assertHelpError(t, signCert(args, ob, eb, nopw), "invalid subnet definition: can only be ipv4, have 100::100/100") 203 assert.Empty(t, ob.String()) 204 assert.Empty(t, eb.String()) 205 206 // mismatched ca key 207 _, caPriv2, _ := ed25519.GenerateKey(rand.Reader) 208 caKeyF2, err := os.CreateTemp("", "sign-cert-2.key") 209 assert.Nil(t, err) 210 defer os.Remove(caKeyF2.Name()) 211 caKeyF2.Write(cert.MarshalEd25519PrivateKey(caPriv2)) 212 213 ob.Reset() 214 eb.Reset() 215 args = []string{"-ca-crt", caCrtF.Name(), "-ca-key", caKeyF2.Name(), "-name", "test", "-ip", "1.1.1.1/24", "-out-crt", "nope", "-out-key", "nope", "-duration", "100m", "-subnets", "a"} 216 assert.EqualError(t, signCert(args, ob, eb, nopw), "refusing to sign, root certificate does not match private key") 217 assert.Empty(t, ob.String()) 218 assert.Empty(t, eb.String()) 219 220 // failed key write 221 ob.Reset() 222 eb.Reset() 223 args = []string{"-ca-crt", caCrtF.Name(), "-ca-key", caKeyF.Name(), "-name", "test", "-ip", "1.1.1.1/24", "-out-crt", "/do/not/write/pleasecrt", "-out-key", "/do/not/write/pleasekey", "-duration", "100m", "-subnets", "10.1.1.1/32"} 224 assert.EqualError(t, signCert(args, ob, eb, nopw), "error while writing out-key: open /do/not/write/pleasekey: "+NoSuchDirError) 225 assert.Empty(t, ob.String()) 226 assert.Empty(t, eb.String()) 227 228 // create temp key file 229 keyF, err := os.CreateTemp("", "test.key") 230 assert.Nil(t, err) 231 os.Remove(keyF.Name()) 232 233 // failed cert write 234 ob.Reset() 235 eb.Reset() 236 args = []string{"-ca-crt", caCrtF.Name(), "-ca-key", caKeyF.Name(), "-name", "test", "-ip", "1.1.1.1/24", "-out-crt", "/do/not/write/pleasecrt", "-out-key", keyF.Name(), "-duration", "100m", "-subnets", "10.1.1.1/32"} 237 assert.EqualError(t, signCert(args, ob, eb, nopw), "error while writing out-crt: open /do/not/write/pleasecrt: "+NoSuchDirError) 238 assert.Empty(t, ob.String()) 239 assert.Empty(t, eb.String()) 240 os.Remove(keyF.Name()) 241 242 // create temp cert file 243 crtF, err := os.CreateTemp("", "test.crt") 244 assert.Nil(t, err) 245 os.Remove(crtF.Name()) 246 247 // test proper cert with removed empty groups and subnets 248 ob.Reset() 249 eb.Reset() 250 args = []string{"-ca-crt", caCrtF.Name(), "-ca-key", caKeyF.Name(), "-name", "test", "-ip", "1.1.1.1/24", "-out-crt", crtF.Name(), "-out-key", keyF.Name(), "-duration", "100m", "-subnets", "10.1.1.1/32, , 10.2.2.2/32 , , ,, 10.5.5.5/32", "-groups", "1,, 2 , ,,,3,4,5"} 251 assert.Nil(t, signCert(args, ob, eb, nopw)) 252 assert.Empty(t, ob.String()) 253 assert.Empty(t, eb.String()) 254 255 // read cert and key files 256 rb, _ := os.ReadFile(keyF.Name()) 257 lKey, b, err := cert.UnmarshalX25519PrivateKey(rb) 258 assert.Len(t, b, 0) 259 assert.Nil(t, err) 260 assert.Len(t, lKey, 32) 261 262 rb, _ = os.ReadFile(crtF.Name()) 263 lCrt, b, err := cert.UnmarshalNebulaCertificateFromPEM(rb) 264 assert.Len(t, b, 0) 265 assert.Nil(t, err) 266 267 assert.Equal(t, "test", lCrt.Details.Name) 268 assert.Equal(t, "1.1.1.1/24", lCrt.Details.Ips[0].String()) 269 assert.Len(t, lCrt.Details.Ips, 1) 270 assert.False(t, lCrt.Details.IsCA) 271 assert.Equal(t, []string{"1", "2", "3", "4", "5"}, lCrt.Details.Groups) 272 assert.Len(t, lCrt.Details.Subnets, 3) 273 assert.Len(t, lCrt.Details.PublicKey, 32) 274 assert.Equal(t, time.Duration(time.Minute*100), lCrt.Details.NotAfter.Sub(lCrt.Details.NotBefore)) 275 276 sns := []string{} 277 for _, sn := range lCrt.Details.Subnets { 278 sns = append(sns, sn.String()) 279 } 280 assert.Equal(t, []string{"10.1.1.1/32", "10.2.2.2/32", "10.5.5.5/32"}, sns) 281 282 issuer, _ := ca.Sha256Sum() 283 assert.Equal(t, issuer, lCrt.Details.Issuer) 284 285 assert.True(t, lCrt.CheckSignature(caPub)) 286 287 // test proper cert with in-pub 288 os.Remove(keyF.Name()) 289 os.Remove(crtF.Name()) 290 ob.Reset() 291 eb.Reset() 292 args = []string{"-ca-crt", caCrtF.Name(), "-ca-key", caKeyF.Name(), "-name", "test", "-ip", "1.1.1.1/24", "-out-crt", crtF.Name(), "-in-pub", inPubF.Name(), "-duration", "100m", "-groups", "1"} 293 assert.Nil(t, signCert(args, ob, eb, nopw)) 294 assert.Empty(t, ob.String()) 295 assert.Empty(t, eb.String()) 296 297 // read cert file and check pub key matches in-pub 298 rb, _ = os.ReadFile(crtF.Name()) 299 lCrt, b, err = cert.UnmarshalNebulaCertificateFromPEM(rb) 300 assert.Len(t, b, 0) 301 assert.Nil(t, err) 302 assert.Equal(t, lCrt.Details.PublicKey, inPub) 303 304 // test refuse to sign cert with duration beyond root 305 ob.Reset() 306 eb.Reset() 307 args = []string{"-ca-crt", caCrtF.Name(), "-ca-key", caKeyF.Name(), "-name", "test", "-ip", "1.1.1.1/24", "-out-crt", crtF.Name(), "-out-key", keyF.Name(), "-duration", "1000m", "-subnets", "10.1.1.1/32, , 10.2.2.2/32 , , ,, 10.5.5.5/32", "-groups", "1,, 2 , ,,,3,4,5"} 308 assert.EqualError(t, signCert(args, ob, eb, nopw), "refusing to sign, root certificate constraints violated: certificate expires after signing certificate") 309 assert.Empty(t, ob.String()) 310 assert.Empty(t, eb.String()) 311 312 // create valid cert/key for overwrite tests 313 os.Remove(keyF.Name()) 314 os.Remove(crtF.Name()) 315 args = []string{"-ca-crt", caCrtF.Name(), "-ca-key", caKeyF.Name(), "-name", "test", "-ip", "1.1.1.1/24", "-out-crt", crtF.Name(), "-out-key", keyF.Name(), "-duration", "100m", "-subnets", "10.1.1.1/32, , 10.2.2.2/32 , , ,, 10.5.5.5/32", "-groups", "1,, 2 , ,,,3,4,5"} 316 assert.Nil(t, signCert(args, ob, eb, nopw)) 317 318 // test that we won't overwrite existing key file 319 os.Remove(crtF.Name()) 320 ob.Reset() 321 eb.Reset() 322 args = []string{"-ca-crt", caCrtF.Name(), "-ca-key", caKeyF.Name(), "-name", "test", "-ip", "1.1.1.1/24", "-out-crt", crtF.Name(), "-out-key", keyF.Name(), "-duration", "100m", "-subnets", "10.1.1.1/32, , 10.2.2.2/32 , , ,, 10.5.5.5/32", "-groups", "1,, 2 , ,,,3,4,5"} 323 assert.EqualError(t, signCert(args, ob, eb, nopw), "refusing to overwrite existing key: "+keyF.Name()) 324 assert.Empty(t, ob.String()) 325 assert.Empty(t, eb.String()) 326 327 // create valid cert/key for overwrite tests 328 os.Remove(keyF.Name()) 329 os.Remove(crtF.Name()) 330 args = []string{"-ca-crt", caCrtF.Name(), "-ca-key", caKeyF.Name(), "-name", "test", "-ip", "1.1.1.1/24", "-out-crt", crtF.Name(), "-out-key", keyF.Name(), "-duration", "100m", "-subnets", "10.1.1.1/32, , 10.2.2.2/32 , , ,, 10.5.5.5/32", "-groups", "1,, 2 , ,,,3,4,5"} 331 assert.Nil(t, signCert(args, ob, eb, nopw)) 332 333 // test that we won't overwrite existing certificate file 334 os.Remove(keyF.Name()) 335 ob.Reset() 336 eb.Reset() 337 args = []string{"-ca-crt", caCrtF.Name(), "-ca-key", caKeyF.Name(), "-name", "test", "-ip", "1.1.1.1/24", "-out-crt", crtF.Name(), "-out-key", keyF.Name(), "-duration", "100m", "-subnets", "10.1.1.1/32, , 10.2.2.2/32 , , ,, 10.5.5.5/32", "-groups", "1,, 2 , ,,,3,4,5"} 338 assert.EqualError(t, signCert(args, ob, eb, nopw), "refusing to overwrite existing cert: "+crtF.Name()) 339 assert.Empty(t, ob.String()) 340 assert.Empty(t, eb.String()) 341 342 // create valid cert/key using encrypted CA key 343 os.Remove(caKeyF.Name()) 344 os.Remove(caCrtF.Name()) 345 os.Remove(keyF.Name()) 346 os.Remove(crtF.Name()) 347 ob.Reset() 348 eb.Reset() 349 350 caKeyF, err = os.CreateTemp("", "sign-cert.key") 351 assert.Nil(t, err) 352 defer os.Remove(caKeyF.Name()) 353 354 caCrtF, err = os.CreateTemp("", "sign-cert.crt") 355 assert.Nil(t, err) 356 defer os.Remove(caCrtF.Name()) 357 358 // generate the encrypted key 359 caPub, caPriv, _ = ed25519.GenerateKey(rand.Reader) 360 kdfParams := cert.NewArgon2Parameters(64*1024, 4, 3) 361 b, _ = cert.EncryptAndMarshalSigningPrivateKey(cert.Curve_CURVE25519, caPriv, passphrase, kdfParams) 362 caKeyF.Write(b) 363 364 ca = cert.NebulaCertificate{ 365 Details: cert.NebulaCertificateDetails{ 366 Name: "ca", 367 NotBefore: time.Now(), 368 NotAfter: time.Now().Add(time.Minute * 200), 369 PublicKey: caPub, 370 IsCA: true, 371 }, 372 } 373 b, _ = ca.MarshalToPEM() 374 caCrtF.Write(b) 375 376 // test with the proper password 377 args = []string{"-ca-crt", caCrtF.Name(), "-ca-key", caKeyF.Name(), "-name", "test", "-ip", "1.1.1.1/24", "-out-crt", crtF.Name(), "-out-key", keyF.Name(), "-duration", "100m", "-subnets", "10.1.1.1/32, , 10.2.2.2/32 , , ,, 10.5.5.5/32", "-groups", "1,, 2 , ,,,3,4,5"} 378 assert.Nil(t, signCert(args, ob, eb, testpw)) 379 assert.Equal(t, "Enter passphrase: ", ob.String()) 380 assert.Empty(t, eb.String()) 381 382 // test with the wrong password 383 ob.Reset() 384 eb.Reset() 385 386 testpw.password = []byte("invalid password") 387 args = []string{"-ca-crt", caCrtF.Name(), "-ca-key", caKeyF.Name(), "-name", "test", "-ip", "1.1.1.1/24", "-out-crt", crtF.Name(), "-out-key", keyF.Name(), "-duration", "100m", "-subnets", "10.1.1.1/32, , 10.2.2.2/32 , , ,, 10.5.5.5/32", "-groups", "1,, 2 , ,,,3,4,5"} 388 assert.Error(t, signCert(args, ob, eb, testpw)) 389 assert.Equal(t, "Enter passphrase: ", ob.String()) 390 assert.Empty(t, eb.String()) 391 392 // test with the user not entering a password 393 ob.Reset() 394 eb.Reset() 395 396 args = []string{"-ca-crt", caCrtF.Name(), "-ca-key", caKeyF.Name(), "-name", "test", "-ip", "1.1.1.1/24", "-out-crt", crtF.Name(), "-out-key", keyF.Name(), "-duration", "100m", "-subnets", "10.1.1.1/32, , 10.2.2.2/32 , , ,, 10.5.5.5/32", "-groups", "1,, 2 , ,,,3,4,5"} 397 assert.Error(t, signCert(args, ob, eb, nopw)) 398 // normally the user hitting enter on the prompt would add newlines between these 399 assert.Equal(t, "Enter passphrase: Enter passphrase: Enter passphrase: Enter passphrase: Enter passphrase: ", ob.String()) 400 assert.Empty(t, eb.String()) 401 402 // test an error condition 403 ob.Reset() 404 eb.Reset() 405 406 args = []string{"-ca-crt", caCrtF.Name(), "-ca-key", caKeyF.Name(), "-name", "test", "-ip", "1.1.1.1/24", "-out-crt", crtF.Name(), "-out-key", keyF.Name(), "-duration", "100m", "-subnets", "10.1.1.1/32, , 10.2.2.2/32 , , ,, 10.5.5.5/32", "-groups", "1,, 2 , ,,,3,4,5"} 407 assert.Error(t, signCert(args, ob, eb, errpw)) 408 assert.Equal(t, "Enter passphrase: ", ob.String()) 409 assert.Empty(t, eb.String()) 410 }