Add Common Name to certificate.

Some applications fail to parse the certificate if the CN is not set,
even if they (correctly) check SANs before the CN when validating a
hostname.  Even though the CN should be ignored if a matching SAN
hostname was found, we can prevent the parse from failing by also
including the hostname as the CN.

Additionally, switch from maps to slices to prevent DNS names and IP
addresses from being reordered when added to the certificate template.
This commit is contained in:
Josh Rickmar 2015-06-11 16:08:04 -04:00
parent 53b0b8cd09
commit 9ffb1ecd80
2 changed files with 71 additions and 35 deletions

View file

@ -1,4 +1,4 @@
// Copyright (c) 2013-2014 The btcsuite developers // Copyright (c) 2013-2015 The btcsuite developers
// Use of this source code is governed by an ISC // Use of this source code is governed by an ISC
// license that can be found in the LICENSE file. // license that can be found in the LICENSE file.
@ -48,33 +48,32 @@ func NewTLSCertPair(organization string, validUntil time.Time, extraHosts []stri
return nil, nil, fmt.Errorf("failed to generate serial number: %s", err) return nil, nil, fmt.Errorf("failed to generate serial number: %s", err)
} }
template := x509.Certificate{
SerialNumber: serialNumber,
Subject: pkix.Name{
Organization: []string{organization},
},
NotBefore: now.Add(-time.Hour * 24),
NotAfter: validUntil,
KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature |
x509.KeyUsageCertSign,
IsCA: true, // so can sign self.
BasicConstraintsValid: true,
}
host, err := os.Hostname() host, err := os.Hostname()
if err != nil { if err != nil {
return nil, nil, err return nil, nil, err
} }
// Use maps to prevent adding duplicates. ipAddresses := []net.IP{net.ParseIP("127.0.0.1"), net.ParseIP("::1")}
ipAddresses := map[string]net.IP{ dnsNames := []string{host}
"127.0.0.1": net.ParseIP("127.0.0.1"), if host != "localhost" {
"::1": net.ParseIP("::1"), dnsNames = append(dnsNames, "localhost")
} }
dnsNames := map[string]bool{
host: true, addIP := func(ipAddr net.IP) {
"localhost": true, for _, ip := range ipAddresses {
if bytes.Equal(ip, ipAddr) {
return
}
}
ipAddresses = append(ipAddresses, ipAddr)
}
addHost := func(host string) {
for _, dnsName := range dnsNames {
if host == dnsName {
return
}
}
dnsNames = append(dnsNames, host)
} }
addrs, err := interfaceAddrs() addrs, err := interfaceAddrs()
@ -82,9 +81,9 @@ func NewTLSCertPair(organization string, validUntil time.Time, extraHosts []stri
return nil, nil, err return nil, nil, err
} }
for _, a := range addrs { for _, a := range addrs {
ip, _, err := net.ParseCIDR(a.String()) ipAddr, _, err := net.ParseCIDR(a.String())
if err == nil { if err == nil {
ipAddresses[ip.String()] = ip addIP(ipAddr)
} }
} }
@ -94,19 +93,28 @@ func NewTLSCertPair(organization string, validUntil time.Time, extraHosts []stri
host = hostStr host = hostStr
} }
if ip := net.ParseIP(host); ip != nil { if ip := net.ParseIP(host); ip != nil {
ipAddresses[ip.String()] = ip addIP(ip)
} else { } else {
dnsNames[host] = true addHost(host)
} }
} }
template.DNSNames = make([]string, 0, len(dnsNames)) template := x509.Certificate{
for dnsName := range dnsNames { SerialNumber: serialNumber,
template.DNSNames = append(template.DNSNames, dnsName) Subject: pkix.Name{
} Organization: []string{organization},
template.IPAddresses = make([]net.IP, 0, len(ipAddresses)) CommonName: host,
for _, ip := range ipAddresses { },
template.IPAddresses = append(template.IPAddresses, ip) NotBefore: now.Add(-time.Hour * 24),
NotAfter: validUntil,
KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature |
x509.KeyUsageCertSign,
IsCA: true, // so can sign self.
BasicConstraintsValid: true,
DNSNames: dnsNames,
IPAddresses: ipAddresses,
} }
derBytes, err := x509.CreateCertificate(rand.Reader, &template, derBytes, err := x509.CreateCertificate(rand.Reader, &template,

View file

@ -1,4 +1,4 @@
// Copyright (c) 2013-2014 The btcsuite developers // Copyright (c) 2013-2015 The btcsuite developers
// Use of this source code is governed by an ISC // Use of this source code is governed by an ISC
// license that can be found in the LICENSE file. // license that can be found in the LICENSE file.
@ -7,6 +7,7 @@ package btcutil_test
import ( import (
"crypto/x509" "crypto/x509"
"encoding/pem" "encoding/pem"
"net"
"testing" "testing"
"time" "time"
@ -21,7 +22,7 @@ func TestNewTLSCertPair(t *testing.T) {
// differences. // differences.
validUntil := time.Unix(time.Now().Add(10*365*24*time.Hour).Unix(), 0) validUntil := time.Unix(time.Now().Add(10*365*24*time.Hour).Unix(), 0)
org := "test autogenerated cert" org := "test autogenerated cert"
extraHosts := []string{"testtlscert.bogus", "127.0.0.1"} extraHosts := []string{"testtlscert.bogus", "localhost", "127.0.0.1"}
cert, key, err := btcutil.NewTLSCertPair(org, validUntil, extraHosts) cert, key, err := btcutil.NewTLSCertPair(org, validUntil, extraHosts)
if err != nil { if err != nil {
t.Fatalf("failed with unexpected error: %v", err) t.Fatalf("failed with unexpected error: %v", err)
@ -76,6 +77,33 @@ func TestNewTLSCertPair(t *testing.T) {
} }
} }
// Ensure that the Common Name is also the first SAN DNS name.
cn := x509Cert.Subject.CommonName
san0 := x509Cert.DNSNames[0]
if cn != san0 {
t.Errorf("common name %s does not match first SAN %s", cn, san0)
}
// Ensure there are no duplicate hosts or IPs.
hostCounts := make(map[string]int)
for _, host := range x509Cert.DNSNames {
hostCounts[host]++
}
ipCounts := make(map[string]int)
for _, ip := range x509Cert.IPAddresses {
ipCounts[string(ip)]++
}
for host, count := range hostCounts {
if count != 1 {
t.Errorf("host %s appears %d times in certificate", host, count)
}
}
for ipStr, count := range ipCounts {
if count != 1 {
t.Errorf("ip %s appears %d times in certificate", net.IP(ipStr), count)
}
}
// Ensure the cert can be use for the intended purposes. // Ensure the cert can be use for the intended purposes.
if !x509Cert.IsCA { if !x509Cert.IsCA {
t.Fatal("generated cert is not a certificate authority") t.Fatal("generated cert is not a certificate authority")