Merge pull request #773 from smallstep/herman/ip-sans-improvements

Improve IP SANS support
This commit is contained in:
Herman Slatman 2021-12-20 19:56:55 +01:00 committed by GitHub
commit 8473164b41
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
2 changed files with 123 additions and 34 deletions

View file

@ -200,6 +200,10 @@ func (o *Order) sans(csr *x509.CertificateRequest) ([]x509util.SubjectAlternativ
var sans []x509util.SubjectAlternativeName var sans []x509util.SubjectAlternativeName
if len(csr.EmailAddresses) > 0 || len(csr.URIs) > 0 {
return sans, NewError(ErrorBadCSRType, "Only DNS names and IP addresses are allowed")
}
// order the DNS names and IP addresses, so that they can be compared against the canonicalized CSR // order the DNS names and IP addresses, so that they can be compared against the canonicalized CSR
orderNames := make([]string, numberOfIdentifierType(DNS, o.Identifiers)) orderNames := make([]string, numberOfIdentifierType(DNS, o.Identifiers))
orderIPs := make([]net.IP, numberOfIdentifierType(IP, o.Identifiers)) orderIPs := make([]net.IP, numberOfIdentifierType(IP, o.Identifiers))
@ -279,7 +283,9 @@ func numberOfIdentifierType(typ IdentifierType, ids []Identifier) int {
// canonicalize canonicalizes a CSR so that it can be compared against an Order // canonicalize canonicalizes a CSR so that it can be compared against an Order
// NOTE: this effectively changes the order of SANs in the CSR, which may be OK, // NOTE: this effectively changes the order of SANs in the CSR, which may be OK,
// but may not be expected. // but may not be expected. It also adds a Subject Common Name to either the IP
// addresses or DNS names slice, depending on whether it can be parsed as an IP
// or not. This might result in an additional SAN in the final certificate.
func canonicalize(csr *x509.CertificateRequest) (canonicalized *x509.CertificateRequest) { func canonicalize(csr *x509.CertificateRequest) (canonicalized *x509.CertificateRequest) {
// for clarity only; we're operating on the same object by pointer // for clarity only; we're operating on the same object by pointer
@ -289,16 +295,20 @@ func canonicalize(csr *x509.CertificateRequest) (canonicalized *x509.Certificate
// identifiers as the initial newOrder request. Identifiers of type "dns" // identifiers as the initial newOrder request. Identifiers of type "dns"
// MUST appear either in the commonName portion of the requested subject // MUST appear either in the commonName portion of the requested subject
// name or in an extensionRequest attribute [RFC2985] requesting a // name or in an extensionRequest attribute [RFC2985] requesting a
// subjectAltName extension, or both. // subjectAltName extension, or both. Subject Common Names that can be
// TODO(hs): we might want to check if the CommonName is in fact a DNS (and cannot // parsed as an IP are included as an IP address for the equality check.
// be parsed as IP). This is related to https://github.com/smallstep/cli/pull/576 // If these were excluded, a certificate could contain an IP as the
// (ACME IP SANS) // common name without having been challenged.
if csr.Subject.CommonName != "" { if csr.Subject.CommonName != "" {
// nolint:gocritic if ip := net.ParseIP(csr.Subject.CommonName); ip != nil {
canonicalized.DNSNames = append(csr.DNSNames, csr.Subject.CommonName) canonicalized.IPAddresses = append(canonicalized.IPAddresses, ip)
} else {
canonicalized.DNSNames = append(canonicalized.DNSNames, csr.Subject.CommonName)
}
} }
canonicalized.DNSNames = uniqueSortedLowerNames(csr.DNSNames)
canonicalized.IPAddresses = uniqueSortedIPs(csr.IPAddresses) canonicalized.DNSNames = uniqueSortedLowerNames(canonicalized.DNSNames)
canonicalized.IPAddresses = uniqueSortedIPs(canonicalized.IPAddresses)
return canonicalized return canonicalized
} }
@ -340,7 +350,10 @@ func uniqueSortedIPs(ips []net.IP) (unique []net.IP) {
} }
ipEntryMap := make(map[string]entry, len(ips)) ipEntryMap := make(map[string]entry, len(ips))
for _, ip := range ips { for _, ip := range ips {
ipEntryMap[ip.String()] = entry{ip: ip} // reparsing the IP results in the IP being represented using 16 bytes
// for both IPv4 as well as IPv6, even when the ips slice contains IPs that
// are represented by 4 bytes. This ensures a fair comparison and thus ordering.
ipEntryMap[ip.String()] = entry{ip: net.ParseIP(ip.String())}
} }
unique = make([]net.IP, 0, len(ipEntryMap)) unique = make([]net.IP, 0, len(ipEntryMap))
for _, entry := range ipEntryMap { for _, entry := range ipEntryMap {

View file

@ -6,10 +6,12 @@ import (
"crypto/x509/pkix" "crypto/x509/pkix"
"encoding/json" "encoding/json"
"net" "net"
"net/url"
"reflect" "reflect"
"testing" "testing"
"time" "time"
"github.com/google/go-cmp/cmp"
"github.com/pkg/errors" "github.com/pkg/errors"
"github.com/smallstep/assert" "github.com/smallstep/assert"
"github.com/smallstep/certificates/authority" "github.com/smallstep/certificates/authority"
@ -825,71 +827,92 @@ func Test_uniqueSortedIPs(t *testing.T) {
ips []net.IP ips []net.IP
} }
tests := []struct { tests := []struct {
name string name string
args args args args
wantUnique []net.IP want []net.IP
}{ }{
{ {
name: "ok/empty", name: "ok/empty",
args: args{ args: args{
ips: []net.IP{}, ips: []net.IP{},
}, },
wantUnique: []net.IP{}, want: []net.IP{},
}, },
{ {
name: "ok/single-ipv4", name: "ok/single-ipv4",
args: args{ args: args{
ips: []net.IP{net.ParseIP("192.168.42.42")}, ips: []net.IP{net.ParseIP("192.168.42.42")},
}, },
wantUnique: []net.IP{net.ParseIP("192.168.42.42")}, want: []net.IP{net.ParseIP("192.168.42.42")},
}, },
{ {
name: "ok/multiple-ipv4", name: "ok/multiple-ipv4",
args: args{ args: args{
ips: []net.IP{net.ParseIP("192.168.42.42"), net.ParseIP("192.168.42.10"), net.ParseIP("192.168.42.1")}, 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")},
}, },
wantUnique: []net.IP{net.ParseIP("192.168.42.1"), net.ParseIP("192.168.42.10"), net.ParseIP("192.168.42.42")}, 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", name: "ok/unique-ipv4",
args: args{ args: args{
ips: []net.IP{net.ParseIP("192.168.42.42"), net.ParseIP("192.168.42.42")}, ips: []net.IP{net.ParseIP("192.168.42.42"), net.ParseIP("192.168.42.42")},
}, },
wantUnique: []net.IP{net.ParseIP("192.168.42.42")}, want: []net.IP{net.ParseIP("192.168.42.42")},
}, },
{ {
name: "ok/single-ipv6", name: "ok/single-ipv6",
args: args{ args: args{
ips: []net.IP{net.ParseIP("2001:db8::30")}, ips: []net.IP{net.ParseIP("2001:db8::30")},
}, },
wantUnique: []net.IP{net.ParseIP("2001:db8::30")}, want: []net.IP{net.ParseIP("2001:db8::30")},
}, },
{ {
name: "ok/multiple-ipv6", name: "ok/multiple-ipv6",
args: args{ args: args{
ips: []net.IP{net.ParseIP("2001:db8::30"), net.ParseIP("2001:db8::20"), net.ParseIP("2001:db8::10")}, ips: []net.IP{net.ParseIP("2001:db8::30"), net.ParseIP("2001:db8::20"), net.ParseIP("2001:db8::10")},
}, },
wantUnique: []net.IP{net.ParseIP("2001:db8::10"), net.ParseIP("2001:db8::20"), net.ParseIP("2001:db8::30")}, want: []net.IP{net.ParseIP("2001:db8::10"), net.ParseIP("2001:db8::20"), net.ParseIP("2001:db8::30")},
}, },
{ {
name: "ok/unique-ipv6", name: "ok/unique-ipv6",
args: args{ args: args{
ips: []net.IP{net.ParseIP("2001:db8::1"), net.ParseIP("2001:db8::1")}, ips: []net.IP{net.ParseIP("2001:db8::1"), net.ParseIP("2001:db8::1")},
}, },
wantUnique: []net.IP{net.ParseIP("2001:db8::1")}, want: []net.IP{net.ParseIP("2001:db8::1")},
}, },
{ {
name: "ok/mixed-ipv4-and-ipv6", name: "ok/mixed-ipv4-and-ipv6",
args: args{ 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")}, 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")},
}, },
wantUnique: []net.IP{net.ParseIP("192.168.42.42"), net.ParseIP("2001:db8::1")}, 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 { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
if gotUnique := uniqueSortedIPs(tt.args.ips); !reflect.DeepEqual(gotUnique, tt.wantUnique) { got := uniqueSortedIPs(tt.args.ips)
t.Errorf("uniqueSortedIPs() = %v, want %v", gotUnique, tt.wantUnique) if !cmp.Equal(tt.want, got) {
t.Errorf("uniqueSortedIPs() diff =\n%s", cmp.Diff(tt.want, got))
} }
}) })
} }
@ -1122,9 +1145,9 @@ func Test_canonicalize(t *testing.T) {
csr *x509.CertificateRequest csr *x509.CertificateRequest
} }
tests := []struct { tests := []struct {
name string name string
args args args args
wantCanonicalized *x509.CertificateRequest want *x509.CertificateRequest
}{ }{
{ {
name: "ok/dns", name: "ok/dns",
@ -1133,7 +1156,7 @@ func Test_canonicalize(t *testing.T) {
DNSNames: []string{"www.example.com", "example.com"}, DNSNames: []string{"www.example.com", "example.com"},
}, },
}, },
wantCanonicalized: &x509.CertificateRequest{ want: &x509.CertificateRequest{
DNSNames: []string{"example.com", "www.example.com"}, DNSNames: []string{"example.com", "www.example.com"},
IPAddresses: []net.IP{}, IPAddresses: []net.IP{},
}, },
@ -1148,7 +1171,7 @@ func Test_canonicalize(t *testing.T) {
DNSNames: []string{"www.example.com"}, DNSNames: []string{"www.example.com"},
}, },
}, },
wantCanonicalized: &x509.CertificateRequest{ want: &x509.CertificateRequest{
Subject: pkix.Name{ Subject: pkix.Name{
CommonName: "example.com", CommonName: "example.com",
}, },
@ -1163,7 +1186,7 @@ func Test_canonicalize(t *testing.T) {
IPAddresses: []net.IP{net.ParseIP("192.168.43.42"), net.ParseIP("192.168.42.42")}, IPAddresses: []net.IP{net.ParseIP("192.168.43.42"), net.ParseIP("192.168.42.42")},
}, },
}, },
wantCanonicalized: &x509.CertificateRequest{ want: &x509.CertificateRequest{
DNSNames: []string{}, DNSNames: []string{},
IPAddresses: []net.IP{net.ParseIP("192.168.42.42"), net.ParseIP("192.168.43.42")}, IPAddresses: []net.IP{net.ParseIP("192.168.42.42"), net.ParseIP("192.168.43.42")},
}, },
@ -1176,7 +1199,7 @@ func Test_canonicalize(t *testing.T) {
IPAddresses: []net.IP{net.ParseIP("192.168.43.42"), net.ParseIP("192.168.42.42")}, IPAddresses: []net.IP{net.ParseIP("192.168.43.42"), net.ParseIP("192.168.42.42")},
}, },
}, },
wantCanonicalized: &x509.CertificateRequest{ want: &x509.CertificateRequest{
DNSNames: []string{"example.com", "www.example.com"}, DNSNames: []string{"example.com", "www.example.com"},
IPAddresses: []net.IP{net.ParseIP("192.168.42.42"), net.ParseIP("192.168.43.42")}, IPAddresses: []net.IP{net.ParseIP("192.168.42.42"), net.ParseIP("192.168.43.42")},
}, },
@ -1192,7 +1215,7 @@ func Test_canonicalize(t *testing.T) {
IPAddresses: []net.IP{net.ParseIP("192.168.43.42"), net.ParseIP("192.168.42.42")}, IPAddresses: []net.IP{net.ParseIP("192.168.43.42"), net.ParseIP("192.168.42.42")},
}, },
}, },
wantCanonicalized: &x509.CertificateRequest{ want: &x509.CertificateRequest{
Subject: pkix.Name{ Subject: pkix.Name{
CommonName: "example.com", CommonName: "example.com",
}, },
@ -1200,11 +1223,31 @@ func Test_canonicalize(t *testing.T) {
IPAddresses: []net.IP{net.ParseIP("192.168.42.42"), net.ParseIP("192.168.43.42")}, 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 { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
if gotCanonicalized := canonicalize(tt.args.csr); !reflect.DeepEqual(gotCanonicalized, tt.wantCanonicalized) { got := canonicalize(tt.args.csr)
t.Errorf("canonicalize() = %v, want %v", gotCanonicalized, tt.wantCanonicalized) if !cmp.Equal(tt.want, got) {
t.Errorf("canonicalize() diff =\n%s", cmp.Diff(tt.want, got))
} }
}) })
} }
@ -1238,6 +1281,39 @@ func TestOrder_sans(t *testing.T) {
}, },
err: nil, 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", name: "fail/error-names-length-mismatch",
fields: fields{ fields: fields{