348 lines
9.3 KiB
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())
|
|
}
|
|
})
|
|
}
|
|
}
|