diff --git a/acme/api/order.go b/acme/api/order.go index 7b2a2be7..446f6d1a 100644 --- a/acme/api/order.go +++ b/acme/api/order.go @@ -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} diff --git a/certcrypto/crypto.go b/certcrypto/crypto.go index c40bbfcc..9f748691 100644 --- a/certcrypto/crypto.go +++ b/certcrypto/crypto.go @@ -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) } diff --git a/challenge/tlsalpn01/tls_alpn_challenge_test.go b/challenge/tlsalpn01/tls_alpn_challenge_test.go index 4bfb47bf..75953e97 100644 --- a/challenge/tlsalpn01/tls_alpn_challenge_test.go +++ b/challenge/tlsalpn01/tls_alpn_challenge_test.go @@ -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)) +} diff --git a/cmd/certs_storage.go b/cmd/certs_storage.go index 5709f1a8..ed18018c 100644 --- a/cmd/certs_storage.go +++ b/cmd/certs_storage.go @@ -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) } diff --git a/e2e/challenges_test.go b/e2e/challenges_test.go index 55fd3f0e..d768e101 100644 --- a/e2e/challenges_test.go +++ b/e2e/challenges_test.go @@ -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()