feat: support for certificate with raw IP SAN (RFC8738) (#1838)
This commit is contained in:
parent
dfcf4412f7
commit
5a70c3661d
5 changed files with 141 additions and 7 deletions
|
@ -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}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
|
||||
|
|
Loading…
Reference in a new issue