feat: support for certificate with raw IP SAN (RFC8738) (#1838)

This commit is contained in:
orangepizza 2023-05-03 02:02:18 +09:00 committed by GitHub
parent dfcf4412f7
commit 5a70c3661d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 141 additions and 7 deletions

View file

@ -3,6 +3,7 @@ package api
import ( import (
"encoding/base64" "encoding/base64"
"errors" "errors"
"net"
"github.com/go-acme/lego/v4/acme" "github.com/go-acme/lego/v4/acme"
) )
@ -13,7 +14,13 @@ type OrderService service
func (o *OrderService) New(domains []string) (acme.ExtendedOrder, error) { func (o *OrderService) New(domains []string) (acme.ExtendedOrder, error) {
var identifiers []acme.Identifier var identifiers []acme.Identifier
for _, domain := range domains { for _, domain := range domains {
identifiers = append(identifiers, acme.Identifier{Type: "dns", Value: domain}) ident := acme.Identifier{Value: domain, Type: "dns"}
if net.ParseIP(domain) != nil {
ident.Type = "ip"
}
identifiers = append(identifiers, ident)
} }
orderReq := acme.Order{Identifiers: identifiers} orderReq := acme.Order{Identifiers: identifiers}

View file

@ -14,6 +14,7 @@ import (
"errors" "errors"
"fmt" "fmt"
"math/big" "math/big"
"net"
"strings" "strings"
"time" "time"
@ -134,9 +135,20 @@ func GeneratePrivateKey(keyType KeyType) (crypto.PrivateKey, error) {
} }
func GenerateCSR(privateKey crypto.PrivateKey, domain string, san []string, mustStaple bool) ([]byte, error) { func GenerateCSR(privateKey crypto.PrivateKey, domain string, san []string, mustStaple bool) ([]byte, error) {
var dnsNames []string
var ipAddresses []net.IP
for _, altname := range san {
if ip := net.ParseIP(altname); ip != nil {
ipAddresses = append(ipAddresses, ip)
} else {
dnsNames = append(dnsNames, altname)
}
}
template := x509.CertificateRequest{ template := x509.CertificateRequest{
Subject: pkix.Name{CommonName: domain}, Subject: pkix.Name{CommonName: domain},
DNSNames: san, DNSNames: dnsNames,
IPAddresses: ipAddresses,
} }
if mustStaple { if mustStaple {
@ -218,6 +230,13 @@ func ExtractDomains(cert *x509.Certificate) []string {
domains = append(domains, sanDomain) domains = append(domains, sanDomain)
} }
commonNameIP := net.ParseIP(cert.Subject.CommonName)
for _, sanIP := range cert.IPAddresses {
if !commonNameIP.Equal(sanIP) {
domains = append(domains, sanIP.String())
}
}
return domains return domains
} }
@ -238,6 +257,13 @@ func ExtractDomainsCSR(csr *x509.CertificateRequest) []string {
domains = append(domains, sanName) domains = append(domains, sanName)
} }
cnip := net.ParseIP(csr.Subject.CommonName)
for _, sanIP := range csr.IPAddresses {
if !cnip.Equal(sanIP) {
domains = append(domains, sanIP.String())
}
}
return domains return domains
} }
@ -280,9 +306,15 @@ func generateDerCert(privateKey *rsa.PrivateKey, expiration time.Time, domain st
KeyUsage: x509.KeyUsageKeyEncipherment, KeyUsage: x509.KeyUsageKeyEncipherment,
BasicConstraintsValid: true, BasicConstraintsValid: true,
DNSNames: []string{domain},
ExtraExtensions: extensions, ExtraExtensions: extensions,
} }
// handling SAN filling as type suspected
if ip := net.ParseIP(domain); ip != nil {
template.IPAddresses = []net.IP{ip}
} else {
template.DNSNames = []string{domain}
}
return x509.CreateCertificate(rand.Reader, &template, &template, &privateKey.PublicKey, privateKey) return x509.CreateCertificate(rand.Reader, &template, &template, &privateKey.PublicKey, privateKey)
} }

View file

@ -7,6 +7,7 @@ import (
"crypto/subtle" "crypto/subtle"
"crypto/tls" "crypto/tls"
"encoding/asn1" "encoding/asn1"
"net"
"net/http" "net/http"
"testing" "testing"
@ -14,6 +15,7 @@ import (
"github.com/go-acme/lego/v4/acme/api" "github.com/go-acme/lego/v4/acme/api"
"github.com/go-acme/lego/v4/challenge" "github.com/go-acme/lego/v4/challenge"
"github.com/go-acme/lego/v4/platform/tester" "github.com/go-acme/lego/v4/platform/tester"
"github.com/miekg/dns"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
) )
@ -21,10 +23,12 @@ import (
func TestChallenge(t *testing.T) { func TestChallenge(t *testing.T) {
_, apiURL := tester.SetupFakeAPI(t) _, apiURL := tester.SetupFakeAPI(t)
domain := "localhost:23457" domain := "localhost"
port := "23457"
mockValidate := func(_ *api.Core, _ string, chlng acme.Challenge) error { mockValidate := func(_ *api.Core, _ string, chlng acme.Challenge) error {
conn, err := tls.Dial("tcp", domain, &tls.Config{ conn, err := tls.Dial("tcp", net.JoinHostPort(domain, port), &tls.Config{
ServerName: domain,
InsecureSkipVerify: true, InsecureSkipVerify: true,
}) })
require.NoError(t, err, "Expected to connect to challenge server without an error") require.NoError(t, err, "Expected to connect to challenge server without an error")
@ -76,6 +80,7 @@ func TestChallenge(t *testing.T) {
authz := acme.Authorization{ authz := acme.Authorization{
Identifier: acme.Identifier{ Identifier: acme.Identifier{
Type: "dns",
Value: domain, Value: domain,
}, },
Challenges: []acme.Challenge{ Challenges: []acme.Challenge{
@ -116,3 +121,73 @@ func TestChallengeInvalidPort(t *testing.T) {
assert.Contains(t, err.Error(), "invalid port") assert.Contains(t, err.Error(), "invalid port")
assert.Contains(t, err.Error(), "123456") assert.Contains(t, err.Error(), "123456")
} }
func TestChallengeIPaddress(t *testing.T) {
_, apiURL := tester.SetupFakeAPI(t)
domain := "127.0.0.1"
port := "23457"
rd, _ := dns.ReverseAddr(domain)
mockValidate := func(_ *api.Core, _ string, chlng acme.Challenge) error {
conn, err := tls.Dial("tcp", net.JoinHostPort(domain, port), &tls.Config{
ServerName: rd,
InsecureSkipVerify: true,
})
require.NoError(t, err, "Expected to connect to challenge server without an error")
// Expect the server to only return one certificate
connState := conn.ConnectionState()
assert.Len(t, connState.PeerCertificates, 1, "Expected the challenge server to return exactly one certificate")
remoteCert := connState.PeerCertificates[0]
assert.Len(t, remoteCert.DNSNames, 0, "Expected the challenge certificate to have no DNSNames entry in context of challenge for IP")
assert.Len(t, remoteCert.IPAddresses, 1, "Expected the challenge certificate to have exactly one IPAddresses entry")
assert.True(t, net.ParseIP("127.0.0.1").Equal(remoteCert.IPAddresses[0]), "challenge certificate IPAddress ")
assert.NotEmpty(t, remoteCert.Extensions, "Expected the challenge certificate to contain extensions")
var foundAcmeIdentifier bool
var extValue []byte
for _, ext := range remoteCert.Extensions {
if idPeAcmeIdentifierV1.Equal(ext.Id) {
assert.True(t, ext.Critical, "Expected the challenge certificate id-pe-acmeIdentifier extension to be marked as critical")
foundAcmeIdentifier = true
extValue = ext.Value
break
}
}
require.True(t, foundAcmeIdentifier, "Expected the challenge certificate to contain an extension with the id-pe-acmeIdentifier id,")
zBytes := sha256.Sum256([]byte(chlng.KeyAuthorization))
value, err := asn1.Marshal(zBytes[:sha256.Size])
require.NoError(t, err, "Expected marshaling of the keyAuth to return no error")
require.EqualValues(t, value, extValue, "Expected the challenge certificate id-pe-acmeIdentifier extension to contain the SHA-256 digest of the keyAuth")
return nil
}
privateKey, err := rsa.GenerateKey(rand.Reader, 512)
require.NoError(t, err, "Could not generate test key")
core, err := api.New(http.DefaultClient, "lego-test", apiURL+"/dir", "", privateKey)
require.NoError(t, err)
solver := NewChallenge(
core,
mockValidate,
&ProviderServer{port: "23457"},
)
authz := acme.Authorization{
Identifier: acme.Identifier{
Type: "ip",
Value: domain,
},
Challenges: []acme.Challenge{
{Type: challenge.TLSALPN01.String(), Token: "tlsalpn1"},
},
}
require.NoError(t, solver.Solve(authz))
}

View file

@ -272,7 +272,7 @@ func (s *CertificatesStorage) MoveToArchive(domain string) error {
// sanitizedDomain Make sure no funny chars are in the cert names (like wildcards ;)). // sanitizedDomain Make sure no funny chars are in the cert names (like wildcards ;)).
func sanitizedDomain(domain string) string { func sanitizedDomain(domain string) string {
safe, err := idna.ToASCII(strings.ReplaceAll(domain, "*", "_")) safe, err := idna.ToASCII(strings.NewReplacer(":", "-", "*", "_").Replace(domain))
if err != nil { if err != nil {
log.Fatal(err) log.Fatal(err)
} }

View file

@ -84,6 +84,26 @@ func TestChallengeTLS_Run_Domains(t *testing.T) {
} }
} }
func TestChallengeTLS_Run_IP(t *testing.T) {
loader.CleanLegoFiles()
output, err := load.RunLego(
"-m", "hubert@hubert.com",
"--accept-tos",
"-s", "https://localhost:14000/dir",
"-d", "127.0.0.1",
"--tls",
"--tls.port", ":5001",
"run")
if len(output) > 0 {
fmt.Fprintf(os.Stdout, "%s\n", output)
}
if err != nil {
t.Fatal(err)
}
}
func TestChallengeTLS_Run_CSR(t *testing.T) { func TestChallengeTLS_Run_CSR(t *testing.T) {
loader.CleanLegoFiles() loader.CleanLegoFiles()