mirror of
https://github.com/slackhq/nebula.git
synced 2025-11-11 06:43:57 +01:00
`func (nc *NebulaCertificate) VerifyPrivateKey(key []byte) error` would previously return an error even if passed the correct private key for a CA certificate `nc`. That function has been updated to support CA certificates, and nebula-cert now calls it before signing a new certificate. Previously, it would perform all constraint checks against the CA certificate provided, take a SHA256 fingerprint of the provided certificate, insert it into the new node certificate, and then finally sign it with the mismatching private key provided.
306 lines
13 KiB
Go
306 lines
13 KiB
Go
// +build !windows
|
|
|
|
package main
|
|
|
|
import (
|
|
"bytes"
|
|
"crypto/rand"
|
|
"io/ioutil"
|
|
"os"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/slackhq/nebula/cert"
|
|
"github.com/stretchr/testify/assert"
|
|
"golang.org/x/crypto/ed25519"
|
|
)
|
|
|
|
//TODO: test file permissions
|
|
|
|
func Test_signSummary(t *testing.T) {
|
|
assert.Equal(t, "sign <flags>: create and sign a certificate", signSummary())
|
|
}
|
|
|
|
func Test_signHelp(t *testing.T) {
|
|
ob := &bytes.Buffer{}
|
|
signHelp(ob)
|
|
assert.Equal(
|
|
t,
|
|
"Usage of "+os.Args[0]+" sign <flags>: create and sign a certificate\n"+
|
|
" -ca-crt string\n"+
|
|
" \tOptional: path to the signing CA cert (default \"ca.crt\")\n"+
|
|
" -ca-key string\n"+
|
|
" \tOptional: path to the signing CA key (default \"ca.key\")\n"+
|
|
" -duration duration\n"+
|
|
" \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"+
|
|
" -groups string\n"+
|
|
" \tOptional: comma separated list of groups\n"+
|
|
" -in-pub string\n"+
|
|
" \tOptional (if out-key not set): path to read a previously generated public key\n"+
|
|
" -ip string\n"+
|
|
" \tRequired: ip and network in CIDR notation to assign the cert\n"+
|
|
" -name string\n"+
|
|
" \tRequired: name of the cert, usually a hostname\n"+
|
|
" -out-crt string\n"+
|
|
" \tOptional: path to write the certificate to\n"+
|
|
" -out-key string\n"+
|
|
" \tOptional (if in-pub not set): path to write the private key to\n"+
|
|
" -out-qr string\n"+
|
|
" \tOptional: output a qr code image (png) of the certificate\n"+
|
|
" -subnets string\n"+
|
|
" \tOptional: comma separated list of subnet this cert can serve for\n",
|
|
ob.String(),
|
|
)
|
|
}
|
|
|
|
func Test_signCert(t *testing.T) {
|
|
ob := &bytes.Buffer{}
|
|
eb := &bytes.Buffer{}
|
|
|
|
// required args
|
|
|
|
assertHelpError(t, signCert([]string{"-ca-crt", "./nope", "-ca-key", "./nope", "-ip", "1.1.1.1/24", "-out-key", "nope", "-out-crt", "nope"}, ob, eb), "-name is required")
|
|
assert.Empty(t, ob.String())
|
|
assert.Empty(t, eb.String())
|
|
|
|
assertHelpError(t, signCert([]string{"-ca-crt", "./nope", "-ca-key", "./nope", "-name", "test", "-out-key", "nope", "-out-crt", "nope"}, ob, eb), "-ip is required")
|
|
assert.Empty(t, ob.String())
|
|
assert.Empty(t, eb.String())
|
|
|
|
// cannot set -in-pub and -out-key
|
|
assertHelpError(t, signCert([]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), "cannot set both -in-pub and -out-key")
|
|
assert.Empty(t, ob.String())
|
|
assert.Empty(t, eb.String())
|
|
|
|
// failed to read key
|
|
ob.Reset()
|
|
eb.Reset()
|
|
args := []string{"-ca-crt", "./nope", "-ca-key", "./nope", "-name", "test", "-ip", "1.1.1.1/24", "-out-crt", "nope", "-out-key", "nope", "-duration", "100m"}
|
|
assert.EqualError(t, signCert(args, ob, eb), "error while reading ca-key: open ./nope: "+NoSuchFileError)
|
|
|
|
// failed to unmarshal key
|
|
ob.Reset()
|
|
eb.Reset()
|
|
caKeyF, err := ioutil.TempFile("", "sign-cert.key")
|
|
assert.Nil(t, err)
|
|
defer os.Remove(caKeyF.Name())
|
|
|
|
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"}
|
|
assert.EqualError(t, signCert(args, ob, eb), "error while parsing ca-key: input did not contain a valid PEM encoded block")
|
|
assert.Empty(t, ob.String())
|
|
assert.Empty(t, eb.String())
|
|
|
|
// Write a proper ca key for later
|
|
ob.Reset()
|
|
eb.Reset()
|
|
caPub, caPriv, _ := ed25519.GenerateKey(rand.Reader)
|
|
caKeyF.Write(cert.MarshalEd25519PrivateKey(caPriv))
|
|
|
|
// failed to read cert
|
|
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"}
|
|
assert.EqualError(t, signCert(args, ob, eb), "error while reading ca-crt: open ./nope: "+NoSuchFileError)
|
|
assert.Empty(t, ob.String())
|
|
assert.Empty(t, eb.String())
|
|
|
|
// failed to unmarshal cert
|
|
ob.Reset()
|
|
eb.Reset()
|
|
caCrtF, err := ioutil.TempFile("", "sign-cert.crt")
|
|
assert.Nil(t, err)
|
|
defer os.Remove(caCrtF.Name())
|
|
|
|
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"}
|
|
assert.EqualError(t, signCert(args, ob, eb), "error while parsing ca-crt: input did not contain a valid PEM encoded block")
|
|
assert.Empty(t, ob.String())
|
|
assert.Empty(t, eb.String())
|
|
|
|
// write a proper ca cert for later
|
|
ca := cert.NebulaCertificate{
|
|
Details: cert.NebulaCertificateDetails{
|
|
Name: "ca",
|
|
NotBefore: time.Now(),
|
|
NotAfter: time.Now().Add(time.Minute * 200),
|
|
PublicKey: caPub,
|
|
IsCA: true,
|
|
},
|
|
}
|
|
b, _ := ca.MarshalToPEM()
|
|
caCrtF.Write(b)
|
|
|
|
// failed to read pub
|
|
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"}
|
|
assert.EqualError(t, signCert(args, ob, eb), "error while reading in-pub: open ./nope: "+NoSuchFileError)
|
|
assert.Empty(t, ob.String())
|
|
assert.Empty(t, eb.String())
|
|
|
|
// failed to unmarshal pub
|
|
ob.Reset()
|
|
eb.Reset()
|
|
inPubF, err := ioutil.TempFile("", "in.pub")
|
|
assert.Nil(t, err)
|
|
defer os.Remove(inPubF.Name())
|
|
|
|
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"}
|
|
assert.EqualError(t, signCert(args, ob, eb), "error while parsing in-pub: input did not contain a valid PEM encoded block")
|
|
assert.Empty(t, ob.String())
|
|
assert.Empty(t, eb.String())
|
|
|
|
// write a proper pub for later
|
|
ob.Reset()
|
|
eb.Reset()
|
|
inPub, _ := x25519Keypair()
|
|
inPubF.Write(cert.MarshalX25519PublicKey(inPub))
|
|
|
|
// bad ip cidr
|
|
ob.Reset()
|
|
eb.Reset()
|
|
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"}
|
|
assertHelpError(t, signCert(args, ob, eb), "invalid ip definition: invalid CIDR address: a1.1.1.1/24")
|
|
assert.Empty(t, ob.String())
|
|
assert.Empty(t, eb.String())
|
|
|
|
// bad subnet cidr
|
|
ob.Reset()
|
|
eb.Reset()
|
|
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"}
|
|
assertHelpError(t, signCert(args, ob, eb), "invalid subnet definition: invalid CIDR address: a")
|
|
assert.Empty(t, ob.String())
|
|
assert.Empty(t, eb.String())
|
|
|
|
// mismatched ca key
|
|
_, caPriv2, _ := ed25519.GenerateKey(rand.Reader)
|
|
caKeyF2, err := ioutil.TempFile("", "sign-cert-2.key")
|
|
assert.Nil(t, err)
|
|
defer os.Remove(caKeyF2.Name())
|
|
caKeyF2.Write(cert.MarshalEd25519PrivateKey(caPriv2))
|
|
|
|
ob.Reset()
|
|
eb.Reset()
|
|
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"}
|
|
assert.EqualError(t, signCert(args, ob, eb), "refusing to sign, root certificate does not match private key")
|
|
assert.Empty(t, ob.String())
|
|
assert.Empty(t, eb.String())
|
|
|
|
// failed key write
|
|
ob.Reset()
|
|
eb.Reset()
|
|
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"}
|
|
assert.EqualError(t, signCert(args, ob, eb), "error while writing out-key: open /do/not/write/pleasekey: "+NoSuchDirError)
|
|
assert.Empty(t, ob.String())
|
|
assert.Empty(t, eb.String())
|
|
|
|
// create temp key file
|
|
keyF, err := ioutil.TempFile("", "test.key")
|
|
assert.Nil(t, err)
|
|
os.Remove(keyF.Name())
|
|
|
|
// failed cert write
|
|
ob.Reset()
|
|
eb.Reset()
|
|
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"}
|
|
assert.EqualError(t, signCert(args, ob, eb), "error while writing out-crt: open /do/not/write/pleasecrt: "+NoSuchDirError)
|
|
assert.Empty(t, ob.String())
|
|
assert.Empty(t, eb.String())
|
|
os.Remove(keyF.Name())
|
|
|
|
// create temp cert file
|
|
crtF, err := ioutil.TempFile("", "test.crt")
|
|
assert.Nil(t, err)
|
|
os.Remove(crtF.Name())
|
|
|
|
// test proper cert with removed empty groups and subnets
|
|
ob.Reset()
|
|
eb.Reset()
|
|
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"}
|
|
assert.Nil(t, signCert(args, ob, eb))
|
|
assert.Empty(t, ob.String())
|
|
assert.Empty(t, eb.String())
|
|
|
|
// read cert and key files
|
|
rb, _ := ioutil.ReadFile(keyF.Name())
|
|
lKey, b, err := cert.UnmarshalX25519PrivateKey(rb)
|
|
assert.Len(t, b, 0)
|
|
assert.Nil(t, err)
|
|
assert.Len(t, lKey, 32)
|
|
|
|
rb, _ = ioutil.ReadFile(crtF.Name())
|
|
lCrt, b, err := cert.UnmarshalNebulaCertificateFromPEM(rb)
|
|
assert.Len(t, b, 0)
|
|
assert.Nil(t, err)
|
|
|
|
assert.Equal(t, "test", lCrt.Details.Name)
|
|
assert.Equal(t, "1.1.1.1/24", lCrt.Details.Ips[0].String())
|
|
assert.Len(t, lCrt.Details.Ips, 1)
|
|
assert.False(t, lCrt.Details.IsCA)
|
|
assert.Equal(t, []string{"1", "2", "3", "4", "5"}, lCrt.Details.Groups)
|
|
assert.Len(t, lCrt.Details.Subnets, 3)
|
|
assert.Len(t, lCrt.Details.PublicKey, 32)
|
|
assert.Equal(t, time.Duration(time.Minute*100), lCrt.Details.NotAfter.Sub(lCrt.Details.NotBefore))
|
|
|
|
sns := []string{}
|
|
for _, sn := range lCrt.Details.Subnets {
|
|
sns = append(sns, sn.String())
|
|
}
|
|
assert.Equal(t, []string{"10.1.1.1/32", "10.2.2.2/32", "10.5.5.5/32"}, sns)
|
|
|
|
issuer, _ := ca.Sha256Sum()
|
|
assert.Equal(t, issuer, lCrt.Details.Issuer)
|
|
|
|
assert.True(t, lCrt.CheckSignature(caPub))
|
|
|
|
// test proper cert with in-pub
|
|
os.Remove(keyF.Name())
|
|
os.Remove(crtF.Name())
|
|
ob.Reset()
|
|
eb.Reset()
|
|
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"}
|
|
assert.Nil(t, signCert(args, ob, eb))
|
|
assert.Empty(t, ob.String())
|
|
assert.Empty(t, eb.String())
|
|
|
|
// read cert file and check pub key matches in-pub
|
|
rb, _ = ioutil.ReadFile(crtF.Name())
|
|
lCrt, b, err = cert.UnmarshalNebulaCertificateFromPEM(rb)
|
|
assert.Len(t, b, 0)
|
|
assert.Nil(t, err)
|
|
assert.Equal(t, lCrt.Details.PublicKey, inPub)
|
|
|
|
// test refuse to sign cert with duration beyond root
|
|
ob.Reset()
|
|
eb.Reset()
|
|
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"}
|
|
assert.EqualError(t, signCert(args, ob, eb), "refusing to sign, root certificate constraints violated: certificate expires after signing certificate")
|
|
assert.Empty(t, ob.String())
|
|
assert.Empty(t, eb.String())
|
|
|
|
// create valid cert/key for overwrite tests
|
|
os.Remove(keyF.Name())
|
|
os.Remove(crtF.Name())
|
|
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"}
|
|
assert.Nil(t, signCert(args, ob, eb))
|
|
|
|
// test that we won't overwrite existing key file
|
|
os.Remove(crtF.Name())
|
|
ob.Reset()
|
|
eb.Reset()
|
|
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"}
|
|
assert.EqualError(t, signCert(args, ob, eb), "refusing to overwrite existing key: "+keyF.Name())
|
|
assert.Empty(t, ob.String())
|
|
assert.Empty(t, eb.String())
|
|
|
|
// create valid cert/key for overwrite tests
|
|
os.Remove(keyF.Name())
|
|
os.Remove(crtF.Name())
|
|
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"}
|
|
assert.Nil(t, signCert(args, ob, eb))
|
|
|
|
// test that we won't overwrite existing certificate file
|
|
os.Remove(keyF.Name())
|
|
ob.Reset()
|
|
eb.Reset()
|
|
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"}
|
|
assert.EqualError(t, signCert(args, ob, eb), "refusing to overwrite existing cert: "+crtF.Name())
|
|
assert.Empty(t, ob.String())
|
|
assert.Empty(t, eb.String())
|
|
}
|