1082 lines
33 KiB
Go
1082 lines
33 KiB
Go
/*
|
|
Copyright 2020 The cert-manager 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 pki
|
|
|
|
import (
|
|
"bytes"
|
|
"crypto"
|
|
"crypto/x509"
|
|
"crypto/x509/pkix"
|
|
"encoding/asn1"
|
|
"fmt"
|
|
"math/big"
|
|
"net"
|
|
"reflect"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
|
|
cmapi "github.com/cert-manager/cert-manager/pkg/apis/certmanager/v1"
|
|
"github.com/cert-manager/cert-manager/pkg/util"
|
|
)
|
|
|
|
func TestKeyUsagesForCertificate(t *testing.T) {
|
|
type testT struct {
|
|
name string
|
|
usages []cmapi.KeyUsage
|
|
isCa bool
|
|
expectedKeyUsage x509.KeyUsage
|
|
expectedExtKeyUsage []x509.ExtKeyUsage
|
|
expectedError bool
|
|
}
|
|
tests := []testT{
|
|
{
|
|
name: "default",
|
|
usages: []cmapi.KeyUsage{},
|
|
expectedKeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment,
|
|
expectedError: false,
|
|
},
|
|
{
|
|
name: "isCa",
|
|
usages: []cmapi.KeyUsage{},
|
|
isCa: true,
|
|
expectedKeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment | x509.KeyUsageCertSign,
|
|
expectedError: false,
|
|
},
|
|
{
|
|
name: "existing keyusage",
|
|
usages: []cmapi.KeyUsage{"crl sign"},
|
|
expectedKeyUsage: x509.KeyUsageCRLSign,
|
|
expectedError: false,
|
|
},
|
|
{
|
|
name: "nonexistent keyusage error",
|
|
usages: []cmapi.KeyUsage{"nonexistent"},
|
|
expectedError: true,
|
|
},
|
|
{
|
|
name: "duplicate keyusage",
|
|
usages: []cmapi.KeyUsage{"signing", "signing"},
|
|
expectedKeyUsage: x509.KeyUsageDigitalSignature,
|
|
expectedError: false,
|
|
},
|
|
{
|
|
name: "existing extkeyusage",
|
|
usages: []cmapi.KeyUsage{"server auth"},
|
|
expectedExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
|
|
expectedError: false,
|
|
},
|
|
{
|
|
name: "duplicate extkeyusage",
|
|
usages: []cmapi.KeyUsage{"s/mime", "s/mime"},
|
|
expectedExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageEmailProtection, x509.ExtKeyUsageEmailProtection},
|
|
expectedError: false,
|
|
},
|
|
}
|
|
testFn := func(test testT) func(*testing.T) {
|
|
return func(t *testing.T) {
|
|
ku, eku, err := KeyUsagesForCertificateOrCertificateRequest(test.usages, test.isCa)
|
|
if err != nil && !test.expectedError {
|
|
t.Errorf("got unexpected error generating cert: %q", err)
|
|
return
|
|
}
|
|
if !reflect.DeepEqual(ku, test.expectedKeyUsage) {
|
|
t.Errorf("keyUsages don't match, got %q, expected %q", ku, test.expectedKeyUsage)
|
|
return
|
|
}
|
|
if !reflect.DeepEqual(eku, test.expectedExtKeyUsage) {
|
|
t.Errorf("extKeyUsages don't match, got %q, expected %q", eku, test.expectedExtKeyUsage)
|
|
return
|
|
}
|
|
}
|
|
}
|
|
for _, test := range tests {
|
|
t.Run(test.name, testFn(test))
|
|
}
|
|
}
|
|
|
|
func TestSignatureAlgorithmForCertificate(t *testing.T) {
|
|
type testT struct {
|
|
name string
|
|
keyAlgo cmapi.PrivateKeyAlgorithm
|
|
keySize int
|
|
expectErr bool
|
|
expectedSigAlgo x509.SignatureAlgorithm
|
|
expectedKeyType x509.PublicKeyAlgorithm
|
|
}
|
|
|
|
tests := []testT{
|
|
{
|
|
name: "certificate with KeyAlgorithm rsa and size 1024",
|
|
keyAlgo: cmapi.RSAKeyAlgorithm,
|
|
keySize: 1024,
|
|
expectErr: true,
|
|
},
|
|
{
|
|
name: "certificate with KeyAlgorithm rsa and no size set should default to rsa256",
|
|
keyAlgo: cmapi.RSAKeyAlgorithm,
|
|
expectedSigAlgo: x509.SHA256WithRSA,
|
|
expectedKeyType: x509.RSA,
|
|
},
|
|
{
|
|
name: "certificate with KeyAlgorithm not set",
|
|
keyAlgo: cmapi.PrivateKeyAlgorithm(""),
|
|
expectedSigAlgo: x509.SHA256WithRSA,
|
|
expectedKeyType: x509.RSA,
|
|
},
|
|
{
|
|
name: "certificate with KeyAlgorithm rsa and size 2048",
|
|
keyAlgo: cmapi.RSAKeyAlgorithm,
|
|
keySize: 2048,
|
|
expectedSigAlgo: x509.SHA256WithRSA,
|
|
expectedKeyType: x509.RSA,
|
|
},
|
|
{
|
|
name: "certificate with KeyAlgorithm rsa and size 3072",
|
|
keyAlgo: cmapi.RSAKeyAlgorithm,
|
|
keySize: 3072,
|
|
expectedSigAlgo: x509.SHA384WithRSA,
|
|
expectedKeyType: x509.RSA,
|
|
},
|
|
{
|
|
name: "certificate with KeyAlgorithm rsa and size 4096",
|
|
keyAlgo: cmapi.RSAKeyAlgorithm,
|
|
keySize: 4096,
|
|
expectedSigAlgo: x509.SHA512WithRSA,
|
|
expectedKeyType: x509.RSA,
|
|
},
|
|
{
|
|
name: "certificate with ecdsa key algorithm set and no key size default to ecdsa256",
|
|
keyAlgo: cmapi.ECDSAKeyAlgorithm,
|
|
expectedSigAlgo: x509.ECDSAWithSHA256,
|
|
expectedKeyType: x509.ECDSA,
|
|
},
|
|
{
|
|
name: "certificate with KeyAlgorithm ecdsa and size 256",
|
|
keyAlgo: cmapi.ECDSAKeyAlgorithm,
|
|
keySize: 256,
|
|
expectedSigAlgo: x509.ECDSAWithSHA256,
|
|
expectedKeyType: x509.ECDSA,
|
|
},
|
|
{
|
|
name: "certificate with KeyAlgorithm ecdsa and size 384",
|
|
keyAlgo: cmapi.ECDSAKeyAlgorithm,
|
|
keySize: 384,
|
|
expectedSigAlgo: x509.ECDSAWithSHA384,
|
|
expectedKeyType: x509.ECDSA,
|
|
},
|
|
{
|
|
name: "certificate with KeyAlgorithm ecdsa and size 521",
|
|
keyAlgo: cmapi.ECDSAKeyAlgorithm,
|
|
keySize: 521,
|
|
expectedSigAlgo: x509.ECDSAWithSHA512,
|
|
expectedKeyType: x509.ECDSA,
|
|
},
|
|
{
|
|
name: "certificate with KeyAlgorithm Ed25519",
|
|
keyAlgo: cmapi.Ed25519KeyAlgorithm,
|
|
expectedSigAlgo: x509.PureEd25519,
|
|
expectedKeyType: x509.Ed25519,
|
|
},
|
|
{
|
|
name: "certificate with KeyAlgorithm ecdsa and size 100",
|
|
keyAlgo: cmapi.ECDSAKeyAlgorithm,
|
|
keySize: 100,
|
|
expectErr: true,
|
|
},
|
|
{
|
|
name: "certificate with KeyAlgorithm set to unknown key algo",
|
|
keyAlgo: cmapi.PrivateKeyAlgorithm("blah"),
|
|
expectErr: true,
|
|
},
|
|
}
|
|
|
|
testFn := func(test testT) func(*testing.T) {
|
|
return func(t *testing.T) {
|
|
actualPKAlgo, actualSigAlgo, err := SignatureAlgorithm(buildCertificateWithKeyParams(test.keyAlgo, test.keySize))
|
|
if test.expectErr && err == nil {
|
|
t.Error("expected err, but got no error")
|
|
return
|
|
}
|
|
|
|
if !test.expectErr {
|
|
if err != nil {
|
|
t.Errorf("expected no err, but got '%q'", err)
|
|
return
|
|
}
|
|
|
|
if actualSigAlgo != test.expectedSigAlgo {
|
|
t.Errorf("expected %q but got %q", test.expectedSigAlgo, actualSigAlgo)
|
|
return
|
|
}
|
|
|
|
if actualPKAlgo != test.expectedKeyType {
|
|
t.Errorf("expected %q but got %q", test.expectedKeyType, actualPKAlgo)
|
|
return
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
for _, test := range tests {
|
|
t.Run(test.name, testFn(test))
|
|
}
|
|
}
|
|
|
|
func TestRemoveDuplicates(t *testing.T) {
|
|
type testT struct {
|
|
input []string
|
|
output []string
|
|
}
|
|
tests := []testT{
|
|
{
|
|
input: []string{"a"},
|
|
output: []string{"a"},
|
|
},
|
|
{
|
|
input: []string{"a", "b"},
|
|
output: []string{"a", "b"},
|
|
},
|
|
{
|
|
input: []string{"a", "a"},
|
|
output: []string{"a"},
|
|
},
|
|
{
|
|
input: []string{"a", "b", "a", "a", "c"},
|
|
output: []string{"a", "b", "c"},
|
|
},
|
|
}
|
|
for _, test := range tests {
|
|
actualOutput := removeDuplicates(test.input)
|
|
if len(actualOutput) != len(test.output) ||
|
|
!util.EqualUnsorted(test.output, actualOutput) {
|
|
t.Errorf("returned %q for %q but expected %q", actualOutput, test.input, test.output)
|
|
continue
|
|
}
|
|
}
|
|
}
|
|
|
|
func removeDuplicates(in []string) []string {
|
|
var found []string
|
|
Outer:
|
|
for _, i := range in {
|
|
for _, i2 := range found {
|
|
if i2 == i {
|
|
continue Outer
|
|
}
|
|
}
|
|
found = append(found, i)
|
|
}
|
|
return found
|
|
}
|
|
|
|
func OtherNameSANRawVal(expectedOID asn1.ObjectIdentifier) (asn1.RawValue, error) {
|
|
var otherNameParam = fmt.Sprintf("tag:%d", nameTypeOtherName)
|
|
|
|
value, err := MarshalUniversalValue(UniversalValue{
|
|
UTF8String: "user@example.org",
|
|
})
|
|
if err != nil {
|
|
return asn1.NullRawValue, err
|
|
}
|
|
|
|
otherNameDer, err := asn1.MarshalWithParams(OtherName{
|
|
TypeID: expectedOID, // UPN OID
|
|
Value: asn1.RawValue{
|
|
Tag: 0,
|
|
Class: asn1.ClassContextSpecific,
|
|
IsCompound: true,
|
|
Bytes: value,
|
|
},
|
|
}, otherNameParam)
|
|
|
|
if err != nil {
|
|
return asn1.NullRawValue, err
|
|
}
|
|
rawVal := asn1.RawValue{
|
|
FullBytes: otherNameDer,
|
|
}
|
|
return rawVal, nil
|
|
}
|
|
|
|
func TestGenerateCSR(t *testing.T) {
|
|
exampleLiteralSubject := "CN=actual-cn, OU=FooLong, OU=Bar, O=example.org"
|
|
exampleMultiValueRDNLiteralSubject := "CN=actual-cn, OU=FooLong+OU=Bar, O=example.org"
|
|
|
|
asn1otherNameUpnSANRawVal, err := OtherNameSANRawVal(asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 311, 20, 2, 3}) // UPN OID
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
asn1otherNamesAMAAccountNameRawVal, err := OtherNameSANRawVal(asn1.ObjectIdentifier{1, 2, 840, 113556, 1, 4, 221}) // sAMAccountName OID
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
// 0xa0 = DigitalSignature and Encipherment usage
|
|
asn1DefaultKeyUsage, err := asn1.Marshal(asn1.BitString{Bytes: []byte{0xa0}, BitLength: asn1BitLength([]byte{0xa0})})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
// 0xa4 = DigitalSignature, Encipherment and KeyCertSign usage
|
|
asn1KeyUsageWithCa, err := asn1.Marshal(asn1.BitString{Bytes: []byte{0xa4}, BitLength: asn1BitLength([]byte{0xa4})})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
asn1ClientAuth, err := asn1.Marshal([]asn1.ObjectIdentifier{oidExtKeyUsageClientAuth})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
asn1ServerClientAuth, err := asn1.Marshal([]asn1.ObjectIdentifier{oidExtKeyUsageServerAuth, oidExtKeyUsageClientAuth})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
asn1ExtKeyUsage, err := asn1.Marshal([]asn1.ObjectIdentifier{oidExtKeyUsageIPSECEndSystem})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
basicConstraintsGenerator := func(t *testing.T, isCA bool) []byte {
|
|
data, err := asn1.Marshal(struct {
|
|
IsCA bool `asn1:"optional"`
|
|
}{
|
|
IsCA: isCA,
|
|
})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return data
|
|
}
|
|
|
|
subjectGenerator := func(t *testing.T, name pkix.Name) []byte {
|
|
data, err := MarshalRDNSequenceToRawDERBytes(name.ToRDNSequence())
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return data
|
|
}
|
|
|
|
sansGenerator := func(t *testing.T, generalNames []asn1.RawValue, critical bool) pkix.Extension {
|
|
val, err := asn1.Marshal(generalNames)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
return pkix.Extension{
|
|
Id: oidExtensionSubjectAltName,
|
|
Critical: critical,
|
|
Value: val,
|
|
}
|
|
}
|
|
|
|
literalSubectGenerator := func(t *testing.T, literal string) []byte {
|
|
rawSubject, err := UnmarshalSubjectStringToRDNSequence(literal)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
asn1Subject, err := MarshalRDNSequenceToRawDERBytes(rawSubject)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return asn1Subject
|
|
}
|
|
|
|
tests := []struct {
|
|
name string
|
|
crt *cmapi.Certificate
|
|
want *x509.CertificateRequest
|
|
wantErr bool
|
|
literalCertificateSubjectFeatureEnabled bool
|
|
basicConstraintsFeatureEnabled bool
|
|
nameConstraintsFeatureEnabled bool
|
|
otherNamesFeatureEnabled bool
|
|
}{
|
|
{
|
|
name: "Generate CSR from certificate with only DNS",
|
|
crt: &cmapi.Certificate{Spec: cmapi.CertificateSpec{DNSNames: []string{"example.org"}}},
|
|
want: &x509.CertificateRequest{
|
|
Version: 0,
|
|
SignatureAlgorithm: x509.SHA256WithRSA,
|
|
PublicKeyAlgorithm: x509.RSA,
|
|
ExtraExtensions: []pkix.Extension{
|
|
sansGenerator(
|
|
t,
|
|
[]asn1.RawValue{
|
|
{Tag: nameTypeDNSName, Class: 2, Bytes: []byte("example.org")},
|
|
},
|
|
true, // SAN is critical as the Subject is empty
|
|
),
|
|
{
|
|
Id: OIDExtensionKeyUsage,
|
|
Value: asn1DefaultKeyUsage,
|
|
Critical: true,
|
|
},
|
|
},
|
|
RawSubject: subjectGenerator(t, pkix.Name{}),
|
|
},
|
|
},
|
|
{
|
|
name: "Generate CSR from certificate with subject and DNS",
|
|
crt: &cmapi.Certificate{Spec: cmapi.CertificateSpec{
|
|
Subject: &cmapi.X509Subject{Organizations: []string{"example inc."}},
|
|
DNSNames: []string{"example.org"},
|
|
}},
|
|
want: &x509.CertificateRequest{
|
|
Version: 0,
|
|
SignatureAlgorithm: x509.SHA256WithRSA,
|
|
PublicKeyAlgorithm: x509.RSA,
|
|
ExtraExtensions: []pkix.Extension{
|
|
sansGenerator(
|
|
t,
|
|
[]asn1.RawValue{
|
|
{Tag: nameTypeDNSName, Class: 2, Bytes: []byte("example.org")},
|
|
},
|
|
false, // SAN is NOT critical as the Subject is not empty
|
|
),
|
|
{
|
|
Id: OIDExtensionKeyUsage,
|
|
Value: asn1DefaultKeyUsage,
|
|
Critical: true,
|
|
},
|
|
},
|
|
RawSubject: subjectGenerator(t, pkix.Name{Organization: []string{"example inc."}}),
|
|
},
|
|
},
|
|
{
|
|
name: "Generate CSR from certificate with only CN",
|
|
crt: &cmapi.Certificate{Spec: cmapi.CertificateSpec{CommonName: "example.org"}},
|
|
want: &x509.CertificateRequest{
|
|
Version: 0,
|
|
SignatureAlgorithm: x509.SHA256WithRSA,
|
|
PublicKeyAlgorithm: x509.RSA,
|
|
ExtraExtensions: []pkix.Extension{
|
|
{
|
|
Id: OIDExtensionKeyUsage,
|
|
Value: asn1DefaultKeyUsage,
|
|
Critical: true,
|
|
},
|
|
},
|
|
RawSubject: subjectGenerator(t, pkix.Name{CommonName: "example.org"}),
|
|
},
|
|
},
|
|
{
|
|
name: "Generate CSR from certificate with isCA set",
|
|
crt: &cmapi.Certificate{Spec: cmapi.CertificateSpec{CommonName: "example.org", IsCA: true}},
|
|
want: &x509.CertificateRequest{
|
|
Version: 0,
|
|
SignatureAlgorithm: x509.SHA256WithRSA,
|
|
PublicKeyAlgorithm: x509.RSA,
|
|
ExtraExtensions: []pkix.Extension{
|
|
{
|
|
Id: OIDExtensionKeyUsage,
|
|
Value: asn1KeyUsageWithCa,
|
|
Critical: true,
|
|
},
|
|
},
|
|
RawSubject: subjectGenerator(t, pkix.Name{CommonName: "example.org"}),
|
|
},
|
|
},
|
|
{
|
|
name: "Generate CSR from certificate with isCA not set and with UseCertificateRequestBasicConstraints flag enabled",
|
|
crt: &cmapi.Certificate{Spec: cmapi.CertificateSpec{CommonName: "example.org"}},
|
|
want: &x509.CertificateRequest{
|
|
Version: 0,
|
|
SignatureAlgorithm: x509.SHA256WithRSA,
|
|
PublicKeyAlgorithm: x509.RSA,
|
|
ExtraExtensions: []pkix.Extension{
|
|
{
|
|
Id: OIDExtensionKeyUsage,
|
|
Value: asn1DefaultKeyUsage,
|
|
Critical: true,
|
|
},
|
|
{
|
|
Id: OIDExtensionBasicConstraints,
|
|
Value: basicConstraintsGenerator(t, false),
|
|
Critical: true,
|
|
},
|
|
},
|
|
RawSubject: subjectGenerator(t, pkix.Name{CommonName: "example.org"}),
|
|
},
|
|
basicConstraintsFeatureEnabled: true,
|
|
},
|
|
{
|
|
name: "Generate CSR from certificate with isCA set and with UseCertificateRequestBasicConstraints flag enabled",
|
|
crt: &cmapi.Certificate{Spec: cmapi.CertificateSpec{CommonName: "example.org", IsCA: true}},
|
|
want: &x509.CertificateRequest{
|
|
Version: 0,
|
|
SignatureAlgorithm: x509.SHA256WithRSA,
|
|
PublicKeyAlgorithm: x509.RSA,
|
|
ExtraExtensions: []pkix.Extension{
|
|
{
|
|
Id: OIDExtensionKeyUsage,
|
|
Value: asn1KeyUsageWithCa,
|
|
Critical: true,
|
|
},
|
|
{
|
|
Id: OIDExtensionBasicConstraints,
|
|
Value: basicConstraintsGenerator(t, true),
|
|
Critical: true,
|
|
},
|
|
},
|
|
RawSubject: subjectGenerator(t, pkix.Name{CommonName: "example.org"}),
|
|
},
|
|
basicConstraintsFeatureEnabled: true,
|
|
},
|
|
{
|
|
name: "Generate CSR from certificate with extended key usages",
|
|
crt: &cmapi.Certificate{Spec: cmapi.CertificateSpec{CommonName: "example.org", Usages: []cmapi.KeyUsage{cmapi.UsageDigitalSignature, cmapi.UsageKeyEncipherment, cmapi.UsageIPsecEndSystem}}},
|
|
want: &x509.CertificateRequest{
|
|
Version: 0,
|
|
SignatureAlgorithm: x509.SHA256WithRSA,
|
|
PublicKeyAlgorithm: x509.RSA,
|
|
ExtraExtensions: []pkix.Extension{
|
|
{
|
|
Id: OIDExtensionKeyUsage,
|
|
Value: asn1DefaultKeyUsage,
|
|
Critical: true,
|
|
},
|
|
{
|
|
Id: OIDExtensionExtendedKeyUsage,
|
|
Value: asn1ExtKeyUsage,
|
|
},
|
|
},
|
|
RawSubject: subjectGenerator(t, pkix.Name{CommonName: "example.org"}),
|
|
},
|
|
},
|
|
{
|
|
name: "Generate CSR from certificate with a single otherNameSAN set to an oid (UPN)", // only a shallow validation is expected
|
|
crt: &cmapi.Certificate{Spec: cmapi.CertificateSpec{OtherNames: []cmapi.OtherName{
|
|
{
|
|
OID: "1.3.6.1.4.1.311.20.2.3",
|
|
UTF8Value: "user@example.org",
|
|
},
|
|
}}},
|
|
want: &x509.CertificateRequest{
|
|
Version: 0,
|
|
SignatureAlgorithm: x509.SHA256WithRSA,
|
|
PublicKeyAlgorithm: x509.RSA,
|
|
ExtraExtensions: []pkix.Extension{
|
|
sansGenerator(
|
|
t,
|
|
[]asn1.RawValue{asn1otherNameUpnSANRawVal},
|
|
true,
|
|
),
|
|
{
|
|
Id: OIDExtensionKeyUsage,
|
|
Value: asn1DefaultKeyUsage,
|
|
Critical: true,
|
|
},
|
|
},
|
|
RawSubject: subjectGenerator(t, pkix.Name{}),
|
|
},
|
|
otherNamesFeatureEnabled: true,
|
|
},
|
|
{
|
|
name: "Generate CSR from certificate with multiple valid otherName oids and emailSANs set",
|
|
crt: &cmapi.Certificate{Spec: cmapi.CertificateSpec{
|
|
EmailAddresses: []string{"user@example.org", "alt-email@example.org"},
|
|
OtherNames: []cmapi.OtherName{
|
|
{
|
|
OID: "1.3.6.1.4.1.311.20.2.3",
|
|
UTF8Value: "user@example.org",
|
|
},
|
|
{
|
|
OID: "1.2.840.113556.1.4.221",
|
|
UTF8Value: "user@example.org",
|
|
},
|
|
}}},
|
|
want: &x509.CertificateRequest{
|
|
Version: 0,
|
|
SignatureAlgorithm: x509.SHA256WithRSA,
|
|
PublicKeyAlgorithm: x509.RSA,
|
|
ExtraExtensions: []pkix.Extension{
|
|
sansGenerator(
|
|
t,
|
|
[]asn1.RawValue{
|
|
{Tag: nameTypeRFC822Name, Class: 2, Bytes: []byte("user@example.org")},
|
|
{Tag: nameTypeRFC822Name, Class: 2, Bytes: []byte("alt-email@example.org")},
|
|
asn1otherNameUpnSANRawVal,
|
|
asn1otherNamesAMAAccountNameRawVal,
|
|
},
|
|
true,
|
|
),
|
|
{
|
|
Id: OIDExtensionKeyUsage,
|
|
Value: asn1DefaultKeyUsage,
|
|
Critical: true,
|
|
},
|
|
},
|
|
RawSubject: subjectGenerator(t, pkix.Name{}),
|
|
},
|
|
otherNamesFeatureEnabled: true,
|
|
},
|
|
{
|
|
name: "Generate CSR from certificate with malformed otherName oid type",
|
|
crt: &cmapi.Certificate{Spec: cmapi.CertificateSpec{OtherNames: []cmapi.OtherName{
|
|
{
|
|
OID: "NOTANOID@garbage",
|
|
UTF8Value: "user@example.org",
|
|
},
|
|
}}},
|
|
wantErr: true,
|
|
},
|
|
{
|
|
name: "Generate CSR from certificate with double signing key usages",
|
|
crt: &cmapi.Certificate{Spec: cmapi.CertificateSpec{CommonName: "example.org", Usages: []cmapi.KeyUsage{cmapi.UsageDigitalSignature, cmapi.UsageKeyEncipherment, cmapi.UsageSigning}}},
|
|
want: &x509.CertificateRequest{
|
|
Version: 0,
|
|
SignatureAlgorithm: x509.SHA256WithRSA,
|
|
PublicKeyAlgorithm: x509.RSA,
|
|
ExtraExtensions: []pkix.Extension{
|
|
{
|
|
Id: OIDExtensionKeyUsage,
|
|
Value: asn1DefaultKeyUsage,
|
|
Critical: true,
|
|
},
|
|
},
|
|
RawSubject: subjectGenerator(t, pkix.Name{CommonName: "example.org"}),
|
|
},
|
|
},
|
|
{
|
|
name: "Error on generating CSR from certificate with no subject",
|
|
crt: &cmapi.Certificate{Spec: cmapi.CertificateSpec{}},
|
|
wantErr: true,
|
|
},
|
|
{
|
|
name: "Generate CSR from certficate with literal subject honouring the exact order",
|
|
crt: &cmapi.Certificate{Spec: cmapi.CertificateSpec{LiteralSubject: exampleLiteralSubject}},
|
|
want: &x509.CertificateRequest{
|
|
Version: 0,
|
|
SignatureAlgorithm: x509.SHA256WithRSA,
|
|
PublicKeyAlgorithm: x509.RSA,
|
|
ExtraExtensions: []pkix.Extension{
|
|
{
|
|
Id: OIDExtensionKeyUsage,
|
|
Value: asn1DefaultKeyUsage,
|
|
Critical: true,
|
|
},
|
|
},
|
|
RawSubject: literalSubectGenerator(t, exampleLiteralSubject),
|
|
},
|
|
literalCertificateSubjectFeatureEnabled: true,
|
|
},
|
|
{
|
|
name: "Generate CSR from certficate with literal multi value subject honouring the exact order",
|
|
crt: &cmapi.Certificate{Spec: cmapi.CertificateSpec{LiteralSubject: exampleMultiValueRDNLiteralSubject}},
|
|
want: &x509.CertificateRequest{
|
|
Version: 0,
|
|
SignatureAlgorithm: x509.SHA256WithRSA,
|
|
PublicKeyAlgorithm: x509.RSA,
|
|
ExtraExtensions: []pkix.Extension{
|
|
{
|
|
Id: OIDExtensionKeyUsage,
|
|
Value: asn1DefaultKeyUsage,
|
|
Critical: true,
|
|
},
|
|
},
|
|
RawSubject: literalSubectGenerator(t, exampleMultiValueRDNLiteralSubject),
|
|
},
|
|
literalCertificateSubjectFeatureEnabled: true,
|
|
},
|
|
{
|
|
name: "Error on generating CSR from certificate without CommonName in LiteralSubject, uri names, email address, ip addresses or otherName set",
|
|
crt: &cmapi.Certificate{Spec: cmapi.CertificateSpec{LiteralSubject: "O=EmptyOrg"}},
|
|
wantErr: true,
|
|
literalCertificateSubjectFeatureEnabled: true,
|
|
},
|
|
{
|
|
name: "KeyUsages and ExtendedKeyUsages: no usages set",
|
|
crt: &cmapi.Certificate{Spec: cmapi.CertificateSpec{DNSNames: []string{"example.org"}}},
|
|
want: &x509.CertificateRequest{
|
|
Version: 0,
|
|
SignatureAlgorithm: x509.SHA256WithRSA,
|
|
PublicKeyAlgorithm: x509.RSA,
|
|
ExtraExtensions: []pkix.Extension{
|
|
sansGenerator(
|
|
t,
|
|
[]asn1.RawValue{
|
|
{Tag: nameTypeDNSName, Class: 2, Bytes: []byte("example.org")},
|
|
},
|
|
true,
|
|
),
|
|
{
|
|
Id: OIDExtensionKeyUsage,
|
|
Value: asn1DefaultKeyUsage,
|
|
Critical: true,
|
|
},
|
|
},
|
|
RawSubject: subjectGenerator(t, pkix.Name{}),
|
|
},
|
|
wantErr: false,
|
|
},
|
|
{
|
|
name: "KeyUsages and ExtendedKeyUsages: client auth extended usage set",
|
|
crt: &cmapi.Certificate{
|
|
Spec: cmapi.CertificateSpec{
|
|
DNSNames: []string{"example.org"},
|
|
Usages: []cmapi.KeyUsage{cmapi.UsageDigitalSignature, cmapi.UsageKeyEncipherment, cmapi.UsageClientAuth},
|
|
},
|
|
},
|
|
want: &x509.CertificateRequest{
|
|
Version: 0,
|
|
SignatureAlgorithm: x509.SHA256WithRSA,
|
|
PublicKeyAlgorithm: x509.RSA,
|
|
ExtraExtensions: []pkix.Extension{
|
|
sansGenerator(
|
|
t,
|
|
[]asn1.RawValue{
|
|
{Tag: nameTypeDNSName, Class: 2, Bytes: []byte("example.org")},
|
|
},
|
|
true,
|
|
),
|
|
{
|
|
Id: OIDExtensionKeyUsage,
|
|
Value: asn1DefaultKeyUsage,
|
|
Critical: true,
|
|
},
|
|
{
|
|
Id: OIDExtensionExtendedKeyUsage,
|
|
Value: asn1ClientAuth,
|
|
},
|
|
},
|
|
RawSubject: subjectGenerator(t, pkix.Name{}),
|
|
},
|
|
wantErr: false,
|
|
},
|
|
{
|
|
name: "KeyUsages and ExtendedKeyUsages: server + client auth extended usage set",
|
|
crt: &cmapi.Certificate{
|
|
Spec: cmapi.CertificateSpec{
|
|
DNSNames: []string{"example.org"},
|
|
Usages: []cmapi.KeyUsage{cmapi.UsageDigitalSignature, cmapi.UsageKeyEncipherment, cmapi.UsageServerAuth, cmapi.UsageClientAuth},
|
|
},
|
|
},
|
|
want: &x509.CertificateRequest{
|
|
Version: 0,
|
|
SignatureAlgorithm: x509.SHA256WithRSA,
|
|
PublicKeyAlgorithm: x509.RSA,
|
|
ExtraExtensions: []pkix.Extension{
|
|
sansGenerator(
|
|
t,
|
|
[]asn1.RawValue{
|
|
{Tag: nameTypeDNSName, Class: 2, Bytes: []byte("example.org")},
|
|
},
|
|
true,
|
|
),
|
|
{
|
|
Id: OIDExtensionKeyUsage,
|
|
Value: asn1DefaultKeyUsage,
|
|
Critical: true,
|
|
},
|
|
{
|
|
Id: OIDExtensionExtendedKeyUsage,
|
|
Value: asn1ServerClientAuth,
|
|
},
|
|
},
|
|
RawSubject: subjectGenerator(t, pkix.Name{}),
|
|
},
|
|
wantErr: false,
|
|
},
|
|
{
|
|
name: "Generate CSR from certificate with NameConstraints flag enabled",
|
|
crt: &cmapi.Certificate{Spec: cmapi.CertificateSpec{
|
|
CommonName: "example.org",
|
|
IsCA: true,
|
|
NameConstraints: &cmapi.NameConstraints{
|
|
Critical: true,
|
|
Permitted: &cmapi.NameConstraintItem{
|
|
DNSDomains: []string{"example.org"},
|
|
IPRanges: []string{"10.10.0.0/16"},
|
|
EmailAddresses: []string{"email@email.org"},
|
|
},
|
|
Excluded: &cmapi.NameConstraintItem{
|
|
IPRanges: []string{"10.10.0.0/24"},
|
|
},
|
|
},
|
|
}},
|
|
want: &x509.CertificateRequest{
|
|
Version: 0,
|
|
SignatureAlgorithm: x509.SHA256WithRSA,
|
|
PublicKeyAlgorithm: x509.RSA,
|
|
ExtraExtensions: []pkix.Extension{
|
|
{
|
|
Id: OIDExtensionKeyUsage,
|
|
Value: asn1KeyUsageWithCa,
|
|
Critical: true,
|
|
},
|
|
{
|
|
Id: OIDExtensionNameConstraints,
|
|
Value: []byte{0x30, 0x3e, 0xa0, 0x2e, 0x30, 0xd, 0x82, 0xb, 0x65, 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x2e, 0x6f, 0x72, 0x67, 0x30, 0xa, 0x87, 0x8, 0xa, 0xa, 0x0, 0x0, 0xff, 0xff, 0x0, 0x0, 0x30, 0x11, 0x81, 0xf, 0x65, 0x6d, 0x61, 0x69, 0x6c, 0x40, 0x65, 0x6d, 0x61, 0x69, 0x6c, 0x2e, 0x6f, 0x72, 0x67, 0xa1, 0xc, 0x30, 0xa, 0x87, 0x8, 0xa, 0xa, 0x0, 0x0, 0xff, 0xff, 0xff, 0x0},
|
|
Critical: true,
|
|
},
|
|
},
|
|
RawSubject: subjectGenerator(t, pkix.Name{CommonName: "example.org"}),
|
|
},
|
|
nameConstraintsFeatureEnabled: true,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
got, err := GenerateCSR(
|
|
tt.crt,
|
|
WithEncodeBasicConstraintsInRequest(tt.basicConstraintsFeatureEnabled),
|
|
WithNameConstraints(tt.nameConstraintsFeatureEnabled),
|
|
WithOtherNames(tt.otherNamesFeatureEnabled),
|
|
WithUseLiteralSubject(tt.literalCertificateSubjectFeatureEnabled),
|
|
)
|
|
if (err != nil) != tt.wantErr {
|
|
t.Errorf("GenerateCSR() error = %v, wantErr %v", err, tt.wantErr)
|
|
return
|
|
}
|
|
if !reflect.DeepEqual(got, tt.want) {
|
|
t.Errorf("GenerateCSR() got = %v, want %v", got, tt.want)
|
|
return
|
|
}
|
|
|
|
// TODO find a better way around the nil check
|
|
if got != nil {
|
|
// also check CSR generates valid certificate
|
|
pk, err := GenerateRSAPrivateKey(2048)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
csrDER, err := EncodeCSR(got, pk)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
_, err = x509.ParseCertificateRequest(csrDER)
|
|
if err != nil {
|
|
t.Errorf("Failed to parse generated certificate %s, Der: %v", err.Error(), csrDER)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestSignCSRTemplate(t *testing.T) {
|
|
// We want to test the behavior of SignCSRTemplate in various contexts;
|
|
// for that, we construct a chain of four certificates:
|
|
// a root CA, two intermediate CA, and a leaf certificate.
|
|
|
|
mustCreatePair := func(issuerCert *x509.Certificate, issuerPK crypto.Signer, name string, isCA bool, nameConstraints *NameConstraints) ([]byte, *x509.Certificate, *x509.Certificate, crypto.Signer) {
|
|
pk, err := GenerateECPrivateKey(256)
|
|
require.NoError(t, err)
|
|
var permittedIPRanges []*net.IPNet
|
|
if nameConstraints != nil {
|
|
permittedIPRanges = nameConstraints.PermittedIPRanges
|
|
}
|
|
tmpl := &x509.Certificate{
|
|
Version: 3,
|
|
BasicConstraintsValid: true,
|
|
SerialNumber: big.NewInt(0),
|
|
Subject: pkix.Name{
|
|
CommonName: name,
|
|
},
|
|
PublicKeyAlgorithm: x509.ECDSA,
|
|
NotBefore: time.Now(),
|
|
NotAfter: time.Now().Add(time.Minute),
|
|
KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature,
|
|
PublicKey: pk.Public(),
|
|
IsCA: isCA,
|
|
PermittedIPRanges: permittedIPRanges,
|
|
}
|
|
|
|
if isCA {
|
|
tmpl.KeyUsage |= x509.KeyUsageCertSign
|
|
}
|
|
|
|
if issuerCert == nil {
|
|
issuerCert = tmpl
|
|
}
|
|
if issuerPK == nil {
|
|
issuerPK = pk
|
|
}
|
|
|
|
pem, cert, err := SignCertificate(tmpl, issuerCert, tmpl.PublicKey, issuerPK)
|
|
require.NoError(t, err)
|
|
return pem, cert, tmpl, pk
|
|
}
|
|
|
|
rootPEM, rootCert, rootTmpl, rootPK := mustCreatePair(nil, nil, "root", true, nil)
|
|
int1PEM, int1Cert, int1Tmpl, int1PK := mustCreatePair(rootCert, rootPK, "int1", true, nil)
|
|
int2PEM, int2Cert, int2Tmpl, int2PK := mustCreatePair(int1Cert, int1PK, "int2", true, nil)
|
|
leafPEM, _, leafTmpl, _ := mustCreatePair(int2Cert, int2PK, "leaf", false, nil)
|
|
|
|
// vars for testing name constraints
|
|
_, permittedIPNet, _ := net.ParseCIDR("10.10.0.0/16")
|
|
_, ncRootCert, _, ncRootPK := mustCreatePair(nil, nil, "ncroot", true, &NameConstraints{PermittedIPRanges: []*net.IPNet{permittedIPNet}})
|
|
_, _, ncLeafTmpl, _ := mustCreatePair(ncRootCert, ncRootPK, "ncleaf", false, nil)
|
|
ncLeafTmpl.IPAddresses = []net.IP{net.ParseIP("10.20.0.5")}
|
|
|
|
tests := map[string]struct {
|
|
caCerts []*x509.Certificate
|
|
caKey crypto.Signer
|
|
template *x509.Certificate
|
|
expectedCertPem []byte
|
|
expectedCaCertPem []byte
|
|
wantErr bool
|
|
}{
|
|
"Sign intermediate 1 template": {
|
|
caCerts: []*x509.Certificate{rootCert},
|
|
caKey: rootPK,
|
|
template: int1Tmpl,
|
|
expectedCertPem: int1PEM,
|
|
expectedCaCertPem: rootPEM,
|
|
wantErr: false,
|
|
},
|
|
"Sign intermediate 2 template": {
|
|
caCerts: []*x509.Certificate{int1Cert, rootCert},
|
|
caKey: int1PK,
|
|
template: int2Tmpl,
|
|
expectedCertPem: append(int2PEM, int1PEM...),
|
|
expectedCaCertPem: rootPEM,
|
|
wantErr: false,
|
|
},
|
|
"Sign leaf template": {
|
|
caCerts: []*x509.Certificate{int2Cert, int1Cert, rootCert},
|
|
caKey: int2PK,
|
|
template: leafTmpl,
|
|
expectedCertPem: append(append(leafPEM, int1PEM...), int2PEM...),
|
|
expectedCaCertPem: rootPEM,
|
|
wantErr: false,
|
|
},
|
|
"Sign leaf template no root": {
|
|
caCerts: []*x509.Certificate{int2Cert, int1Cert},
|
|
caKey: int2PK,
|
|
template: leafTmpl,
|
|
expectedCertPem: append(leafPEM, int2PEM...),
|
|
expectedCaCertPem: int1PEM,
|
|
wantErr: false,
|
|
},
|
|
"Error on no CA": {
|
|
caKey: rootPK,
|
|
template: rootTmpl,
|
|
wantErr: true,
|
|
},
|
|
}
|
|
|
|
for name, test := range tests {
|
|
t.Run(name, func(t *testing.T) {
|
|
actualBundle, err := SignCSRTemplate(test.caCerts, test.caKey, test.template)
|
|
if (err != nil) != test.wantErr {
|
|
t.Errorf("TestSignCSRTemplate() error = %v, wantErr %v", err, test.wantErr)
|
|
return
|
|
}
|
|
|
|
if !bytes.Equal(test.expectedCertPem, actualBundle.ChainPEM) {
|
|
// To help us identify where the mismatch is, we decode turn the
|
|
// into strings and do a textual diff.
|
|
expected, _ := DecodeX509CertificateBytes(test.expectedCertPem)
|
|
actual, _ := DecodeX509CertificateBytes(actualBundle.ChainPEM)
|
|
|
|
assert.Equal(t, expected.Subject.String(), actual.Subject.String())
|
|
}
|
|
|
|
if !bytes.Equal(test.expectedCaCertPem, actualBundle.CAPEM) {
|
|
// To help us identify where the mismatch is, we decode turn the
|
|
// into strings and do a textual diff.
|
|
expected, _ := DecodeX509CertificateBytes(test.expectedCaCertPem)
|
|
actual, _ := DecodeX509CertificateBytes(actualBundle.CAPEM)
|
|
|
|
assert.Equal(t, expected.Subject.String(), actual.Subject.String())
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestEncodeX509Chain(t *testing.T) {
|
|
root := mustCreateBundle(t, nil, "root")
|
|
intA1 := mustCreateBundle(t, root, "intA-1")
|
|
intA2 := mustCreateBundle(t, intA1, "intA-2")
|
|
leafA1 := mustCreateBundle(t, intA1, "leaf-a1")
|
|
leafA2 := mustCreateBundle(t, intA2, "leaf-a2")
|
|
leafInterCN := mustCreateBundle(t, intA1, intA1.cert.Subject.CommonName)
|
|
|
|
tests := map[string]struct {
|
|
inputCerts []*x509.Certificate
|
|
expChain []byte
|
|
expErr bool
|
|
}{
|
|
"simple 3 cert chain should be encoded in the same order as passed, with no root": {
|
|
inputCerts: []*x509.Certificate{root.cert, intA1.cert, leafA1.cert},
|
|
expChain: joinPEM(intA1.pem, leafA1.pem),
|
|
expErr: false,
|
|
},
|
|
"simple 4 cert chain should be encoded in the same order as passed, with no root": {
|
|
inputCerts: []*x509.Certificate{root.cert, intA1.cert, intA2.cert, leafA2.cert},
|
|
expChain: joinPEM(intA1.pem, intA2.pem, leafA2.pem),
|
|
expErr: false,
|
|
},
|
|
"3 cert chain with no leaf be encoded in the same order as passed, with no root": {
|
|
inputCerts: []*x509.Certificate{root.cert, intA1.cert, intA2.cert},
|
|
expChain: joinPEM(intA1.pem, intA2.pem),
|
|
expErr: false,
|
|
},
|
|
"chain with a non-root cert where issuer matches subject should include that cert but not root": {
|
|
// see https://github.com/cert-manager/cert-manager/issues/4142#issuecomment-884248923
|
|
inputCerts: []*x509.Certificate{root.cert, intA1.cert, leafInterCN.cert},
|
|
expChain: joinPEM(intA1.pem, leafInterCN.pem),
|
|
expErr: false,
|
|
},
|
|
"empty input chain should result in no output and no error": {
|
|
inputCerts: []*x509.Certificate{},
|
|
expChain: []byte(""),
|
|
expErr: false,
|
|
},
|
|
"chain with just a root should result in no output and no error": {
|
|
inputCerts: []*x509.Certificate{root.cert},
|
|
expChain: []byte(""),
|
|
expErr: false,
|
|
},
|
|
"chain with just a leaf should result in just the leaf": {
|
|
inputCerts: []*x509.Certificate{leafA1.cert},
|
|
expChain: leafA1.pem,
|
|
expErr: false,
|
|
},
|
|
"nil certs are ignored": {
|
|
inputCerts: []*x509.Certificate{leafA1.cert, nil},
|
|
expChain: leafA1.pem,
|
|
expErr: false,
|
|
},
|
|
}
|
|
|
|
for name, test := range tests {
|
|
t.Run(name, func(t *testing.T) {
|
|
chainOut, err := EncodeX509Chain(test.inputCerts)
|
|
|
|
if (err != nil) != test.expErr {
|
|
t.Errorf("unexpected error, exp=%t got=%v",
|
|
test.expErr, err)
|
|
}
|
|
|
|
if !reflect.DeepEqual(chainOut, test.expChain) {
|
|
t.Errorf("unexpected output from EncodeX509Chain, exp=%+s got=%+s",
|
|
test.expChain, chainOut)
|
|
}
|
|
})
|
|
}
|
|
}
|