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 (
|
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}
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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))
|
||||||
|
}
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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()
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue