certificates/acme/order_test.go
Mariano Cano 6ba20209c2
Verify CSR key fingerprint with attestation certificate key
This commit makes sure that the attestation certificate key matches the
key used on the CSR on an ACME device attestation flow.
2023-02-09 16:48:43 -08:00

1945 lines
55 KiB
Go

package acme
import (
"context"
"crypto"
"crypto/x509"
"crypto/x509/pkix"
"encoding/asn1"
"encoding/json"
"fmt"
"net"
"net/url"
"reflect"
"testing"
"time"
"github.com/google/go-cmp/cmp"
"github.com/pkg/errors"
"github.com/smallstep/assert"
"github.com/smallstep/certificates/authority"
"github.com/smallstep/certificates/authority/provisioner"
"go.step.sm/crypto/keyutil"
"go.step.sm/crypto/x509util"
)
func TestOrder_UpdateStatus(t *testing.T) {
type test struct {
o *Order
err *Error
db DB
}
tests := map[string]func(t *testing.T) test{
"ok/already-invalid": func(t *testing.T) test {
o := &Order{
Status: StatusInvalid,
}
return test{
o: o,
}
},
"ok/already-valid": func(t *testing.T) test {
o := &Order{
Status: StatusInvalid,
}
return test{
o: o,
}
},
"fail/error-unexpected-status": func(t *testing.T) test {
o := &Order{
Status: "foo",
}
return test{
o: o,
err: NewErrorISE("unrecognized order status: %s", o.Status),
}
},
"ok/ready-expired": func(t *testing.T) test {
now := clock.Now()
o := &Order{
ID: "oID",
AccountID: "accID",
Status: StatusReady,
ExpiresAt: now.Add(-5 * time.Minute),
}
return test{
o: o,
db: &MockDB{
MockUpdateOrder: func(ctx context.Context, updo *Order) error {
assert.Equals(t, updo.ID, o.ID)
assert.Equals(t, updo.AccountID, o.AccountID)
assert.Equals(t, updo.Status, StatusInvalid)
assert.Equals(t, updo.ExpiresAt, o.ExpiresAt)
return nil
},
},
}
},
"fail/ready-expired-db.UpdateOrder-error": func(t *testing.T) test {
now := clock.Now()
o := &Order{
ID: "oID",
AccountID: "accID",
Status: StatusReady,
ExpiresAt: now.Add(-5 * time.Minute),
}
return test{
o: o,
db: &MockDB{
MockUpdateOrder: func(ctx context.Context, updo *Order) error {
assert.Equals(t, updo.ID, o.ID)
assert.Equals(t, updo.AccountID, o.AccountID)
assert.Equals(t, updo.Status, StatusInvalid)
assert.Equals(t, updo.ExpiresAt, o.ExpiresAt)
return errors.New("force")
},
},
err: NewErrorISE("error updating order: force"),
}
},
"ok/pending-expired": func(t *testing.T) test {
now := clock.Now()
o := &Order{
ID: "oID",
AccountID: "accID",
Status: StatusPending,
ExpiresAt: now.Add(-5 * time.Minute),
}
return test{
o: o,
db: &MockDB{
MockUpdateOrder: func(ctx context.Context, updo *Order) error {
assert.Equals(t, updo.ID, o.ID)
assert.Equals(t, updo.AccountID, o.AccountID)
assert.Equals(t, updo.Status, StatusInvalid)
assert.Equals(t, updo.ExpiresAt, o.ExpiresAt)
err := NewError(ErrorMalformedType, "order has expired")
assert.HasPrefix(t, updo.Error.Err.Error(), err.Err.Error())
assert.Equals(t, updo.Error.Type, err.Type)
assert.Equals(t, updo.Error.Detail, err.Detail)
assert.Equals(t, updo.Error.Status, err.Status)
assert.Equals(t, updo.Error.Detail, err.Detail)
return nil
},
},
}
},
"ok/invalid": func(t *testing.T) test {
now := clock.Now()
o := &Order{
ID: "oID",
AccountID: "accID",
Status: StatusPending,
ExpiresAt: now.Add(5 * time.Minute),
AuthorizationIDs: []string{"a", "b"},
}
az1 := &Authorization{
ID: "a",
Status: StatusValid,
}
az2 := &Authorization{
ID: "b",
Status: StatusInvalid,
}
return test{
o: o,
db: &MockDB{
MockUpdateOrder: func(ctx context.Context, updo *Order) error {
assert.Equals(t, updo.ID, o.ID)
assert.Equals(t, updo.AccountID, o.AccountID)
assert.Equals(t, updo.Status, StatusInvalid)
assert.Equals(t, updo.ExpiresAt, o.ExpiresAt)
return nil
},
MockGetAuthorization: func(ctx context.Context, id string) (*Authorization, error) {
switch id {
case az1.ID:
return az1, nil
case az2.ID:
return az2, nil
default:
assert.FatalError(t, errors.Errorf("unexpected authz key %s", id))
return nil, errors.New("force")
}
},
},
}
},
"ok/still-pending": func(t *testing.T) test {
now := clock.Now()
o := &Order{
ID: "oID",
AccountID: "accID",
Status: StatusPending,
ExpiresAt: now.Add(5 * time.Minute),
AuthorizationIDs: []string{"a", "b"},
}
az1 := &Authorization{
ID: "a",
Status: StatusValid,
}
az2 := &Authorization{
ID: "b",
Status: StatusPending,
}
return test{
o: o,
db: &MockDB{
MockGetAuthorization: func(ctx context.Context, id string) (*Authorization, error) {
switch id {
case az1.ID:
return az1, nil
case az2.ID:
return az2, nil
default:
assert.FatalError(t, errors.Errorf("unexpected authz key %s", id))
return nil, errors.New("force")
}
},
},
}
},
"ok/valid": func(t *testing.T) test {
now := clock.Now()
o := &Order{
ID: "oID",
AccountID: "accID",
Status: StatusPending,
ExpiresAt: now.Add(5 * time.Minute),
AuthorizationIDs: []string{"a", "b"},
}
az1 := &Authorization{
ID: "a",
Status: StatusValid,
}
az2 := &Authorization{
ID: "b",
Status: StatusValid,
}
return test{
o: o,
db: &MockDB{
MockUpdateOrder: func(ctx context.Context, updo *Order) error {
assert.Equals(t, updo.ID, o.ID)
assert.Equals(t, updo.AccountID, o.AccountID)
assert.Equals(t, updo.Status, StatusReady)
assert.Equals(t, updo.ExpiresAt, o.ExpiresAt)
return nil
},
MockGetAuthorization: func(ctx context.Context, id string) (*Authorization, error) {
switch id {
case az1.ID:
return az1, nil
case az2.ID:
return az2, nil
default:
assert.FatalError(t, errors.Errorf("unexpected authz key %s", id))
return nil, errors.New("force")
}
},
},
}
},
}
for name, run := range tests {
t.Run(name, func(t *testing.T) {
tc := run(t)
if err := tc.o.UpdateStatus(context.Background(), tc.db); err != nil {
if assert.NotNil(t, tc.err) {
var k *Error
if errors.As(err, &k) {
assert.Equals(t, k.Type, tc.err.Type)
assert.Equals(t, k.Detail, tc.err.Detail)
assert.Equals(t, k.Status, tc.err.Status)
assert.Equals(t, k.Err.Error(), tc.err.Err.Error())
assert.Equals(t, k.Detail, tc.err.Detail)
} else {
assert.FatalError(t, errors.New("unexpected error type"))
}
}
} else {
assert.Nil(t, tc.err)
}
})
}
}
type mockSignAuth struct {
sign func(csr *x509.CertificateRequest, signOpts provisioner.SignOptions, extraOpts ...provisioner.SignOption) ([]*x509.Certificate, error)
areSANsAllowed func(ctx context.Context, sans []string) error
loadProvisionerByName func(string) (provisioner.Interface, error)
ret1, ret2 interface{}
err error
}
func (m *mockSignAuth) Sign(csr *x509.CertificateRequest, signOpts provisioner.SignOptions, extraOpts ...provisioner.SignOption) ([]*x509.Certificate, error) {
if m.sign != nil {
return m.sign(csr, signOpts, extraOpts...)
} else if m.err != nil {
return nil, m.err
}
return []*x509.Certificate{m.ret1.(*x509.Certificate), m.ret2.(*x509.Certificate)}, m.err
}
func (m *mockSignAuth) AreSANsAllowed(ctx context.Context, sans []string) error {
if m.areSANsAllowed != nil {
return m.areSANsAllowed(ctx, sans)
}
return m.err
}
func (m *mockSignAuth) LoadProvisionerByName(name string) (provisioner.Interface, error) {
if m.loadProvisionerByName != nil {
return m.loadProvisionerByName(name)
}
return m.ret1.(provisioner.Interface), m.err
}
func (m *mockSignAuth) IsRevoked(sn string) (bool, error) {
return false, nil
}
func (m *mockSignAuth) Revoke(context.Context, *authority.RevokeOptions) error {
return nil
}
func TestOrder_Finalize(t *testing.T) {
mustSigner := func(kty, crv string, size int) crypto.Signer {
s, err := keyutil.GenerateSigner(kty, crv, size)
if err != nil {
t.Fatal(err)
}
return s
}
type test struct {
o *Order
err *Error
db DB
ca CertificateAuthority
csr *x509.CertificateRequest
prov Provisioner
}
tests := map[string]func(t *testing.T) test{
"fail/invalid": func(t *testing.T) test {
o := &Order{
ID: "oid",
Status: StatusInvalid,
}
return test{
o: o,
err: NewError(ErrorOrderNotReadyType, "order %s has been abandoned", o.ID),
}
},
"fail/pending": func(t *testing.T) test {
now := clock.Now()
o := &Order{
ID: "oID",
AccountID: "accID",
Status: StatusPending,
ExpiresAt: now.Add(5 * time.Minute),
AuthorizationIDs: []string{"a", "b"},
}
az1 := &Authorization{
ID: "a",
Status: StatusValid,
}
az2 := &Authorization{
ID: "b",
Status: StatusPending,
ExpiresAt: now.Add(5 * time.Minute),
}
return test{
o: o,
db: &MockDB{
MockGetAuthorization: func(ctx context.Context, id string) (*Authorization, error) {
switch id {
case az1.ID:
return az1, nil
case az2.ID:
return az2, nil
default:
assert.FatalError(t, errors.Errorf("unexpected authz key %s", id))
return nil, errors.New("force")
}
},
},
err: NewError(ErrorOrderNotReadyType, "order %s is not ready", o.ID),
}
},
"ok/already-valid": func(t *testing.T) test {
o := &Order{
ID: "oid",
Status: StatusValid,
}
return test{
o: o,
}
},
"fail/error-unexpected-status": func(t *testing.T) test {
now := clock.Now()
o := &Order{
ID: "oID",
AccountID: "accID",
Status: "foo",
ExpiresAt: now.Add(5 * time.Minute),
AuthorizationIDs: []string{"a", "b"},
}
return test{
o: o,
err: NewErrorISE("unrecognized order status: %s", o.Status),
}
},
"fail/non-matching-permanent-identifier-common-name": func(t *testing.T) test {
now := clock.Now()
o := &Order{
ID: "oID",
AccountID: "accID",
Status: StatusReady,
ExpiresAt: now.Add(5 * time.Minute),
AuthorizationIDs: []string{"a", "b"},
Identifiers: []Identifier{
{Type: "permanent-identifier", Value: "a-permanent-identifier"},
},
}
signer := mustSigner("EC", "P-256", 0)
fingerprint, err := keyutil.Fingerprint(signer.Public())
if err != nil {
t.Fatal(err)
}
csr := &x509.CertificateRequest{
Subject: pkix.Name{
CommonName: "a-different-identifier",
},
PublicKey: signer.Public(),
ExtraExtensions: []pkix.Extension{
{
Id: asn1.ObjectIdentifier{1, 3, 6, 1, 5, 5, 7, 8, 3},
Value: []byte("a-permanent-identifier"),
},
},
}
return test{
o: o,
csr: csr,
db: &MockDB{
MockGetAuthorization: func(ctx context.Context, id string) (*Authorization, error) {
switch id {
case "a":
return &Authorization{
ID: id,
Status: StatusValid,
}, nil
case "b":
return &Authorization{
ID: id,
Fingerprint: fingerprint,
Status: StatusValid,
}, nil
default:
assert.FatalError(t, errors.Errorf("unexpected authorization %s", id))
return nil, errors.New("force")
}
},
MockUpdateOrder: func(ctx context.Context, o *Order) error {
return nil
},
},
err: &Error{
Type: "urn:ietf:params:acme:error:badCSR",
Detail: "The CSR is unacceptable",
Status: 400,
Err: fmt.Errorf("CSR Subject Common Name does not match identifiers exactly: "+
"CSR Subject Common Name = %s, Order Permanent Identifier = %s", csr.Subject.CommonName, "a-permanent-identifier"),
},
}
},
"fail/error-provisioner-auth": func(t *testing.T) test {
now := clock.Now()
o := &Order{
ID: "oID",
AccountID: "accID",
Status: StatusReady,
ExpiresAt: now.Add(5 * time.Minute),
AuthorizationIDs: []string{"a", "b"},
Identifiers: []Identifier{
{Type: "dns", Value: "foo.internal"},
{Type: "dns", Value: "bar.internal"},
},
}
csr := &x509.CertificateRequest{
Subject: pkix.Name{
CommonName: "foo.internal",
},
DNSNames: []string{"bar.internal"},
}
return test{
o: o,
csr: csr,
prov: &MockProvisioner{
MauthorizeSign: func(ctx context.Context, token string) ([]provisioner.SignOption, error) {
assert.Equals(t, token, "")
return nil, errors.New("force")
},
},
db: &MockDB{
MockGetAuthorization: func(ctx context.Context, id string) (*Authorization, error) {
return &Authorization{ID: id, Status: StatusValid}, nil
},
},
err: NewErrorISE("error retrieving authorization options from ACME provisioner: force"),
}
},
"fail/error-template-options": func(t *testing.T) test {
now := clock.Now()
o := &Order{
ID: "oID",
AccountID: "accID",
Status: StatusReady,
ExpiresAt: now.Add(5 * time.Minute),
AuthorizationIDs: []string{"a", "b"},
Identifiers: []Identifier{
{Type: "dns", Value: "foo.internal"},
{Type: "dns", Value: "bar.internal"},
},
}
csr := &x509.CertificateRequest{
Subject: pkix.Name{
CommonName: "foo.internal",
},
DNSNames: []string{"bar.internal"},
}
return test{
o: o,
csr: csr,
prov: &MockProvisioner{
MauthorizeSign: func(ctx context.Context, token string) ([]provisioner.SignOption, error) {
assert.Equals(t, token, "")
return nil, nil
},
MgetOptions: func() *provisioner.Options {
return &provisioner.Options{
X509: &provisioner.X509Options{
TemplateData: json.RawMessage([]byte("fo{o")),
},
}
},
},
db: &MockDB{
MockGetAuthorization: func(ctx context.Context, id string) (*Authorization, error) {
return &Authorization{ID: id, Status: StatusValid}, nil
},
},
err: NewErrorISE("error creating template options from ACME provisioner: error unmarshaling template data: invalid character 'o' in literal false (expecting 'a')"),
}
},
"fail/error-ca-sign": func(t *testing.T) test {
now := clock.Now()
o := &Order{
ID: "oID",
AccountID: "accID",
Status: StatusReady,
ExpiresAt: now.Add(5 * time.Minute),
AuthorizationIDs: []string{"a", "b"},
Identifiers: []Identifier{
{Type: "dns", Value: "foo.internal"},
{Type: "dns", Value: "bar.internal"},
},
}
csr := &x509.CertificateRequest{
Subject: pkix.Name{
CommonName: "foo.internal",
},
DNSNames: []string{"bar.internal"},
}
return test{
o: o,
csr: csr,
prov: &MockProvisioner{
MauthorizeSign: func(ctx context.Context, token string) ([]provisioner.SignOption, error) {
assert.Equals(t, token, "")
return nil, nil
},
MgetOptions: func() *provisioner.Options {
return nil
},
},
ca: &mockSignAuth{
sign: func(_csr *x509.CertificateRequest, signOpts provisioner.SignOptions, extraOpts ...provisioner.SignOption) ([]*x509.Certificate, error) {
assert.Equals(t, _csr, csr)
return nil, errors.New("force")
},
},
db: &MockDB{
MockGetAuthorization: func(ctx context.Context, id string) (*Authorization, error) {
return &Authorization{ID: id, Status: StatusValid}, nil
},
},
err: NewErrorISE("error signing certificate for order oID: force"),
}
},
"fail/error-db.CreateCertificate": func(t *testing.T) test {
now := clock.Now()
o := &Order{
ID: "oID",
AccountID: "accID",
Status: StatusReady,
ExpiresAt: now.Add(5 * time.Minute),
AuthorizationIDs: []string{"a", "b"},
Identifiers: []Identifier{
{Type: "dns", Value: "foo.internal"},
{Type: "dns", Value: "bar.internal"},
},
}
csr := &x509.CertificateRequest{
Subject: pkix.Name{
CommonName: "foo.internal",
},
DNSNames: []string{"bar.internal"},
}
foo := &x509.Certificate{Subject: pkix.Name{CommonName: "foo"}}
bar := &x509.Certificate{Subject: pkix.Name{CommonName: "bar"}}
baz := &x509.Certificate{Subject: pkix.Name{CommonName: "baz"}}
return test{
o: o,
csr: csr,
prov: &MockProvisioner{
MauthorizeSign: func(ctx context.Context, token string) ([]provisioner.SignOption, error) {
assert.Equals(t, token, "")
return nil, nil
},
MgetOptions: func() *provisioner.Options {
return nil
},
},
ca: &mockSignAuth{
sign: func(_csr *x509.CertificateRequest, signOpts provisioner.SignOptions, extraOpts ...provisioner.SignOption) ([]*x509.Certificate, error) {
assert.Equals(t, _csr, csr)
return []*x509.Certificate{foo, bar, baz}, nil
},
},
db: &MockDB{
MockGetAuthorization: func(ctx context.Context, id string) (*Authorization, error) {
return &Authorization{ID: id, Status: StatusValid}, nil
},
MockCreateCertificate: func(ctx context.Context, cert *Certificate) error {
assert.Equals(t, cert.AccountID, o.AccountID)
assert.Equals(t, cert.OrderID, o.ID)
assert.Equals(t, cert.Leaf, foo)
assert.Equals(t, cert.Intermediates, []*x509.Certificate{bar, baz})
return errors.New("force")
},
},
err: NewErrorISE("error creating certificate for order oID: force"),
}
},
"fail/error-db.UpdateOrder": func(t *testing.T) test {
now := clock.Now()
o := &Order{
ID: "oID",
AccountID: "accID",
Status: StatusReady,
ExpiresAt: now.Add(5 * time.Minute),
AuthorizationIDs: []string{"a", "b"},
Identifiers: []Identifier{
{Type: "dns", Value: "foo.internal"},
{Type: "dns", Value: "bar.internal"},
},
}
csr := &x509.CertificateRequest{
Subject: pkix.Name{
CommonName: "foo.internal",
},
DNSNames: []string{"bar.internal"},
}
foo := &x509.Certificate{Subject: pkix.Name{CommonName: "foo"}}
bar := &x509.Certificate{Subject: pkix.Name{CommonName: "bar"}}
baz := &x509.Certificate{Subject: pkix.Name{CommonName: "baz"}}
return test{
o: o,
csr: csr,
prov: &MockProvisioner{
MauthorizeSign: func(ctx context.Context, token string) ([]provisioner.SignOption, error) {
assert.Equals(t, token, "")
return nil, nil
},
MgetOptions: func() *provisioner.Options {
return nil
},
},
ca: &mockSignAuth{
sign: func(_csr *x509.CertificateRequest, signOpts provisioner.SignOptions, extraOpts ...provisioner.SignOption) ([]*x509.Certificate, error) {
assert.Equals(t, _csr, csr)
return []*x509.Certificate{foo, bar, baz}, nil
},
},
db: &MockDB{
MockGetAuthorization: func(ctx context.Context, id string) (*Authorization, error) {
return &Authorization{ID: id, Status: StatusValid}, nil
},
MockCreateCertificate: func(ctx context.Context, cert *Certificate) error {
cert.ID = "certID"
assert.Equals(t, cert.AccountID, o.AccountID)
assert.Equals(t, cert.OrderID, o.ID)
assert.Equals(t, cert.Leaf, foo)
assert.Equals(t, cert.Intermediates, []*x509.Certificate{bar, baz})
return nil
},
MockUpdateOrder: func(ctx context.Context, updo *Order) error {
assert.Equals(t, updo.CertificateID, "certID")
assert.Equals(t, updo.Status, StatusValid)
assert.Equals(t, updo.ID, o.ID)
assert.Equals(t, updo.AccountID, o.AccountID)
assert.Equals(t, updo.ExpiresAt, o.ExpiresAt)
assert.Equals(t, updo.AuthorizationIDs, o.AuthorizationIDs)
assert.Equals(t, updo.Identifiers, o.Identifiers)
return errors.New("force")
},
},
err: NewErrorISE("error updating order oID: force"),
}
},
"fail/csr-fingerprint": func(t *testing.T) test {
now := clock.Now()
o := &Order{
ID: "oID",
AccountID: "accID",
Status: StatusReady,
ExpiresAt: now.Add(5 * time.Minute),
AuthorizationIDs: []string{"a", "b"},
Identifiers: []Identifier{
{Type: "permanent-identifier", Value: "a-permanent-identifier"},
},
}
signer := mustSigner("EC", "P-256", 0)
csr := &x509.CertificateRequest{
Subject: pkix.Name{
CommonName: "a-permanent-identifier",
},
PublicKey: signer.Public(),
ExtraExtensions: []pkix.Extension{
{
Id: asn1.ObjectIdentifier{1, 3, 6, 1, 5, 5, 7, 8, 3},
Value: []byte("a-permanent-identifier"),
},
},
}
leaf := &x509.Certificate{
Subject: pkix.Name{CommonName: "a-permanent-identifier"},
PublicKey: signer.Public(),
ExtraExtensions: []pkix.Extension{
{
Id: asn1.ObjectIdentifier{1, 3, 6, 1, 5, 5, 7, 8, 3},
Value: []byte("a-permanent-identifier"),
},
},
}
inter := &x509.Certificate{Subject: pkix.Name{CommonName: "inter"}}
root := &x509.Certificate{Subject: pkix.Name{CommonName: "root"}}
return test{
o: o,
csr: csr,
prov: &MockProvisioner{
MauthorizeSign: func(ctx context.Context, token string) ([]provisioner.SignOption, error) {
assert.Equals(t, token, "")
return nil, nil
},
MgetOptions: func() *provisioner.Options {
return nil
},
},
ca: &mockSignAuth{
sign: func(_csr *x509.CertificateRequest, signOpts provisioner.SignOptions, extraOpts ...provisioner.SignOption) ([]*x509.Certificate, error) {
assert.Equals(t, _csr, csr)
return []*x509.Certificate{leaf, inter, root}, nil
},
},
db: &MockDB{
MockGetAuthorization: func(ctx context.Context, id string) (*Authorization, error) {
return &Authorization{
ID: id,
Fingerprint: "other-fingerprint",
Status: StatusValid,
}, nil
},
MockCreateCertificate: func(ctx context.Context, cert *Certificate) error {
cert.ID = "certID"
assert.Equals(t, cert.AccountID, o.AccountID)
assert.Equals(t, cert.OrderID, o.ID)
assert.Equals(t, cert.Leaf, leaf)
assert.Equals(t, cert.Intermediates, []*x509.Certificate{inter, root})
return nil
},
MockUpdateOrder: func(ctx context.Context, updo *Order) error {
assert.Equals(t, updo.CertificateID, "certID")
assert.Equals(t, updo.Status, StatusValid)
assert.Equals(t, updo.ID, o.ID)
assert.Equals(t, updo.AccountID, o.AccountID)
assert.Equals(t, updo.ExpiresAt, o.ExpiresAt)
assert.Equals(t, updo.AuthorizationIDs, o.AuthorizationIDs)
assert.Equals(t, updo.Identifiers, o.Identifiers)
return nil
},
},
err: NewError(ErrorUnauthorizedType, "order oID csr does not match the attested key"),
}
},
"ok/permanent-identifier": func(t *testing.T) test {
now := clock.Now()
o := &Order{
ID: "oID",
AccountID: "accID",
Status: StatusReady,
ExpiresAt: now.Add(5 * time.Minute),
AuthorizationIDs: []string{"a", "b"},
Identifiers: []Identifier{
{Type: "permanent-identifier", Value: "a-permanent-identifier"},
},
}
signer := mustSigner("EC", "P-256", 0)
fingerprint, err := keyutil.Fingerprint(signer.Public())
if err != nil {
t.Fatal(err)
}
csr := &x509.CertificateRequest{
Subject: pkix.Name{
CommonName: "a-permanent-identifier",
},
PublicKey: signer.Public(),
ExtraExtensions: []pkix.Extension{
{
Id: asn1.ObjectIdentifier{1, 3, 6, 1, 5, 5, 7, 8, 3},
Value: []byte("a-permanent-identifier"),
},
},
}
leaf := &x509.Certificate{
Subject: pkix.Name{CommonName: "a-permanent-identifier"},
PublicKey: signer.Public(),
ExtraExtensions: []pkix.Extension{
{
Id: asn1.ObjectIdentifier{1, 3, 6, 1, 5, 5, 7, 8, 3},
Value: []byte("a-permanent-identifier"),
},
},
}
inter := &x509.Certificate{Subject: pkix.Name{CommonName: "inter"}}
root := &x509.Certificate{Subject: pkix.Name{CommonName: "root"}}
return test{
o: o,
csr: csr,
prov: &MockProvisioner{
MauthorizeSign: func(ctx context.Context, token string) ([]provisioner.SignOption, error) {
assert.Equals(t, token, "")
return nil, nil
},
MgetOptions: func() *provisioner.Options {
return nil
},
},
ca: &mockSignAuth{
sign: func(_csr *x509.CertificateRequest, signOpts provisioner.SignOptions, extraOpts ...provisioner.SignOption) ([]*x509.Certificate, error) {
assert.Equals(t, _csr, csr)
return []*x509.Certificate{leaf, inter, root}, nil
},
},
db: &MockDB{
MockGetAuthorization: func(ctx context.Context, id string) (*Authorization, error) {
switch id {
case "a":
return &Authorization{
ID: id,
Status: StatusValid,
}, nil
case "b":
return &Authorization{
ID: id,
Fingerprint: fingerprint,
Status: StatusValid,
}, nil
default:
assert.FatalError(t, errors.Errorf("unexpected authorization %s", id))
return nil, errors.New("force")
}
},
MockCreateCertificate: func(ctx context.Context, cert *Certificate) error {
cert.ID = "certID"
assert.Equals(t, cert.AccountID, o.AccountID)
assert.Equals(t, cert.OrderID, o.ID)
assert.Equals(t, cert.Leaf, leaf)
assert.Equals(t, cert.Intermediates, []*x509.Certificate{inter, root})
return nil
},
MockUpdateOrder: func(ctx context.Context, updo *Order) error {
assert.Equals(t, updo.CertificateID, "certID")
assert.Equals(t, updo.Status, StatusValid)
assert.Equals(t, updo.ID, o.ID)
assert.Equals(t, updo.AccountID, o.AccountID)
assert.Equals(t, updo.ExpiresAt, o.ExpiresAt)
assert.Equals(t, updo.AuthorizationIDs, o.AuthorizationIDs)
assert.Equals(t, updo.Identifiers, o.Identifiers)
return nil
},
},
}
},
"ok/permanent-identifier-only": func(t *testing.T) test {
now := clock.Now()
o := &Order{
ID: "oID",
AccountID: "accID",
Status: StatusReady,
ExpiresAt: now.Add(5 * time.Minute),
AuthorizationIDs: []string{"a", "b"},
Identifiers: []Identifier{
{Type: "dns", Value: "foo.internal"},
{Type: "permanent-identifier", Value: "a-permanent-identifier"},
},
}
signer := mustSigner("EC", "P-256", 0)
fingerprint, err := keyutil.Fingerprint(signer.Public())
if err != nil {
t.Fatal(err)
}
csr := &x509.CertificateRequest{
Subject: pkix.Name{
CommonName: "a-permanent-identifier",
},
DNSNames: []string{"foo.internal"},
PublicKey: signer.Public(),
ExtraExtensions: []pkix.Extension{
{
Id: asn1.ObjectIdentifier{1, 3, 6, 1, 5, 5, 7, 8, 3},
Value: []byte("a-permanent-identifier"),
},
},
}
leaf := &x509.Certificate{
Subject: pkix.Name{CommonName: "a-permanent-identifier"},
PublicKey: signer.Public(),
ExtraExtensions: []pkix.Extension{
{
Id: asn1.ObjectIdentifier{1, 3, 6, 1, 5, 5, 7, 8, 3},
Value: []byte("a-permanent-identifier"),
},
},
}
inter := &x509.Certificate{Subject: pkix.Name{CommonName: "inter"}}
root := &x509.Certificate{Subject: pkix.Name{CommonName: "root"}}
return test{
o: o,
csr: csr,
prov: &MockProvisioner{
MauthorizeSign: func(ctx context.Context, token string) ([]provisioner.SignOption, error) {
assert.Equals(t, token, "")
return nil, nil
},
MgetOptions: func() *provisioner.Options {
return nil
},
},
// TODO(hs): we should work on making the mocks more realistic. Ideally, we should get rid of
// the mock entirely, relying on an instances of provisioner, authority and DB (possibly hardest), so
// that behavior of the tests is what an actual CA would do. We could gradually phase them out by
// using the mocking functions as a wrapper for actual test helpers generated per test case or per
// function that's tested.
ca: &mockSignAuth{
sign: func(_csr *x509.CertificateRequest, signOpts provisioner.SignOptions, extraOpts ...provisioner.SignOption) ([]*x509.Certificate, error) {
assert.Equals(t, _csr, csr)
return []*x509.Certificate{leaf, inter, root}, nil
},
},
db: &MockDB{
MockGetAuthorization: func(ctx context.Context, id string) (*Authorization, error) {
return &Authorization{
ID: id,
Fingerprint: fingerprint,
Status: StatusValid,
}, nil
},
MockCreateCertificate: func(ctx context.Context, cert *Certificate) error {
cert.ID = "certID"
assert.Equals(t, cert.AccountID, o.AccountID)
assert.Equals(t, cert.OrderID, o.ID)
assert.Equals(t, cert.Leaf, leaf)
assert.Equals(t, cert.Intermediates, []*x509.Certificate{inter, root})
return nil
},
MockUpdateOrder: func(ctx context.Context, updo *Order) error {
assert.Equals(t, updo.CertificateID, "certID")
assert.Equals(t, updo.Status, StatusValid)
assert.Equals(t, updo.ID, o.ID)
assert.Equals(t, updo.AccountID, o.AccountID)
assert.Equals(t, updo.ExpiresAt, o.ExpiresAt)
assert.Equals(t, updo.AuthorizationIDs, o.AuthorizationIDs)
assert.Equals(t, updo.Identifiers, o.Identifiers)
return nil
},
},
}
},
"ok/new-cert-dns": func(t *testing.T) test {
now := clock.Now()
o := &Order{
ID: "oID",
AccountID: "accID",
Status: StatusReady,
ExpiresAt: now.Add(5 * time.Minute),
AuthorizationIDs: []string{"a", "b"},
Identifiers: []Identifier{
{Type: "dns", Value: "foo.internal"},
{Type: "dns", Value: "bar.internal"},
},
}
csr := &x509.CertificateRequest{
Subject: pkix.Name{
CommonName: "foo.internal",
},
DNSNames: []string{"bar.internal"},
}
foo := &x509.Certificate{Subject: pkix.Name{CommonName: "foo"}}
bar := &x509.Certificate{Subject: pkix.Name{CommonName: "bar"}}
baz := &x509.Certificate{Subject: pkix.Name{CommonName: "baz"}}
return test{
o: o,
csr: csr,
prov: &MockProvisioner{
MauthorizeSign: func(ctx context.Context, token string) ([]provisioner.SignOption, error) {
assert.Equals(t, token, "")
return nil, nil
},
MgetOptions: func() *provisioner.Options {
return nil
},
},
ca: &mockSignAuth{
sign: func(_csr *x509.CertificateRequest, signOpts provisioner.SignOptions, extraOpts ...provisioner.SignOption) ([]*x509.Certificate, error) {
assert.Equals(t, _csr, csr)
return []*x509.Certificate{foo, bar, baz}, nil
},
},
db: &MockDB{
MockGetAuthorization: func(ctx context.Context, id string) (*Authorization, error) {
return &Authorization{ID: id, Status: StatusValid}, nil
},
MockCreateCertificate: func(ctx context.Context, cert *Certificate) error {
cert.ID = "certID"
assert.Equals(t, cert.AccountID, o.AccountID)
assert.Equals(t, cert.OrderID, o.ID)
assert.Equals(t, cert.Leaf, foo)
assert.Equals(t, cert.Intermediates, []*x509.Certificate{bar, baz})
return nil
},
MockUpdateOrder: func(ctx context.Context, updo *Order) error {
assert.Equals(t, updo.CertificateID, "certID")
assert.Equals(t, updo.Status, StatusValid)
assert.Equals(t, updo.ID, o.ID)
assert.Equals(t, updo.AccountID, o.AccountID)
assert.Equals(t, updo.ExpiresAt, o.ExpiresAt)
assert.Equals(t, updo.AuthorizationIDs, o.AuthorizationIDs)
assert.Equals(t, updo.Identifiers, o.Identifiers)
return nil
},
},
}
},
"ok/new-cert-ip": func(t *testing.T) test {
now := clock.Now()
o := &Order{
ID: "oID",
AccountID: "accID",
Status: StatusReady,
ExpiresAt: now.Add(5 * time.Minute),
AuthorizationIDs: []string{"a", "b"},
Identifiers: []Identifier{
{Type: "ip", Value: "192.168.42.42"},
{Type: "ip", Value: "192.168.43.42"},
},
}
csr := &x509.CertificateRequest{
IPAddresses: []net.IP{net.ParseIP("192.168.42.42"), net.ParseIP("192.168.43.42")}, // in case of IPs, no Common Name
}
foo := &x509.Certificate{Subject: pkix.Name{CommonName: "foo"}}
bar := &x509.Certificate{Subject: pkix.Name{CommonName: "bar"}}
baz := &x509.Certificate{Subject: pkix.Name{CommonName: "baz"}}
return test{
o: o,
csr: csr,
prov: &MockProvisioner{
MauthorizeSign: func(ctx context.Context, token string) ([]provisioner.SignOption, error) {
assert.Equals(t, token, "")
return nil, nil
},
MgetOptions: func() *provisioner.Options {
return nil
},
},
ca: &mockSignAuth{
sign: func(_csr *x509.CertificateRequest, signOpts provisioner.SignOptions, extraOpts ...provisioner.SignOption) ([]*x509.Certificate, error) {
assert.Equals(t, _csr, csr)
return []*x509.Certificate{foo, bar, baz}, nil
},
},
db: &MockDB{
MockGetAuthorization: func(ctx context.Context, id string) (*Authorization, error) {
return &Authorization{ID: id, Status: StatusValid}, nil
},
MockCreateCertificate: func(ctx context.Context, cert *Certificate) error {
cert.ID = "certID"
assert.Equals(t, cert.AccountID, o.AccountID)
assert.Equals(t, cert.OrderID, o.ID)
assert.Equals(t, cert.Leaf, foo)
assert.Equals(t, cert.Intermediates, []*x509.Certificate{bar, baz})
return nil
},
MockUpdateOrder: func(ctx context.Context, updo *Order) error {
assert.Equals(t, updo.CertificateID, "certID")
assert.Equals(t, updo.Status, StatusValid)
assert.Equals(t, updo.ID, o.ID)
assert.Equals(t, updo.AccountID, o.AccountID)
assert.Equals(t, updo.ExpiresAt, o.ExpiresAt)
assert.Equals(t, updo.AuthorizationIDs, o.AuthorizationIDs)
assert.Equals(t, updo.Identifiers, o.Identifiers)
return nil
},
},
}
},
"ok/new-cert-dns-and-ip": func(t *testing.T) test {
now := clock.Now()
o := &Order{
ID: "oID",
AccountID: "accID",
Status: StatusReady,
ExpiresAt: now.Add(5 * time.Minute),
AuthorizationIDs: []string{"a", "b"},
Identifiers: []Identifier{
{Type: "dns", Value: "foo.internal"},
{Type: "ip", Value: "192.168.42.42"},
},
}
csr := &x509.CertificateRequest{
Subject: pkix.Name{
CommonName: "foo.internal",
},
IPAddresses: []net.IP{net.ParseIP("192.168.42.42")},
}
foo := &x509.Certificate{Subject: pkix.Name{CommonName: "foo"}}
bar := &x509.Certificate{Subject: pkix.Name{CommonName: "bar"}}
baz := &x509.Certificate{Subject: pkix.Name{CommonName: "baz"}}
return test{
o: o,
csr: csr,
prov: &MockProvisioner{
MauthorizeSign: func(ctx context.Context, token string) ([]provisioner.SignOption, error) {
assert.Equals(t, token, "")
return nil, nil
},
MgetOptions: func() *provisioner.Options {
return nil
},
},
ca: &mockSignAuth{
sign: func(_csr *x509.CertificateRequest, signOpts provisioner.SignOptions, extraOpts ...provisioner.SignOption) ([]*x509.Certificate, error) {
assert.Equals(t, _csr, csr)
return []*x509.Certificate{foo, bar, baz}, nil
},
},
db: &MockDB{
MockGetAuthorization: func(ctx context.Context, id string) (*Authorization, error) {
return &Authorization{ID: id, Status: StatusValid}, nil
},
MockCreateCertificate: func(ctx context.Context, cert *Certificate) error {
cert.ID = "certID"
assert.Equals(t, cert.AccountID, o.AccountID)
assert.Equals(t, cert.OrderID, o.ID)
assert.Equals(t, cert.Leaf, foo)
assert.Equals(t, cert.Intermediates, []*x509.Certificate{bar, baz})
return nil
},
MockUpdateOrder: func(ctx context.Context, updo *Order) error {
assert.Equals(t, updo.CertificateID, "certID")
assert.Equals(t, updo.Status, StatusValid)
assert.Equals(t, updo.ID, o.ID)
assert.Equals(t, updo.AccountID, o.AccountID)
assert.Equals(t, updo.ExpiresAt, o.ExpiresAt)
assert.Equals(t, updo.AuthorizationIDs, o.AuthorizationIDs)
assert.Equals(t, updo.Identifiers, o.Identifiers)
return nil
},
},
}
},
}
for name, run := range tests {
t.Run(name, func(t *testing.T) {
tc := run(t)
if err := tc.o.Finalize(context.Background(), tc.db, tc.csr, tc.ca, tc.prov); err != nil {
if assert.NotNil(t, tc.err) {
var k *Error
if errors.As(err, &k) {
assert.Equals(t, k.Type, tc.err.Type)
assert.Equals(t, k.Detail, tc.err.Detail)
assert.Equals(t, k.Status, tc.err.Status)
assert.Equals(t, k.Err.Error(), tc.err.Err.Error())
assert.Equals(t, k.Detail, tc.err.Detail)
} else {
assert.FatalError(t, errors.New("unexpected error type"))
}
}
} else {
assert.Nil(t, tc.err)
}
})
}
}
func Test_uniqueSortedIPs(t *testing.T) {
type args struct {
ips []net.IP
}
tests := []struct {
name string
args args
want []net.IP
}{
{
name: "ok/empty",
args: args{
ips: []net.IP{},
},
want: []net.IP{},
},
{
name: "ok/single-ipv4",
args: args{
ips: []net.IP{net.ParseIP("192.168.42.42")},
},
want: []net.IP{net.ParseIP("192.168.42.42")},
},
{
name: "ok/multiple-ipv4",
args: args{
ips: []net.IP{net.ParseIP("192.168.42.42"), net.ParseIP("192.168.42.10"), net.ParseIP("192.168.42.1"), net.ParseIP("127.0.0.1")},
},
want: []net.IP{net.ParseIP("127.0.0.1"), net.ParseIP("192.168.42.1"), net.ParseIP("192.168.42.10"), net.ParseIP("192.168.42.42")},
}, {
name: "ok/multiple-ipv4-with-varying-byte-representations",
args: args{
ips: []net.IP{net.ParseIP("192.168.42.42"), net.ParseIP("192.168.42.10"), net.ParseIP("192.168.42.1"), []byte{0x7f, 0x0, 0x0, 0x1}},
},
want: []net.IP{net.ParseIP("127.0.0.1"), net.ParseIP("192.168.42.1"), net.ParseIP("192.168.42.10"), net.ParseIP("192.168.42.42")},
},
{
name: "ok/unique-ipv4",
args: args{
ips: []net.IP{net.ParseIP("192.168.42.42"), net.ParseIP("192.168.42.42")},
},
want: []net.IP{net.ParseIP("192.168.42.42")},
},
{
name: "ok/single-ipv6",
args: args{
ips: []net.IP{net.ParseIP("2001:db8::30")},
},
want: []net.IP{net.ParseIP("2001:db8::30")},
},
{
name: "ok/multiple-ipv6",
args: args{
ips: []net.IP{net.ParseIP("2001:db8::30"), net.ParseIP("2001:db8::20"), net.ParseIP("2001:db8::10")},
},
want: []net.IP{net.ParseIP("2001:db8::10"), net.ParseIP("2001:db8::20"), net.ParseIP("2001:db8::30")},
},
{
name: "ok/unique-ipv6",
args: args{
ips: []net.IP{net.ParseIP("2001:db8::1"), net.ParseIP("2001:db8::1")},
},
want: []net.IP{net.ParseIP("2001:db8::1")},
},
{
name: "ok/mixed-ipv4-and-ipv6",
args: args{
ips: []net.IP{net.ParseIP("2001:db8::1"), net.ParseIP("2001:db8::1"), net.ParseIP("192.168.42.42"), net.ParseIP("192.168.42.42")},
},
want: []net.IP{net.ParseIP("192.168.42.42"), net.ParseIP("2001:db8::1")},
},
{
name: "ok/mixed-ipv4-and-ipv6-and-varying-byte-representations",
args: args{
ips: []net.IP{net.ParseIP("2001:db8::1"), net.ParseIP("2001:db8::1"), net.ParseIP("192.168.42.42"), net.ParseIP("192.168.42.42"), []byte{0x7f, 0x0, 0x0, 0x1}},
},
want: []net.IP{net.ParseIP("127.0.0.1"), net.ParseIP("192.168.42.42"), net.ParseIP("2001:db8::1")},
},
{
name: "ok/mixed-ipv4-and-ipv6-and-more-varying-byte-representations",
args: args{
ips: []net.IP{net.ParseIP("2001:db8::1"), net.ParseIP("2001:db8::1"), net.ParseIP("192.168.42.42"), net.ParseIP("2001:db8::2"), net.ParseIP("192.168.42.42"), []byte{0x7f, 0x0, 0x0, 0x1}, []byte{0x7f, 0x0, 0x0, 0x1}, []byte{0x7f, 0x0, 0x0, 0x2}},
},
want: []net.IP{net.ParseIP("127.0.0.1"), net.ParseIP("127.0.0.2"), net.ParseIP("192.168.42.42"), net.ParseIP("2001:db8::1"), net.ParseIP("2001:db8::2")},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := uniqueSortedIPs(tt.args.ips)
if !cmp.Equal(tt.want, got) {
t.Errorf("uniqueSortedIPs() diff =\n%s", cmp.Diff(tt.want, got))
}
})
}
}
func Test_numberOfIdentifierType(t *testing.T) {
type args struct {
typ IdentifierType
ids []Identifier
}
tests := []struct {
name string
args args
want int
}{
{
name: "ok/no-identifiers",
args: args{
typ: DNS,
ids: []Identifier{},
},
want: 0,
},
{
name: "ok/no-dns",
args: args{
typ: DNS,
ids: []Identifier{
{
Type: IP,
Value: "192.168.42.42",
},
},
},
want: 0,
},
{
name: "ok/no-ips",
args: args{
typ: IP,
ids: []Identifier{
{
Type: DNS,
Value: "example.com",
},
},
},
want: 0,
},
{
name: "ok/one-dns",
args: args{
typ: DNS,
ids: []Identifier{
{
Type: DNS,
Value: "example.com",
},
{
Type: IP,
Value: "192.168.42.42",
},
},
},
want: 1,
},
{
name: "ok/one-ip",
args: args{
typ: IP,
ids: []Identifier{
{
Type: DNS,
Value: "example.com",
},
{
Type: IP,
Value: "192.168.42.42",
},
},
},
want: 1,
},
{
name: "ok/more-dns",
args: args{
typ: DNS,
ids: []Identifier{
{
Type: DNS,
Value: "example.com",
},
{
Type: DNS,
Value: "*.example.com",
},
{
Type: IP,
Value: "192.168.42.42",
},
},
},
want: 2,
},
{
name: "ok/more-ips",
args: args{
typ: IP,
ids: []Identifier{
{
Type: DNS,
Value: "example.com",
},
{
Type: IP,
Value: "192.168.42.42",
},
{
Type: IP,
Value: "192.168.42.43",
},
},
},
want: 2,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := numberOfIdentifierType(tt.args.typ, tt.args.ids); got != tt.want {
t.Errorf("numberOfIdentifierType() = %v, want %v", got, tt.want)
}
})
}
}
func Test_ipsAreEqual(t *testing.T) {
type args struct {
x net.IP
y net.IP
}
tests := []struct {
name string
args args
want bool
}{
{
name: "ok/ipv4",
args: args{
x: net.ParseIP("192.168.42.42"),
y: net.ParseIP("192.168.42.42"),
},
want: true,
},
{
name: "fail/ipv4",
args: args{
x: net.ParseIP("192.168.42.42"),
y: net.ParseIP("192.168.42.43"),
},
want: false,
},
{
name: "ok/ipv6",
args: args{
x: net.ParseIP("2001:0db8:85a3:0000:0000:8a2e:0370:7334"),
y: net.ParseIP("2001:0db8:85a3:0000:0000:8a2e:0370:7334"),
},
want: true,
},
{
name: "fail/ipv6",
args: args{
x: net.ParseIP("2001:0db8:85a3:0000:0000:8a2e:0370:7334"),
y: net.ParseIP("2001:0db8:85a3:0000:0000:8a2e:0370:7335"),
},
want: false,
},
{
name: "fail/ipv4-and-ipv6",
args: args{
x: net.ParseIP("192.168.42.42"),
y: net.ParseIP("2001:0db8:85a3:0000:0000:8a2e:0370:7334"),
},
want: false,
},
{
name: "ok/ipv4-mapped-to-ipv6",
args: args{
x: net.ParseIP("192.168.42.42"),
y: net.ParseIP("::ffff:192.168.42.42"), // parsed to the same IPv4 by Go
},
want: true, // we expect this to happen; a known issue in which ipv4 mapped ipv6 addresses are considered the same as their ipv4 counterpart
},
{
name: "fail/invalid-ipv4-and-valid-ipv6",
args: args{
x: net.ParseIP("192.168.42.1000"),
y: net.ParseIP("2001:0db8:85a3:0000:0000:8a2e:0370:7334"),
},
want: false,
},
{
name: "fail/valid-ipv4-and-invalid-ipv6",
args: args{
x: net.ParseIP("192.168.42.42"),
y: net.ParseIP("2001:0db8:85a3:0000:0000:8a2e:0370:733400"),
},
want: false,
},
{
name: "fail/invalid-ipv4-and-invalid-ipv6",
args: args{
x: net.ParseIP("192.168.42.1000"),
y: net.ParseIP("2001:0db8:85a3:0000:0000:8a2e:0370:1000000"),
},
want: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := ipsAreEqual(tt.args.x, tt.args.y); got != tt.want {
t.Errorf("ipsAreEqual() = %v, want %v", got, tt.want)
}
})
}
}
func Test_canonicalize(t *testing.T) {
type args struct {
csr *x509.CertificateRequest
}
tests := []struct {
name string
args args
want *x509.CertificateRequest
}{
{
name: "ok/dns",
args: args{
csr: &x509.CertificateRequest{
DNSNames: []string{"www.example.com", "example.com"},
},
},
want: &x509.CertificateRequest{
DNSNames: []string{"example.com", "www.example.com"},
IPAddresses: []net.IP{},
},
},
{
name: "ok/common-name",
args: args{
csr: &x509.CertificateRequest{
Subject: pkix.Name{
CommonName: "example.com",
},
DNSNames: []string{"www.example.com"},
},
},
want: &x509.CertificateRequest{
Subject: pkix.Name{
CommonName: "example.com",
},
DNSNames: []string{"example.com", "www.example.com"},
IPAddresses: []net.IP{},
},
},
{
name: "ok/ipv4",
args: args{
csr: &x509.CertificateRequest{
IPAddresses: []net.IP{net.ParseIP("192.168.43.42"), net.ParseIP("192.168.42.42")},
},
},
want: &x509.CertificateRequest{
DNSNames: []string{},
IPAddresses: []net.IP{net.ParseIP("192.168.42.42"), net.ParseIP("192.168.43.42")},
},
},
{
name: "ok/mixed",
args: args{
csr: &x509.CertificateRequest{
DNSNames: []string{"www.example.com", "example.com"},
IPAddresses: []net.IP{net.ParseIP("192.168.43.42"), net.ParseIP("192.168.42.42")},
},
},
want: &x509.CertificateRequest{
DNSNames: []string{"example.com", "www.example.com"},
IPAddresses: []net.IP{net.ParseIP("192.168.42.42"), net.ParseIP("192.168.43.42")},
},
},
{
name: "ok/mixed-common-name",
args: args{
csr: &x509.CertificateRequest{
Subject: pkix.Name{
CommonName: "example.com",
},
DNSNames: []string{"www.example.com"},
IPAddresses: []net.IP{net.ParseIP("192.168.43.42"), net.ParseIP("192.168.42.42")},
},
},
want: &x509.CertificateRequest{
Subject: pkix.Name{
CommonName: "example.com",
},
DNSNames: []string{"example.com", "www.example.com"},
IPAddresses: []net.IP{net.ParseIP("192.168.42.42"), net.ParseIP("192.168.43.42")},
},
},
{
name: "ok/ip-common-name",
args: args{
csr: &x509.CertificateRequest{
Subject: pkix.Name{
CommonName: "127.0.0.1",
},
DNSNames: []string{"example.com"},
IPAddresses: []net.IP{net.ParseIP("192.168.43.42"), net.ParseIP("192.168.42.42")},
},
},
want: &x509.CertificateRequest{
Subject: pkix.Name{
CommonName: "127.0.0.1",
},
DNSNames: []string{"example.com"},
IPAddresses: []net.IP{net.ParseIP("127.0.0.1"), net.ParseIP("192.168.42.42"), net.ParseIP("192.168.43.42")},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := canonicalize(tt.args.csr)
if !cmp.Equal(tt.want, got) {
t.Errorf("canonicalize() diff =\n%s", cmp.Diff(tt.want, got))
}
})
}
}
func TestOrder_sans(t *testing.T) {
type fields struct {
Identifiers []Identifier
}
tests := []struct {
name string
fields fields
csr *x509.CertificateRequest
want []x509util.SubjectAlternativeName
err *Error
}{
{
name: "ok/dns",
fields: fields{
Identifiers: []Identifier{
{Type: "dns", Value: "example.com"},
},
},
csr: &x509.CertificateRequest{
Subject: pkix.Name{
CommonName: "example.com",
},
},
want: []x509util.SubjectAlternativeName{
{Type: "dns", Value: "example.com"},
},
err: nil,
},
{
name: "fail/invalid-alternative-name-email",
fields: fields{
Identifiers: []Identifier{},
},
csr: &x509.CertificateRequest{
Subject: pkix.Name{
CommonName: "foo.internal",
},
EmailAddresses: []string{"test@example.com"},
},
want: []x509util.SubjectAlternativeName{},
err: NewError(ErrorBadCSRType, "Only DNS names and IP addresses are allowed"),
},
{
name: "fail/invalid-alternative-name-uri",
fields: fields{
Identifiers: []Identifier{},
},
csr: &x509.CertificateRequest{
Subject: pkix.Name{
CommonName: "foo.internal",
},
URIs: []*url.URL{
{
Scheme: "https://",
Host: "smallstep.com",
},
},
},
want: []x509util.SubjectAlternativeName{},
err: NewError(ErrorBadCSRType, "Only DNS names and IP addresses are allowed"),
},
{
name: "fail/error-names-length-mismatch",
fields: fields{
Identifiers: []Identifier{
{Type: "dns", Value: "foo.internal"},
{Type: "dns", Value: "bar.internal"},
},
},
csr: &x509.CertificateRequest{
Subject: pkix.Name{
CommonName: "foo.internal",
},
},
want: []x509util.SubjectAlternativeName{},
err: NewError(ErrorBadCSRType, "CSR names do not match identifiers exactly: "+
"CSR names = %v, Order names = %v", []string{"foo.internal"}, []string{"bar.internal", "foo.internal"}),
},
{
name: "fail/error-names-mismatch",
fields: fields{
Identifiers: []Identifier{
{Type: "dns", Value: "foo.internal"},
{Type: "dns", Value: "bar.internal"},
},
},
csr: &x509.CertificateRequest{
Subject: pkix.Name{
CommonName: "foo.internal",
},
DNSNames: []string{"zap.internal"},
},
want: []x509util.SubjectAlternativeName{},
err: NewError(ErrorBadCSRType, "CSR names do not match identifiers exactly: "+
"CSR names = %v, Order names = %v", []string{"foo.internal", "zap.internal"}, []string{"bar.internal", "foo.internal"}),
},
{
name: "ok/ipv4",
fields: fields{
Identifiers: []Identifier{
{Type: "ip", Value: "192.168.43.42"},
{Type: "ip", Value: "192.168.42.42"},
},
},
csr: &x509.CertificateRequest{
IPAddresses: []net.IP{net.ParseIP("192.168.43.42"), net.ParseIP("192.168.42.42")},
},
want: []x509util.SubjectAlternativeName{
{Type: "ip", Value: "192.168.42.42"},
{Type: "ip", Value: "192.168.43.42"},
},
err: nil,
},
{
name: "ok/ipv6",
fields: fields{
Identifiers: []Identifier{
{Type: "ip", Value: "2001:0db8:85a3::8a2e:0370:7335"},
{Type: "ip", Value: "2001:0db8:85a3::8a2e:0370:7334"},
},
},
csr: &x509.CertificateRequest{
IPAddresses: []net.IP{net.ParseIP("2001:0db8:85a3:0000:0000:8a2e:0370:7335"), net.ParseIP("2001:0db8:85a3:0000:0000:8a2e:0370:7334")},
},
want: []x509util.SubjectAlternativeName{
{Type: "ip", Value: "2001:db8:85a3::8a2e:370:7334"},
{Type: "ip", Value: "2001:db8:85a3::8a2e:370:7335"},
},
err: nil,
},
{
name: "fail/error-ips-length-mismatch",
fields: fields{
Identifiers: []Identifier{
{Type: "ip", Value: "192.168.42.42"},
{Type: "ip", Value: "192.168.43.42"},
},
},
csr: &x509.CertificateRequest{
IPAddresses: []net.IP{net.ParseIP("192.168.42.42")},
},
want: []x509util.SubjectAlternativeName{},
err: NewError(ErrorBadCSRType, "CSR IPs do not match identifiers exactly: "+
"CSR IPs = %v, Order IPs = %v", []net.IP{net.ParseIP("192.168.42.42")}, []net.IP{net.ParseIP("192.168.42.42"), net.ParseIP("192.168.43.42")}),
},
{
name: "fail/error-ips-mismatch",
fields: fields{
Identifiers: []Identifier{
{Type: "ip", Value: "192.168.42.42"},
{Type: "ip", Value: "192.168.43.42"},
},
},
csr: &x509.CertificateRequest{
IPAddresses: []net.IP{net.ParseIP("192.168.42.42"), net.ParseIP("192.168.42.32")},
},
want: []x509util.SubjectAlternativeName{},
err: NewError(ErrorBadCSRType, "CSR IPs do not match identifiers exactly: "+
"CSR IPs = %v, Order IPs = %v", []net.IP{net.ParseIP("192.168.42.32"), net.ParseIP("192.168.42.42")}, []net.IP{net.ParseIP("192.168.42.42"), net.ParseIP("192.168.43.42")}),
},
{
name: "ok/mixed",
fields: fields{
Identifiers: []Identifier{
{Type: "dns", Value: "foo.internal"},
{Type: "dns", Value: "bar.internal"},
{Type: "ip", Value: "192.168.43.42"},
{Type: "ip", Value: "192.168.42.42"},
{Type: "ip", Value: "2001:0db8:85a3:0000:0000:8a2e:0370:7334"},
},
},
csr: &x509.CertificateRequest{
Subject: pkix.Name{
CommonName: "bar.internal",
},
DNSNames: []string{"foo.internal"},
IPAddresses: []net.IP{net.ParseIP("192.168.43.42"), net.ParseIP("192.168.42.42"), net.ParseIP("2001:0db8:85a3:0000:0000:8a2e:0370:7334")},
},
want: []x509util.SubjectAlternativeName{
{Type: "dns", Value: "bar.internal"},
{Type: "dns", Value: "foo.internal"},
{Type: "ip", Value: "192.168.42.42"},
{Type: "ip", Value: "192.168.43.42"},
{Type: "ip", Value: "2001:db8:85a3::8a2e:370:7334"},
},
err: nil,
},
{
name: "fail/unsupported-identifier-type",
fields: fields{
Identifiers: []Identifier{
{Type: "ipv4", Value: "192.168.42.42"},
},
},
csr: &x509.CertificateRequest{
IPAddresses: []net.IP{net.ParseIP("192.168.42.42")},
},
want: []x509util.SubjectAlternativeName{},
err: NewError(ErrorServerInternalType, "unsupported identifier type in order: ipv4"),
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
o := &Order{
Identifiers: tt.fields.Identifiers,
}
canonicalizedCSR := canonicalize(tt.csr)
got, err := o.sans(canonicalizedCSR)
if tt.err != nil {
if err == nil {
t.Errorf("Order.sans() = %v, want error; got none", got)
return
}
var k *Error
if errors.As(err, &k) {
assert.Equals(t, k.Type, tt.err.Type)
assert.Equals(t, k.Detail, tt.err.Detail)
assert.Equals(t, k.Status, tt.err.Status)
assert.Equals(t, k.Err.Error(), tt.err.Err.Error())
assert.Equals(t, k.Detail, tt.err.Detail)
} else {
assert.FatalError(t, errors.New("unexpected error type"))
}
return
}
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("Order.sans() = %v, want %v", got, tt.want)
}
})
}
}
func TestOrder_getAuthorizationFingerprint(t *testing.T) {
ctx := context.Background()
type fields struct {
AuthorizationIDs []string
}
type args struct {
ctx context.Context
db DB
}
tests := []struct {
name string
fields fields
args args
want string
wantErr bool
}{
{"ok", fields{[]string{"az1", "az2"}}, args{ctx, &MockDB{
MockGetAuthorization: func(ctx context.Context, id string) (*Authorization, error) {
return &Authorization{ID: id, Status: StatusValid}, nil
},
}}, "", false},
{"ok fingerprint", fields{[]string{"az1", "az2"}}, args{ctx, &MockDB{
MockGetAuthorization: func(ctx context.Context, id string) (*Authorization, error) {
if id == "az1" {
return &Authorization{ID: id, Status: StatusValid}, nil
}
return &Authorization{ID: id, Fingerprint: "fingerprint", Status: StatusValid}, nil
},
}}, "fingerprint", false},
{"fail", fields{[]string{"az1", "az2"}}, args{ctx, &MockDB{
MockGetAuthorization: func(ctx context.Context, id string) (*Authorization, error) {
return nil, errors.New("force")
},
}}, "", true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
o := &Order{
AuthorizationIDs: tt.fields.AuthorizationIDs,
}
got, err := o.getAuthorizationFingerprint(tt.args.ctx, tt.args.db)
if (err != nil) != tt.wantErr {
t.Errorf("Order.getAuthorizationFingerprint() error = %v, wantErr %v", err, tt.wantErr)
return
}
if got != tt.want {
t.Errorf("Order.getAuthorizationFingerprint() = %v, want %v", got, tt.want)
}
})
}
}