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 (
"encoding/base64"
"errors"
"net"
"github.com/go-acme/lego/v4/acme"
)
@ -13,7 +14,13 @@ type OrderService service
func (o *OrderService) New(domains []string) (acme.ExtendedOrder, error) {
var identifiers []acme.Identifier
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}

View file

@ -14,6 +14,7 @@ import (
"errors"
"fmt"
"math/big"
"net"
"strings"
"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) {
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{
Subject: pkix.Name{CommonName: domain},
DNSNames: san,
Subject: pkix.Name{CommonName: domain},
DNSNames: dnsNames,
IPAddresses: ipAddresses,
}
if mustStaple {
@ -218,6 +230,13 @@ func ExtractDomains(cert *x509.Certificate) []string {
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
}
@ -238,6 +257,13 @@ func ExtractDomainsCSR(csr *x509.CertificateRequest) []string {
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
}
@ -280,9 +306,15 @@ func generateDerCert(privateKey *rsa.PrivateKey, expiration time.Time, domain st
KeyUsage: x509.KeyUsageKeyEncipherment,
BasicConstraintsValid: true,
DNSNames: []string{domain},
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)
}

View file

@ -7,6 +7,7 @@ import (
"crypto/subtle"
"crypto/tls"
"encoding/asn1"
"net"
"net/http"
"testing"
@ -14,6 +15,7 @@ import (
"github.com/go-acme/lego/v4/acme/api"
"github.com/go-acme/lego/v4/challenge"
"github.com/go-acme/lego/v4/platform/tester"
"github.com/miekg/dns"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
@ -21,10 +23,12 @@ import (
func TestChallenge(t *testing.T) {
_, apiURL := tester.SetupFakeAPI(t)
domain := "localhost:23457"
domain := "localhost"
port := "23457"
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,
})
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{
Identifier: acme.Identifier{
Type: "dns",
Value: domain,
},
Challenges: []acme.Challenge{
@ -116,3 +121,73 @@ func TestChallengeInvalidPort(t *testing.T) {
assert.Contains(t, err.Error(), "invalid port")
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 ;)).
func sanitizedDomain(domain string) string {
safe, err := idna.ToASCII(strings.ReplaceAll(domain, "*", "_"))
safe, err := idna.ToASCII(strings.NewReplacer(":", "-", "*", "_").Replace(domain))
if err != nil {
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) {
loader.CleanLegoFiles()