github.com/letsencrypt/boulder@v0.20251208.0/identifier/identifier_test.go (about)

     1  package identifier
     2  
     3  import (
     4  	"crypto/x509"
     5  	"crypto/x509/pkix"
     6  	"net"
     7  	"net/netip"
     8  	"reflect"
     9  	"slices"
    10  	"testing"
    11  )
    12  
    13  func TestNewIP(t *testing.T) {
    14  	cases := []struct {
    15  		name string
    16  		ip   netip.Addr
    17  		want ACMEIdentifier
    18  	}{
    19  		{
    20  			name: "IPv4 address",
    21  			ip:   netip.MustParseAddr("9.9.9.9"),
    22  			want: ACMEIdentifier{Type: TypeIP, Value: "9.9.9.9"},
    23  		},
    24  		{
    25  			name: "IPv6 address",
    26  			ip:   netip.MustParseAddr("fe80::cafe"),
    27  			want: ACMEIdentifier{Type: TypeIP, Value: "fe80::cafe"},
    28  		},
    29  		{
    30  			name: "IPv6 address with scope zone",
    31  			ip:   netip.MustParseAddr("fe80::cafe%lo"),
    32  			want: ACMEIdentifier{Type: TypeIP, Value: "fe80::cafe"},
    33  		},
    34  	}
    35  	for _, tc := range cases {
    36  		t.Run(tc.name, func(t *testing.T) {
    37  			t.Parallel()
    38  			got := NewIP(tc.ip)
    39  			if got != tc.want {
    40  				t.Errorf("NewIP(%#v) = %#v, but want %#v", tc.ip, got, tc.want)
    41  			}
    42  		})
    43  	}
    44  }
    45  
    46  // TestFromX509 tests FromCert and FromCSR, which are fromX509's public
    47  // wrappers.
    48  func TestFromX509(t *testing.T) {
    49  	cases := []struct {
    50  		name        string
    51  		subject     pkix.Name
    52  		dnsNames    []string
    53  		ipAddresses []net.IP
    54  		want        ACMEIdentifiers
    55  	}{
    56  		{
    57  			name:     "no explicit CN",
    58  			dnsNames: []string{"a.com"},
    59  			want:     ACMEIdentifiers{NewDNS("a.com")},
    60  		},
    61  		{
    62  			name:     "explicit uppercase CN",
    63  			subject:  pkix.Name{CommonName: "A.com"},
    64  			dnsNames: []string{"a.com"},
    65  			want:     ACMEIdentifiers{NewDNS("a.com")},
    66  		},
    67  		{
    68  			name:     "no explicit CN, uppercase SAN",
    69  			dnsNames: []string{"A.com"},
    70  			want:     ACMEIdentifiers{NewDNS("a.com")},
    71  		},
    72  		{
    73  			name:     "duplicate SANs",
    74  			dnsNames: []string{"b.com", "b.com", "a.com", "a.com"},
    75  			want:     ACMEIdentifiers{NewDNS("a.com"), NewDNS("b.com")},
    76  		},
    77  		{
    78  			name:     "explicit CN not found in SANs",
    79  			subject:  pkix.Name{CommonName: "a.com"},
    80  			dnsNames: []string{"b.com"},
    81  			want:     ACMEIdentifiers{NewDNS("a.com"), NewDNS("b.com")},
    82  		},
    83  		{
    84  			name:        "mix of DNSNames and IPAddresses",
    85  			dnsNames:    []string{"a.com"},
    86  			ipAddresses: []net.IP{{192, 168, 1, 1}},
    87  			want:        ACMEIdentifiers{NewDNS("a.com"), NewIP(netip.MustParseAddr("192.168.1.1"))},
    88  		},
    89  	}
    90  	for _, tc := range cases {
    91  		t.Run("cert/"+tc.name, func(t *testing.T) {
    92  			t.Parallel()
    93  			got := FromCert(&x509.Certificate{Subject: tc.subject, DNSNames: tc.dnsNames, IPAddresses: tc.ipAddresses})
    94  			if !slices.Equal(got, tc.want) {
    95  				t.Errorf("FromCert() got %#v, but want %#v", got, tc.want)
    96  			}
    97  		})
    98  		t.Run("csr/"+tc.name, func(t *testing.T) {
    99  			t.Parallel()
   100  			got := FromCSR(&x509.CertificateRequest{Subject: tc.subject, DNSNames: tc.dnsNames, IPAddresses: tc.ipAddresses})
   101  			if !slices.Equal(got, tc.want) {
   102  				t.Errorf("FromCSR() got %#v, but want %#v", got, tc.want)
   103  			}
   104  		})
   105  	}
   106  }
   107  
   108  func TestNormalize(t *testing.T) {
   109  	cases := []struct {
   110  		name   string
   111  		idents ACMEIdentifiers
   112  		want   ACMEIdentifiers
   113  	}{
   114  		{
   115  			name: "convert to lowercase",
   116  			idents: ACMEIdentifiers{
   117  				{Type: TypeDNS, Value: "AlPha.example.coM"},
   118  				{Type: TypeIP, Value: "fe80::CAFE"},
   119  			},
   120  			want: ACMEIdentifiers{
   121  				{Type: TypeDNS, Value: "alpha.example.com"},
   122  				{Type: TypeIP, Value: "fe80::cafe"},
   123  			},
   124  		},
   125  		{
   126  			name: "sort",
   127  			idents: ACMEIdentifiers{
   128  				{Type: TypeDNS, Value: "foobar.com"},
   129  				{Type: TypeDNS, Value: "bar.com"},
   130  				{Type: TypeDNS, Value: "baz.com"},
   131  				{Type: TypeDNS, Value: "a.com"},
   132  				{Type: TypeIP, Value: "fe80::cafe"},
   133  				{Type: TypeIP, Value: "2001:db8::1dea"},
   134  				{Type: TypeIP, Value: "192.168.1.1"},
   135  			},
   136  			want: ACMEIdentifiers{
   137  				{Type: TypeDNS, Value: "a.com"},
   138  				{Type: TypeDNS, Value: "bar.com"},
   139  				{Type: TypeDNS, Value: "baz.com"},
   140  				{Type: TypeDNS, Value: "foobar.com"},
   141  				{Type: TypeIP, Value: "192.168.1.1"},
   142  				{Type: TypeIP, Value: "2001:db8::1dea"},
   143  				{Type: TypeIP, Value: "fe80::cafe"},
   144  			},
   145  		},
   146  		{
   147  			name: "de-duplicate",
   148  			idents: ACMEIdentifiers{
   149  				{Type: TypeDNS, Value: "AlPha.example.coM"},
   150  				{Type: TypeIP, Value: "fe80::CAFE"},
   151  				{Type: TypeDNS, Value: "alpha.example.com"},
   152  				{Type: TypeIP, Value: "fe80::cafe"},
   153  				NewIP(netip.MustParseAddr("fe80:0000:0000:0000:0000:0000:0000:cafe")),
   154  			},
   155  			want: ACMEIdentifiers{
   156  				{Type: TypeDNS, Value: "alpha.example.com"},
   157  				{Type: TypeIP, Value: "fe80::cafe"},
   158  			},
   159  		},
   160  		{
   161  			name: "DNS before IP",
   162  			idents: ACMEIdentifiers{
   163  				{Type: TypeIP, Value: "fe80::cafe"},
   164  				{Type: TypeDNS, Value: "alpha.example.com"},
   165  			},
   166  			want: ACMEIdentifiers{
   167  				{Type: TypeDNS, Value: "alpha.example.com"},
   168  				{Type: TypeIP, Value: "fe80::cafe"},
   169  			},
   170  		},
   171  	}
   172  	for _, tc := range cases {
   173  		t.Run(tc.name, func(t *testing.T) {
   174  			t.Parallel()
   175  			got := Normalize(tc.idents)
   176  			if !slices.Equal(got, tc.want) {
   177  				t.Errorf("Got %#v, but want %#v", got, tc.want)
   178  			}
   179  		})
   180  	}
   181  }
   182  
   183  func TestToValues(t *testing.T) {
   184  	cases := []struct {
   185  		name            string
   186  		idents          ACMEIdentifiers
   187  		wantErr         string
   188  		wantDnsNames    []string
   189  		wantIpAddresses []net.IP
   190  	}{
   191  		{
   192  			name: "DNS names and IP addresses",
   193  			// These are deliberately out of alphabetical and type order, to
   194  			// ensure ToValues doesn't do normalization, which ought to be done
   195  			// explicitly.
   196  			idents: ACMEIdentifiers{
   197  				{Type: TypeDNS, Value: "beta.example.com"},
   198  				{Type: TypeIP, Value: "fe80::cafe"},
   199  				{Type: TypeDNS, Value: "alpha.example.com"},
   200  				{Type: TypeIP, Value: "127.0.0.1"},
   201  			},
   202  			wantErr:         "",
   203  			wantDnsNames:    []string{"beta.example.com", "alpha.example.com"},
   204  			wantIpAddresses: []net.IP{net.ParseIP("fe80::cafe"), net.ParseIP("127.0.0.1")},
   205  		},
   206  		{
   207  			name: "DNS names only",
   208  			idents: ACMEIdentifiers{
   209  				{Type: TypeDNS, Value: "alpha.example.com"},
   210  				{Type: TypeDNS, Value: "beta.example.com"},
   211  			},
   212  			wantErr:         "",
   213  			wantDnsNames:    []string{"alpha.example.com", "beta.example.com"},
   214  			wantIpAddresses: nil,
   215  		},
   216  		{
   217  			name: "IP addresses only",
   218  			idents: ACMEIdentifiers{
   219  				{Type: TypeIP, Value: "127.0.0.1"},
   220  				{Type: TypeIP, Value: "fe80::cafe"},
   221  			},
   222  			wantErr:         "",
   223  			wantDnsNames:    nil,
   224  			wantIpAddresses: []net.IP{net.ParseIP("127.0.0.1"), net.ParseIP("fe80::cafe")},
   225  		},
   226  		{
   227  			name: "invalid IP address",
   228  			idents: ACMEIdentifiers{
   229  				{Type: TypeIP, Value: "fe80::c0ffee"},
   230  			},
   231  			wantErr:         "parsing IP address: fe80::c0ffee",
   232  			wantDnsNames:    nil,
   233  			wantIpAddresses: nil,
   234  		},
   235  		{
   236  			name: "invalid identifier type",
   237  			idents: ACMEIdentifiers{
   238  				{Type: "fnord", Value: "panic.example.com"},
   239  			},
   240  			wantErr:         "evaluating identifier type: fnord for panic.example.com",
   241  			wantDnsNames:    nil,
   242  			wantIpAddresses: nil,
   243  		},
   244  	}
   245  	for _, tc := range cases {
   246  		t.Run(tc.name, func(t *testing.T) {
   247  			t.Parallel()
   248  			gotDnsNames, gotIpAddresses, gotErr := tc.idents.ToValues()
   249  			if !slices.Equal(gotDnsNames, tc.wantDnsNames) {
   250  				t.Errorf("Got DNS names %#v, but want %#v", gotDnsNames, tc.wantDnsNames)
   251  			}
   252  			if !reflect.DeepEqual(gotIpAddresses, tc.wantIpAddresses) {
   253  				t.Errorf("Got IP addresses %#v, but want %#v", gotIpAddresses, tc.wantIpAddresses)
   254  			}
   255  			if tc.wantErr != "" && (gotErr.Error() != tc.wantErr) {
   256  				t.Errorf("Got error %#v, but want %#v", gotErr.Error(), tc.wantErr)
   257  			}
   258  			if tc.wantErr == "" && gotErr != nil {
   259  				t.Errorf("Got error %#v, but didn't want one", gotErr.Error())
   260  			}
   261  		})
   262  	}
   263  }