blob: b8a2155a65fe7ce4c1ae9ee99b11d70876e545d6 [file] [log] [blame]
// Copyright Istio Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package ca
import (
"bytes"
"context"
"crypto/tls"
"crypto/x509"
"os"
"reflect"
"testing"
"time"
)
import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/client-go/kubernetes/fake"
)
import (
k8ssecret "github.com/apache/dubbo-go-pixiu/security/pkg/k8s/secret"
caerror "github.com/apache/dubbo-go-pixiu/security/pkg/pki/error"
"github.com/apache/dubbo-go-pixiu/security/pkg/pki/util"
)
var (
cert1Pem = `
-----BEGIN CERTIFICATE-----
MIIC3jCCAcagAwIBAgIJAMwyWk0iqlOoMA0GCSqGSIb3DQEBCwUAMBwxGjAYBgNV
BAoMEWs4cy5jbHVzdGVyLmxvY2FsMB4XDTE4MDkyMTAyMjAzNFoXDTI4MDkxODAy
MjAzNFowHDEaMBgGA1UECgwRazhzLmNsdXN0ZXIubG9jYWwwggEiMA0GCSqGSIb3
DQEBAQUAA4IBDwAwggEKAoIBAQC8TDtfy23OKCRnkSYrKZwuHG5lOmTZgLwoFR1h
3NDTkjR9406CjnAy6Gl73CRG3zRYVgY/2dGNqTzAKRCeKZlOzBlK6Kilb0NIJ6it
s6ooMAxwXlr7jOKiSn6xbaexVMrP0VPUbCgJxQtGs3++hQ14D6WnyfdzPBZJLKbI
tVdDnAcl/FJXKVV9gIg+MM0gETWOYj5Yd8Ye0FTvoFcgs8NKkxhEZe/LeYa7XYsk
S0PymwbHwNZcfC4znp2bzu28LUmUe6kL97YU8ubvhR0muRy6h5MnQNMQrRG5Q5j4
A2+tkO0vto8gOb6/lacEUVYuQdSkMZJiqWEjWgWKeAYdkTJDAgMBAAGjIzAhMA4G
A1UdDwEB/wQEAwICBDAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IB
AQAxWP3MT0IelJcb+e7fNTfMS0r3UhpiNkRU368Z7gJ4tDNOGRPzntW6CLnaE+3g
IjOMAE8jlXeEmNuXtDQqQoZwWc1D5ma3jyc83E5H9LJzjfmn5rAHafr29YH85Ms2
VlKdpP+teYg8Cag9u4ar/AUR4zMUEpGK5U+T9IH44lVqVH23T+DxAT+btsyuGiB0
DsM76XVDj4g3OKCUalu7a8FHvgTkBpUJBl7vwh9kqo9HwCaj4iC2CwveOm0WtSgy
K9PpVDxTGNSxqsxKn7DJQ15NTOP+gr29ABqFKwRr+S8ggw6evzHbABQTUMebaRSr
iH7cSgrzZBiUvJmZRi7/BrYU
-----END CERTIFICATE-----`
key1Pem = `
-----BEGIN PRIVATE KEY-----
MIIEwAIBADANBgkqhkiG9w0BAQEFAASCBKowggSmAgEAAoIBAQC8TDtfy23OKCRn
kSYrKZwuHG5lOmTZgLwoFR1h3NDTkjR9406CjnAy6Gl73CRG3zRYVgY/2dGNqTzA
KRCeKZlOzBlK6Kilb0NIJ6its6ooMAxwXlr7jOKiSn6xbaexVMrP0VPUbCgJxQtG
s3++hQ14D6WnyfdzPBZJLKbItVdDnAcl/FJXKVV9gIg+MM0gETWOYj5Yd8Ye0FTv
oFcgs8NKkxhEZe/LeYa7XYskS0PymwbHwNZcfC4znp2bzu28LUmUe6kL97YU8ubv
hR0muRy6h5MnQNMQrRG5Q5j4A2+tkO0vto8gOb6/lacEUVYuQdSkMZJiqWEjWgWK
eAYdkTJDAgMBAAECggEBAJTemFqmVQwWxKF1Kn4ZibcTF1zFDBLCKwBtoStMD3YW
M5YL7nhd8OruwOcCJ1Q5CAOHD63PolOjp7otPUwui1y3FJAa3areCo2zfTLHxxG6
2zrD/p6+xjeVOhFBJsGWzjn7v5FEaWs/9ChTpf2U6A8yH8BGd3MN4Hi96qboaDO0
fFz3zOu7sgjkDNZiapZpUuqs7a6MCCr2T3FPwdWUiILZF2t5yWd/l8KabP+3QvvR
tDU6sNv4j8e+dsF2l9ZT81JLkN+f6HvWcLVAADvcBqMcd8lmMSPgxSbytzKanx7o
wtzIiGkNZBCVKGO7IK2ByCluiyHDpGul60Th7HUluDECgYEA9/Q1gT8LTHz1n6vM
2n2umQN9R+xOaEYN304D5DQqptN3S0BCJ4dihD0uqEB5osstRTf4QpP/qb2hMDP4
qWbWyrc7Z5Lyt6HI1ly6VpVnYKb3HDeJ9M+5Se1ttdwyRCzuT4ZBhT5bbqBatsOU
V7+dyrJKbk8r9K4qy29UFozz/38CgYEAwmhzPVak99rVmqTpe0gPERW//n+PdW3P
Ta6ongU8zkkw9LAFwgjGtNpd4nlk0iQigiM4jdJDFl6edrRXv2cisEfJ9+s53AOb
hXui4HAn2rusPK+Dq2InkHYTGjEGDpx94zC/bjYR1GBIsthIh0w2G9ql8yvLatxG
x6oXEsb7Lz0CgYEA7Oj+/mDYUNrMbSVfdBvF6Rl2aHQWbncQ5h3Khg55+i/uuY3K
J66pqKQ0ojoIfk0XEh3qLOLv0qUHD+F4Y5OJAuOT9OBo3J/OH1M2D2hs/+JIFUPT
on+fEE21F6AuvwkXIhCrJb5w6gB47Etuv3CsOXGkwEURQJXw+bODapB+yc0CgYEA
t7zoTay6NdcJ0yLR2MZ+FvOrhekhuSaTqyPMEa15jq32KwzCJGUPCJbp7MY217V3
N+/533A+H8JFmoNP+4KKcnknFb2n7Z0rO7licyUNRdniK2jm1O/r3Mj7vOFgjCaz
hCnqg0tvBn4Jt55aziTlbuXzuiRGGTUfYE4NiJ2vgTECgYEA8di9yqGhETYQkoT3
E70JpEmkCWiHl/h2ClLcDkj0gXKFxmhzmvs8G5On4S8toNiJ6efmz0KlHN1F7Ldi
2iVd9LZnFVP1YwG0mvTJxxc5P5Uy5q/EhCLBAetqoTkWYlPcpkcathmCbCpJG4/x
iOmuuOfQWnMfcVk8I0YDL5+G9Pg=
-----END PRIVATE KEY-----`
)
// TODO (myidpt): Test Istio CA can load plugin key/certs from secret.
func TestCreateSelfSignedIstioCAWithoutSecret(t *testing.T) {
caCertTTL := time.Hour
defaultCertTTL := 30 * time.Minute
maxCertTTL := time.Hour
org := "test.ca.Org"
const caNamespace = "default"
client := fake.NewSimpleClientset()
rootCertFile := ""
rootCertCheckInverval := time.Hour
rsaKeySize := 2048
caopts, err := NewSelfSignedIstioCAOptions(context.Background(),
0, caCertTTL, rootCertCheckInverval, defaultCertTTL,
maxCertTTL, org, false, caNamespace, -1, client.CoreV1(),
rootCertFile, false, rsaKeySize)
if err != nil {
t.Fatalf("Failed to create a self-signed CA Options: %v", err)
}
ca, err := NewIstioCA(caopts)
if err != nil {
t.Errorf("Got error while creating self-signed CA: %v", err)
}
if ca == nil {
t.Fatalf("Failed to create a self-signed CA.")
}
signingCert, _, certChainBytes, rootCertBytes := ca.GetCAKeyCertBundle().GetAll()
rootCert, err := util.ParsePemEncodedCertificate(rootCertBytes)
if err != nil {
t.Error(err)
}
// Root cert and siging cert are the same for self-signed CA.
if !rootCert.Equal(signingCert) {
t.Error("CA root cert does not match signing cert")
}
if ttl := rootCert.NotAfter.Sub(rootCert.NotBefore); ttl != caCertTTL {
t.Errorf("Unexpected CA certificate TTL (expecting %v, actual %v)", caCertTTL, ttl)
}
if certOrg := rootCert.Issuer.Organization[0]; certOrg != org {
t.Errorf("Unexpected CA certificate organization (expecting %v, actual %v)", org, certOrg)
}
if len(certChainBytes) != 0 {
t.Errorf("Cert chain should be empty")
}
// Check the signing cert stored in K8s secret.
caSecret, err := client.CoreV1().Secrets("default").Get(context.TODO(), CASecret, metav1.GetOptions{})
if err != nil {
t.Errorf("Failed to get secret (error: %s)", err)
}
signingCertFromSecret, err := util.ParsePemEncodedCertificate(caSecret.Data[CACertFile])
if err != nil {
t.Errorf("Failed to parse cert (error: %s)", err)
}
if !signingCertFromSecret.Equal(signingCert) {
t.Error("CA signing cert does not match the K8s secret")
}
}
func TestCreateSelfSignedIstioCAWithSecret(t *testing.T) {
rootCertPem := cert1Pem
// Use the same signing cert and root cert for self-signed CA.
signingCertPem := []byte(cert1Pem)
signingKeyPem := []byte(key1Pem)
client := fake.NewSimpleClientset()
initSecret := k8ssecret.BuildSecret("", CASecret, "default",
nil, nil, nil, signingCertPem, signingKeyPem, istioCASecretType)
_, err := client.CoreV1().Secrets("default").Create(context.TODO(), initSecret, metav1.CreateOptions{})
if err != nil {
t.Errorf("Failed to create secret (error: %s)", err)
}
caCertTTL := time.Hour
defaultCertTTL := 30 * time.Minute
maxCertTTL := time.Hour
org := "test.ca.Org"
caNamespace := "default"
const rootCertFile = ""
rootCertCheckInverval := time.Hour
rsaKeySize := 2048
caopts, err := NewSelfSignedIstioCAOptions(context.Background(),
0, caCertTTL, rootCertCheckInverval, defaultCertTTL, maxCertTTL,
org, false, caNamespace, -1, client.CoreV1(),
rootCertFile, false, rsaKeySize)
if err != nil {
t.Fatalf("Failed to create a self-signed CA Options: %v", err)
}
ca, err := NewIstioCA(caopts)
if err != nil {
t.Errorf("Got error while creating self-signed CA: %v", err)
}
if ca == nil {
t.Fatalf("Failed to create a self-signed CA.")
}
signingCert, err := util.ParsePemEncodedCertificate(signingCertPem)
if err != nil {
t.Errorf("Failed to parse cert (error: %s)", err)
}
signingCertFromCA, _, certChainBytesFromCA, rootCertBytesFromCA := ca.GetCAKeyCertBundle().GetAll()
if !signingCert.Equal(signingCertFromCA) {
t.Error("Signing cert does not match")
}
if !bytes.Equal(rootCertBytesFromCA, []byte(rootCertPem)) {
t.Error("Root cert does not match")
}
if len(certChainBytesFromCA) != 0 {
t.Errorf("Cert chain should be empty")
}
}
func TestCreateSelfSignedIstioCAReadSigningCertOnly(t *testing.T) {
rootCertPem := cert1Pem
// Use the same signing cert and root cert for self-signed CA.
signingCertPem := []byte(cert1Pem)
signingKeyPem := []byte(key1Pem)
caCertTTL := time.Hour
defaultCertTTL := 30 * time.Minute
maxCertTTL := time.Hour
org := "test.ca.Org"
caNamespace := "default"
const rootCertFile = ""
rootCertCheckInverval := time.Hour
rsaKeySize := 2048
client := fake.NewSimpleClientset()
// Should abort with timeout.
expectedErr := "secret waiting thread is terminated"
ctx0, cancel0 := context.WithTimeout(context.Background(), time.Millisecond*50)
defer cancel0()
_, err := NewSelfSignedIstioCAOptions(ctx0, 0,
caCertTTL, defaultCertTTL, rootCertCheckInverval, maxCertTTL, org, false,
caNamespace, time.Millisecond*10, client.CoreV1(), rootCertFile, false, rsaKeySize)
if err == nil {
t.Errorf("Expected error, but succeeded.")
} else if err.Error() != expectedErr {
t.Errorf("Unexpected error message: %s VS (expected) %s", err.Error(), expectedErr)
return
}
// Should succeed once secret is ready.
secret := k8ssecret.BuildSecret("", CASecret, "default", nil, nil, nil, signingCertPem, signingKeyPem, istioCASecretType)
_, err = client.CoreV1().Secrets("default").Create(context.TODO(), secret, metav1.CreateOptions{})
if err != nil {
t.Errorf("Failed to create secret (error: %s)", err)
}
ctx1, cancel1 := context.WithCancel(context.Background())
defer cancel1()
caopts, err := NewSelfSignedIstioCAOptions(ctx1, 0,
caCertTTL, defaultCertTTL, rootCertCheckInverval, maxCertTTL, org, false,
caNamespace, time.Millisecond*10, client.CoreV1(), rootCertFile, false, rsaKeySize)
if err != nil {
t.Errorf("Unexpected error: %v", err)
}
ca, err := NewIstioCA(caopts)
if err != nil {
t.Errorf("Got error while creating self-signed CA: %v", err)
}
if ca == nil {
t.Fatalf("Failed to create a self-signed CA.")
}
signingCert, err := util.ParsePemEncodedCertificate(signingCertPem)
if err != nil {
t.Errorf("Failed to parse cert (error: %s)", err)
}
signingCertFromCA, _, certChainBytesFromCA, rootCertBytesFromCA := ca.GetCAKeyCertBundle().GetAll()
if !signingCert.Equal(signingCertFromCA) {
t.Error("Signing cert does not match")
}
if !bytes.Equal(rootCertBytesFromCA, []byte(rootCertPem)) {
t.Error("Root cert does not match")
}
if len(certChainBytesFromCA) != 0 {
t.Errorf("Cert chain should be empty")
}
}
func TestCreatePluggedCertCA(t *testing.T) {
rootCertFile := "../testdata/multilevelpki/root-cert.pem"
certChainFile := []string{"../testdata/multilevelpki/int2-cert-chain.pem"}
signingCertFile := "../testdata/multilevelpki/int2-cert.pem"
signingKeyFile := "../testdata/multilevelpki/int2-key.pem"
rsaKeySize := 2048
defaultWorkloadCertTTL := 99999 * time.Hour
maxWorkloadCertTTL := time.Hour
caopts, err := NewPluggedCertIstioCAOptions(SigningCAFileBundle{rootCertFile, certChainFile, signingCertFile, signingKeyFile},
defaultWorkloadCertTTL, maxWorkloadCertTTL, rsaKeySize)
if err != nil {
t.Fatalf("Failed to create a plugged-cert CA Options: %v", err)
}
t0 := time.Now()
ca, err := NewIstioCA(caopts)
if err != nil {
t.Errorf("Got error while creating plugged-cert CA: %v", err)
}
if ca == nil {
t.Fatalf("Failed to create a plugged-cert CA.")
}
signingCertBytes, signingKeyBytes, certChainBytes, rootCertBytes := ca.GetCAKeyCertBundle().GetAllPem()
if !comparePem(signingCertBytes, signingCertFile) {
t.Errorf("Failed to verify loading of signing cert pem.")
}
if !comparePem(signingKeyBytes, signingKeyFile) {
t.Errorf("Failed to verify loading of signing key pem.")
}
if !comparePem(certChainBytes, certChainFile[0]) {
t.Errorf("Failed to verify loading of cert chain pem.")
}
if !comparePem(rootCertBytes, rootCertFile) {
t.Errorf("Failed to verify loading of root cert pem.")
}
certChain, err := util.ParsePemEncodedCertificate(certChainBytes)
if err != nil {
t.Fatalf("Failed to parse cert chain pem.")
}
// if CA cert becomes invalid before workload cert it's going to cause workload cert to be invalid too,
// however citatel won't rotate if that happens
delta := certChain.NotAfter.Sub(t0.Add(ca.defaultCertTTL))
if delta >= time.Second*2 {
t.Errorf("Invalid default cert TTL, should be the same as cert chain: %v VS (expected) %v",
t0.Add(ca.defaultCertTTL),
certChain.NotAfter)
}
}
func TestSignCSR(t *testing.T) {
subjectID := "spiffe://example.com/ns/foo/sa/bar"
cases := map[string]struct {
forCA bool
certOpts util.CertOptions
maxTTL time.Duration
requestedTTL time.Duration
verifyFields util.VerifyFields
expectedError string
}{
"Workload uses RSA": {
forCA: false,
certOpts: util.CertOptions{
// This value is not used, instead, subjectID should be used in certificate.
Host: "spiffe://different.com/test",
RSAKeySize: 2048,
IsCA: false,
},
maxTTL: time.Hour,
requestedTTL: 30 * time.Minute,
verifyFields: util.VerifyFields{
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth, x509.ExtKeyUsageServerAuth},
KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment,
IsCA: false,
Host: subjectID,
},
expectedError: "",
},
"Workload uses EC": {
forCA: false,
certOpts: util.CertOptions{
// This value is not used, instead, subjectID should be used in certificate.
Host: "spiffe://different.com/test",
ECSigAlg: util.EcdsaSigAlg,
IsCA: false,
},
maxTTL: time.Hour,
requestedTTL: 30 * time.Minute,
verifyFields: util.VerifyFields{
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth, x509.ExtKeyUsageServerAuth},
KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment,
IsCA: false,
Host: subjectID,
},
expectedError: "",
},
"CA uses RSA": {
forCA: true,
certOpts: util.CertOptions{
RSAKeySize: 2048,
IsCA: true,
},
maxTTL: 365 * 24 * time.Hour,
requestedTTL: 30 * 24 * time.Hour,
verifyFields: util.VerifyFields{
KeyUsage: x509.KeyUsageCertSign,
IsCA: true,
Host: subjectID,
},
expectedError: "",
},
"CA uses EC": {
forCA: true,
certOpts: util.CertOptions{
ECSigAlg: util.EcdsaSigAlg,
IsCA: true,
},
maxTTL: 365 * 24 * time.Hour,
requestedTTL: 30 * 24 * time.Hour,
verifyFields: util.VerifyFields{
KeyUsage: x509.KeyUsageCertSign,
IsCA: true,
Host: subjectID,
},
expectedError: "",
},
"CSR uses RSA TTL error": {
forCA: false,
certOpts: util.CertOptions{
Org: "istio.io",
RSAKeySize: 2048,
},
maxTTL: 2 * time.Hour,
requestedTTL: 3 * time.Hour,
expectedError: "requested TTL 3h0m0s is greater than the max allowed TTL 2h0m0s",
},
"CSR uses EC TTL error": {
forCA: false,
certOpts: util.CertOptions{
Org: "istio.io",
ECSigAlg: util.EcdsaSigAlg,
},
maxTTL: 2 * time.Hour,
requestedTTL: 3 * time.Hour,
expectedError: "requested TTL 3h0m0s is greater than the max allowed TTL 2h0m0s",
},
}
for id, tc := range cases {
csrPEM, keyPEM, err := util.GenCSR(tc.certOpts)
if err != nil {
t.Errorf("%s: GenCSR error: %v", id, err)
}
ca, err := createCA(tc.maxTTL, tc.certOpts.ECSigAlg)
if err != nil {
t.Errorf("%s: createCA error: %v", id, err)
}
caCertOpts := CertOpts{
SubjectIDs: []string{subjectID},
TTL: tc.requestedTTL,
ForCA: tc.forCA,
}
certPEM, signErr := ca.Sign(csrPEM, caCertOpts)
if signErr != nil {
if tc.expectedError == "" {
t.Errorf("%s: Sign error: %v", id, err)
}
if certPEM != nil {
t.Errorf("%s: Expected null cert be obtained a non-null cert.", id)
}
if signErr.(*caerror.Error).Error() != tc.expectedError {
t.Errorf("%s: Expected error: %s but got error: %s.", id, tc.expectedError, signErr.(*caerror.Error).Error())
}
continue
}
_, _, certChainBytes, rootCertBytes := ca.GetCAKeyCertBundle().GetAll()
if err = util.VerifyCertificate(
keyPEM, append(certPEM, certChainBytes...), rootCertBytes, &tc.verifyFields); err != nil {
t.Errorf("%s: VerifyCertificate error: %v", id, err)
}
cert, err := util.ParsePemEncodedCertificate(certPEM)
if err != nil {
t.Errorf("%s: ParsePemEncodedCertificate error: %v", id, err)
}
if ttl := cert.NotAfter.Sub(cert.NotBefore) - util.ClockSkewGracePeriod; ttl != tc.requestedTTL {
t.Errorf("%s: Unexpected certificate TTL (expecting %v, actual %v)", id, tc.requestedTTL, ttl)
}
san := util.ExtractSANExtension(cert.Extensions)
if san == nil {
t.Errorf("%s: No SAN extension is found in the certificate", id)
}
expected, err := util.BuildSubjectAltNameExtension(subjectID)
if err != nil {
t.Errorf("%s: BuildSubjectAltNameExtension error: %v", id, err)
}
if !reflect.DeepEqual(expected, san) {
t.Errorf("%s: Unexpected extensions: wanted %v but got %v", id, expected, san)
}
}
}
func TestAppendRootCerts(t *testing.T) {
root1 := "root-cert-1"
expRootCerts := `root-cert-1
root-cert-2
root-cert-3`
rootCerts, err := util.AppendRootCerts([]byte(root1), "./root-certs-for-testing.pem")
if err != nil {
t.Errorf("AppendRootCerts() returns an error: %v", err)
} else if expRootCerts != string(rootCerts) {
t.Errorf("the root certificates do not match. Expect:%v. Actual:%v.",
expRootCerts, string(rootCerts))
}
}
func TestAppendRootCertsToNullCert(t *testing.T) {
// nil certificate
var root1 []byte
expRootCerts := `root-cert-2
root-cert-3`
rootCerts, err := util.AppendRootCerts(root1, "./root-certs-for-testing.pem")
if err != nil {
t.Errorf("AppendRootCerts() returns an error: %v", err)
} else if expRootCerts != string(rootCerts) {
t.Errorf("the root certificates do not match. Expect:%v. Actual:%v.",
expRootCerts, string(rootCerts))
}
}
func TestSignWithCertChain(t *testing.T) {
rootCertFile := "../testdata/multilevelpki/root-cert.pem"
certChainFile := []string{"../testdata/multilevelpki/int-cert-chain.pem"}
signingCertFile := "../testdata/multilevelpki/int-cert.pem"
signingKeyFile := "../testdata/multilevelpki/int-key.pem"
rsaKeySize := 2048
defaultWorkloadCertTTL := 30 * time.Minute
maxWorkloadCertTTL := time.Hour
caopts, err := NewPluggedCertIstioCAOptions(SigningCAFileBundle{rootCertFile, certChainFile, signingCertFile, signingKeyFile},
defaultWorkloadCertTTL, maxWorkloadCertTTL, rsaKeySize)
if err != nil {
t.Fatalf("Failed to create a plugged-cert CA Options: %v", err)
}
ca, err := NewIstioCA(caopts)
if err != nil {
t.Errorf("Got error while creating plugged-cert CA: %v", err)
}
if ca == nil {
t.Fatalf("Failed to create a plugged-cert CA.")
}
opts := util.CertOptions{
// This value is not used, instead, subjectID should be used in certificate.
Host: "spiffe://different.com/test",
RSAKeySize: 2048,
IsCA: false,
}
csrPEM, privPEM, err := util.GenCSR(opts)
if err != nil {
t.Error(err)
}
caCertOpts := CertOpts{
SubjectIDs: []string{"localhost"},
TTL: time.Hour,
ForCA: false,
}
certPEM, signErr := ca.signWithCertChain(csrPEM, caCertOpts.SubjectIDs, caCertOpts.TTL, true, caCertOpts.ForCA)
if signErr != nil {
t.Error(err)
}
cert, err := tls.X509KeyPair(certPEM, privPEM)
if err != nil {
t.Error(err)
}
if len(cert.Certificate) != 3 {
t.Errorf("Unexpected number of certificates returned: %d (expected 4)", len(cert.Certificate))
}
}
func TestGenKeyCert(t *testing.T) {
cases := map[string]struct {
rootCertFile string
certChainFile []string
signingCertFile string
signingKeyFile string
certLifetime time.Duration
checkCertLifetime bool
expectedError string
}{
"RSA cryptography": {
rootCertFile: "../testdata/multilevelpki/root-cert.pem",
certChainFile: []string{"../testdata/multilevelpki/int-cert-chain.pem"},
signingCertFile: "../testdata/multilevelpki/int-cert.pem",
signingKeyFile: "../testdata/multilevelpki/int-key.pem",
certLifetime: 3650 * 24 * time.Hour,
checkCertLifetime: false,
expectedError: "",
},
"EC cryptography": {
rootCertFile: "../testdata/multilevelpki/ecc-root-cert.pem",
certChainFile: []string{"../testdata/multilevelpki/ecc-int-cert-chain.pem"},
signingCertFile: "../testdata/multilevelpki/ecc-int-cert.pem",
signingKeyFile: "../testdata/multilevelpki/ecc-int-key.pem",
certLifetime: 3650 * 24 * time.Hour,
checkCertLifetime: false,
expectedError: "",
},
"Pass lifetime check": {
rootCertFile: "../testdata/multilevelpki/ecc-root-cert.pem",
certChainFile: []string{"../testdata/multilevelpki/ecc-int-cert-chain.pem"},
signingCertFile: "../testdata/multilevelpki/ecc-int-cert.pem",
signingKeyFile: "../testdata/multilevelpki/ecc-int-key.pem",
certLifetime: 24 * time.Hour,
checkCertLifetime: true,
expectedError: "",
},
"Error lifetime check": {
rootCertFile: "../testdata/multilevelpki/ecc-root-cert.pem",
certChainFile: []string{"../testdata/multilevelpki/ecc-int-cert-chain.pem"},
signingCertFile: "../testdata/multilevelpki/ecc-int-cert.pem",
signingKeyFile: "../testdata/multilevelpki/ecc-int-key.pem",
certLifetime: 25 * time.Hour,
checkCertLifetime: true,
expectedError: "requested TTL 25h0m0s is greater than the max allowed TTL 24h0m0s",
},
}
defaultWorkloadCertTTL := 30 * time.Minute
maxWorkloadCertTTL := 24 * time.Hour
rsaKeySize := 2048
for id, tc := range cases {
caopts, err := NewPluggedCertIstioCAOptions(SigningCAFileBundle{tc.rootCertFile, tc.certChainFile, tc.signingCertFile, tc.signingKeyFile},
defaultWorkloadCertTTL, maxWorkloadCertTTL, rsaKeySize)
if err != nil {
t.Fatalf("%s: failed to create a plugged-cert CA Options: %v", id, err)
}
ca, err := NewIstioCA(caopts)
if err != nil {
t.Fatalf("%s: got error while creating plugged-cert CA: %v", id, err)
}
if ca == nil {
t.Fatalf("failed to create a plugged-cert CA.")
}
certPEM, privPEM, err := ca.GenKeyCert([]string{"host1", "host2"}, tc.certLifetime, tc.checkCertLifetime)
if err != nil {
if tc.expectedError == "" {
t.Fatalf("[%s] Unexpected error: %v", id, err)
}
if err.Error() != tc.expectedError {
t.Fatalf("[%s] Error returned does not match expectation: %v VS (expected) %v", id, err, tc.expectedError)
}
continue
} else if tc.expectedError != "" {
t.Fatalf("[%s] GenKeyCert succeeded but expected error: %v", id, tc.expectedError)
}
cert, err := tls.X509KeyPair(certPEM, privPEM)
if err != nil {
t.Fatalf("[%s] X509KeyPair error: %v", id, err)
}
if len(cert.Certificate) != 3 {
t.Fatalf("[%s] unexpected number of certificates returned: %d (expected 3)", id, len(cert.Certificate))
}
}
}
func createCA(maxTTL time.Duration, ecSigAlg util.SupportedECSignatureAlgorithms) (*IstioCA, error) {
// Generate root CA key and cert.
rootCAOpts := util.CertOptions{
IsCA: true,
IsSelfSigned: true,
TTL: time.Hour,
Org: "Root CA",
RSAKeySize: 2048,
ECSigAlg: ecSigAlg,
}
rootCertBytes, rootKeyBytes, err := util.GenCertKeyFromOptions(rootCAOpts)
if err != nil {
return nil, err
}
rootCert, err := util.ParsePemEncodedCertificate(rootCertBytes)
if err != nil {
return nil, err
}
rootKey, err := util.ParsePemEncodedKey(rootKeyBytes)
if err != nil {
return nil, err
}
intermediateCAOpts := util.CertOptions{
IsCA: true,
IsSelfSigned: false,
TTL: time.Hour,
Org: "Intermediate CA",
RSAKeySize: 2048,
SignerCert: rootCert,
SignerPriv: rootKey,
ECSigAlg: ecSigAlg,
}
intermediateCert, intermediateKey, err := util.GenCertKeyFromOptions(intermediateCAOpts)
if err != nil {
return nil, err
}
bundle, err := util.NewVerifiedKeyCertBundleFromPem(
intermediateCert, intermediateKey, intermediateCert, rootCertBytes)
if err != nil {
return nil, err
}
// Disable root cert rotator by setting root cert check interval to 0ns.
rootCertCheckInverval := time.Duration(0)
caOpts := &IstioCAOptions{
DefaultCertTTL: time.Hour,
MaxCertTTL: maxTTL,
KeyCertBundle: bundle,
RotatorConfig: &SelfSignedCARootCertRotatorConfig{
CheckInterval: rootCertCheckInverval,
},
}
return NewIstioCA(caOpts)
}
func comparePem(expectedBytes []byte, file string) bool {
fileBytes, err := os.ReadFile(file)
if err != nil {
return false
}
if !bytes.Equal(fileBytes, expectedBytes) {
return false
}
return true
}