Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add minimal NewCertificateFromX509 implementation #248

Merged
merged 5 commits into from
May 30, 2023
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 54 additions & 2 deletions x509util/certificate.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,8 @@ type Certificate struct {
PublicKey interface{} `json:"-"`
}

// NewCertificate creates a new Certificate from an x509.Certificate request and
// some template options.
// NewCertificate creates a new Certificate from an x509.CertificateRequest and
// will apply some template options.
func NewCertificate(cr *x509.CertificateRequest, opts ...Option) (*Certificate, error) {
if err := cr.CheckSignature(); err != nil {
return nil, errors.Wrap(err, "error validating certificate request")
Expand Down Expand Up @@ -83,6 +83,58 @@ func NewCertificate(cr *x509.CertificateRequest, opts ...Option) (*Certificate,
return &cert, nil
}

// NewCertificateFromX509 creates a new Certificate from an x509.Certificate and
// will apply template options. A new (unsigned) x509.CertificateRequest is created,
// with date from the x509.Certificate template. This function is primarily useful
// when signing a certificate for a key that can't sign a CSR or when the private
// key is not available.
func NewCertificateFromX509(template *x509.Certificate, opts ...Option) (*Certificate, error) {
// Copy data from the template to a new, unsigned CSR.
o, err := new(Options).apply(&x509.CertificateRequest{
PublicKey: template.PublicKey,
PublicKeyAlgorithm: template.PublicKeyAlgorithm,
Subject: template.Subject,
DNSNames: template.DNSNames,
EmailAddresses: template.EmailAddresses,
IPAddresses: template.IPAddresses,
URIs: template.URIs,
Extensions: template.ExtraExtensions,
}, opts)
if err != nil {
return nil, err
}

// If no template use only the certificate request with the
// default leaf key usages.
if o.CertBuffer == nil {
return nil, errors.New("not implemented yet; use FromX509WithTemplate option")
}

// With templates
var cert Certificate
if err := json.NewDecoder(o.CertBuffer).Decode(&cert); err != nil {
return nil, errors.Wrap(err, "error unmarshaling certificate")
}

// Enforce the public key from the template
cert.PublicKey = template.PublicKey
cert.PublicKeyAlgorithm = template.PublicKeyAlgorithm

// Generate the subjectAltName extension if the certificate contains SANs
// that are not supported in the Go standard library.
if cert.hasExtendedSANs() && !cert.hasExtension(oidExtensionSubjectAltName) {
ext, err := createCertificateSubjectAltNameExtension(cert, cert.Subject.IsEmpty())
if err != nil {
return nil, err
}
// Prepend extension to achieve a certificate as similar as possible to
// the one generated by the Go standard library.
cert.Extensions = append([]Extension{ext}, cert.Extensions...)
}

return &cert, nil
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is duplicated logic that can be combined into one internal function.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in f1025e3

}

// GetCertificate returns the x509.Certificate representation of the
// certificate.
func (c *Certificate) GetCertificate() *x509.Certificate {
Expand Down
149 changes: 149 additions & 0 deletions x509util/certificate_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@ package x509util
import (
"bytes"
"crypto"
"crypto/ecdsa"
"crypto/ed25519"
"crypto/elliptic"
"crypto/rand"
"crypto/x509"
"crypto/x509/pkix"
Expand All @@ -17,6 +19,9 @@ import (
"reflect"
"testing"
"time"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

func createCertificateRequest(t *testing.T, commonName string, sans []string) (*x509.CertificateRequest, crypto.Signer) {
Expand Down Expand Up @@ -287,6 +292,150 @@ func TestNewCertificate(t *testing.T) {
}
}

func TestNewCertificateFromX509(t *testing.T) {
priv, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
require.NoError(t, err)
template := &x509.Certificate{ // similar template as the certificate request for TestNewCertificate
PublicKey: priv.Public(),
PublicKeyAlgorithm: x509.ECDSA,
Subject: pkix.Name{CommonName: "commonName"},
DNSNames: []string{"foo.com"},
EmailAddresses: []string{"root@foo.com"},
}
customSANsData := CreateTemplateData("commonName", nil)
customSANsData.Set(SANsKey, []SubjectAlternativeName{
{Type: PermanentIdentifierType, Value: "123456"},
{Type: "1.2.3.4", Value: "utf8:otherName"},
})
badCustomSANsData := CreateTemplateData("commonName", nil)
badCustomSANsData.Set(SANsKey, []SubjectAlternativeName{
{Type: "1.2.3.4", Value: "int:not-an-int"},
})
ipNet := func(s string) *net.IPNet {
_, ipNet, err := net.ParseCIDR(s)
require.NoError(t, err)
return ipNet
}
type args struct {
template *x509.Certificate
opts []Option
}
tests := []struct {
name string
args args
want *Certificate
wantErr bool
}{
{"okDefaultTemplate", args{template, []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.ECDSA,
}, false},
{"okCustomSANs", args{template, []Option{WithTemplate(DefaultLeafTemplate, customSANsData)}}, &Certificate{
Subject: Subject{CommonName: "commonName"},
SANs: []SubjectAlternativeName{
{Type: PermanentIdentifierType, Value: "123456"},
{Type: "1.2.3.4", Value: "utf8:otherName"},
},
Extensions: []Extension{{
ID: ObjectIdentifier{2, 5, 29, 17},
Critical: false,
Value: []byte{48, 44, 160, 22, 6, 8, 43, 6, 1, 5, 5, 7, 8, 3, 160, 10, 48, 8, 12, 6, 49, 50, 51, 52, 53, 54, 160, 18, 6, 3, 42, 3, 4, 160, 11, 12, 9, 111, 116, 104, 101, 114, 78, 97, 109, 101},
}},
KeyUsage: KeyUsage(x509.KeyUsageDigitalSignature),
ExtKeyUsage: ExtKeyUsage([]x509.ExtKeyUsage{
x509.ExtKeyUsageServerAuth,
x509.ExtKeyUsageClientAuth,
}),
PublicKey: priv.Public(),
PublicKeyAlgorithm: x509.ECDSA,
}, false},
{"okExample", args{template, []Option{WithTemplateFile("./testdata/example.tpl", TemplateData{
SANsKey: []SubjectAlternativeName{
{Type: "dns", Value: "foo.com"},
},
TokenKey: map[string]interface{}{
"iss": "https://iss",
"sub": "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.ECDSA,
}, false},
{"okFullSimple", args{template, []Option{WithTemplateFile("./testdata/fullsimple.tpl", TemplateData{})}}, &Certificate{
Version: 3,
Subject: Subject{CommonName: "subjectCommonName"},
SerialNumber: SerialNumber{big.NewInt(78187493520)},
Issuer: Issuer{CommonName: "issuerCommonName"},
DNSNames: []string{"doe.com"},
IPAddresses: []net.IP{net.ParseIP("127.0.0.1")},
EmailAddresses: []string{"jane@doe.com"},
URIs: []*url.URL{{Scheme: "https", Host: "doe.com"}},
SANs: []SubjectAlternativeName{{Type: DNSType, Value: "www.doe.com"}},
Extensions: []Extension{{ID: []int{1, 2, 3, 4}, Critical: true, Value: []byte("extension")}},
KeyUsage: KeyUsage(x509.KeyUsageDigitalSignature),
ExtKeyUsage: ExtKeyUsage([]x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}),
UnknownExtKeyUsage: []asn1.ObjectIdentifier{[]int{1, 3, 6, 1, 4, 1, 44924, 1, 6}, []int{1, 3, 6, 1, 4, 1, 44924, 1, 7}},
SubjectKeyID: []byte("subjectKeyId"),
AuthorityKeyID: []byte("authorityKeyId"),
OCSPServer: []string{"https://ocsp.server"},
IssuingCertificateURL: []string{"https://ca.com"},
CRLDistributionPoints: []string{"https://ca.com/ca.crl"},
PolicyIdentifiers: PolicyIdentifiers{[]int{1, 2, 3, 4, 5, 6}},
BasicConstraints: &BasicConstraints{
IsCA: false,
MaxPathLen: 0,
},
NameConstraints: &NameConstraints{
Critical: true,
PermittedDNSDomains: []string{"jane.doe.com"},
ExcludedDNSDomains: []string{"john.doe.com"},
PermittedIPRanges: []*net.IPNet{ipNet("127.0.0.1/32")},
ExcludedIPRanges: []*net.IPNet{ipNet("0.0.0.0/0")},
PermittedEmailAddresses: []string{"jane@doe.com"},
ExcludedEmailAddresses: []string{"john@doe.com"},
PermittedURIDomains: []string{"https://jane.doe.com"},
ExcludedURIDomains: []string{"https://john.doe.com"},
},
SignatureAlgorithm: SignatureAlgorithm(x509.PureEd25519),
PublicKey: priv.Public(),
PublicKeyAlgorithm: x509.ECDSA,
}, false},
{"failNoTemplate", args{template, nil}, nil, true},
{"failTemplate", args{template, []Option{WithTemplate(`{{ fail "fatal error }}`, CreateTemplateData("commonName", []string{"foo.com"}))}}, nil, true},
{"missingTemplate", args{template, []Option{WithTemplateFile("./testdata/missing.tpl", CreateTemplateData("commonName", []string{"foo.com"}))}}, nil, true},
{"badJson", args{template, []Option{WithTemplate(`"this is not a json object"`, CreateTemplateData("commonName", []string{"foo.com"}))}}, nil, true},
{"failCustomSANs", args{template, []Option{WithTemplate(DefaultLeafTemplate, badCustomSANsData)}}, nil, true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := NewCertificateFromX509(tt.args.template, tt.args.opts...)
if tt.wantErr {
assert.Error(t, err)
return
}

assert.NoError(t, err)
assert.Equal(t, tt.want, got)
})
}
}

func TestCertificate_GetCertificate(t *testing.T) {
type fields struct {
Version int
Expand Down