From 7cd2decb37255a2fb615c29bd5cd4f64fe1ccd4f Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Tue, 15 Mar 2022 16:48:04 -0700 Subject: [PATCH 1/5] Add initial implementation of the minica. --- README.md | 6 +- minica/minica.go | 158 ++++++++++++++++++++++++++++++++++++++++++++++ minica/options.go | 106 +++++++++++++++++++++++++++++++ 3 files changed, 269 insertions(+), 1 deletion(-) create mode 100644 minica/minica.go create mode 100644 minica/options.go diff --git a/README.md b/README.md index 8ffb0e20..54f4762e 100644 --- a/README.md +++ b/README.md @@ -60,4 +60,8 @@ utilities to parse and generate JWT, JWK and JWKSets. ### x25519 Package `x25519` adds support for X25519 keys and the -[XEdDSA](https://signal.org/docs/specifications/xeddsa/) signature scheme. \ No newline at end of file +[XEdDSA](https://signal.org/docs/specifications/xeddsa/) signature scheme. + +### minica + +Package `minica` implements a simple certificate authority. diff --git a/minica/minica.go b/minica/minica.go new file mode 100644 index 00000000..41fc428a --- /dev/null +++ b/minica/minica.go @@ -0,0 +1,158 @@ +package minica + +import ( + "crypto" + "crypto/x509" + "fmt" + "time" + + "go.step.sm/crypto/sshutil" + "go.step.sm/crypto/x509util" + "golang.org/x/crypto/ssh" +) + +// MiniCA is the implementation of a simple X.509 and SSH CA. +type MiniCA struct { + Root *x509.Certificate + Intermediate *x509.Certificate + Signer crypto.Signer + SSHHostSigner ssh.Signer + SSHUserSigner ssh.Signer +} + +// New creates a new MiniCA, the custom options allows to overwrite templates, +// signer types and certificate names. +func New(opts ...Option) (*MiniCA, error) { + now := time.Now() + o := newOptions().apply(opts) + + // Create root + rootSubject := o.Name + " Root CA" + rootSigner, err := o.GetSigner() + if err != nil { + return nil, err + } + rootCR, err := x509util.CreateCertificateRequest(rootSubject, []string{}, rootSigner) + if err != nil { + return nil, err + } + cert, err := x509util.NewCertificate(rootCR, x509util.WithTemplate(x509util.DefaultRootTemplate, x509util.CreateTemplateData(rootSubject, []string{}))) + if err != nil { + return nil, err + } + template := cert.GetCertificate() + template.NotBefore = now + template.NotAfter = now.Add(24 * time.Hour) + root, err := x509util.CreateCertificate(template, template, rootSigner.Public(), rootSigner) + if err != nil { + return nil, err + } + + // Create intermediate + intSubject := o.Name + " Intermediate CA" + intSigner, err := o.GetSigner() + if err != nil { + return nil, err + } + intCR, err := x509util.CreateCertificateRequest(intSubject, []string{}, intSigner) + if err != nil { + return nil, err + } + cert, err = x509util.NewCertificate(intCR, x509util.WithTemplate(x509util.DefaultIntermediateTemplate, x509util.CreateTemplateData(intSubject, []string{}))) + if err != nil { + return nil, err + } + template = cert.GetCertificate() + template.NotBefore = now + template.NotAfter = now.Add(24 * time.Hour) + intermediate, err := x509util.CreateCertificate(template, root, intSigner.Public(), rootSigner) + if err != nil { + return nil, err + } + + // Ssh host signer + signer, err := o.GetSigner() + if err != nil { + return nil, err + } + sshHostSigner, err := ssh.NewSignerFromSigner(signer) + if err != nil { + return nil, err + } + + // Ssh user signer + signer, err = o.GetSigner() + if err != nil { + return nil, err + } + sshUserSigner, err := ssh.NewSignerFromSigner(signer) + if err != nil { + return nil, err + } + + return &MiniCA{ + Root: root, + Intermediate: intermediate, + Signer: intSigner, + SSHHostSigner: sshHostSigner, + SSHUserSigner: sshUserSigner, + }, nil +} + +// Sign signs an X.509 certificate template using the intermediate certificate. +func (c *MiniCA) Sign(template *x509.Certificate) (*x509.Certificate, error) { + if template.NotBefore.IsZero() { + template.NotBefore = time.Now() + } + if template.NotAfter.IsZero() { + template.NotAfter = template.NotBefore.Add(24 * time.Hour) + } + return x509util.CreateCertificate(template, c.Intermediate, template.PublicKey, c.Signer) +} + +// SignCSR signs an X.509 certificate signing request. The custom options allows to change the template used for +func (c *MiniCA) SignCSR(csr *x509.CertificateRequest, opts ...SignOption) (*x509.Certificate, error) { + sans := append([]string{}, csr.DNSNames...) + sans = append(sans, csr.EmailAddresses...) + for _, ip := range csr.IPAddresses { + sans = append(sans, ip.String()) + } + for _, u := range csr.URIs { + sans = append(sans, u.String()) + } + + o := newSignOptions().apply(opts) + crt, err := x509util.NewCertificate(csr, x509util.WithTemplate(o.Template, x509util.CreateTemplateData(csr.Subject.CommonName, sans))) + if err != nil { + return nil, err + } + + cert := crt.GetCertificate() + if o.Modify != nil { + if err := o.Modify(cert); err != nil { + return nil, err + } + } + + return c.Sign(cert) +} + +// SignSSH signs an SSH host or user certificate. +func (c *MiniCA) SignSSH(cert *ssh.Certificate) (*ssh.Certificate, error) { + if cert.ValidAfter == 0 { + cert.ValidAfter = uint64(time.Now().Unix()) + } + if cert.ValidBefore == 0 { + cert.ValidBefore = cert.ValidAfter + 24*60*60 + } + + switch cert.CertType { + case ssh.HostCert: + return sshutil.CreateCertificate(cert, c.SSHHostSigner) + case ssh.UserCert: + return sshutil.CreateCertificate(cert, c.SSHUserSigner) + default: + return nil, fmt.Errorf("unknown certificate type") + } + +} diff --git a/minica/options.go b/minica/options.go new file mode 100644 index 00000000..fe630be1 --- /dev/null +++ b/minica/options.go @@ -0,0 +1,106 @@ +package minica + +import ( + "crypto" + "crypto/x509" + + "go.step.sm/crypto/keyutil" + "go.step.sm/crypto/x509util" +) + +type options struct { + Name string + RootTemplate string + IntermediateTemplate string + GetSigner func() (crypto.Signer, error) +} + +// Option is the type used to pass custom attributes to the constructor. +type Option func(o *options) + +func newOptions() *options { + return &options{ + Name: "MiniCA", + RootTemplate: x509util.DefaultRootTemplate, + IntermediateTemplate: x509util.DefaultIntermediateTemplate, + GetSigner: keyutil.GenerateDefaultSigner, + } +} + +func (o *options) apply(opts []Option) *options { + for _, fn := range opts { + fn(o) + } + return o +} + +// WithName is an option that allows to overwrite the default name MiniCA. With +// the default templates, the root and intermediate certificate common names +// would be " Root CA" and " Intermediate CA". +func WithName(name string) Option { + return func(o *options) { + o.Name = name + } +} + +// WithRootTemplate is an option that allows to overwrite the template used to +// create the root certificate. +func WithRootTemplate(template string) Option { + return func(o *options) { + o.RootTemplate = template + } +} + +// WithIntermediateTemplate is an option that allows to overwrite the template +// used to create the intermediate certificate. +func WithIntermediateTemplate(template string) Option { + return func(o *options) { + o.IntermediateTemplate = template + } +} + +// WithGetSignerFunc is an option that allows to overwrite the default function to +// create a signer. +func WithGetSignerFunc(fn func() (crypto.Signer, error)) Option { + return func(o *options) { + o.GetSigner = fn + } +} + +type signOptions struct { + Template string + Modify func(*x509.Certificate) error +} + +// SignOption is the type used to pass custom attributes when signing a +// certificate request. +type SignOption func(o *signOptions) + +func newSignOptions() *signOptions { + return &signOptions{ + Template: x509util.DefaultLeafTemplate, + } +} + +func (o *signOptions) apply(opts []SignOption) *signOptions { + for _, fn := range opts { + fn(o) + } + return o +} + +// WithTemplate allows to update the template used to convert a CSR into a +// certificate. +func WithTemplate(template string) SignOption { + return func(o *signOptions) { + o.Template = template + } +} + +// WithModifyFunc allows to update the certificate template before the signing +// it. +func WithModifyFunc(fn func(*x509.Certificate) error) SignOption { + return func(o *signOptions) { + o.Modify = fn + } +} From 1ff8afddedfea3512da4c93c77c02f554e7f0477 Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Tue, 15 Mar 2022 19:22:15 -0700 Subject: [PATCH 2/5] Add minica test and fix template options. --- minica/minica.go | 4 +- minica/minica_test.go | 371 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 373 insertions(+), 2 deletions(-) create mode 100644 minica/minica_test.go diff --git a/minica/minica.go b/minica/minica.go index 41fc428a..9bd10a4e 100644 --- a/minica/minica.go +++ b/minica/minica.go @@ -36,7 +36,7 @@ func New(opts ...Option) (*MiniCA, error) { if err != nil { return nil, err } - cert, err := x509util.NewCertificate(rootCR, x509util.WithTemplate(x509util.DefaultRootTemplate, x509util.CreateTemplateData(rootSubject, []string{}))) + cert, err := x509util.NewCertificate(rootCR, x509util.WithTemplate(o.RootTemplate, x509util.CreateTemplateData(rootSubject, []string{}))) if err != nil { return nil, err } @@ -58,7 +58,7 @@ func New(opts ...Option) (*MiniCA, error) { if err != nil { return nil, err } - cert, err = x509util.NewCertificate(intCR, x509util.WithTemplate(x509util.DefaultIntermediateTemplate, x509util.CreateTemplateData(intSubject, []string{}))) + cert, err = x509util.NewCertificate(intCR, x509util.WithTemplate(o.IntermediateTemplate, x509util.CreateTemplateData(intSubject, []string{}))) if err != nil { return nil, err } diff --git a/minica/minica_test.go b/minica/minica_test.go new file mode 100644 index 00000000..70810b7d --- /dev/null +++ b/minica/minica_test.go @@ -0,0 +1,371 @@ +package minica + +import ( + "crypto" + "crypto/ed25519" + "crypto/rand" + "crypto/x509" + "errors" + "io" + "net" + "reflect" + "testing" + "time" + + "go.step.sm/crypto/keyutil" + "go.step.sm/crypto/x509util" + "golang.org/x/crypto/ssh" +) + +type badSigner struct{} + +func (p badSigner) Public() crypto.PublicKey { + return []byte("foo") +} + +func (p badSigner) Sign(r io.Reader, digest []byte, opts crypto.SignerOpts) (signature []byte, err error) { + return nil, errors.New("foo") +} + +type mockConnMetadata string + +func (c mockConnMetadata) User() string { + return string(c) +} +func (c mockConnMetadata) SessionID() []byte { + return []byte{1, 2, 3} +} +func (c mockConnMetadata) ClientVersion() []byte { + return []byte{1, 2, 3} +} +func (c mockConnMetadata) ServerVersion() []byte { + return []byte{1, 2, 3} +} +func (c mockConnMetadata) RemoteAddr() net.Addr { + return &net.IPAddr{IP: net.IP{1, 2, 3, 4}} +} +func (c mockConnMetadata) LocalAddr() net.Addr { + return &net.IPAddr{IP: net.IP{1, 2, 3, 4}} +} + +func mustCA(t *testing.T, opts ...Option) *MiniCA { + t.Helper() + ca, err := New(opts...) + if err != nil { + t.Fatal(err) + } + return ca +} + +func TestNew(t *testing.T) { + _, signer, err := ed25519.GenerateKey(rand.Reader) + if err != nil { + t.Fatal(err) + } + + failGetSigner := func(n int) func() (crypto.Signer, error) { + var callNumber int + return func() (crypto.Signer, error) { + callNumber++ + if callNumber == n { + return nil, errors.New("an error") + } + return signer, nil + } + } + failSigner := func(n int) func() (crypto.Signer, error) { + var callNumber int + return func() (crypto.Signer, error) { + callNumber++ + if callNumber == n { + return badSigner{}, nil + } + return signer, nil + } + } + + type args struct { + opts []Option + } + tests := []struct { + name string + args args + wantName string + wantErr bool + }{ + {"ok", args{}, "MiniCA", false}, + {"ok with options", args{[]Option{WithName("Test"), WithGetSignerFunc(func() (crypto.Signer, error) { + _, s, err := ed25519.GenerateKey(rand.Reader) + return s, err + })}}, "Test", false}, + {"fail root signer", args{[]Option{WithGetSignerFunc(failGetSigner(1))}}, "", true}, + {"fail intermediate signer", args{[]Option{WithGetSignerFunc(failGetSigner(2))}}, "", true}, + {"fail host signer", args{[]Option{WithGetSignerFunc(failGetSigner(3))}}, "", true}, + {"fail user signer", args{[]Option{WithGetSignerFunc(failGetSigner(4))}}, "", true}, + {"fail root template", args{[]Option{WithRootTemplate(`fail "foo"`)}}, "", true}, + {"fail intermediate template", args{[]Option{WithIntermediateTemplate(`fail "foo"`)}}, "", true}, + {"fail root csr", args{[]Option{WithGetSignerFunc(failSigner(1))}}, "", true}, + {"fail intermediate csr", args{[]Option{WithGetSignerFunc(failSigner(2))}}, "", true}, + {"fail host ssh signer", args{[]Option{WithGetSignerFunc(failSigner(3))}}, "", true}, + {"fail user ssh signer", args{[]Option{WithGetSignerFunc(failSigner(4))}}, "", true}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := New(tt.args.opts...) + if (err != nil) != tt.wantErr { + t.Errorf("New() error = %v, wantErr %v", err, tt.wantErr) + return + } + if tt.wantErr { + if got != nil { + t.Errorf("New() = %v, want nil", got) + } + } else { + if got.Root == nil { + t.Errorf("MiniCA.Root should not be nil") + } + if got.Intermediate == nil { + t.Errorf("MiniCA.Intermediate should not be nil") + } + if got.Signer == nil { + t.Errorf("MiniCA.Signer should not be nil") + } + if got.SSHHostSigner == nil { + t.Errorf("MiniCA.SSHHostSigner should not be nil") + } + if got.SSHUserSigner == nil { + t.Errorf("MiniCA.SSHUserSigner should not be nil") + } + + // Check common names + if cn := got.Root.Subject.CommonName; cn != tt.wantName+" Root CA" { + t.Errorf("MiniCA.Root.Subject.CommonName = %s, want %s Root CA", cn, tt.wantName) + } + if cn := got.Root.Issuer.CommonName; cn != tt.wantName+" Root CA" { + t.Errorf("MiniCA.Root.Issuer.CommonName = %s, want %s Root CA", cn, tt.wantName) + } + if cn := got.Intermediate.Subject.CommonName; cn != tt.wantName+" Intermediate CA" { + t.Errorf("MiniCA.Intermediate.Subject.CommonName = %s, want %s Intermediate CA", cn, tt.wantName) + } + if cn := got.Intermediate.Issuer.CommonName; cn != tt.wantName+" Root CA" { + t.Errorf("MiniCA.Root.Intermediate.Issuer.CommonName = %s, want %s Root CA", cn, tt.wantName) + } + + // Verify intermediate + pool := x509.NewCertPool() + pool.AddCert(got.Root) + if _, err := got.Intermediate.Verify(x509.VerifyOptions{ + Roots: pool, + }); err != nil { + t.Errorf("MiniCA.Intermediate.Verify() error = %v", err) + } + } + }) + } +} + +func TestMiniCA_Sign(t *testing.T) { + signer, err := keyutil.GenerateDefaultSigner() + if err != nil { + t.Fatal(err) + } + + type args struct { + template *x509.Certificate + } + tests := []struct { + name string + ca *MiniCA + args args + wantErr bool + }{ + {"ok", mustCA(t), args{&x509.Certificate{ + DNSNames: []string{"leaf.test.com"}, + PublicKey: signer.Public(), + }}, false}, + {"ok with lifetime", mustCA(t), args{&x509.Certificate{ + DNSNames: []string{"leaf.test.com"}, + PublicKey: signer.Public(), + NotBefore: time.Now(), + NotAfter: time.Now().Add(1 * time.Hour), + }}, false}, + {"fail", mustCA(t), args{&x509.Certificate{ + DNSNames: []string{"leaf.test.com"}, + PublicKey: []byte("not a key"), + }}, true}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := tt.ca.Sign(tt.args.template) + if (err != nil) != tt.wantErr { + t.Errorf("MiniCA.Sign() error = %v, wantErr %v", err, tt.wantErr) + return + } + if tt.wantErr { + if got != nil { + t.Errorf("MiniCA.Sign() = %v, want nil", got) + } + } else { + roots := x509.NewCertPool() + roots.AddCert(tt.ca.Root) + ints := x509.NewCertPool() + ints.AddCert(tt.ca.Intermediate) + + if _, err := got.Verify(x509.VerifyOptions{ + Roots: roots, + Intermediates: ints, + DNSName: "leaf.test.com", + KeyUsages: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth, x509.ExtKeyUsageServerAuth}, + }); err != nil { + t.Errorf("Certificate.Verify() error = %v", err) + } + } + }) + } +} + +func TestMiniCA_SignCSR(t *testing.T) { + signer, err := keyutil.GenerateDefaultSigner() + if err != nil { + t.Fatal(err) + } + csr, err := x509util.CreateCertificateRequest("", []string{"leaf.test.com", "127.0.0.1", "test@test.com", "uuid:64757c7c-33b0-4125-9a73-be41e17f9f98"}, signer) + if err != nil { + t.Fatal(err) + } + + type args struct { + csr *x509.CertificateRequest + opts []SignOption + } + tests := []struct { + name string + ca *MiniCA + args args + wantDNSName string + wantKeyUsages []x509.ExtKeyUsage + wantErr bool + }{ + {"ok", mustCA(t), args{csr, nil}, "leaf.test.com", []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth, x509.ExtKeyUsageServerAuth}, false}, + {"ok with modify", mustCA(t), args{csr, []SignOption{WithModifyFunc(func(cert *x509.Certificate) error { + cert.DNSNames = []string{"foo.test.com"} + cert.ExtKeyUsage = []x509.ExtKeyUsage{x509.ExtKeyUsageCodeSigning} + return nil + })}}, "foo.test.com", []x509.ExtKeyUsage{x509.ExtKeyUsageCodeSigning}, false}, + {"fail new certificate", mustCA(t), args{&x509.CertificateRequest{}, nil}, "", nil, true}, + {"fail modify", mustCA(t), args{csr, []SignOption{WithModifyFunc(func(cert *x509.Certificate) error { + return errors.New("an error") + })}}, "", nil, true}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := tt.ca.SignCSR(tt.args.csr, tt.args.opts...) + if (err != nil) != tt.wantErr { + t.Errorf("MiniCA.SignCSR() error = %v, wantErr %v", err, tt.wantErr) + return + } + if tt.wantErr { + if got != nil { + t.Errorf("MiniCA.Sign() = %v, want nil", got) + } + } else { + roots := x509.NewCertPool() + roots.AddCert(tt.ca.Root) + ints := x509.NewCertPool() + ints.AddCert(tt.ca.Intermediate) + + if _, err := got.Verify(x509.VerifyOptions{ + Roots: roots, + Intermediates: ints, + DNSName: tt.wantDNSName, + KeyUsages: tt.wantKeyUsages, + }); err != nil { + t.Errorf("Certificate.Verify() error = %v", err) + } + } + }) + } +} + +func TestMiniCA_SignSSH(t *testing.T) { + signer, err := keyutil.GenerateDefaultSigner() + if err != nil { + t.Fatal(err) + } + publicKey, err := ssh.NewPublicKey(signer.Public()) + if err != nil { + t.Fatal(err) + } + + type args struct { + cert *ssh.Certificate + } + tests := []struct { + name string + ca *MiniCA + args args + wantCertType uint32 + wantPrincipal string + wantErr bool + }{ + {"ok host", mustCA(t), args{&ssh.Certificate{ + Key: publicKey, + Serial: 1234, + CertType: ssh.HostCert, + KeyId: "ssh.test.com", + ValidPrincipals: []string{"ssh.test.com"}, + }}, ssh.HostCert, "ssh.test.com", false}, + {"ok user", mustCA(t), args{&ssh.Certificate{ + Key: publicKey, + Serial: 1234, + CertType: ssh.UserCert, + KeyId: "jane@test.com", + ValidPrincipals: []string{"jane"}, + ValidAfter: uint64(time.Now().Unix()), + ValidBefore: uint64(time.Now().Add(time.Hour).Unix()), + }}, ssh.UserCert, "jane", false}, + {"fail type", mustCA(t), args{&ssh.Certificate{ + Key: publicKey, + Serial: 1234, + CertType: 100, + KeyId: "jane@test.com", + ValidPrincipals: []string{"jane"}, + }}, 0, "", true}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := tt.ca.SignSSH(tt.args.cert) + if (err != nil) != tt.wantErr { + t.Errorf("MiniCA.SignSSH() error = %v, wantErr %v", err, tt.wantErr) + return + } + + if tt.wantErr { + if got != nil { + t.Errorf("MiniCA.SignSSH() = %v, want nil", got) + } + } else { + checker := ssh.CertChecker{ + IsUserAuthority: func(auth ssh.PublicKey) bool { + return reflect.DeepEqual(auth, tt.ca.SSHUserSigner.PublicKey()) + }, + IsHostAuthority: func(auth ssh.PublicKey, address string) bool { + return reflect.DeepEqual(auth, tt.ca.SSHHostSigner.PublicKey()) + }, + } + switch tt.wantCertType { + case ssh.HostCert: + if err := checker.CheckHostKey(tt.wantPrincipal+":22", &net.IPAddr{IP: net.IP{1, 2, 3, 4}}, got); err != nil { + t.Errorf("CertChecker.CheckHostKey() error = %v", err) + } + case ssh.UserCert: + if _, err := checker.Authenticate(mockConnMetadata(tt.wantPrincipal), got); err != nil { + t.Errorf("CertChecker.Authenticate() error = %v", err) + } + default: + t.Fatalf("unknown cert type %v", tt.wantCertType) + } + } + }) + } +} From 99fbedea8963cc4bc37bb71f3521f425453e1bc2 Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Wed, 16 Mar 2022 10:56:54 -0700 Subject: [PATCH 3/5] Rename struct from MiniCA to CA. --- minica/minica.go | 14 +++++++------- minica/minica_test.go | 8 ++++---- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/minica/minica.go b/minica/minica.go index 9bd10a4e..fbcd56b5 100644 --- a/minica/minica.go +++ b/minica/minica.go @@ -11,8 +11,8 @@ import ( "golang.org/x/crypto/ssh" ) -// MiniCA is the implementation of a simple X.509 and SSH CA. -type MiniCA struct { +// CA is the implementation of a simple X.509 and SSH CA. +type CA struct { Root *x509.Certificate Intermediate *x509.Certificate Signer crypto.Signer @@ -22,7 +22,7 @@ type MiniCA struct { // New creates a new MiniCA, the custom options allows to overwrite templates, // signer types and certificate names. -func New(opts ...Option) (*MiniCA, error) { +func New(opts ...Option) (*CA, error) { now := time.Now() o := newOptions().apply(opts) @@ -90,7 +90,7 @@ func New(opts ...Option) (*MiniCA, error) { return nil, err } - return &MiniCA{ + return &CA{ Root: root, Intermediate: intermediate, Signer: intSigner, @@ -100,7 +100,7 @@ func New(opts ...Option) (*MiniCA, error) { } // Sign signs an X.509 certificate template using the intermediate certificate. -func (c *MiniCA) Sign(template *x509.Certificate) (*x509.Certificate, error) { +func (c *CA) Sign(template *x509.Certificate) (*x509.Certificate, error) { if template.NotBefore.IsZero() { template.NotBefore = time.Now() } @@ -111,7 +111,7 @@ func (c *MiniCA) Sign(template *x509.Certificate) (*x509.Certificate, error) { } // SignCSR signs an X.509 certificate signing request. The custom options allows to change the template used for -func (c *MiniCA) SignCSR(csr *x509.CertificateRequest, opts ...SignOption) (*x509.Certificate, error) { +func (c *CA) SignCSR(csr *x509.CertificateRequest, opts ...SignOption) (*x509.Certificate, error) { sans := append([]string{}, csr.DNSNames...) sans = append(sans, csr.EmailAddresses...) for _, ip := range csr.IPAddresses { @@ -138,7 +138,7 @@ func (c *MiniCA) SignCSR(csr *x509.CertificateRequest, opts ...SignOption) (*x50 } // SignSSH signs an SSH host or user certificate. -func (c *MiniCA) SignSSH(cert *ssh.Certificate) (*ssh.Certificate, error) { +func (c *CA) SignSSH(cert *ssh.Certificate) (*ssh.Certificate, error) { if cert.ValidAfter == 0 { cert.ValidAfter = uint64(time.Now().Unix()) } diff --git a/minica/minica_test.go b/minica/minica_test.go index 70810b7d..adca773f 100644 --- a/minica/minica_test.go +++ b/minica/minica_test.go @@ -48,7 +48,7 @@ func (c mockConnMetadata) LocalAddr() net.Addr { return &net.IPAddr{IP: net.IP{1, 2, 3, 4}} } -func mustCA(t *testing.T, opts ...Option) *MiniCA { +func mustCA(t *testing.T, opts ...Option) *CA { t.Helper() ca, err := New(opts...) if err != nil { @@ -175,7 +175,7 @@ func TestMiniCA_Sign(t *testing.T) { } tests := []struct { name string - ca *MiniCA + ca *CA args args wantErr bool }{ @@ -240,7 +240,7 @@ func TestMiniCA_SignCSR(t *testing.T) { } tests := []struct { name string - ca *MiniCA + ca *CA args args wantDNSName string wantKeyUsages []x509.ExtKeyUsage @@ -302,7 +302,7 @@ func TestMiniCA_SignSSH(t *testing.T) { } tests := []struct { name string - ca *MiniCA + ca *CA args args wantCertType uint32 wantPrincipal string From cd2e52da0b66ce3a0fde8470ba7751d1e9a970eb Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Wed, 16 Mar 2022 11:39:51 -0700 Subject: [PATCH 4/5] Do not mutate input in Sign and SSHSign. This commit also adds more documentation on which fields will be automatically pupulated if they are not set. --- minica/minica.go | 56 ++++++++---- minica/minica_test.go | 208 ++++++++++++++++++++++++++++++++++++++---- 2 files changed, 227 insertions(+), 37 deletions(-) diff --git a/minica/minica.go b/minica/minica.go index fbcd56b5..0385a58b 100644 --- a/minica/minica.go +++ b/minica/minica.go @@ -100,17 +100,26 @@ func New(opts ...Option) (*CA, error) { } // Sign signs an X.509 certificate template using the intermediate certificate. +// Sign will automatically populate the following fields if they are not +// specified: +// +// - NotBefore will be set to the current time. +// - NotAfter will be set to 24 hours after NotBefore. +// - SerialNumber will be automatically generated. +// - SubjectKeyId will be automatically generated. func (c *CA) Sign(template *x509.Certificate) (*x509.Certificate, error) { - if template.NotBefore.IsZero() { - template.NotBefore = time.Now() + mut := *template + if mut.NotBefore.IsZero() { + mut.NotBefore = time.Now() } - if template.NotAfter.IsZero() { - template.NotAfter = template.NotBefore.Add(24 * time.Hour) + if mut.NotAfter.IsZero() { + mut.NotAfter = mut.NotBefore.Add(24 * time.Hour) } - return x509util.CreateCertificate(template, c.Intermediate, template.PublicKey, c.Signer) + return x509util.CreateCertificate(&mut, c.Intermediate, mut.PublicKey, c.Signer) } -// SignCSR signs an X.509 certificate signing request. The custom options allows to change the template used for +// SignCSR signs an X.509 certificate signing request. The custom options allows +// to change the template used to convert the CSR to a certificate. func (c *CA) SignCSR(csr *x509.CertificateRequest, opts ...SignOption) (*x509.Certificate, error) { sans := append([]string{}, csr.DNSNames...) sans = append(sans, csr.EmailAddresses...) @@ -137,20 +146,31 @@ func (c *CA) SignCSR(csr *x509.CertificateRequest, opts ...SignOption) (*x509.Ce return c.Sign(cert) } -// SignSSH signs an SSH host or user certificate. -func (c *CA) SignSSH(cert *ssh.Certificate) (*ssh.Certificate, error) { - if cert.ValidAfter == 0 { - cert.ValidAfter = uint64(time.Now().Unix()) - } - if cert.ValidBefore == 0 { - cert.ValidBefore = cert.ValidAfter + 24*60*60 - } - - switch cert.CertType { +// SignSSH signs an SSH host or user certificate. SignSSH will automatically +// populate the following fields if they are not specified: +// +// - ValidAfter will be set to the current time unless ValidBefore is set to ssh.CertTimeInfinity. +// - ValidBefore will be set to 24 hours after ValidAfter. +// - Nonce will be automatically generated. +// - Serial will be automatically generated. +// +// If the SSH signer is an RSA key, it will use rsa-sha2-256 instead of the +// default ssh-rsa (SHA-1), this method is currently deprecated and +// rsa-sha2-256/512 are supported since OpenSSH 7.2 (2016). +func (c *CA) SignSSH(template *ssh.Certificate) (*ssh.Certificate, error) { + mut := *template + if mut.ValidAfter == 0 && mut.ValidBefore != ssh.CertTimeInfinity { + mut.ValidAfter = uint64(time.Now().Unix()) + } + if mut.ValidBefore == 0 { + mut.ValidBefore = mut.ValidAfter + 24*60*60 + } + + switch mut.CertType { case ssh.HostCert: - return sshutil.CreateCertificate(cert, c.SSHHostSigner) + return sshutil.CreateCertificate(&mut, c.SSHHostSigner) case ssh.UserCert: - return sshutil.CreateCertificate(cert, c.SSHUserSigner) + return sshutil.CreateCertificate(&mut, c.SSHUserSigner) default: return nil, fmt.Errorf("unknown certificate type") } diff --git a/minica/minica_test.go b/minica/minica_test.go index adca773f..335c9e6a 100644 --- a/minica/minica_test.go +++ b/minica/minica_test.go @@ -122,33 +122,33 @@ func TestNew(t *testing.T) { } } else { if got.Root == nil { - t.Errorf("MiniCA.Root should not be nil") + t.Errorf("CA.Root should not be nil") } if got.Intermediate == nil { - t.Errorf("MiniCA.Intermediate should not be nil") + t.Errorf("CA.Intermediate should not be nil") } if got.Signer == nil { - t.Errorf("MiniCA.Signer should not be nil") + t.Errorf("CA.Signer should not be nil") } if got.SSHHostSigner == nil { - t.Errorf("MiniCA.SSHHostSigner should not be nil") + t.Errorf("CA.SSHHostSigner should not be nil") } if got.SSHUserSigner == nil { - t.Errorf("MiniCA.SSHUserSigner should not be nil") + t.Errorf("CA.SSHUserSigner should not be nil") } // Check common names if cn := got.Root.Subject.CommonName; cn != tt.wantName+" Root CA" { - t.Errorf("MiniCA.Root.Subject.CommonName = %s, want %s Root CA", cn, tt.wantName) + t.Errorf("CA.Root.Subject.CommonName = %s, want %s Root CA", cn, tt.wantName) } if cn := got.Root.Issuer.CommonName; cn != tt.wantName+" Root CA" { - t.Errorf("MiniCA.Root.Issuer.CommonName = %s, want %s Root CA", cn, tt.wantName) + t.Errorf("CA.Root.Issuer.CommonName = %s, want %s Root CA", cn, tt.wantName) } if cn := got.Intermediate.Subject.CommonName; cn != tt.wantName+" Intermediate CA" { - t.Errorf("MiniCA.Intermediate.Subject.CommonName = %s, want %s Intermediate CA", cn, tt.wantName) + t.Errorf("CA.Intermediate.Subject.CommonName = %s, want %s Intermediate CA", cn, tt.wantName) } if cn := got.Intermediate.Issuer.CommonName; cn != tt.wantName+" Root CA" { - t.Errorf("MiniCA.Root.Intermediate.Issuer.CommonName = %s, want %s Root CA", cn, tt.wantName) + t.Errorf("CA.Root.Intermediate.Issuer.CommonName = %s, want %s Root CA", cn, tt.wantName) } // Verify intermediate @@ -157,14 +157,14 @@ func TestNew(t *testing.T) { if _, err := got.Intermediate.Verify(x509.VerifyOptions{ Roots: pool, }); err != nil { - t.Errorf("MiniCA.Intermediate.Verify() error = %v", err) + t.Errorf("CA.Intermediate.Verify() error = %v", err) } } }) } } -func TestMiniCA_Sign(t *testing.T) { +func TestCA_Sign(t *testing.T) { signer, err := keyutil.GenerateDefaultSigner() if err != nil { t.Fatal(err) @@ -198,12 +198,12 @@ func TestMiniCA_Sign(t *testing.T) { t.Run(tt.name, func(t *testing.T) { got, err := tt.ca.Sign(tt.args.template) if (err != nil) != tt.wantErr { - t.Errorf("MiniCA.Sign() error = %v, wantErr %v", err, tt.wantErr) + t.Errorf("CA.Sign() error = %v, wantErr %v", err, tt.wantErr) return } if tt.wantErr { if got != nil { - t.Errorf("MiniCA.Sign() = %v, want nil", got) + t.Errorf("CA.Sign() = %v, want nil", got) } } else { roots := x509.NewCertPool() @@ -224,7 +224,65 @@ func TestMiniCA_Sign(t *testing.T) { } } -func TestMiniCA_SignCSR(t *testing.T) { +func TestCA_Sign_mutation(t *testing.T) { + signer, err := keyutil.GenerateDefaultSigner() + if err != nil { + t.Fatal(err) + } + + ca := mustCA(t) + template := &x509.Certificate{ + DNSNames: []string{"leaf.test.com"}, + PublicKey: signer.Public(), + } + got, err := ca.Sign(template) + if err != nil { + t.Fatal(err) + } + + // Verify certificate + roots := x509.NewCertPool() + roots.AddCert(ca.Root) + ints := x509.NewCertPool() + ints.AddCert(ca.Intermediate) + if _, err := got.Verify(x509.VerifyOptions{ + Roots: roots, + Intermediates: ints, + DNSName: "leaf.test.com", + KeyUsages: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth, x509.ExtKeyUsageServerAuth}, + }); err != nil { + t.Errorf("Certificate.Verify() error = %v", err) + } + + // Check mutation + if !template.NotBefore.IsZero() { + t.Errorf("CA.Sign() mutated template.NotBefore") + } + if !template.NotAfter.IsZero() { + t.Errorf("CA.Sign() mutated template.NotAfter") + } + if template.SerialNumber != nil { + t.Errorf("CA.Sign() mutated template.SerialNumber") + } + if template.SubjectKeyId != nil { + t.Errorf("CA.Sign() mutated template.SubjectKeyId") + } + + if got.NotBefore.IsZero() { + t.Errorf("CA.Sign() got.NotBefore should not be 0") + } + if got.NotAfter.IsZero() { + t.Errorf("CA.Sign() got.NotAfter should not be 0") + } + if got.SerialNumber == nil { + t.Errorf("CA.Sign() got.SerialNumber should not be nil") + } + if got.SubjectKeyId == nil { + t.Errorf("CA.Sign() got.SubjectKeyId should not be nil") + } +} + +func TestCA_SignCSR(t *testing.T) { signer, err := keyutil.GenerateDefaultSigner() if err != nil { t.Fatal(err) @@ -261,12 +319,12 @@ func TestMiniCA_SignCSR(t *testing.T) { t.Run(tt.name, func(t *testing.T) { got, err := tt.ca.SignCSR(tt.args.csr, tt.args.opts...) if (err != nil) != tt.wantErr { - t.Errorf("MiniCA.SignCSR() error = %v, wantErr %v", err, tt.wantErr) + t.Errorf("CA.SignCSR() error = %v, wantErr %v", err, tt.wantErr) return } if tt.wantErr { if got != nil { - t.Errorf("MiniCA.Sign() = %v, want nil", got) + t.Errorf("CA.Sign() = %v, want nil", got) } } else { roots := x509.NewCertPool() @@ -287,7 +345,7 @@ func TestMiniCA_SignCSR(t *testing.T) { } } -func TestMiniCA_SignSSH(t *testing.T) { +func TestCA_SignSSH(t *testing.T) { signer, err := keyutil.GenerateDefaultSigner() if err != nil { t.Fatal(err) @@ -324,6 +382,14 @@ func TestMiniCA_SignSSH(t *testing.T) { ValidAfter: uint64(time.Now().Unix()), ValidBefore: uint64(time.Now().Add(time.Hour).Unix()), }}, ssh.UserCert, "jane", false}, + {"ok infinity", mustCA(t), args{&ssh.Certificate{ + Key: publicKey, + Serial: 1234, + CertType: ssh.UserCert, + KeyId: "jane@test.com", + ValidPrincipals: []string{"jane"}, + ValidBefore: ssh.CertTimeInfinity, + }}, ssh.UserCert, "jane", false}, {"fail type", mustCA(t), args{&ssh.Certificate{ Key: publicKey, Serial: 1234, @@ -336,13 +402,13 @@ func TestMiniCA_SignSSH(t *testing.T) { t.Run(tt.name, func(t *testing.T) { got, err := tt.ca.SignSSH(tt.args.cert) if (err != nil) != tt.wantErr { - t.Errorf("MiniCA.SignSSH() error = %v, wantErr %v", err, tt.wantErr) + t.Errorf("CA.SignSSH() error = %v, wantErr %v", err, tt.wantErr) return } if tt.wantErr { if got != nil { - t.Errorf("MiniCA.SignSSH() = %v, want nil", got) + t.Errorf("CA.SignSSH() = %v, want nil", got) } } else { checker := ssh.CertChecker{ @@ -369,3 +435,107 @@ func TestMiniCA_SignSSH(t *testing.T) { }) } } + +func TestCA_SignSSH_mutation(t *testing.T) { + signer, err := keyutil.GenerateDefaultSigner() + if err != nil { + t.Fatal(err) + } + publicKey, err := ssh.NewPublicKey(signer.Public()) + if err != nil { + t.Fatal(err) + } + + template := &ssh.Certificate{ + Key: publicKey, + CertType: ssh.HostCert, + KeyId: "ssh.test.com", + ValidPrincipals: []string{"ssh.test.com"}, + } + + ca := mustCA(t) + got, err := ca.SignSSH(template) + if err != nil { + t.Fatal(err) + } + + // Validate certificate + checker := ssh.CertChecker{ + IsHostAuthority: func(auth ssh.PublicKey, address string) bool { + return reflect.DeepEqual(auth, ca.SSHHostSigner.PublicKey()) + }, + } + if err := checker.CheckHostKey("ssh.test.com:22", &net.IPAddr{IP: net.IP{1, 2, 3, 4}}, got); err != nil { + t.Errorf("CertChecker.CheckHostKey() error = %v", err) + } + + // Validate mutation + if template.ValidAfter != 0 { + t.Errorf("CA.SignSSH() mutated template.ValidAfter") + } + if template.ValidBefore != 0 { + t.Errorf("CA.SignSSH() mutated template.ValidBefore") + } + if template.Nonce != nil { + t.Errorf("CA.SignSSH() mutated template.Nonce") + } + if template.Serial != 0 { + t.Errorf("CA.SignSSH() mutated template.Serial") + } + + if got.ValidAfter == 0 { + t.Errorf("CA.SignSSH() got.ValidAfter should not be 0") + } + if got.ValidBefore == 0 { + t.Errorf("CA.SignSSH() got.ValidBefore should not be 0") + } + if len(got.Nonce) == 0 { + t.Errorf("CA.SignSSH() got.Nonce should not be empty") + } + if got.Serial == 0 { + t.Errorf("CA.SignSSH() got.Serial should not be 0") + } +} + +func TestCA_SignSSH_infinity(t *testing.T) { + signer, err := keyutil.GenerateDefaultSigner() + if err != nil { + t.Fatal(err) + } + publicKey, err := ssh.NewPublicKey(signer.Public()) + if err != nil { + t.Fatal(err) + } + + template := &ssh.Certificate{ + Key: publicKey, + Serial: 1234, + CertType: ssh.UserCert, + KeyId: "jane@test.com", + ValidPrincipals: []string{"jane"}, + ValidBefore: ssh.CertTimeInfinity, + } + + ca := mustCA(t) + got, err := ca.SignSSH(template) + if err != nil { + t.Fatal(err) + } + + // Validate certificate + checker := ssh.CertChecker{ + IsUserAuthority: func(auth ssh.PublicKey) bool { + return reflect.DeepEqual(auth, ca.SSHUserSigner.PublicKey()) + }, + } + if _, err := checker.Authenticate(mockConnMetadata("jane"), got); err != nil { + t.Errorf("CertChecker.Authenticate() error = %v", err) + } + + if got.ValidAfter != 0 { + t.Errorf("CA.SignSSH() got.ValidAfter should be 0") + } + if got.ValidBefore != ssh.CertTimeInfinity { + t.Errorf("CA.SignSSH() got.ValidBefore should not be ssh.CertTimInfinity") + } +} From 98fa65a1a9bed10c77649aa43921fc4b1f392ab3 Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Wed, 16 Mar 2022 12:33:31 -0700 Subject: [PATCH 5/5] Use t.Error() instead of t.Errorf() if there's no formatting. --- minica/minica_test.go | 46 +++++++++++++++++++++---------------------- 1 file changed, 23 insertions(+), 23 deletions(-) diff --git a/minica/minica_test.go b/minica/minica_test.go index 335c9e6a..f3a10921 100644 --- a/minica/minica_test.go +++ b/minica/minica_test.go @@ -122,19 +122,19 @@ func TestNew(t *testing.T) { } } else { if got.Root == nil { - t.Errorf("CA.Root should not be nil") + t.Error("CA.Root should not be nil") } if got.Intermediate == nil { - t.Errorf("CA.Intermediate should not be nil") + t.Error("CA.Intermediate should not be nil") } if got.Signer == nil { - t.Errorf("CA.Signer should not be nil") + t.Error("CA.Signer should not be nil") } if got.SSHHostSigner == nil { - t.Errorf("CA.SSHHostSigner should not be nil") + t.Error("CA.SSHHostSigner should not be nil") } if got.SSHUserSigner == nil { - t.Errorf("CA.SSHUserSigner should not be nil") + t.Error("CA.SSHUserSigner should not be nil") } // Check common names @@ -256,29 +256,29 @@ func TestCA_Sign_mutation(t *testing.T) { // Check mutation if !template.NotBefore.IsZero() { - t.Errorf("CA.Sign() mutated template.NotBefore") + t.Error("CA.Sign() mutated template.NotBefore") } if !template.NotAfter.IsZero() { - t.Errorf("CA.Sign() mutated template.NotAfter") + t.Error("CA.Sign() mutated template.NotAfter") } if template.SerialNumber != nil { - t.Errorf("CA.Sign() mutated template.SerialNumber") + t.Error("CA.Sign() mutated template.SerialNumber") } if template.SubjectKeyId != nil { - t.Errorf("CA.Sign() mutated template.SubjectKeyId") + t.Error("CA.Sign() mutated template.SubjectKeyId") } if got.NotBefore.IsZero() { - t.Errorf("CA.Sign() got.NotBefore should not be 0") + t.Error("CA.Sign() got.NotBefore should not be 0") } if got.NotAfter.IsZero() { - t.Errorf("CA.Sign() got.NotAfter should not be 0") + t.Error("CA.Sign() got.NotAfter should not be 0") } if got.SerialNumber == nil { - t.Errorf("CA.Sign() got.SerialNumber should not be nil") + t.Error("CA.Sign() got.SerialNumber should not be nil") } if got.SubjectKeyId == nil { - t.Errorf("CA.Sign() got.SubjectKeyId should not be nil") + t.Error("CA.Sign() got.SubjectKeyId should not be nil") } } @@ -471,29 +471,29 @@ func TestCA_SignSSH_mutation(t *testing.T) { // Validate mutation if template.ValidAfter != 0 { - t.Errorf("CA.SignSSH() mutated template.ValidAfter") + t.Error("CA.SignSSH() mutated template.ValidAfter") } if template.ValidBefore != 0 { - t.Errorf("CA.SignSSH() mutated template.ValidBefore") + t.Error("CA.SignSSH() mutated template.ValidBefore") } if template.Nonce != nil { - t.Errorf("CA.SignSSH() mutated template.Nonce") + t.Error("CA.SignSSH() mutated template.Nonce") } if template.Serial != 0 { - t.Errorf("CA.SignSSH() mutated template.Serial") + t.Error("CA.SignSSH() mutated template.Serial") } if got.ValidAfter == 0 { - t.Errorf("CA.SignSSH() got.ValidAfter should not be 0") + t.Error("CA.SignSSH() got.ValidAfter should not be 0") } if got.ValidBefore == 0 { - t.Errorf("CA.SignSSH() got.ValidBefore should not be 0") + t.Error("CA.SignSSH() got.ValidBefore should not be 0") } if len(got.Nonce) == 0 { - t.Errorf("CA.SignSSH() got.Nonce should not be empty") + t.Error("CA.SignSSH() got.Nonce should not be empty") } if got.Serial == 0 { - t.Errorf("CA.SignSSH() got.Serial should not be 0") + t.Error("CA.SignSSH() got.Serial should not be 0") } } @@ -533,9 +533,9 @@ func TestCA_SignSSH_infinity(t *testing.T) { } if got.ValidAfter != 0 { - t.Errorf("CA.SignSSH() got.ValidAfter should be 0") + t.Error("CA.SignSSH() got.ValidAfter should be 0") } if got.ValidBefore != ssh.CertTimeInfinity { - t.Errorf("CA.SignSSH() got.ValidBefore should not be ssh.CertTimInfinity") + t.Error("CA.SignSSH() got.ValidBefore should not be ssh.CertTimInfinity") } }