blob: 96c80eb978b8b24d5c9f3d69a8955349ac775965 [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 util
import (
"fmt"
"strings"
"testing"
"time"
)
const (
rootCertFile = "../testdata/multilevelpki/root-cert.pem"
rootKeyFile = "../testdata/multilevelpki/root-key.pem"
intCertFile = "../testdata/multilevelpki/int-cert.pem"
intKeyFile = "../testdata/multilevelpki/int-key.pem"
intCertChainFile = "../testdata/multilevelpki/int-cert-chain.pem"
int2CertFile = "../testdata/multilevelpki/int2-cert.pem"
int2KeyFile = "../testdata/multilevelpki/int2-key.pem"
int2CertChainFile = "../testdata/multilevelpki/int2-cert-chain.pem"
badCertFile = "../testdata/cert-parse-fail.pem"
badKeyFile = "../testdata/key-parse-fail.pem"
anotherKeyFile = "../testdata/key.pem"
anotherRootCertFile = "../testdata/cert.pem"
// These key/cert contain workload key/cert, and a self-signed root cert,
// all with TTL 100 years.
rootCertFile1 = "../testdata/self-signed-root-cert.pem"
certChainFile1 = "../testdata/workload-cert.pem"
keyFile1 = "../testdata/workload-key.pem"
ecRootCertFile = "../testdata/ec-root-cert.pem"
ecRootKeyFile = "../testdata/ec-root-key.pem"
ecClientCertFile = "../testdata/ec-workload-cert.pem"
ecClientKeyFile = "../testdata/ec-workload-key.pem"
)
func TestKeyCertBundleWithRootCertFromFile(t *testing.T) {
testCases := map[string]struct {
rootCertFile string
expectedErr string
}{
"File not found": {
rootCertFile: "bad.pem",
expectedErr: "open bad.pem: no such file or directory",
},
"With RSA root cert": {
rootCertFile: rootCertFile,
expectedErr: "",
},
"With EC root cert": {
rootCertFile: rootCertFile1,
expectedErr: "",
},
}
for id, tc := range testCases {
bundle, err := NewKeyCertBundleWithRootCertFromFile(tc.rootCertFile)
if err != nil {
if tc.expectedErr == "" {
t.Errorf("%s: Unexpected error: %v", id, err)
} else if strings.Compare(err.Error(), tc.expectedErr) != 0 {
t.Errorf("%s: Unexpected error: %v VS (expected) %s", id, err, tc.expectedErr)
}
} else if tc.expectedErr != "" {
t.Errorf("%s: Expected error %s but succeeded", id, tc.expectedErr)
} else if bundle == nil {
t.Errorf("%s: the bundle should not be empty", id)
} else {
cert, key, chain, root := bundle.GetAllPem()
if len(cert) != 0 {
t.Errorf("%s: certBytes should be empty", id)
}
if len(key) != 0 {
t.Errorf("%s: privateKeyBytes should be empty", id)
}
if len(chain) != 0 {
t.Errorf("%s: certChainBytes should be empty", id)
}
if len(root) == 0 {
t.Errorf("%s: rootCertBytes should not be empty", id)
}
chain = bundle.GetCertChainPem()
if len(chain) != 0 {
t.Errorf("%s: certChainBytes should be empty", id)
}
root = bundle.GetRootCertPem()
if len(root) == 0 {
t.Errorf("%s: rootCertBytes should not be empty", id)
}
x509Cert, privKey, chain, root := bundle.GetAll()
if x509Cert != nil {
t.Errorf("%s: cert should be nil", id)
}
if privKey != nil {
t.Errorf("%s: private key should be nil", id)
}
if len(chain) != 0 {
t.Errorf("%s: certChainBytes should be empty", id)
}
if len(root) == 0 {
t.Errorf("%s: rootCertBytes should not be empty", id)
}
}
}
}
// The test of CertOptions
func TestCertOptionsAndRetrieveID(t *testing.T) {
testCases := map[string]struct {
caCertFile string
caKeyFile string
certChainFile []string
rootCertFile string
certOptions *CertOptions
expectedErr string
}{
"No SAN RSA": {
caCertFile: rootCertFile,
caKeyFile: rootKeyFile,
certChainFile: nil,
rootCertFile: rootCertFile,
certOptions: &CertOptions{
Host: "test_ca.com",
TTL: time.Hour,
Org: "MyOrg",
IsCA: true,
RSAKeySize: 2048,
},
expectedErr: "failed to extract id the SAN extension does not exist",
},
"RSA Success": {
caCertFile: certChainFile1,
caKeyFile: keyFile1,
certChainFile: nil,
rootCertFile: rootCertFile1,
certOptions: &CertOptions{
Host: "watt",
TTL: 100 * 365 * 24 * time.Hour,
Org: "Juju org",
IsCA: false,
RSAKeySize: 2048,
},
expectedErr: "",
},
"No SAN EC": {
caCertFile: ecRootCertFile,
caKeyFile: ecRootKeyFile,
certChainFile: nil,
rootCertFile: ecRootCertFile,
certOptions: &CertOptions{
Host: "watt",
TTL: 100 * 365 * 24 * time.Hour,
Org: "Juju org",
IsCA: true,
ECSigAlg: EcdsaSigAlg,
},
expectedErr: "failed to extract id the SAN extension does not exist",
},
"EC Success": {
caCertFile: ecClientCertFile,
caKeyFile: ecClientKeyFile,
certChainFile: nil,
rootCertFile: ecRootCertFile,
certOptions: &CertOptions{
Host: "watt",
TTL: 10 * 365 * 24 * time.Hour,
Org: "Juju org",
IsCA: false,
ECSigAlg: EcdsaSigAlg,
},
expectedErr: "",
},
}
for id, tc := range testCases {
k, err := NewVerifiedKeyCertBundleFromFile(tc.caCertFile, tc.caKeyFile, tc.certChainFile, tc.rootCertFile)
if err != nil {
t.Fatalf("%s: Unexpected error: %v", id, err)
}
opts, err := k.CertOptions()
if err != nil {
if tc.expectedErr == "" {
t.Errorf("%s: Unexpected error: %v", id, err)
} else if strings.Compare(err.Error(), tc.expectedErr) != 0 {
t.Errorf("%s: Unexpected error: %v VS (expected) %s", id, err, tc.expectedErr)
}
} else if tc.expectedErr != "" {
t.Errorf("%s: expected error %s but have error %v", id, tc.expectedErr, err)
} else {
compareCertOptions(opts, tc.certOptions, t)
}
}
}
func compareCertOptions(actual, expected *CertOptions, t *testing.T) {
if actual.Host != expected.Host {
t.Errorf("host does not match, %s vs %s", actual.Host, expected.Host)
}
if actual.TTL != expected.TTL {
t.Errorf("TTL does not match")
}
if actual.Org != expected.Org {
t.Errorf("Org does not match")
}
if actual.IsCA != expected.IsCA {
t.Errorf("IsCA does not match")
}
if actual.RSAKeySize != expected.RSAKeySize {
t.Errorf("RSAKeySize does not match")
}
}
// The test of NewVerifiedKeyCertBundleFromPem, VerifyAndSetAll can be covered by this test.
func TestNewVerifiedKeyCertBundleFromFile(t *testing.T) {
testCases := map[string]struct {
caCertFile string
caKeyFile string
certChainFile []string
rootCertFile string
expectedErr string
}{
"Success - 1 level CA": {
caCertFile: rootCertFile,
caKeyFile: rootKeyFile,
certChainFile: nil,
rootCertFile: rootCertFile,
expectedErr: "",
},
"Success - 2 level CA": {
caCertFile: intCertFile,
caKeyFile: intKeyFile,
certChainFile: []string{intCertChainFile},
rootCertFile: rootCertFile,
expectedErr: "",
},
"Success - 3 level CA": {
caCertFile: int2CertFile,
caKeyFile: int2KeyFile,
certChainFile: []string{int2CertChainFile},
rootCertFile: rootCertFile,
expectedErr: "",
},
"Success - 2 level CA without cert chain file": {
caCertFile: intCertFile,
caKeyFile: intKeyFile,
certChainFile: nil,
rootCertFile: rootCertFile,
expectedErr: "",
},
"Failure - invalid cert chain file": {
caCertFile: intCertFile,
caKeyFile: intKeyFile,
certChainFile: []string{"bad.pem"},
rootCertFile: rootCertFile,
expectedErr: "open bad.pem: no such file or directory",
},
"Failure - no root cert file": {
caCertFile: intCertFile,
caKeyFile: intKeyFile,
certChainFile: nil,
rootCertFile: "bad.pem",
expectedErr: "open bad.pem: no such file or directory",
},
"Failure - cert and key do not match": {
caCertFile: int2CertFile,
caKeyFile: anotherKeyFile,
certChainFile: []string{int2CertChainFile},
rootCertFile: rootCertFile,
expectedErr: "the cert does not match the key",
},
"Failure - 3 level CA without cert chain file": {
caCertFile: int2CertFile,
caKeyFile: int2KeyFile,
certChainFile: nil,
rootCertFile: rootCertFile,
expectedErr: "cannot verify the cert with the provided root chain and " +
"cert pool with error: x509: certificate signed by unknown authority",
},
"Failure - cert not verifiable from root cert": {
caCertFile: intCertFile,
caKeyFile: intKeyFile,
certChainFile: []string{intCertChainFile},
rootCertFile: anotherRootCertFile,
expectedErr: "cannot verify the cert with the provided root chain and " +
"cert pool with error: x509: certificate is not authorized to sign " +
"other certificates",
},
"Failure - invalid cert": {
caCertFile: badCertFile,
caKeyFile: intKeyFile,
certChainFile: nil,
rootCertFile: rootCertFile,
expectedErr: "failed to parse cert PEM: invalid PEM encoded certificate",
},
"Failure - not existing private key": {
caCertFile: intCertFile,
caKeyFile: "bad.pem",
certChainFile: nil,
rootCertFile: rootCertFile,
expectedErr: "open bad.pem: no such file or directory",
},
"Failure - invalid private key": {
caCertFile: intCertFile,
caKeyFile: badKeyFile,
certChainFile: nil,
rootCertFile: rootCertFile,
expectedErr: "failed to parse private key PEM: invalid PEM-encoded key",
},
"Failure - file does not exist": {
caCertFile: "random/path/does/not/exist",
caKeyFile: intKeyFile,
certChainFile: nil,
rootCertFile: rootCertFile,
expectedErr: "open random/path/does/not/exist: no such file or directory",
},
}
for id, tc := range testCases {
_, err := NewVerifiedKeyCertBundleFromFile(
tc.caCertFile, tc.caKeyFile, tc.certChainFile, tc.rootCertFile)
if err != nil {
if tc.expectedErr == "" {
t.Errorf("%s: Unexpected error: %v", id, err)
} else if strings.Compare(err.Error(), tc.expectedErr) != 0 {
t.Errorf("%s: Unexpected error: %v VS (expected) %s", id, err, tc.expectedErr)
}
} else if tc.expectedErr != "" {
t.Errorf("%s: Expected error %s but succeeded", id, tc.expectedErr)
}
}
}
// Test the root cert expiry timestamp can be extracted correctly.
func TestExtractRootCertExpiryTimestamp(t *testing.T) {
t0 := time.Now()
cert, key, err := GenCertKeyFromOptions(CertOptions{
Host: "citadel.testing.istio.io",
NotBefore: t0,
TTL: time.Minute,
Org: "MyOrg",
IsCA: true,
IsSelfSigned: true,
IsServer: true,
RSAKeySize: 2048,
})
if err != nil {
t.Errorf("failed to gen cert for Citadel self signed cert %v", err)
}
kb, err := NewVerifiedKeyCertBundleFromPem(cert, key, nil, cert)
if err != nil {
t.Errorf("failed to create key cert bundle: %v", err)
}
testCases := []struct {
name string
ttl float64
time time.Time
}{
{
name: "ttl valid",
ttl: 30,
time: t0.Add(time.Second * 30),
},
{
name: "ttl almost expired",
ttl: 2,
time: t0.Add(time.Second * 58),
},
{
name: "ttl just expired",
ttl: 0,
time: t0.Add(time.Second * 60),
},
{
name: "ttl-invalid",
ttl: -30,
time: t0.Add(time.Second * 90),
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
expiryTimestamp, _ := kb.ExtractRootCertExpiryTimestamp()
// Ignore error; it just indicates cert is expired which we check via `tc.ttl`
sec := expiryTimestamp - float64(tc.time.Unix())
if sec != tc.ttl {
t.Fatalf("expected ttl %v, got %v", tc.ttl, sec)
}
})
}
}
// Test the CA cert expiry timestamp can be extracted correctly.
func TestExtractCACertExpiryTimestamp(t *testing.T) {
t0 := time.Now()
rootCertBytes, rootKeyBytes, err := GenCertKeyFromOptions(CertOptions{
Host: "citadel.testing.istio.io",
Org: "MyOrg",
NotBefore: t0,
IsCA: true,
IsSelfSigned: true,
TTL: time.Hour,
RSAKeySize: 2048,
})
if err != nil {
t.Errorf("failed to gen root cert for Citadel self signed cert %v", err)
}
rootCert, err := ParsePemEncodedCertificate(rootCertBytes)
if err != nil {
t.Errorf("failed to parsing pem for root cert %v", err)
}
rootKey, err := ParsePemEncodedKey(rootKeyBytes)
if err != nil {
t.Errorf("failed to parsing pem for root key cert %v", err)
}
caCertBytes, caCertKeyBytes, err := GenCertKeyFromOptions(CertOptions{
Host: "citadel.testing.istio.io",
Org: "MyOrg",
NotBefore: t0,
TTL: time.Second * 60,
IsServer: true,
IsCA: true,
IsSelfSigned: false,
RSAKeySize: 2048,
SignerCert: rootCert,
SignerPriv: rootKey,
})
if err != nil {
t.Fatalf("failed to gen CA cert for Citadel self signed cert %v", err)
}
kb, err := NewVerifiedKeyCertBundleFromPem(
caCertBytes, caCertKeyBytes, caCertBytes, rootCertBytes)
if err != nil {
t.Fatalf("failed to create key cert bundle: %v", err)
}
testCases := []struct {
name string
ttl float64
time time.Time
}{
{
name: "ttl valid",
ttl: 30,
time: t0.Add(time.Second * 30),
},
{
name: "ttl almost expired",
ttl: 2,
time: t0.Add(time.Second * 58),
},
{
name: "ttl just expired",
ttl: 0,
time: t0.Add(time.Second * 60),
},
{
name: "ttl-invalid",
ttl: -30,
time: t0.Add(time.Second * 90),
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
expiryTimestamp, _ := kb.ExtractCACertExpiryTimestamp()
// Ignore error; it just indicates cert is expired which we check via `tc.ttl`
sec := expiryTimestamp - float64(tc.time.Unix())
if sec != tc.ttl {
t.Fatalf("expected ttl %v, got %v", tc.ttl, sec)
}
})
}
}
func TestTimeBeforeCertExpires(t *testing.T) {
t0 := time.Now()
certTTL := time.Second * 60
rootCertBytes, _, err := GenCertKeyFromOptions(CertOptions{
Host: "citadel.testing.istio.io",
Org: "MyOrg",
NotBefore: t0,
IsCA: true,
IsSelfSigned: true,
TTL: certTTL,
RSAKeySize: 2048,
})
if err != nil {
t.Errorf("failed to gen root cert for Citadel self signed cert %v", err)
}
testCases := []struct {
name string
cert []byte
expectedTime time.Duration
timeNow time.Time
expectedErr error
}{
{
name: "TTL left should be equal to cert TTL",
cert: rootCertBytes,
timeNow: t0,
expectedTime: certTTL,
},
{
name: "TTL left should be ca cert ttl minus 5 seconds",
cert: rootCertBytes,
timeNow: t0.Add(5 * time.Second),
expectedTime: 55 * time.Second,
},
{
name: "TTL left should be negative because already got expired",
cert: rootCertBytes,
timeNow: t0.Add(120 * time.Second),
expectedTime: -60 * time.Second,
},
{
name: "no cert, so it should return an error",
cert: nil,
timeNow: t0,
expectedErr: fmt.Errorf("no certificate found"),
},
{
name: "invalid cert",
cert: []byte("invalid cert"),
timeNow: t0,
expectedErr: fmt.Errorf("failed to extract cert expiration timestamp: failed to parse the cert: invalid PEM encoded certificate"),
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
time, err := TimeBeforeCertExpires(tc.cert, tc.timeNow)
if err != nil {
if tc.expectedErr == nil {
t.Fatalf("Unexpected error: %v", err)
} else if strings.Compare(err.Error(), tc.expectedErr.Error()) != 0 {
t.Errorf("expected error: %v got %v", err, tc.expectedErr)
}
return
}
if time != tc.expectedTime {
t.Fatalf("expected time %v, got %v", tc.expectedTime, time)
}
})
}
}