Commit 5b20a18f authored by Adam Langley's avatar Adam Langley

crypto/x509: support IP SANs.

Subject Alternative Names in X.509 certificates may include IP
addresses. This change adds support for marshaling, unmarshaling and
verifying this form of SAN.

It also causes IP addresses to only be checked against IP SANs,
rather than against hostnames as was previously the case. This
reflects RFC 6125.

Fixes #4658.

R=golang-dev, mikioh.mikioh, bradfitz
CC=golang-dev
https://golang.org/cl/7336046
parent a9824f17
...@@ -16,36 +16,80 @@ import ( ...@@ -16,36 +16,80 @@ import (
"crypto/x509/pkix" "crypto/x509/pkix"
"encoding/pem" "encoding/pem"
"flag" "flag"
"fmt"
"log" "log"
"math/big" "math/big"
"net"
"os" "os"
"strings"
"time" "time"
) )
var hostName *string = flag.String("host", "127.0.0.1", "Hostname to generate a certificate for") var (
host = flag.String("host", "", "Comma-separated hostnames and IPs to generate a certificate for")
validFrom = flag.String("start-date", "", "Creation date formatted as Jan 1 15:04:05 2011")
validFor = flag.Duration("duration", 365*24*time.Hour, "Duration that certificate is valid for")
isCA = flag.Bool("ca", false, "whether this cert should be its own Certificate Authority")
rsaBits = flag.Int("rsa-bits", 1024, "Size of RSA key to generate")
)
func main() { func main() {
flag.Parse() flag.Parse()
priv, err := rsa.GenerateKey(rand.Reader, 1024) if len(*host) == 0 {
log.Fatalf("Missing required --host parameter")
}
priv, err := rsa.GenerateKey(rand.Reader, *rsaBits)
if err != nil { if err != nil {
log.Fatalf("failed to generate private key: %s", err) log.Fatalf("failed to generate private key: %s", err)
return return
} }
now := time.Now() var notBefore time.Time
if len(*validFrom) == 0 {
notBefore = time.Now()
} else {
notBefore, err = time.Parse("Jan 2 15:04:05 2006", *validFrom)
if err != nil {
fmt.Fprintf(os.Stderr, "Failed to parse creation date: %s\n", err)
os.Exit(1)
}
}
notAfter := notBefore.Add(*validFor)
// end of ASN.1 time
endOfTime := time.Date(2049, 12, 31, 23, 59, 59, 0, time.UTC)
if notAfter.After(endOfTime) {
notAfter = endOfTime
}
template := x509.Certificate{ template := x509.Certificate{
SerialNumber: new(big.Int).SetInt64(0), SerialNumber: new(big.Int).SetInt64(0),
Subject: pkix.Name{ Subject: pkix.Name{
CommonName: *hostName,
Organization: []string{"Acme Co"}, Organization: []string{"Acme Co"},
}, },
NotBefore: now.Add(-5 * time.Minute).UTC(), NotBefore: notBefore,
NotAfter: now.AddDate(1, 0, 0).UTC(), // valid for 1 year. NotAfter: notAfter,
KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature,
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
BasicConstraintsValid: true,
}
hosts := strings.Split(*host, ",")
for _, h := range hosts {
if ip := net.ParseIP(h); ip != nil {
template.IPAddresses = append(template.IPAddresses, ip)
} else {
template.DNSNames = append(template.DNSNames, h)
}
}
SubjectKeyId: []byte{1, 2, 3, 4}, if *isCA {
KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature, template.IsCA = true
template.KeyUsage |= x509.KeyUsageCertSign
} }
derBytes, err := x509.CreateCertificate(rand.Reader, &template, &template, &priv.PublicKey, priv) derBytes, err := x509.CreateCertificate(rand.Reader, &template, &template, &priv.PublicKey, priv)
......
...@@ -5,6 +5,7 @@ ...@@ -5,6 +5,7 @@
package x509 package x509
import ( import (
"net"
"runtime" "runtime"
"strings" "strings"
"time" "time"
...@@ -63,14 +64,28 @@ type HostnameError struct { ...@@ -63,14 +64,28 @@ type HostnameError struct {
} }
func (h HostnameError) Error() string { func (h HostnameError) Error() string {
var valid string
c := h.Certificate c := h.Certificate
if len(c.DNSNames) > 0 {
valid = strings.Join(c.DNSNames, ", ") var valid string
if ip := net.ParseIP(h.Host); ip != nil {
// Trying to validate an IP
if len(c.IPAddresses) == 0 {
return "x509: cannot validate certificate for " + h.Host + " because it doesn't contain any IP SANs"
}
for _, san := range c.IPAddresses {
if len(valid) > 0 {
valid += ", "
}
valid += san.String()
}
} else { } else {
valid = c.Subject.CommonName if len(c.DNSNames) > 0 {
valid = strings.Join(c.DNSNames, ", ")
} else {
valid = c.Subject.CommonName
}
} }
return "certificate is valid for " + valid + ", not " + h.Host return "x509: certificate is valid for " + valid + ", not " + h.Host
} }
// UnknownAuthorityError results when the certificate issuer is unknown // UnknownAuthorityError results when the certificate issuer is unknown
...@@ -334,6 +349,22 @@ func toLowerCaseASCII(in string) string { ...@@ -334,6 +349,22 @@ func toLowerCaseASCII(in string) string {
// VerifyHostname returns nil if c is a valid certificate for the named host. // VerifyHostname returns nil if c is a valid certificate for the named host.
// Otherwise it returns an error describing the mismatch. // Otherwise it returns an error describing the mismatch.
func (c *Certificate) VerifyHostname(h string) error { func (c *Certificate) VerifyHostname(h string) error {
// IP addresses may be written in [ ].
candidateIP := h
if len(h) >= 3 && h[0] == '[' && h[len(h)-1] == ']' {
candidateIP = h[1 : len(h)-1]
}
if ip := net.ParseIP(candidateIP); ip != nil {
// We only match IP addresses against IP SANs.
// https://tools.ietf.org/html/rfc6125#appendix-B.2
for _, candidate := range c.IPAddresses {
if ip.Equal(candidate) {
return nil
}
}
return HostnameError{c, candidateIP}
}
lowered := toLowerCaseASCII(h) lowered := toLowerCaseASCII(h)
if len(c.DNSNames) > 0 { if len(c.DNSNames) > 0 {
......
...@@ -19,6 +19,8 @@ import ( ...@@ -19,6 +19,8 @@ import (
"errors" "errors"
"io" "io"
"math/big" "math/big"
"net"
"strconv"
"time" "time"
) )
...@@ -464,6 +466,7 @@ type Certificate struct { ...@@ -464,6 +466,7 @@ type Certificate struct {
// Subject Alternate Name values // Subject Alternate Name values
DNSNames []string DNSNames []string
EmailAddresses []string EmailAddresses []string
IPAddresses []net.IP
// Name constraints // Name constraints
PermittedDNSDomainsCritical bool // if true then the name constraints are marked critical. PermittedDNSDomainsCritical bool // if true then the name constraints are marked critical.
...@@ -841,6 +844,13 @@ func parseCertificate(in *certificate) (*Certificate, error) { ...@@ -841,6 +844,13 @@ func parseCertificate(in *certificate) (*Certificate, error) {
case 2: case 2:
out.DNSNames = append(out.DNSNames, string(v.Bytes)) out.DNSNames = append(out.DNSNames, string(v.Bytes))
parsedName = true parsedName = true
case 7:
switch len(v.Bytes) {
case net.IPv4len, net.IPv6len:
out.IPAddresses = append(out.IPAddresses, v.Bytes)
default:
return nil, errors.New("x509: certificate contained IP address of length " + strconv.Itoa(len(v.Bytes)))
}
} }
} }
...@@ -1085,11 +1095,22 @@ func buildExtensions(template *Certificate) (ret []pkix.Extension, err error) { ...@@ -1085,11 +1095,22 @@ func buildExtensions(template *Certificate) (ret []pkix.Extension, err error) {
n++ n++
} }
if len(template.DNSNames) > 0 { if len(template.DNSNames) > 0 || len(template.EmailAddresses) > 0 || len(template.IPAddresses) > 0 {
ret[n].Id = oidExtensionSubjectAltName ret[n].Id = oidExtensionSubjectAltName
rawValues := make([]asn1.RawValue, len(template.DNSNames)) var rawValues []asn1.RawValue
for i, name := range template.DNSNames { for _, name := range template.DNSNames {
rawValues[i] = asn1.RawValue{Tag: 2, Class: 2, Bytes: []byte(name)} rawValues = append(rawValues, asn1.RawValue{Tag: 2, Class: 2, Bytes: []byte(name)})
}
for _, email := range template.EmailAddresses {
rawValues = append(rawValues, asn1.RawValue{Tag: 1, Class: 2, Bytes: []byte(email)})
}
for _, rawIP := range template.IPAddresses {
// If possible, we always want to encode IPv4 addresses in 4 bytes.
ip := rawIP.To4()
if ip == nil {
ip = rawIP
}
rawValues = append(rawValues, asn1.RawValue{Tag: 7, Class: 2, Bytes: ip})
} }
ret[n].Value, err = asn1.Marshal(rawValues) ret[n].Value, err = asn1.Marshal(rawValues)
if err != nil { if err != nil {
......
...@@ -19,6 +19,7 @@ import ( ...@@ -19,6 +19,7 @@ import (
"encoding/hex" "encoding/hex"
"encoding/pem" "encoding/pem"
"math/big" "math/big"
"net"
"reflect" "reflect"
"testing" "testing"
"time" "time"
...@@ -174,6 +175,49 @@ func TestMatchHostnames(t *testing.T) { ...@@ -174,6 +175,49 @@ func TestMatchHostnames(t *testing.T) {
} }
} }
func TestMatchIP(t *testing.T) {
// Check that pattern matching is working.
c := &Certificate{
DNSNames: []string{"*.foo.bar.baz"},
Subject: pkix.Name{
CommonName: "*.foo.bar.baz",
},
}
err := c.VerifyHostname("quux.foo.bar.baz")
if err != nil {
t.Fatalf("VerifyHostname(quux.foo.bar.baz): %v", err)
}
// But check that if we change it to be matching against an IP address,
// it is rejected.
c = &Certificate{
DNSNames: []string{"*.2.3.4"},
Subject: pkix.Name{
CommonName: "*.2.3.4",
},
}
err = c.VerifyHostname("1.2.3.4")
if err == nil {
t.Fatalf("VerifyHostname(1.2.3.4) should have failed, did not")
}
c = &Certificate{
IPAddresses: []net.IP{net.ParseIP("127.0.0.1"), net.ParseIP("::1")},
}
err = c.VerifyHostname("127.0.0.1")
if err != nil {
t.Fatalf("VerifyHostname(127.0.0.1): %v", err)
}
err = c.VerifyHostname("::1")
if err != nil {
t.Fatalf("VerifyHostname(::1): %v", err)
}
err = c.VerifyHostname("[::1]")
if err != nil {
t.Fatalf("VerifyHostname([::1]): %v", err)
}
}
func TestCertificateParse(t *testing.T) { func TestCertificateParse(t *testing.T) {
s, _ := hex.DecodeString(certBytes) s, _ := hex.DecodeString(certBytes)
certs, err := ParseCertificates(s) certs, err := ParseCertificates(s)
...@@ -284,8 +328,11 @@ func TestCreateSelfSignedCertificate(t *testing.T) { ...@@ -284,8 +328,11 @@ func TestCreateSelfSignedCertificate(t *testing.T) {
UnknownExtKeyUsage: testUnknownExtKeyUsage, UnknownExtKeyUsage: testUnknownExtKeyUsage,
BasicConstraintsValid: true, BasicConstraintsValid: true,
IsCA: true, IsCA: true,
DNSNames: []string{"test.example.com"},
DNSNames: []string{"test.example.com"},
EmailAddresses: []string{"gopher@golang.org"},
IPAddresses: []net.IP{net.IPv4(127, 0, 0, 1).To4(), net.ParseIP("2001:4860:0:2001::68")},
PolicyIdentifiers: []asn1.ObjectIdentifier{[]int{1, 2, 3}}, PolicyIdentifiers: []asn1.ObjectIdentifier{[]int{1, 2, 3}},
PermittedDNSDomains: []string{".example.com", "example.com"}, PermittedDNSDomains: []string{".example.com", "example.com"},
...@@ -327,6 +374,18 @@ func TestCreateSelfSignedCertificate(t *testing.T) { ...@@ -327,6 +374,18 @@ func TestCreateSelfSignedCertificate(t *testing.T) {
t.Errorf("%s: unknown extkeyusage wasn't correctly copied from the template. Got %v, want %v", test.name, cert.UnknownExtKeyUsage, testUnknownExtKeyUsage) t.Errorf("%s: unknown extkeyusage wasn't correctly copied from the template. Got %v, want %v", test.name, cert.UnknownExtKeyUsage, testUnknownExtKeyUsage)
} }
if !reflect.DeepEqual(cert.DNSNames, template.DNSNames) {
t.Errorf("%s: SAN DNS names differ from template. Got %v, want %v", test.name, cert.DNSNames, template.DNSNames)
}
if !reflect.DeepEqual(cert.EmailAddresses, template.EmailAddresses) {
t.Errorf("%s: SAN emails differ from template. Got %v, want %v", test.name, cert.EmailAddresses, template.EmailAddresses)
}
if !reflect.DeepEqual(cert.IPAddresses, template.IPAddresses) {
t.Errorf("%s: SAN IPs differ from template. Got %v, want %v", test.name, cert.IPAddresses, template.IPAddresses)
}
if test.checkSig { if test.checkSig {
err = cert.CheckSignatureFrom(cert) err = cert.CheckSignatureFrom(cert)
if err != nil { if err != nil {
......
...@@ -306,7 +306,7 @@ var pkgDeps = map[string][]string{ ...@@ -306,7 +306,7 @@ var pkgDeps = map[string][]string{
}, },
"crypto/x509": { "crypto/x509": {
"L4", "CRYPTO-MATH", "OS", "CGO", "L4", "CRYPTO-MATH", "OS", "CGO",
"crypto/x509/pkix", "encoding/pem", "encoding/hex", "syscall", "crypto/x509/pkix", "encoding/pem", "encoding/hex", "net", "syscall",
}, },
"crypto/x509/pkix": {"L4", "CRYPTO-MATH"}, "crypto/x509/pkix": {"L4", "CRYPTO-MATH"},
......
...@@ -200,28 +200,29 @@ func (h *waitGroupHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { ...@@ -200,28 +200,29 @@ func (h *waitGroupHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
h.h.ServeHTTP(w, r) h.h.ServeHTTP(w, r)
} }
// localhostCert is a PEM-encoded TLS cert with SAN DNS names // localhostCert is a PEM-encoded TLS cert with SAN IPs
// "127.0.0.1" and "[::1]", expiring at the last second of 2049 (the end // "127.0.0.1" and "[::1]", expiring at the last second of 2049 (the end
// of ASN.1 time). // of ASN.1 time).
// generated from src/pkg/crypto/tls:
// go run generate_cert.go --rsa-bits 512 --host 127.0.0.1,::1,example.com --ca --start-date "Jan 1 00:00:00 1970" --duration=1000000h
var localhostCert = []byte(`-----BEGIN CERTIFICATE----- var localhostCert = []byte(`-----BEGIN CERTIFICATE-----
MIIBTTCB+qADAgECAgEAMAsGCSqGSIb3DQEBBTAAMB4XDTcwMDEwMTAwMDAwMFoX MIIBdzCCASOgAwIBAgIBADALBgkqhkiG9w0BAQUwEjEQMA4GA1UEChMHQWNtZSBD
DTQ5MTIzMTIzNTk1OVowADBaMAsGCSqGSIb3DQEBAQNLADBIAkEAsuA5mAFMj6Q7 bzAeFw03MDAxMDEwMDAwMDBaFw00OTEyMzEyMzU5NTlaMBIxEDAOBgNVBAoTB0Fj
qoBzcvKzIq4kzuT5epSp2AkcQfyBHm7K13Ws7u+0b5Vb9gqTf5cAiIKcrtrXVqkL bWUgQ28wWjALBgkqhkiG9w0BAQEDSwAwSAJBAN55NcYKZeInyTuhcCwFMhDHCmwa
8i1UQF6AzwIDAQABo2MwYTAOBgNVHQ8BAf8EBAMCACQwEgYDVR0TAQH/BAgwBgEB IUSdtXdcbItRB/yfXGBhiex00IaLXQnSU+QZPRZWYqeTEbFSgihqi1PUDy8CAwEA
/wIBATANBgNVHQ4EBgQEAQIDBDAPBgNVHSMECDAGgAQBAgMEMBsGA1UdEQQUMBKC AaNoMGYwDgYDVR0PAQH/BAQDAgCkMBMGA1UdJQQMMAoGCCsGAQUFBwMBMA8GA1Ud
CTEyNy4wLjAuMYIFWzo6MV0wCwYJKoZIhvcNAQEFA0EAj1Jsn/h2KHy7dgqutZNB EwEB/wQFMAMBAf8wLgYDVR0RBCcwJYILZXhhbXBsZS5jb22HBH8AAAGHEAAAAAAA
nCGlNN+8vw263Bax9MklR85Ti6a0VWSvp/fDQZUADvmFTDkcXeA24pqmdUxeQDWw AAAAAAAAAAAAAAEwCwYJKoZIhvcNAQEFA0EAAoQn/ytgqpiLcZu9XKbCJsJcvkgk
Pg== Se6AbGXgSlq+ZCEVo0qIwSgeBqmsJxUu7NCSOwVJLYNEBO2DtIxoYVk+MA==
-----END CERTIFICATE-----`) -----END CERTIFICATE-----`)
// localhostKey is the private key for localhostCert. // localhostKey is the private key for localhostCert.
var localhostKey = []byte(`-----BEGIN RSA PRIVATE KEY----- var localhostKey = []byte(`-----BEGIN RSA PRIVATE KEY-----
MIIBPQIBAAJBALLgOZgBTI+kO6qAc3LysyKuJM7k+XqUqdgJHEH8gR5uytd1rO7v MIIBPAIBAAJBAN55NcYKZeInyTuhcCwFMhDHCmwaIUSdtXdcbItRB/yfXGBhiex0
tG+VW/YKk3+XAIiCnK7a11apC/ItVEBegM8CAwEAAQJBAI5sxq7naeR9ahyqRkJi 0IaLXQnSU+QZPRZWYqeTEbFSgihqi1PUDy8CAwEAAQJBAQdUx66rfh8sYsgfdcvV
SIv2iMxLuPEHaezf5CYOPWjSjBPyVhyRevkhtqEjF/WkgL7C2nWpYHsUcBDBQVF0 NoafYpnEcB5s4m/vSVe6SU7dCK6eYec9f9wpT353ljhDUHq3EbmE4foNzJngh35d
3KECIQDtEGB2ulnkZAahl3WuJziXGLB+p8Wgx7wzSM6bHu1c6QIhAMEp++CaS+SJ AekCIQDhRQG5Li0Wj8TM4obOnnXUXf1jRv0UkzE9AHWLG5q3AwIhAPzSjpYUDjVW
/TrU0zwY/fW4SvQeb49BPZUF3oqR8Xz3AiEA1rAJHBzBgdOQKdE3ksMUPcnvNJSN MCUXgckTpKCuGwbJk7424Nb8bLzf3kllAiA5mUBgjfr/WtFSJdWcPQ4Zt9KTMNKD
poCcELmz2clVXtkCIQCLytuLV38XHToTipR4yMl6O+6arzAjZ56uq7m7ZRV0TwIh EUO0ukpTwEIl6wIhAMbGqZK3zAAFdq8DD2jPx+UJXnh0rnOkZBzDtJ6/iN69AiEA
AM65XAOw8Dsg9Kq78aYXiOEDc5DL0sbFUu/SlmRcCg93 1Aq8MJgTaYsDQWyU/hDq5YkDJc9e9DSCvUIzqxQWMQE=
-----END RSA PRIVATE KEY----- -----END RSA PRIVATE KEY-----`)
`)
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment