385 lines
13 KiB
Go
385 lines
13 KiB
Go
package x509util
|
|
|
|
import (
|
|
"crypto"
|
|
"crypto/ed25519"
|
|
"crypto/rand"
|
|
"crypto/x509"
|
|
"crypto/x509/pkix"
|
|
"encoding/asn1"
|
|
"fmt"
|
|
"io"
|
|
"math/big"
|
|
"net"
|
|
"net/url"
|
|
"reflect"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
func createCertificateRequest(t *testing.T, commonName string, sans []string) (*x509.CertificateRequest, crypto.Signer) {
|
|
dnsNames, ips, emails, uris := SplitSANs(sans)
|
|
t.Helper()
|
|
_, priv, err := ed25519.GenerateKey(rand.Reader)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
asn1Data, err := x509.CreateCertificateRequest(rand.Reader, &x509.CertificateRequest{
|
|
Subject: pkix.Name{CommonName: commonName},
|
|
DNSNames: dnsNames,
|
|
IPAddresses: ips,
|
|
EmailAddresses: emails,
|
|
URIs: uris,
|
|
SignatureAlgorithm: x509.PureEd25519,
|
|
}, priv)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
cr, err := x509.ParseCertificateRequest(asn1Data)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return cr, priv
|
|
}
|
|
|
|
func createIssuerCertificate(t *testing.T, commonName string) (*x509.Certificate, crypto.Signer) {
|
|
t.Helper()
|
|
now := time.Now()
|
|
pub, priv, err := ed25519.GenerateKey(rand.Reader)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
subjectKeyID, err := generateSubjectKeyID(pub)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
sn, err := generateSerialNumber()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
template := &x509.Certificate{
|
|
IsCA: true,
|
|
NotBefore: now,
|
|
NotAfter: now.Add(time.Hour),
|
|
KeyUsage: x509.KeyUsageCertSign | x509.KeyUsageCRLSign,
|
|
BasicConstraintsValid: true,
|
|
MaxPathLen: 0,
|
|
MaxPathLenZero: true,
|
|
Issuer: pkix.Name{CommonName: "issuer"},
|
|
Subject: pkix.Name{CommonName: "issuer"},
|
|
SerialNumber: sn,
|
|
SubjectKeyId: subjectKeyID,
|
|
}
|
|
asn1Data, err := x509.CreateCertificate(rand.Reader, template, template, pub, priv)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
crt, err := x509.ParseCertificate(asn1Data)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return crt, priv
|
|
}
|
|
|
|
type badSigner struct {
|
|
pub crypto.PublicKey
|
|
}
|
|
|
|
func createBadSigner(t *testing.T) *badSigner {
|
|
pub, _, err := ed25519.GenerateKey(rand.Reader)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return &badSigner{
|
|
pub: pub,
|
|
}
|
|
}
|
|
|
|
func (b *badSigner) Public() crypto.PublicKey {
|
|
return b.pub
|
|
}
|
|
|
|
func (b *badSigner) Sign(rand io.Reader, digest []byte, opts crypto.SignerOpts) ([]byte, error) {
|
|
return nil, fmt.Errorf("💥")
|
|
}
|
|
|
|
func TestNewCertificate(t *testing.T) {
|
|
cr, priv := createCertificateRequest(t, "commonName", []string{"foo.com", "root@foo.com"})
|
|
crBadSignateure, _ := createCertificateRequest(t, "fail", []string{"foo.com"})
|
|
crBadSignateure.PublicKey = priv.Public()
|
|
|
|
type args struct {
|
|
cr *x509.CertificateRequest
|
|
opts []Option
|
|
}
|
|
tests := []struct {
|
|
name string
|
|
args args
|
|
want *Certificate
|
|
wantErr bool
|
|
}{
|
|
{"okSimple", args{cr, nil}, &Certificate{
|
|
Subject: Subject{CommonName: "commonName"},
|
|
DNSNames: []string{"foo.com"},
|
|
EmailAddresses: []string{"root@foo.com"},
|
|
KeyUsage: KeyUsage(x509.KeyUsageDigitalSignature),
|
|
ExtKeyUsage: ExtKeyUsage([]x509.ExtKeyUsage{
|
|
x509.ExtKeyUsageServerAuth,
|
|
x509.ExtKeyUsageClientAuth,
|
|
}),
|
|
Extensions: newExtensions(cr.Extensions),
|
|
PublicKey: priv.Public(),
|
|
PublicKeyAlgorithm: x509.Ed25519,
|
|
}, false},
|
|
{"okDefaultTemplate", args{cr, []Option{WithTemplate(DefaultLeafTemplate, CreateTemplateData("commonName", []string{"foo.com"}))}}, &Certificate{
|
|
Subject: Subject{CommonName: "commonName"},
|
|
SANs: []SubjectAlternativeName{{Type: DNSType, Value: "foo.com"}},
|
|
KeyUsage: KeyUsage(x509.KeyUsageDigitalSignature),
|
|
ExtKeyUsage: ExtKeyUsage([]x509.ExtKeyUsage{
|
|
x509.ExtKeyUsageServerAuth,
|
|
x509.ExtKeyUsageClientAuth,
|
|
}),
|
|
PublicKey: priv.Public(),
|
|
PublicKeyAlgorithm: x509.Ed25519,
|
|
}, false},
|
|
{"okExample", args{cr, []Option{WithTemplateFile("./testdata/example.tpl", TemplateData{
|
|
SANsKey: []SubjectAlternativeName{
|
|
{Type: "dns", Value: "foo.com"},
|
|
},
|
|
TokenKey: map[string]string{
|
|
"Issuer": "https://iss",
|
|
"Subject": "sub",
|
|
},
|
|
})}}, &Certificate{
|
|
Subject: Subject{CommonName: "commonName"},
|
|
SANs: []SubjectAlternativeName{{Type: DNSType, Value: "foo.com"}},
|
|
EmailAddresses: []string{"root@foo.com"},
|
|
URIs: []*url.URL{{Scheme: "https", Host: "iss", Fragment: "sub"}},
|
|
KeyUsage: KeyUsage(x509.KeyUsageDigitalSignature),
|
|
ExtKeyUsage: ExtKeyUsage([]x509.ExtKeyUsage{
|
|
x509.ExtKeyUsageServerAuth,
|
|
x509.ExtKeyUsageClientAuth,
|
|
}),
|
|
PublicKey: priv.Public(),
|
|
PublicKeyAlgorithm: x509.Ed25519,
|
|
}, false},
|
|
{"badSignature", args{crBadSignateure, nil}, nil, true},
|
|
{"failTemplate", args{cr, []Option{WithTemplate(`{{ fail "fatal error }}`, CreateTemplateData("commonName", []string{"foo.com"}))}}, nil, true},
|
|
{"missingTemplate", args{cr, []Option{WithTemplateFile("./testdata/missing.tpl", CreateTemplateData("commonName", []string{"foo.com"}))}}, nil, true},
|
|
{"badJson", args{cr, []Option{WithTemplate(`"this is not a json object"`, CreateTemplateData("commonName", []string{"foo.com"}))}}, nil, true},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
got, err := NewCertificate(tt.args.cr, tt.args.opts...)
|
|
if (err != nil) != tt.wantErr {
|
|
t.Errorf("NewCertificate() error = %v, wantErr %v", err, tt.wantErr)
|
|
return
|
|
}
|
|
if !reflect.DeepEqual(got, tt.want) {
|
|
t.Errorf("NewCertificate() = \n%v, want \n%v", got, tt.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestCertificate_GetCertificate(t *testing.T) {
|
|
type fields struct {
|
|
Version int
|
|
Subject Subject
|
|
Issuer Issuer
|
|
SerialNumber SerialNumber
|
|
DNSNames MultiString
|
|
EmailAddresses MultiString
|
|
IPAddresses MultiIP
|
|
URIs MultiURL
|
|
SANs []SubjectAlternativeName
|
|
Extensions []Extension
|
|
KeyUsage KeyUsage
|
|
ExtKeyUsage ExtKeyUsage
|
|
SubjectKeyID SubjectKeyID
|
|
AuthorityKeyID AuthorityKeyID
|
|
OCSPServer OCSPServer
|
|
IssuingCertificateURL IssuingCertificateURL
|
|
CRLDistributionPoints CRLDistributionPoints
|
|
PolicyIdentifiers PolicyIdentifiers
|
|
BasicConstraints *BasicConstraints
|
|
NameConstaints *NameConstraints
|
|
SignatureAlgorithm SignatureAlgorithm
|
|
PublicKeyAlgorithm x509.PublicKeyAlgorithm
|
|
PublicKey interface{}
|
|
}
|
|
tests := []struct {
|
|
name string
|
|
fields fields
|
|
want *x509.Certificate
|
|
}{
|
|
{"ok", fields{
|
|
Version: 3,
|
|
Subject: Subject{CommonName: "commonName", Organization: []string{"smallstep"}},
|
|
Issuer: Issuer{CommonName: "issuer", Organization: []string{"smallstep"}},
|
|
SerialNumber: SerialNumber{big.NewInt(123)},
|
|
DNSNames: []string{"foo.bar"},
|
|
EmailAddresses: []string{"root@foo.com"},
|
|
IPAddresses: []net.IP{net.ParseIP("::1")},
|
|
URIs: []*url.URL{{Scheme: "mailto", Opaque: "root@foo.com"}},
|
|
SANs: []SubjectAlternativeName{
|
|
{Type: DNSType, Value: "www.foo.bar"},
|
|
{Type: IPType, Value: "127.0.0.1"},
|
|
{Type: EmailType, Value: "admin@foo.com"},
|
|
{Type: URIType, Value: "mailto:admin@foo.com"},
|
|
},
|
|
Extensions: []Extension{{ID: []int{1, 2, 3, 4}, Critical: true, Value: []byte("custom extension")}},
|
|
KeyUsage: KeyUsage(x509.KeyUsageDigitalSignature),
|
|
ExtKeyUsage: ExtKeyUsage([]x509.ExtKeyUsage{
|
|
x509.ExtKeyUsageServerAuth,
|
|
x509.ExtKeyUsageClientAuth,
|
|
}),
|
|
SubjectKeyID: []byte("subject-key-id"),
|
|
AuthorityKeyID: []byte("authority-key-id"),
|
|
OCSPServer: []string{"https://oscp.server"},
|
|
IssuingCertificateURL: []string{"https://ca.com"},
|
|
CRLDistributionPoints: []string{"https://ca.com/crl"},
|
|
PolicyIdentifiers: []asn1.ObjectIdentifier{[]int{1, 2, 3, 4}},
|
|
BasicConstraints: &BasicConstraints{IsCA: true, MaxPathLen: 0},
|
|
NameConstaints: &NameConstraints{PermittedDNSDomains: []string{"foo.bar"}},
|
|
SignatureAlgorithm: SignatureAlgorithm(x509.PureEd25519),
|
|
PublicKeyAlgorithm: x509.Ed25519,
|
|
PublicKey: ed25519.PublicKey("public key"),
|
|
}, &x509.Certificate{
|
|
Version: 0,
|
|
Subject: pkix.Name{CommonName: "commonName", Organization: []string{"smallstep"}},
|
|
Issuer: pkix.Name{},
|
|
SerialNumber: big.NewInt(123),
|
|
DNSNames: []string{"foo.bar", "www.foo.bar"},
|
|
EmailAddresses: []string{"root@foo.com", "admin@foo.com"},
|
|
IPAddresses: []net.IP{net.ParseIP("::1"), net.ParseIP("127.0.0.1")},
|
|
URIs: []*url.URL{{Scheme: "mailto", Opaque: "root@foo.com"}, {Scheme: "mailto", Opaque: "admin@foo.com"}},
|
|
ExtraExtensions: []pkix.Extension{{Id: []int{1, 2, 3, 4}, Critical: true, Value: []byte("custom extension")}},
|
|
KeyUsage: x509.KeyUsageDigitalSignature,
|
|
ExtKeyUsage: []x509.ExtKeyUsage{
|
|
x509.ExtKeyUsageServerAuth,
|
|
x509.ExtKeyUsageClientAuth,
|
|
},
|
|
SubjectKeyId: []byte("subject-key-id"),
|
|
AuthorityKeyId: []byte("authority-key-id"),
|
|
OCSPServer: []string{"https://oscp.server"},
|
|
IssuingCertificateURL: []string{"https://ca.com"},
|
|
CRLDistributionPoints: []string{"https://ca.com/crl"},
|
|
PolicyIdentifiers: []asn1.ObjectIdentifier{[]int{1, 2, 3, 4}},
|
|
IsCA: true,
|
|
MaxPathLen: 0,
|
|
MaxPathLenZero: true,
|
|
BasicConstraintsValid: true,
|
|
PermittedDNSDomains: []string{"foo.bar"},
|
|
SignatureAlgorithm: x509.PureEd25519,
|
|
PublicKeyAlgorithm: x509.Ed25519,
|
|
PublicKey: ed25519.PublicKey("public key"),
|
|
}},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
c := &Certificate{
|
|
Version: tt.fields.Version,
|
|
Subject: tt.fields.Subject,
|
|
Issuer: tt.fields.Issuer,
|
|
SerialNumber: tt.fields.SerialNumber,
|
|
DNSNames: tt.fields.DNSNames,
|
|
EmailAddresses: tt.fields.EmailAddresses,
|
|
IPAddresses: tt.fields.IPAddresses,
|
|
URIs: tt.fields.URIs,
|
|
SANs: tt.fields.SANs,
|
|
Extensions: tt.fields.Extensions,
|
|
KeyUsage: tt.fields.KeyUsage,
|
|
ExtKeyUsage: tt.fields.ExtKeyUsage,
|
|
SubjectKeyID: tt.fields.SubjectKeyID,
|
|
AuthorityKeyID: tt.fields.AuthorityKeyID,
|
|
OCSPServer: tt.fields.OCSPServer,
|
|
IssuingCertificateURL: tt.fields.IssuingCertificateURL,
|
|
CRLDistributionPoints: tt.fields.CRLDistributionPoints,
|
|
PolicyIdentifiers: tt.fields.PolicyIdentifiers,
|
|
BasicConstraints: tt.fields.BasicConstraints,
|
|
NameConstaints: tt.fields.NameConstaints,
|
|
SignatureAlgorithm: tt.fields.SignatureAlgorithm,
|
|
PublicKeyAlgorithm: tt.fields.PublicKeyAlgorithm,
|
|
PublicKey: tt.fields.PublicKey,
|
|
}
|
|
if got := c.GetCertificate(); !reflect.DeepEqual(got, tt.want) {
|
|
t.Errorf("Certificate.GetCertificate() = \n%+v, want \n%+v", got, tt.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestCreateCertificate(t *testing.T) {
|
|
iss, issPriv := createIssuerCertificate(t, "issuer")
|
|
|
|
mustSerialNumber := func() *big.Int {
|
|
sn, err := generateSerialNumber()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return sn
|
|
}
|
|
mustSubjectKeyID := func(pub crypto.PublicKey) []byte {
|
|
b, err := generateSubjectKeyID(pub)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return b
|
|
}
|
|
|
|
cr1, priv1 := createCertificateRequest(t, "commonName", []string{"foo.com"})
|
|
crt1 := newCertificateRequest(cr1).GetLeafCertificate().GetCertificate()
|
|
crt1.SerialNumber = mustSerialNumber()
|
|
crt1.SubjectKeyId = mustSubjectKeyID(priv1.Public())
|
|
|
|
cr2, priv2 := createCertificateRequest(t, "commonName", []string{"foo.com"})
|
|
crt2 := newCertificateRequest(cr2).GetLeafCertificate().GetCertificate()
|
|
crt2.SerialNumber = mustSerialNumber()
|
|
|
|
cr3, priv3 := createCertificateRequest(t, "commonName", []string{"foo.com"})
|
|
crt3 := newCertificateRequest(cr3).GetLeafCertificate().GetCertificate()
|
|
crt3.SubjectKeyId = mustSubjectKeyID(priv1.Public())
|
|
|
|
cr4, priv4 := createCertificateRequest(t, "commonName", []string{"foo.com"})
|
|
crt4 := newCertificateRequest(cr4).GetLeafCertificate().GetCertificate()
|
|
|
|
cr5, _ := createCertificateRequest(t, "commonName", []string{"foo.com"})
|
|
crt5 := newCertificateRequest(cr5).GetLeafCertificate().GetCertificate()
|
|
|
|
badSigner := createBadSigner(t)
|
|
|
|
type args struct {
|
|
template *x509.Certificate
|
|
parent *x509.Certificate
|
|
pub crypto.PublicKey
|
|
signer crypto.Signer
|
|
}
|
|
tests := []struct {
|
|
name string
|
|
args args
|
|
wantErr bool
|
|
}{
|
|
{"ok", args{crt1, iss, priv1.Public(), issPriv}, false},
|
|
{"okNoSubjectKeyID", args{crt2, iss, priv2.Public(), issPriv}, false},
|
|
{"okNoSerialNumber", args{crt3, iss, priv3.Public(), issPriv}, false},
|
|
{"okNothing", args{crt4, iss, priv4.Public(), issPriv}, false},
|
|
{"failSubjectKeyID", args{crt5, iss, []byte("foo"), issPriv}, true},
|
|
{"failSign", args{crt1, iss, priv1.Public(), badSigner}, true},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
got, err := CreateCertificate(tt.args.template, tt.args.parent, tt.args.pub, tt.args.signer)
|
|
if (err != nil) != tt.wantErr {
|
|
t.Errorf("CreateCertificate() error = %v, wantErr %v", err, tt.wantErr)
|
|
return
|
|
}
|
|
if !tt.wantErr {
|
|
if err := got.CheckSignatureFrom(iss); err != nil {
|
|
t.Errorf("Certificate.CheckSignatureFrom() error = %v", err)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|