boulder/identifier/identifier_test.go

348 lines
9.3 KiB
Go

package identifier
import (
"crypto/x509"
"crypto/x509/pkix"
"net"
"net/netip"
"reflect"
"slices"
"testing"
corepb "github.com/letsencrypt/boulder/core/proto"
)
type withDefaultTestCases struct {
Name string
InputIdents []*corepb.Identifier
InputNames []string
want ACMEIdentifiers
}
func (tc withDefaultTestCases) GetIdentifiers() []*corepb.Identifier {
return tc.InputIdents
}
func (tc withDefaultTestCases) GetDnsNames() []string {
return tc.InputNames
}
func TestFromProtoSliceWithDefault(t *testing.T) {
testCases := []withDefaultTestCases{
{
Name: "Populated identifiers, populated names, same values",
InputIdents: []*corepb.Identifier{
{Type: "dns", Value: "a.example.com"},
{Type: "dns", Value: "b.example.com"},
},
InputNames: []string{"a.example.com", "b.example.com"},
want: ACMEIdentifiers{
{Type: TypeDNS, Value: "a.example.com"},
{Type: TypeDNS, Value: "b.example.com"},
},
},
{
Name: "Populated identifiers, populated names, different values",
InputIdents: []*corepb.Identifier{
{Type: "dns", Value: "coffee.example.com"},
},
InputNames: []string{"tea.example.com"},
want: ACMEIdentifiers{
{Type: TypeDNS, Value: "coffee.example.com"},
},
},
{
Name: "Populated identifiers, empty names",
InputIdents: []*corepb.Identifier{
{Type: "dns", Value: "example.com"},
},
InputNames: []string{},
want: ACMEIdentifiers{
{Type: TypeDNS, Value: "example.com"},
},
},
{
Name: "Populated identifiers, nil names",
InputIdents: []*corepb.Identifier{
{Type: "dns", Value: "example.com"},
},
InputNames: nil,
want: ACMEIdentifiers{
{Type: TypeDNS, Value: "example.com"},
},
},
{
Name: "Empty identifiers, populated names",
InputIdents: []*corepb.Identifier{},
InputNames: []string{"a.example.com", "b.example.com"},
want: ACMEIdentifiers{
{Type: TypeDNS, Value: "a.example.com"},
{Type: TypeDNS, Value: "b.example.com"},
},
},
{
Name: "Empty identifiers, empty names",
InputIdents: []*corepb.Identifier{},
InputNames: []string{},
want: nil,
},
{
Name: "Empty identifiers, nil names",
InputIdents: []*corepb.Identifier{},
InputNames: nil,
want: nil,
},
{
Name: "Nil identifiers, populated names",
InputIdents: nil,
InputNames: []string{"a.example.com", "b.example.com"},
want: ACMEIdentifiers{
{Type: TypeDNS, Value: "a.example.com"},
{Type: TypeDNS, Value: "b.example.com"},
},
},
{
Name: "Nil identifiers, empty names",
InputIdents: nil,
InputNames: []string{},
want: nil,
},
{
Name: "Nil identifiers, nil names",
InputIdents: nil,
InputNames: nil,
want: nil,
},
}
for _, tc := range testCases {
t.Run(tc.Name, func(t *testing.T) {
t.Parallel()
got := FromProtoSliceWithDefault(tc)
if !slices.Equal(got, tc.want) {
t.Errorf("Got %#v, but want %#v", got, tc.want)
}
})
}
}
// TestFromX509 tests FromCert and FromCSR, which are fromX509's public
// wrappers.
func TestFromX509(t *testing.T) {
cases := []struct {
name string
subject pkix.Name
dnsNames []string
ipAddresses []net.IP
want ACMEIdentifiers
}{
{
name: "no explicit CN",
dnsNames: []string{"a.com"},
want: ACMEIdentifiers{NewDNS("a.com")},
},
{
name: "explicit uppercase CN",
subject: pkix.Name{CommonName: "A.com"},
dnsNames: []string{"a.com"},
want: ACMEIdentifiers{NewDNS("a.com")},
},
{
name: "no explicit CN, uppercase SAN",
dnsNames: []string{"A.com"},
want: ACMEIdentifiers{NewDNS("a.com")},
},
{
name: "duplicate SANs",
dnsNames: []string{"b.com", "b.com", "a.com", "a.com"},
want: ACMEIdentifiers{NewDNS("a.com"), NewDNS("b.com")},
},
{
name: "explicit CN not found in SANs",
subject: pkix.Name{CommonName: "a.com"},
dnsNames: []string{"b.com"},
want: ACMEIdentifiers{NewDNS("a.com"), NewDNS("b.com")},
},
{
name: "mix of DNSNames and IPAddresses",
dnsNames: []string{"a.com"},
ipAddresses: []net.IP{{192, 168, 1, 1}},
want: ACMEIdentifiers{NewDNS("a.com"), NewIP(netip.MustParseAddr("192.168.1.1"))},
},
}
for _, tc := range cases {
t.Run("cert/"+tc.name, func(t *testing.T) {
t.Parallel()
got := FromCert(&x509.Certificate{Subject: tc.subject, DNSNames: tc.dnsNames, IPAddresses: tc.ipAddresses})
if !slices.Equal(got, tc.want) {
t.Errorf("FromCert() got %#v, but want %#v", got, tc.want)
}
})
t.Run("csr/"+tc.name, func(t *testing.T) {
t.Parallel()
got := FromCSR(&x509.CertificateRequest{Subject: tc.subject, DNSNames: tc.dnsNames, IPAddresses: tc.ipAddresses})
if !slices.Equal(got, tc.want) {
t.Errorf("FromCSR() got %#v, but want %#v", got, tc.want)
}
})
}
}
func TestNormalize(t *testing.T) {
cases := []struct {
name string
idents ACMEIdentifiers
want ACMEIdentifiers
}{
{
name: "convert to lowercase",
idents: ACMEIdentifiers{
{Type: TypeDNS, Value: "AlPha.example.coM"},
{Type: TypeIP, Value: "fe80::CAFE"},
},
want: ACMEIdentifiers{
{Type: TypeDNS, Value: "alpha.example.com"},
{Type: TypeIP, Value: "fe80::cafe"},
},
},
{
name: "sort",
idents: ACMEIdentifiers{
{Type: TypeDNS, Value: "foobar.com"},
{Type: TypeDNS, Value: "bar.com"},
{Type: TypeDNS, Value: "baz.com"},
{Type: TypeDNS, Value: "a.com"},
{Type: TypeIP, Value: "fe80::cafe"},
{Type: TypeIP, Value: "2001:db8::1dea"},
{Type: TypeIP, Value: "192.168.1.1"},
},
want: ACMEIdentifiers{
{Type: TypeDNS, Value: "a.com"},
{Type: TypeDNS, Value: "bar.com"},
{Type: TypeDNS, Value: "baz.com"},
{Type: TypeDNS, Value: "foobar.com"},
{Type: TypeIP, Value: "192.168.1.1"},
{Type: TypeIP, Value: "2001:db8::1dea"},
{Type: TypeIP, Value: "fe80::cafe"},
},
},
{
name: "de-duplicate",
idents: ACMEIdentifiers{
{Type: TypeDNS, Value: "AlPha.example.coM"},
{Type: TypeIP, Value: "fe80::CAFE"},
{Type: TypeDNS, Value: "alpha.example.com"},
{Type: TypeIP, Value: "fe80::cafe"},
NewIP(netip.MustParseAddr("fe80:0000:0000:0000:0000:0000:0000:cafe")),
},
want: ACMEIdentifiers{
{Type: TypeDNS, Value: "alpha.example.com"},
{Type: TypeIP, Value: "fe80::cafe"},
},
},
{
name: "DNS before IP",
idents: ACMEIdentifiers{
{Type: TypeIP, Value: "fe80::cafe"},
{Type: TypeDNS, Value: "alpha.example.com"},
},
want: ACMEIdentifiers{
{Type: TypeDNS, Value: "alpha.example.com"},
{Type: TypeIP, Value: "fe80::cafe"},
},
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
got := Normalize(tc.idents)
if !slices.Equal(got, tc.want) {
t.Errorf("Got %#v, but want %#v", got, tc.want)
}
})
}
}
func TestToValues(t *testing.T) {
cases := []struct {
name string
idents ACMEIdentifiers
wantErr string
wantDnsNames []string
wantIpAddresses []net.IP
}{
{
name: "DNS names and IP addresses",
// These are deliberately out of alphabetical and type order, to
// ensure ToValues doesn't do normalization, which ought to be done
// explicitly.
idents: ACMEIdentifiers{
{Type: TypeDNS, Value: "beta.example.com"},
{Type: TypeIP, Value: "fe80::cafe"},
{Type: TypeDNS, Value: "alpha.example.com"},
{Type: TypeIP, Value: "127.0.0.1"},
},
wantErr: "",
wantDnsNames: []string{"beta.example.com", "alpha.example.com"},
wantIpAddresses: []net.IP{net.ParseIP("fe80::cafe"), net.ParseIP("127.0.0.1")},
},
{
name: "DNS names only",
idents: ACMEIdentifiers{
{Type: TypeDNS, Value: "alpha.example.com"},
{Type: TypeDNS, Value: "beta.example.com"},
},
wantErr: "",
wantDnsNames: []string{"alpha.example.com", "beta.example.com"},
wantIpAddresses: nil,
},
{
name: "IP addresses only",
idents: ACMEIdentifiers{
{Type: TypeIP, Value: "127.0.0.1"},
{Type: TypeIP, Value: "fe80::cafe"},
},
wantErr: "",
wantDnsNames: nil,
wantIpAddresses: []net.IP{net.ParseIP("127.0.0.1"), net.ParseIP("fe80::cafe")},
},
{
name: "invalid IP address",
idents: ACMEIdentifiers{
{Type: TypeIP, Value: "fe80::c0ffee"},
},
wantErr: "parsing IP address: fe80::c0ffee",
wantDnsNames: nil,
wantIpAddresses: nil,
},
{
name: "invalid identifier type",
idents: ACMEIdentifiers{
{Type: "fnord", Value: "panic.example.com"},
},
wantErr: "evaluating identifier type: fnord for panic.example.com",
wantDnsNames: nil,
wantIpAddresses: nil,
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
gotDnsNames, gotIpAddresses, gotErr := tc.idents.ToValues()
if !slices.Equal(gotDnsNames, tc.wantDnsNames) {
t.Errorf("Got DNS names %#v, but want %#v", gotDnsNames, tc.wantDnsNames)
}
if !reflect.DeepEqual(gotIpAddresses, tc.wantIpAddresses) {
t.Errorf("Got IP addresses %#v, but want %#v", gotIpAddresses, tc.wantIpAddresses)
}
if tc.wantErr != "" && (gotErr.Error() != tc.wantErr) {
t.Errorf("Got error %#v, but want %#v", gotErr.Error(), tc.wantErr)
}
if tc.wantErr == "" && gotErr != nil {
t.Errorf("Got error %#v, but didn't want one", gotErr.Error())
}
})
}
}