WIP: rebase fork onto upstream/main (v1.103.0) #15

Closed
codinget wants to merge 670 commits from webnet into save/webnet-2026-07-29
15 changed files with 377 additions and 220 deletions
Showing only changes of commit a84a264228 - Show all commits
+1 -1
View File
@@ -173,4 +173,4 @@
});
};
}
# nix-direnv cache busting line: sha256-XZtd1uvpeO4djloYPurifkxc6BnhRDtBfIO6lE38Usc=
# nix-direnv cache busting line: sha256-plOIT+B2mVkUr038G4+qZt8BlUrmAH2oaVf99wK7iik=
+2 -2
View File
@@ -4,7 +4,7 @@
"sri": "sha256-TwIaPmGVHO9ZwdaO/jDStgWGQtKa4smS3er1i5aTNTw="
},
"vendor": {
"goModSum": "sha256-EDmslq4EjYvjO5S6HwsddmmtxMqB3cVijhYBbcyBGPQ=",
"sri": "sha256-XZtd1uvpeO4djloYPurifkxc6BnhRDtBfIO6lE38Usc="
"goModSum": "sha256-nSe6X/h4KPoFvqpszhGr6u9BO7BQgL6yjpdRAGEABzE=",
"sri": "sha256-plOIT+B2mVkUr038G4+qZt8BlUrmAH2oaVf99wK7iik="
}
}
+1 -1
View File
@@ -103,7 +103,7 @@ require (
github.com/tailscale/depaware v0.0.0-20251001183927-9c2ad255ef3f
github.com/tailscale/gliderssh v0.3.4-0.20260716005906-1a0f895faf28
github.com/tailscale/goexpect v0.0.0-20210902213824-6e8c725cea41
github.com/tailscale/golang-x-crypto v0.0.0-20250404221719-a5573b049869
github.com/tailscale/golang-x-crypto v0.0.0-20260720153645-2ba0bf7866ed
github.com/tailscale/hujson v0.0.0-20260302212456-ecc657c15afd
github.com/tailscale/mkctr v0.0.0-20260107121656-ea857e3e500b
github.com/tailscale/netlink v1.1.1-0.20240822203006-4d49adab4de7
+2 -2
View File
@@ -1195,8 +1195,8 @@ github.com/tailscale/go-winio v0.0.0-20231025203758-c4f33415bf55 h1:Gzfnfk2TWrk8
github.com/tailscale/go-winio v0.0.0-20231025203758-c4f33415bf55/go.mod h1:4k4QO+dQ3R5FofL+SanAUZe+/QfeK0+OIuwDIRu2vSg=
github.com/tailscale/goexpect v0.0.0-20210902213824-6e8c725cea41 h1:/V2rCMMWcsjYaYO2MeovLw+ClP63OtXgCF2Y1eb8+Ns=
github.com/tailscale/goexpect v0.0.0-20210902213824-6e8c725cea41/go.mod h1:/roCdA6gg6lQyw/Oz6gIIGu3ggJKYhF+WC/AQReE5XQ=
github.com/tailscale/golang-x-crypto v0.0.0-20250404221719-a5573b049869 h1:SRL6irQkKGQKKLzvQP/ke/2ZuB7Py5+XuqtOgSj+iMM=
github.com/tailscale/golang-x-crypto v0.0.0-20250404221719-a5573b049869/go.mod h1:ikbF+YT089eInTp9f2vmvy4+ZVnW5hzX1q2WknxSprQ=
github.com/tailscale/golang-x-crypto v0.0.0-20260720153645-2ba0bf7866ed h1:uyvHhX1FQada0vVk8CSHa4tJT96EEAkTypaYz8Tq5Nc=
github.com/tailscale/golang-x-crypto v0.0.0-20260720153645-2ba0bf7866ed/go.mod h1:NC3xRCu4UR+m4n6ix8b6oLLbHa820Y0StbOQEdWTDo0=
github.com/tailscale/hujson v0.0.0-20260302212456-ecc657c15afd h1:Rf9uhF1+VJ7ZHqxrG8pJ6YacmHvVCmByDmGbAWCc/gA=
github.com/tailscale/hujson v0.0.0-20260302212456-ecc657c15afd/go.mod h1:EbW0wDK/qEUYI0A5bqq0C2kF8JTQwWONmGDBbzsxxHo=
github.com/tailscale/mkctr v0.0.0-20260107121656-ea857e3e500b h1:QKqCnmp0qHWUHySySKjpuhZANzRn7XrTVZWUuUgJ3lQ=
+1 -1
View File
@@ -16,4 +16,4 @@
) {
src = ./.;
}).shellNix
# nix-direnv cache busting line: sha256-XZtd1uvpeO4djloYPurifkxc6BnhRDtBfIO6lE38Usc=
# nix-direnv cache busting line: sha256-plOIT+B2mVkUr038G4+qZt8BlUrmAH2oaVf99wK7iik=
+47 -59
View File
@@ -31,12 +31,11 @@ import (
"crypto/x509/pkix"
"encoding/asn1"
"encoding/base64"
"encoding/hex"
"encoding/json"
"encoding/pem"
"errors"
"fmt"
"math/big"
"net"
"net/http"
"strings"
"sync"
@@ -187,10 +186,11 @@ func (c *Client) Discover(ctx context.Context) (Directory, error) {
KeyChange string `json:"keyChange"`
RenewalInfo string `json:"renewalInfo"`
Meta struct {
Terms string `json:"termsOfService"`
Website string `json:"website"`
CAA []string `json:"caaIdentities"`
ExternalAcct bool `json:"externalAccountRequired"`
Terms string `json:"termsOfService"`
Website string `json:"website"`
CAA []string `json:"caaIdentities"`
ExternalAcct bool `json:"externalAccountRequired"`
Profiles map[string]string `json:"profiles"`
}
}
if err := json.NewDecoder(res.Body).Decode(&v); err != nil {
@@ -211,6 +211,7 @@ func (c *Client) Discover(ctx context.Context) (Directory, error) {
Website: v.Meta.Website,
CAA: v.Meta.CAA,
ExternalAccountRequired: v.Meta.ExternalAcct,
Profiles: v.Meta.Profiles,
}
return *c.dir, nil
}
@@ -397,6 +398,10 @@ func (c *Client) authorize(ctx context.Context, typ, val string) (*Authorization
if _, err := c.Discover(ctx); err != nil {
return nil, err
}
if c.dir.AuthzURL == "" {
// Pre-Authorization is unsupported
return nil, errPreAuthorizationNotSupported
}
type authzID struct {
Type string `json:"type"`
@@ -511,7 +516,7 @@ func (c *Client) WaitAuthorization(ctx context.Context, url string) (*Authorizat
// while waiting for a final authorization status.
d := retryAfter(res.Header.Get("Retry-After"))
if d == 0 {
// Given that the fastest challenges TLS-SNI and HTTP-01
// Given that the fastest challenges TLS-ALPN and HTTP-01
// require a CA to make at least 1 network round trip
// and most likely persist a challenge state,
// this default delay seems reasonable.
@@ -612,50 +617,28 @@ func (c *Client) HTTP01ChallengePath(token string) string {
}
// TLSSNI01ChallengeCert creates a certificate for TLS-SNI-01 challenge response.
// Always returns an error.
//
// Deprecated: This challenge type is unused in both draft-02 and RFC versions of the ACME spec.
func (c *Client) TLSSNI01ChallengeCert(token string, opt ...CertOption) (cert tls.Certificate, name string, err error) {
ka, err := keyAuth(c.Key.Public(), token)
if err != nil {
return tls.Certificate{}, "", err
}
b := sha256.Sum256([]byte(ka))
h := hex.EncodeToString(b[:])
name = fmt.Sprintf("%s.%s.acme.invalid", h[:32], h[32:])
cert, err = tlsChallengeCert([]string{name}, opt)
if err != nil {
return tls.Certificate{}, "", err
}
return cert, name, nil
// Deprecated: This challenge type was only present in pre-standardized ACME
// protocol drafts and is insecure for use in shared hosting environments.
func (c *Client) TLSSNI01ChallengeCert(token string, opt ...CertOption) (tls.Certificate, string, error) {
return tls.Certificate{}, "", errPreRFC
}
// TLSSNI02ChallengeCert creates a certificate for TLS-SNI-02 challenge response.
// Always returns an error.
//
// Deprecated: This challenge type is unused in both draft-02 and RFC versions of the ACME spec.
func (c *Client) TLSSNI02ChallengeCert(token string, opt ...CertOption) (cert tls.Certificate, name string, err error) {
b := sha256.Sum256([]byte(token))
h := hex.EncodeToString(b[:])
sanA := fmt.Sprintf("%s.%s.token.acme.invalid", h[:32], h[32:])
ka, err := keyAuth(c.Key.Public(), token)
if err != nil {
return tls.Certificate{}, "", err
}
b = sha256.Sum256([]byte(ka))
h = hex.EncodeToString(b[:])
sanB := fmt.Sprintf("%s.%s.ka.acme.invalid", h[:32], h[32:])
cert, err = tlsChallengeCert([]string{sanA, sanB}, opt)
if err != nil {
return tls.Certificate{}, "", err
}
return cert, sanA, nil
// Deprecated: This challenge type was only present in pre-standardized ACME
// protocol drafts and is insecure for use in shared hosting environments.
func (c *Client) TLSSNI02ChallengeCert(token string, opt ...CertOption) (tls.Certificate, string, error) {
return tls.Certificate{}, "", errPreRFC
}
// TLSALPN01ChallengeCert creates a certificate for TLS-ALPN-01 challenge response.
// Servers can present the certificate to validate the challenge and prove control
// over a domain name. For more details on TLS-ALPN-01 see
// https://tools.ietf.org/html/draft-shoemaker-acme-tls-alpn-00#section-3
// over an identifier (either a DNS name or the textual form of an IPv4 or IPv6
// address). For more details on TLS-ALPN-01 see
// https://www.rfc-editor.org/rfc/rfc8737 and https://www.rfc-editor.org/rfc/rfc8738
//
// The token argument is a Challenge.Token value.
// If a WithKey option is provided, its private part signs the returned cert,
@@ -663,9 +646,13 @@ func (c *Client) TLSSNI02ChallengeCert(token string, opt ...CertOption) (cert tl
// If no WithKey option is provided, a new ECDSA key is generated using P-256 curve.
//
// The returned certificate is valid for the next 24 hours and must be presented only when
// the server name in the TLS ClientHello matches the domain, and the special acme-tls/1 ALPN protocol
// the server name in the TLS ClientHello matches the identifier, and the special acme-tls/1 ALPN protocol
// has been specified.
func (c *Client) TLSALPN01ChallengeCert(token, domain string, opt ...CertOption) (cert tls.Certificate, err error) {
//
// Validation requests for IP address identifiers will use the reverse DNS form in the server name
// in the TLS ClientHello since the SNI extension is not supported for IP addresses.
// See RFC 8738 Section 6 for more information.
func (c *Client) TLSALPN01ChallengeCert(token, identifier string, opt ...CertOption) (cert tls.Certificate, err error) {
ka, err := keyAuth(c.Key.Public(), token)
if err != nil {
return tls.Certificate{}, err
@@ -695,7 +682,7 @@ func (c *Client) TLSALPN01ChallengeCert(token, domain string, opt ...CertOption)
}
tmpl.ExtraExtensions = append(tmpl.ExtraExtensions, acmeExtension)
newOpt = append(newOpt, WithTemplate(tmpl))
return tlsChallengeCert([]string{domain}, newOpt)
return tlsChallengeCert(identifier, newOpt)
}
// popNonce returns a nonce value previously stored with c.addNonce
@@ -749,7 +736,7 @@ func (c *Client) addNonce(h http.Header) {
}
func (c *Client) fetchNonce(ctx context.Context, url string) (string, error) {
r, err := http.NewRequest("HEAD", url, nil)
r, err := http.NewRequestWithContext(ctx, "HEAD", url, nil)
if err != nil {
return "", err
}
@@ -813,11 +800,15 @@ func defaultTLSChallengeCertTemplate() *x509.Certificate {
}
}
// tlsChallengeCert creates a temporary certificate for TLS-SNI challenges
// with the given SANs and auto-generated public/private key pair.
// The Subject Common Name is set to the first SAN to aid debugging.
// tlsChallengeCert creates a temporary certificate for TLS-ALPN challenges
// for the given identifier, using an auto-generated public/private key pair.
//
// If the provided identifier is a domain name, it will be used as a DNS type SAN and for the
// subject common name. If the provided identifier is an IP address it will be used as an IP type
// SAN.
//
// To create a cert with a custom key pair, specify WithKey option.
func tlsChallengeCert(san []string, opt []CertOption) (tls.Certificate, error) {
func tlsChallengeCert(identifier string, opt []CertOption) (tls.Certificate, error) {
var key crypto.Signer
tmpl := defaultTLSChallengeCertTemplate()
for _, o := range opt {
@@ -841,9 +832,12 @@ func tlsChallengeCert(san []string, opt []CertOption) (tls.Certificate, error) {
return tls.Certificate{}, err
}
}
tmpl.DNSNames = san
if len(san) > 0 {
tmpl.Subject.CommonName = san[0]
if ip := net.ParseIP(identifier); ip != nil {
tmpl.IPAddresses = []net.IP{ip}
} else {
tmpl.DNSNames = []string{identifier}
tmpl.Subject.CommonName = identifier
}
der, err := x509.CreateCertificate(rand.Reader, tmpl, tmpl, key.Public(), key)
@@ -856,11 +850,5 @@ func tlsChallengeCert(san []string, opt []CertOption) (tls.Certificate, error) {
}, nil
}
// encodePEM returns b encoded as PEM with block of type typ.
func encodePEM(typ string, b []byte) []byte {
pb := &pem.Block{Type: typ, Bytes: b}
return pem.EncodeToMemory(pb)
}
// timeNow is time.Now, except in tests which can mess with it.
var timeNow = time.Now
+86 -109
View File
@@ -9,13 +9,13 @@ import (
"context"
"crypto/rand"
"crypto/rsa"
"crypto/tls"
"crypto/x509"
"crypto/x509/pkix"
"encoding/base64"
"encoding/hex"
"encoding/json"
"encoding/pem"
"errors"
"fmt"
"io"
"math/big"
@@ -23,7 +23,6 @@ import (
"net/http/httptest"
"net/url"
"reflect"
"sort"
"strings"
"testing"
"time"
@@ -267,6 +266,57 @@ func TestAuthorizeValid(t *testing.T) {
}
}
func TestAuthorizeUnsupported(t *testing.T) {
const (
nonce = "https://example.com/acme/new-nonce"
reg = "https://example.com/acme/new-acct"
order = "https://example.com/acme/new-order"
revoke = "https://example.com/acme/revoke-cert"
keychange = "https://example.com/acme/key-change"
metaTerms = "https://example.com/acme/terms/2017-5-30"
metaWebsite = "https://www.example.com/"
metaCAA = "example.com"
)
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Replay-Nonce", "nonce")
if r.Method == http.MethodHead {
return
}
switch r.URL.Path {
case "/": // Directory
w.Header().Set("Content-Type", "application/json")
fmt.Fprintf(w, `{
"newNonce": %q,
"newAccount": %q,
"newOrder": %q,
"revokeCert": %q,
"keyChange": %q,
"meta": {
"termsOfService": %q,
"website": %q,
"caaIdentities": [%q],
"externalAccountRequired": true
}
}`, nonce, reg, order, revoke, keychange, metaTerms, metaWebsite, metaCAA)
w.WriteHeader(http.StatusOK)
case "/acme/new-authz":
w.WriteHeader(http.StatusBadRequest)
}
}))
defer ts.Close()
client := &Client{Key: testKey, DirectoryURL: ts.URL}
dir, err := client.Discover(context.Background())
if err != nil {
t.Fatal(err)
}
if dir.AuthzURL != "" {
t.Fatalf("expected AuthzURL to be empty, got %q", dir.AuthzURL)
}
if _, err := client.Authorize(context.Background(), "example.com"); !errors.Is(err, errPreAuthorizationNotSupported) {
t.Errorf("expected err to indicate pre-authorization is unsupported, got %+v", err)
}
}
func TestWaitAuthorization(t *testing.T) {
t.Run("wait loop", func(t *testing.T) {
var count int
@@ -755,71 +805,6 @@ func TestLinkHeader(t *testing.T) {
}
}
func TestTLSSNI01ChallengeCert(t *testing.T) {
const (
token = "evaGxfADs6pSRb2LAv9IZf17Dt3juxGJ-PCt92wr-oA"
// echo -n <token.testKeyECThumbprint> | shasum -a 256
san = "dbbd5eefe7b4d06eb9d1d9f5acb4c7cd.a27d320e4b30332f0b6cb441734ad7b0.acme.invalid"
)
tlscert, name, err := newTestClient().TLSSNI01ChallengeCert(token)
if err != nil {
t.Fatal(err)
}
if n := len(tlscert.Certificate); n != 1 {
t.Fatalf("len(tlscert.Certificate) = %d; want 1", n)
}
cert, err := x509.ParseCertificate(tlscert.Certificate[0])
if err != nil {
t.Fatal(err)
}
if len(cert.DNSNames) != 1 || cert.DNSNames[0] != san {
t.Fatalf("cert.DNSNames = %v; want %q", cert.DNSNames, san)
}
if cert.DNSNames[0] != name {
t.Errorf("cert.DNSNames[0] != name: %q vs %q", cert.DNSNames[0], name)
}
if cn := cert.Subject.CommonName; cn != san {
t.Errorf("cert.Subject.CommonName = %q; want %q", cn, san)
}
}
func TestTLSSNI02ChallengeCert(t *testing.T) {
const (
token = "evaGxfADs6pSRb2LAv9IZf17Dt3juxGJ-PCt92wr-oA"
// echo -n evaGxfADs6pSRb2LAv9IZf17Dt3juxGJ-PCt92wr-oA | shasum -a 256
sanA = "7ea0aaa69214e71e02cebb18bb867736.09b730209baabf60e43d4999979ff139.token.acme.invalid"
// echo -n <token.testKeyECThumbprint> | shasum -a 256
sanB = "dbbd5eefe7b4d06eb9d1d9f5acb4c7cd.a27d320e4b30332f0b6cb441734ad7b0.ka.acme.invalid"
)
tlscert, name, err := newTestClient().TLSSNI02ChallengeCert(token)
if err != nil {
t.Fatal(err)
}
if n := len(tlscert.Certificate); n != 1 {
t.Fatalf("len(tlscert.Certificate) = %d; want 1", n)
}
cert, err := x509.ParseCertificate(tlscert.Certificate[0])
if err != nil {
t.Fatal(err)
}
names := []string{sanA, sanB}
if !reflect.DeepEqual(cert.DNSNames, names) {
t.Fatalf("cert.DNSNames = %v;\nwant %v", cert.DNSNames, names)
}
sort.Strings(cert.DNSNames)
i := sort.SearchStrings(cert.DNSNames, name)
if i >= len(cert.DNSNames) || cert.DNSNames[i] != name {
t.Errorf("%v doesn't have %q", cert.DNSNames, name)
}
if cn := cert.Subject.CommonName; cn != sanA {
t.Errorf("CommonName = %q; want %q", cn, sanA)
}
}
func TestTLSALPN01ChallengeCert(t *testing.T) {
const (
token = "evaGxfADs6pSRb2LAv9IZf17Dt3juxGJ-PCt92wr-oA"
@@ -876,6 +861,7 @@ func TestTLSChallengeCertOpt(t *testing.T) {
if err != nil {
t.Fatal(err)
}
domain := "example.com"
tmpl := &x509.Certificate{
SerialNumber: big.NewInt(2),
Subject: pkix.Name{Organization: []string{"Test"}},
@@ -884,52 +870,43 @@ func TestTLSChallengeCertOpt(t *testing.T) {
opts := []CertOption{WithKey(key), WithTemplate(tmpl)}
client := newTestClient()
cert1, _, err := client.TLSSNI01ChallengeCert("token", opts...)
if err != nil {
t.Fatal(err)
}
cert2, _, err := client.TLSSNI02ChallengeCert("token", opts...)
cert, err := client.TLSALPN01ChallengeCert("token", domain, opts...)
if err != nil {
t.Fatal(err)
}
for i, tlscert := range []tls.Certificate{cert1, cert2} {
// verify generated cert private key
tlskey, ok := tlscert.PrivateKey.(*rsa.PrivateKey)
if !ok {
t.Errorf("%d: tlscert.PrivateKey is %T; want *rsa.PrivateKey", i, tlscert.PrivateKey)
continue
}
if tlskey.D.Cmp(key.D) != 0 {
t.Errorf("%d: tlskey.D = %v; want %v", i, tlskey.D, key.D)
}
// verify generated cert public key
x509Cert, err := x509.ParseCertificate(tlscert.Certificate[0])
if err != nil {
t.Errorf("%d: %v", i, err)
continue
}
tlspub, ok := x509Cert.PublicKey.(*rsa.PublicKey)
if !ok {
t.Errorf("%d: x509Cert.PublicKey is %T; want *rsa.PublicKey", i, x509Cert.PublicKey)
continue
}
if tlspub.N.Cmp(key.N) != 0 {
t.Errorf("%d: tlspub.N = %v; want %v", i, tlspub.N, key.N)
}
// verify template option
sn := big.NewInt(2)
if x509Cert.SerialNumber.Cmp(sn) != 0 {
t.Errorf("%d: SerialNumber = %v; want %v", i, x509Cert.SerialNumber, sn)
}
org := []string{"Test"}
if !reflect.DeepEqual(x509Cert.Subject.Organization, org) {
t.Errorf("%d: Subject.Organization = %+v; want %+v", i, x509Cert.Subject.Organization, org)
}
for _, v := range x509Cert.DNSNames {
if !strings.HasSuffix(v, ".acme.invalid") {
t.Errorf("%d: invalid DNSNames element: %q", i, v)
}
// verify generated cert private key
tlskey, ok := cert.PrivateKey.(*rsa.PrivateKey)
if !ok {
t.Fatalf("tlscert.PrivateKey is %T; want *rsa.PrivateKey", cert.PrivateKey)
}
if tlskey.D.Cmp(key.D) != 0 {
t.Errorf("tlskey.D = %v; want %v", tlskey.D, key.D)
}
// verify generated cert public key
x509Cert, err := x509.ParseCertificate(cert.Certificate[0])
if err != nil {
t.Fatal(err)
}
tlspub, ok := x509Cert.PublicKey.(*rsa.PublicKey)
if !ok {
t.Fatalf("x509Cert.PublicKey is %T; want *rsa.PublicKey", x509Cert.PublicKey)
}
if tlspub.N.Cmp(key.N) != 0 {
t.Errorf("tlspub.N = %v; want %v", tlspub.N, key.N)
}
// verify template option
sn := big.NewInt(2)
if x509Cert.SerialNumber.Cmp(sn) != 0 {
t.Errorf("SerialNumber = %v; want %v", x509Cert.SerialNumber, sn)
}
org := []string{"Test"}
if !reflect.DeepEqual(x509Cert.Subject.Organization, org) {
t.Errorf("Subject.Organization = %+v; want %+v", x509Cert.Subject.Organization, org)
}
for _, v := range x509Cert.DNSNames {
if v != domain {
t.Errorf("invalid DNSNames element: %q", v)
}
}
}
+4 -7
View File
@@ -66,7 +66,7 @@ func (c *Client) retryTimer() *retryTimer {
// The n argument is always bounded between 1 and 30.
// The returned value is always greater than 0.
func defaultBackoff(n int, r *http.Request, res *http.Response) time.Duration {
const max = 10 * time.Second
const maxVal = 10 * time.Second
var jitter time.Duration
if x, err := rand.Int(rand.Reader, big.NewInt(1000)); err == nil {
// Set the minimum to 1ms to avoid a case where
@@ -86,10 +86,7 @@ func defaultBackoff(n int, r *http.Request, res *http.Response) time.Duration {
n = 30
}
d := time.Duration(1<<uint(n-1))*time.Second + jitter
if d > max {
return max
}
return d
return min(d, maxVal)
}
// retryAfter parses a Retry-After HTTP header value,
@@ -131,7 +128,7 @@ func wantStatus(codes ...int) resOkay {
func (c *Client) get(ctx context.Context, url string, ok resOkay) (*http.Response, error) {
retry := c.retryTimer()
for {
req, err := http.NewRequest("GET", url, nil)
req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
if err != nil {
return nil, err
}
@@ -231,7 +228,7 @@ func (c *Client) postNoRetry(ctx context.Context, key crypto.Signer, url string,
if err != nil {
return nil, nil, err
}
req, err := http.NewRequest("POST", url, bytes.NewReader(b))
req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewReader(b))
if err != nil {
return nil, nil, err
}
+1 -1
View File
@@ -92,7 +92,7 @@ func jwsEncodeJSON(claimset interface{}, key crypto.Signer, kid KeyID, nonce, ur
if err != nil {
return nil, err
}
phead := base64.RawURLEncoding.EncodeToString([]byte(phJSON))
phead := base64.RawURLEncoding.EncodeToString(phJSON)
var payload string
if val, ok := claimset.(string); ok {
payload = val
+16 -3
View File
@@ -54,6 +54,9 @@ func (c *Client) registerRFC(ctx context.Context, acct *Account, prompt func(tos
Contact: acct.Contact,
}
if c.dir.Terms != "" {
if prompt == nil {
return nil, errors.New("acme: missing Manager.Prompt to accept server's terms of service")
}
req.TermsAgreed = prompt(c.dir.Terms)
}
@@ -207,6 +210,7 @@ func (c *Client) AuthorizeOrder(ctx context.Context, id []AuthzID, opt ...OrderO
NotBefore string `json:"notBefore,omitempty"`
NotAfter string `json:"notAfter,omitempty"`
Replaces string `json:"replaces,omitempty"`
Profile string `json:"profile,omitempty"`
}{}
for _, v := range id {
req.Identifiers = append(req.Identifiers, wireAuthzID{
@@ -228,6 +232,15 @@ func (c *Client) AuthorizeOrder(ctx context.Context, id []AuthzID, opt ...OrderO
return nil, fmt.Errorf("failed to parse certificate being replaced: %w", err)
}
req.Replaces = certRenewalIdentifier(cert)
case orderProfileOpt:
if !dir.Profiles.isSupported() {
return nil, ErrCADoesNotSupportProfiles
}
profileName := string(o)
if !dir.Profiles.Has(profileName) {
return nil, fmt.Errorf("%w %s", ErrProfileNotInSetOfSupportedProfiles, profileName)
}
req.Profile = profileName
default:
// Package's fault if we let this happen.
panic(fmt.Sprintf("unsupported order option type %T", o))
@@ -242,7 +255,7 @@ func (c *Client) AuthorizeOrder(ctx context.Context, id []AuthzID, opt ...OrderO
return responseOrder(res)
}
// GetOrder retrives an order identified by the given URL.
// GetOrder retrieves an order identified by the given URL.
// For orders created with AuthorizeOrder, the url value is Order.URI.
//
// If a caller needs to poll an order until its status is final,
@@ -282,7 +295,7 @@ func (c *Client) WaitOrder(ctx context.Context, url string) (*Order, error) {
case err != nil:
// Skip and retry.
case o.Status == StatusInvalid:
return nil, &OrderError{OrderURL: o.URI, Status: o.Status}
return nil, &OrderError{OrderURL: o.URI, Status: o.Status, Problem: o.Error}
case o.Status == StatusReady || o.Status == StatusValid:
return o, nil
}
@@ -379,7 +392,7 @@ func (c *Client) CreateOrderCert(ctx context.Context, url string, csr []byte, bu
}
// The only acceptable status post finalize and WaitOrder is "valid".
if o.Status != StatusValid {
return nil, "", &OrderError{OrderURL: o.URI, Status: o.Status}
return nil, "", &OrderError{OrderURL: o.URI, Status: o.Status, Problem: o.Error}
}
crt, err := c.fetchCertRFC(ctx, o.CertURL, bundle)
return crt, o.CertURL, err
+137 -3
View File
@@ -99,6 +99,61 @@ func TestRFC_Discover(t *testing.T) {
if !dir.ExternalAccountRequired {
t.Error("dir.Meta.ExternalAccountRequired is false")
}
if dir.Profiles != nil {
t.Errorf("dir.Profiles is expected to be nil, got %+v", dir.Profiles)
}
}
func TestDiscover_WithProfiles(t *testing.T) {
const (
nonce = "https://example.com/acme/new-nonce"
reg = "https://example.com/acme/new-acct"
order = "https://example.com/acme/new-order"
authz = "https://example.com/acme/new-authz"
revoke = "https://example.com/acme/revoke-cert"
keychange = "https://example.com/acme/key-change"
metaTerms = "https://example.com/acme/terms/2017-5-30"
metaWebsite = "https://www.example.com/"
metaCAA = "example.com"
)
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
fmt.Fprintf(w, `{
"newNonce": %q,
"newAccount": %q,
"newOrder": %q,
"newAuthz": %q,
"revokeCert": %q,
"keyChange": %q,
"meta": {
"termsOfService": %q,
"website": %q,
"caaIdentities": [%q],
"externalAccountRequired": true,
"profiles": {
"default": "Your favorite default profile",
"tlsserver": "New and improved",
"client": "For all your mutual TLS needs"
}
}
}`, nonce, reg, order, authz, revoke, keychange, metaTerms, metaWebsite, metaCAA)
}))
defer ts.Close()
c := &Client{DirectoryURL: ts.URL}
dir, err := c.Discover(context.Background())
if err != nil {
t.Fatal(err)
}
expected := Profiles(map[string]string{"default": "Your favorite default profile", "tlsserver": "New and improved", "client": "For all your mutual TLS needs"})
if dir.Profiles == nil {
t.Errorf("expected directory to be %+v; got nil", expected)
}
for key, value := range dir.Profiles {
if expValue := expected.GetDescription(key); value != expValue {
t.Errorf("expected key %+q to have description %+q; got %+q", key, expected, value)
}
}
}
func TestRFC_popNonce(t *testing.T) {
@@ -247,6 +302,27 @@ func (s *acmeServer) start() {
return
}
if r.URL.Path == "/directory-with-profiles" {
fmt.Fprintf(w, `{
"newNonce": %q,
"newAccount": %q,
"newOrder": %q,
"newAuthz": %q,
"revokeCert": %q,
"keyChange": %q,
"meta": {"termsOfService": %q, "profiles": {"default": "Default", "server": "Server", "client": "Client"}}
}`,
s.url("/acme/new-nonce"),
s.url("/acme/new-account"),
s.url("/acme/new-order"),
s.url("/acme/new-authz"),
s.url("/acme/revoke-cert"),
s.url("/acme/key-change"),
s.url("/terms"),
)
return
}
// All other responses contain a nonce value unconditionally.
w.Header().Set("Replay-Nonce", s.nonce())
if r.URL.Path == "/acme/new-nonce" {
@@ -795,6 +871,51 @@ func TestRFC_AuthorizeOrder(t *testing.T) {
}
}
func TestRFC_AuthorizeOrder_WithOrderProfile(t *testing.T) {
s := newACMEServer()
s.handle("/acme/new-account", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Location", s.url("/accounts/1"))
w.WriteHeader(http.StatusOK)
w.Write([]byte(`{"status": "valid"}`))
})
s.handle("/acme/new-order", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Location", s.url("/orders/1"))
w.WriteHeader(http.StatusCreated)
fmt.Fprintf(w, `{
"status": "pending",
"expires": "2019-09-01T00:00:00Z",
"notBefore": "2019-08-31T00:00:00Z",
"notAfter": "2019-09-02T00:00:00Z",
"identifiers": [{"type":"dns", "value":"example.org"}],
"authorizations": [%q]
}`, s.url("/authz/1"))
})
s.start()
defer s.close()
cl := &Client{Key: testKeyEC, DirectoryURL: s.url("/directory-with-profiles")}
o, err := cl.AuthorizeOrder(context.Background(), DomainIDs("example.org"),
WithOrderNotBefore(time.Date(2019, 8, 31, 0, 0, 0, 0, time.UTC)),
WithOrderNotAfter(time.Date(2019, 9, 2, 0, 0, 0, 0, time.UTC)),
WithOrderProfile("server"),
)
if err != nil {
t.Fatal(err)
}
okOrder := &Order{
URI: s.url("/orders/1"),
Status: StatusPending,
Expires: time.Date(2019, 9, 1, 0, 0, 0, 0, time.UTC),
NotBefore: time.Date(2019, 8, 31, 0, 0, 0, 0, time.UTC),
NotAfter: time.Date(2019, 9, 2, 0, 0, 0, 0, time.UTC),
Identifiers: []AuthzID{{Type: "dns", Value: "example.org"}},
AuthzURLs: []string{s.url("/authz/1")},
}
if !reflect.DeepEqual(o, okOrder) {
t.Errorf("AuthorizeOrder = %+v; want %+v", o, okOrder)
}
}
func TestRFC_GetOrder(t *testing.T) {
s := newACMEServer()
s.handle("/acme/new-account", func(w http.ResponseWriter, r *http.Request) {
@@ -892,11 +1013,17 @@ func TestRFC_WaitOrderError(t *testing.T) {
s.handle("/orders/1", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Location", s.url("/orders/1"))
w.WriteHeader(http.StatusOK)
s := StatusPending
if count > 0 {
s = StatusInvalid
// https://www.rfc-editor.org/rfc/rfc8555#section-7.3.3
errorData := `{
"type": "urn:ietf:params:acme:error:userActionRequired",
"detail": "Terms of service have changed",
"instance": "https://example.com/acme/agreement/?token=W8Ih3PswD-8"
}`
fmt.Fprintf(w, `{"status": %q, "error": %s}`, StatusInvalid, errorData)
} else {
fmt.Fprintf(w, `{"status": %q}`, StatusPending)
}
fmt.Fprintf(w, `{"status": %q}`, s)
count++
})
s.start()
@@ -917,6 +1044,13 @@ func TestRFC_WaitOrderError(t *testing.T) {
if e.Status != StatusInvalid {
t.Errorf("e.Status = %q; want %q", e.Status, StatusInvalid)
}
if e.Problem == nil {
t.Errorf("e.Problem = nil")
}
expectedProbType := "urn:ietf:params:acme:error:userActionRequired"
if e.Problem.ProblemType != expectedProbType {
t.Errorf("e.Problem.ProblemType = %q; want %q", e.Problem.ProblemType, expectedProbType)
}
}
func TestRFC_CreateOrderCert(t *testing.T) {
+8 -1
View File
@@ -45,7 +45,14 @@ func TestSyncedToUpstream(t *testing.T) {
}
for _, de := range ents {
name := de.Name()
if name == "sync_to_upstream_test.go" {
switch name {
case "sync_to_upstream_test.go":
continue
case "pebble_test.go":
// We don't vendor upstream's pebble_test.go: its
// TestWithPebble downloads the Pebble module from
// outside our go.mod, then builds and runs its
// binaries during tests.
continue
}
if !strings.HasSuffix(name, ".go") {
+55 -3
View File
@@ -56,6 +56,19 @@ var (
// ErrNoAccount indicates that the Client's key has not been registered with the CA.
ErrNoAccount = errors.New("acme: account does not exist")
// errPreAuthorizationNotSupported indicates that the server does not
// support pre-authorization of identifiers.
errPreAuthorizationNotSupported = errors.New("acme: pre-authorization is not supported")
// ErrCADoesNotSupportProfiles indicates that [WithOrderProfile] was
// included with a CA that does not advertise support for profiles in
// their directory.
ErrCADoesNotSupportProfiles = errors.New("acme: certificate authority does not support profiles")
// ErrProfileNotInSetOfSupportedProfiles indicates that the profile
// specified with [WithOrderProfile} is not one supported by the CA
ErrProfileNotInSetOfSupportedProfiles = errors.New("acme: certificate authority does not advertise a profile with name")
)
// A Subproblem describes an ACME subproblem as reported in an Error.
@@ -150,17 +163,24 @@ func (a *AuthorizationError) Error() string {
// OrderError is returned from Client's order related methods.
// It indicates the order is unusable and the clients should start over with
// AuthorizeOrder.
// AuthorizeOrder. A Problem description may be provided with details on
// what caused the order to become unusable.
//
// The clients can still fetch the order object from CA using GetOrder
// to inspect its state.
type OrderError struct {
OrderURL string
Status string
// Problem is the error that occurred while processing the order.
Problem *Error
}
func (oe *OrderError) Error() string {
return fmt.Sprintf("acme: order %s status: %s", oe.OrderURL, oe.Status)
str := fmt.Sprintf("acme: order %s status: %s", oe.OrderURL, oe.Status)
if oe.Problem != nil {
str += fmt.Sprintf("; problem: %s", oe.Problem)
}
return str
}
// RateLimit reports whether err represents a rate limit error and
@@ -308,6 +328,10 @@ type Directory struct {
// ExternalAccountRequired indicates that the CA requires for all account-related
// requests to include external account binding information.
ExternalAccountRequired bool
// Profiles indicates that the CA supports specifying a profile for an
// order. See also [WithOrderNotAfter].
Profiles Profiles
}
// Order represents a client's request for a certificate.
@@ -383,6 +407,15 @@ func WithOrderNotAfter(t time.Time) OrderOption {
return orderNotAfterOpt(t)
}
// WithOrderProfile sets an order's Profile field for servers which support
// profiles.
// See also:
// * https://datatracker.ietf.org/doc/draft-aaron-acme-profiles/
// * https://letsencrypt.org/docs/profiles/
func WithOrderProfile(name string) OrderOption {
return orderProfileOpt(name)
}
type orderNotBeforeOpt time.Time
func (orderNotBeforeOpt) privateOrderOpt() {}
@@ -415,6 +448,10 @@ type orderReplacesCertDER []byte
func (orderReplacesCertDER) privateOrderOpt() {}
type orderProfileOpt string
func (orderProfileOpt) privateOrderOpt() {}
// Authorization encodes an authorization response.
type Authorization struct {
// URI uniquely identifies a authorization.
@@ -643,7 +680,7 @@ func (*certOptKey) privateCertOpt() {}
//
// In TLS ChallengeCert methods, the template is also used as parent,
// resulting in a self-signed certificate.
// The DNSNames field of t is always overwritten for tls-sni challenge certs.
// The DNSNames or IPAddresses fields of t are always overwritten for tls-alpn challenge certs.
func WithTemplate(t *x509.Certificate) CertOption {
return (*certOptTemplate)(t)
}
@@ -665,3 +702,18 @@ type RenewalInfo struct {
SuggestedWindow RenewalInfoWindow `json:"suggestedWindow"`
ExplanationURL string `json:"explanationURL"`
}
type Profiles map[string]string
func (ps Profiles) isSupported() bool {
return len(ps) > 0
}
func (ps Profiles) GetDescription(name string) string {
return ps[name]
}
func (ps Profiles) Has(name string) bool {
_, ok := ps[name]
return ok
}
+16
View File
@@ -217,3 +217,19 @@ func TestErrorStringerWithSubproblems(t *testing.T) {
t.Errorf("Unexpected error string: wanted %q, got %q", expectedStr, err.Error())
}
}
func TestOrderErrorProblem(t *testing.T) {
oe := &OrderError{
OrderURL: "url",
Status: "invalid",
Problem: &Error{
StatusCode: 400,
ProblemType: "type",
Detail: "detail",
},
}
want := "acme: order url status: invalid; problem: 400 type: detail"
if got := oe.Error(); got != want {
t.Errorf("oe.Error() = %q, want %q", got, want)
}
}
-27
View File
@@ -1,27 +0,0 @@
// Copyright 2019 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
//go:build go1.12
package acme
import "runtime/debug"
func init() {
// Set packageVersion if the binary was built in modules mode and x/crypto
// was not replaced with a different module.
info, ok := debug.ReadBuildInfo()
if !ok {
return
}
for _, m := range info.Deps {
if m.Path != "golang.org/x/crypto" {
continue
}
if m.Replace == nil {
packageVersion = m.Version
}
break
}
}