| // Copyright 2018 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. |
| |
| // Package acmetest provides types for testing acme and autocert packages. |
| // |
| // TODO: Consider moving this to x/crypto/acme/internal/acmetest for acme tests as well. |
| package acmetest |
| |
| import ( |
| "context" |
| "crypto" |
| "crypto/ecdsa" |
| "crypto/elliptic" |
| "crypto/rand" |
| "crypto/rsa" |
| "crypto/tls" |
| "crypto/x509" |
| "crypto/x509/pkix" |
| "encoding/asn1" |
| "encoding/base64" |
| "encoding/json" |
| "encoding/pem" |
| "fmt" |
| "io" |
| "math/big" |
| "net" |
| "net/http" |
| "net/http/httptest" |
| "path" |
| "strconv" |
| "strings" |
| "sync" |
| "testing" |
| "time" |
| |
| "golang.org/x/crypto/acme" |
| ) |
| |
| // CAServer is a simple test server which implements ACME spec bits needed for testing. |
| type CAServer struct { |
| rootKey crypto.Signer |
| rootCert []byte // DER encoding |
| rootTemplate *x509.Certificate |
| |
| t *testing.T |
| server *httptest.Server |
| issuer pkix.Name |
| challengeTypes []string |
| url string |
| roots *x509.CertPool |
| |
| mu sync.Mutex |
| certCount int // number of issued certs |
| acctRegistered bool // set once an account has been registered |
| domainAddr map[string]string // domain name to addr:port resolution |
| domainGetCert map[string]getCertificateFunc // domain name to GetCertificate function |
| domainHandler map[string]http.Handler // domain name to Handle function |
| validAuthz map[string]*authorization // valid authz, keyed by domain name |
| authorizations []*authorization // all authz, index is used as ID |
| orders []*order // index is used as order ID |
| errors []error // encountered client errors |
| } |
| |
| type getCertificateFunc func(hello *tls.ClientHelloInfo) (*tls.Certificate, error) |
| |
| // NewCAServer creates a new ACME test server. The returned CAServer issues |
| // certs signed with the CA roots available in the Roots field. |
| func NewCAServer(t *testing.T) *CAServer { |
| ca := &CAServer{t: t, |
| challengeTypes: []string{"fake-01", "tls-alpn-01", "http-01"}, |
| domainAddr: make(map[string]string), |
| domainGetCert: make(map[string]getCertificateFunc), |
| domainHandler: make(map[string]http.Handler), |
| validAuthz: make(map[string]*authorization), |
| } |
| |
| ca.server = httptest.NewUnstartedServer(http.HandlerFunc(ca.handle)) |
| |
| r, err := rand.Int(rand.Reader, big.NewInt(1000000)) |
| if err != nil { |
| panic(fmt.Sprintf("rand.Int: %v", err)) |
| } |
| ca.issuer = pkix.Name{ |
| Organization: []string{"Test Acme Co"}, |
| CommonName: "Root CA " + r.String(), |
| } |
| |
| return ca |
| } |
| |
| func (ca *CAServer) generateRoot() { |
| key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) |
| if err != nil { |
| panic(fmt.Sprintf("ecdsa.GenerateKey: %v", err)) |
| } |
| tmpl := &x509.Certificate{ |
| SerialNumber: big.NewInt(1), |
| Subject: ca.issuer, |
| NotBefore: time.Now(), |
| NotAfter: time.Now().Add(365 * 24 * time.Hour), |
| KeyUsage: x509.KeyUsageCertSign, |
| BasicConstraintsValid: true, |
| IsCA: true, |
| } |
| der, err := x509.CreateCertificate(rand.Reader, tmpl, tmpl, &key.PublicKey, key) |
| if err != nil { |
| panic(fmt.Sprintf("x509.CreateCertificate: %v", err)) |
| } |
| cert, err := x509.ParseCertificate(der) |
| if err != nil { |
| panic(fmt.Sprintf("x509.ParseCertificate: %v", err)) |
| } |
| ca.roots = x509.NewCertPool() |
| ca.roots.AddCert(cert) |
| ca.rootKey = key |
| ca.rootCert = der |
| ca.rootTemplate = tmpl |
| } |
| |
| // IssuerName sets the name of the issuing CA. |
| func (ca *CAServer) IssuerName(name pkix.Name) *CAServer { |
| if ca.url != "" { |
| panic("IssuerName must be called before Start") |
| } |
| ca.issuer = name |
| return ca |
| } |
| |
| // ChallengeTypes sets the supported challenge types. |
| func (ca *CAServer) ChallengeTypes(types ...string) *CAServer { |
| if ca.url != "" { |
| panic("ChallengeTypes must be called before Start") |
| } |
| ca.challengeTypes = types |
| return ca |
| } |
| |
| // URL returns the server address, after Start has been called. |
| func (ca *CAServer) URL() string { |
| if ca.url == "" { |
| panic("URL called before Start") |
| } |
| return ca.url |
| } |
| |
| // Roots returns a pool cointaining the CA root. |
| func (ca *CAServer) Roots() *x509.CertPool { |
| if ca.url == "" { |
| panic("Roots called before Start") |
| } |
| return ca.roots |
| } |
| |
| // Start starts serving requests. The server address becomes available in the |
| // URL field. |
| func (ca *CAServer) Start() *CAServer { |
| if ca.url == "" { |
| ca.generateRoot() |
| ca.server.Start() |
| ca.t.Cleanup(ca.server.Close) |
| ca.url = ca.server.URL |
| } |
| return ca |
| } |
| |
| func (ca *CAServer) serverURL(format string, arg ...interface{}) string { |
| return ca.server.URL + fmt.Sprintf(format, arg...) |
| } |
| |
| func (ca *CAServer) addr(domain string) (string, bool) { |
| ca.mu.Lock() |
| defer ca.mu.Unlock() |
| addr, ok := ca.domainAddr[domain] |
| return addr, ok |
| } |
| |
| func (ca *CAServer) getCert(domain string) (getCertificateFunc, bool) { |
| ca.mu.Lock() |
| defer ca.mu.Unlock() |
| f, ok := ca.domainGetCert[domain] |
| return f, ok |
| } |
| |
| func (ca *CAServer) getHandler(domain string) (http.Handler, bool) { |
| ca.mu.Lock() |
| defer ca.mu.Unlock() |
| h, ok := ca.domainHandler[domain] |
| return h, ok |
| } |
| |
| func (ca *CAServer) httpErrorf(w http.ResponseWriter, code int, format string, a ...interface{}) { |
| s := fmt.Sprintf(format, a...) |
| ca.t.Errorf(format, a...) |
| http.Error(w, s, code) |
| } |
| |
| // Resolve adds a domain to address resolution for the ca to dial to |
| // when validating challenges for the domain authorization. |
| func (ca *CAServer) Resolve(domain, addr string) { |
| ca.mu.Lock() |
| defer ca.mu.Unlock() |
| ca.domainAddr[domain] = addr |
| } |
| |
| // ResolveGetCertificate redirects TLS connections for domain to f when |
| // validating challenges for the domain authorization. |
| func (ca *CAServer) ResolveGetCertificate(domain string, f getCertificateFunc) { |
| ca.mu.Lock() |
| defer ca.mu.Unlock() |
| ca.domainGetCert[domain] = f |
| } |
| |
| // ResolveHandler redirects HTTP requests for domain to f when |
| // validating challenges for the domain authorization. |
| func (ca *CAServer) ResolveHandler(domain string, h http.Handler) { |
| ca.mu.Lock() |
| defer ca.mu.Unlock() |
| ca.domainHandler[domain] = h |
| } |
| |
| type discovery struct { |
| NewNonce string `json:"newNonce"` |
| NewReg string `json:"newAccount"` |
| NewOrder string `json:"newOrder"` |
| NewAuthz string `json:"newAuthz"` |
| } |
| |
| type challenge struct { |
| URI string `json:"uri"` |
| Type string `json:"type"` |
| Token string `json:"token"` |
| } |
| |
| type authorization struct { |
| Status string `json:"status"` |
| Challenges []challenge `json:"challenges"` |
| |
| domain string |
| id int |
| } |
| |
| type order struct { |
| Status string `json:"status"` |
| AuthzURLs []string `json:"authorizations"` |
| FinalizeURL string `json:"finalize"` // CSR submit URL |
| CertURL string `json:"certificate"` // already issued cert |
| |
| leaf []byte // issued cert in DER format |
| } |
| |
| func (ca *CAServer) handle(w http.ResponseWriter, r *http.Request) { |
| ca.t.Logf("%s %s", r.Method, r.URL) |
| w.Header().Set("Replay-Nonce", "nonce") |
| // TODO: Verify nonce header for all POST requests. |
| |
| switch { |
| default: |
| ca.httpErrorf(w, http.StatusBadRequest, "unrecognized r.URL.Path: %s", r.URL.Path) |
| |
| // Discovery request. |
| case r.URL.Path == "/": |
| resp := &discovery{ |
| NewNonce: ca.serverURL("/new-nonce"), |
| NewReg: ca.serverURL("/new-reg"), |
| NewOrder: ca.serverURL("/new-order"), |
| } |
| if err := json.NewEncoder(w).Encode(resp); err != nil { |
| panic(fmt.Sprintf("discovery response: %v", err)) |
| } |
| |
| // Nonce requests. |
| case r.URL.Path == "/new-nonce": |
| // Nonce values are always set. Nothing else to do. |
| return |
| |
| // Client key registration request. |
| case r.URL.Path == "/new-reg": |
| ca.mu.Lock() |
| defer ca.mu.Unlock() |
| if ca.acctRegistered { |
| ca.httpErrorf(w, http.StatusServiceUnavailable, "multiple accounts are not implemented") |
| return |
| } |
| ca.acctRegistered = true |
| // TODO: Check the user account key against a ca.accountKeys? |
| w.Header().Set("Location", ca.serverURL("/accounts/1")) |
| w.WriteHeader(http.StatusCreated) |
| w.Write([]byte("{}")) |
| |
| // New order request. |
| case r.URL.Path == "/new-order": |
| var req struct { |
| Identifiers []struct{ Value string } |
| } |
| if err := decodePayload(&req, r.Body); err != nil { |
| ca.httpErrorf(w, http.StatusBadRequest, err.Error()) |
| return |
| } |
| ca.mu.Lock() |
| defer ca.mu.Unlock() |
| o := &order{Status: acme.StatusPending} |
| for _, id := range req.Identifiers { |
| z := ca.authz(id.Value) |
| o.AuthzURLs = append(o.AuthzURLs, ca.serverURL("/authz/%d", z.id)) |
| } |
| orderID := len(ca.orders) |
| ca.orders = append(ca.orders, o) |
| w.Header().Set("Location", ca.serverURL("/orders/%d", orderID)) |
| w.WriteHeader(http.StatusCreated) |
| if err := json.NewEncoder(w).Encode(o); err != nil { |
| panic(err) |
| } |
| |
| // Existing order status requests. |
| case strings.HasPrefix(r.URL.Path, "/orders/"): |
| ca.mu.Lock() |
| defer ca.mu.Unlock() |
| o, err := ca.storedOrder(strings.TrimPrefix(r.URL.Path, "/orders/")) |
| if err != nil { |
| ca.httpErrorf(w, http.StatusBadRequest, err.Error()) |
| return |
| } |
| if err := json.NewEncoder(w).Encode(o); err != nil { |
| panic(err) |
| } |
| |
| // Accept challenge requests. |
| case strings.HasPrefix(r.URL.Path, "/challenge/"): |
| parts := strings.Split(r.URL.Path, "/") |
| typ, id := parts[len(parts)-2], parts[len(parts)-1] |
| ca.mu.Lock() |
| supported := false |
| for _, suppTyp := range ca.challengeTypes { |
| if suppTyp == typ { |
| supported = true |
| } |
| } |
| a, err := ca.storedAuthz(id) |
| ca.mu.Unlock() |
| if !supported { |
| ca.httpErrorf(w, http.StatusBadRequest, "unsupported challenge: %v", typ) |
| return |
| } |
| if err != nil { |
| ca.httpErrorf(w, http.StatusBadRequest, "challenge accept: %v", err) |
| return |
| } |
| go ca.validateChallenge(a, typ) |
| w.Write([]byte("{}")) |
| |
| // Get authorization status requests. |
| case strings.HasPrefix(r.URL.Path, "/authz/"): |
| var req struct{ Status string } |
| decodePayload(&req, r.Body) |
| deactivate := req.Status == "deactivated" |
| ca.mu.Lock() |
| defer ca.mu.Unlock() |
| authz, err := ca.storedAuthz(strings.TrimPrefix(r.URL.Path, "/authz/")) |
| if err != nil { |
| ca.httpErrorf(w, http.StatusNotFound, "%v", err) |
| return |
| } |
| if deactivate { |
| // Note we don't invalidate authorized orders as we should. |
| authz.Status = "deactivated" |
| ca.t.Logf("authz %d is now %s", authz.id, authz.Status) |
| } |
| if err := json.NewEncoder(w).Encode(authz); err != nil { |
| panic(fmt.Sprintf("encoding authz %d: %v", authz.id, err)) |
| } |
| |
| // Certificate issuance request. |
| case strings.HasPrefix(r.URL.Path, "/new-cert/"): |
| ca.mu.Lock() |
| defer ca.mu.Unlock() |
| orderID := strings.TrimPrefix(r.URL.Path, "/new-cert/") |
| o, err := ca.storedOrder(orderID) |
| if err != nil { |
| ca.httpErrorf(w, http.StatusBadRequest, err.Error()) |
| return |
| } |
| if o.Status != acme.StatusReady { |
| ca.httpErrorf(w, http.StatusForbidden, "order status: %s", o.Status) |
| return |
| } |
| // Validate CSR request. |
| var req struct { |
| CSR string `json:"csr"` |
| } |
| decodePayload(&req, r.Body) |
| b, _ := base64.RawURLEncoding.DecodeString(req.CSR) |
| csr, err := x509.ParseCertificateRequest(b) |
| if err != nil { |
| ca.httpErrorf(w, http.StatusBadRequest, err.Error()) |
| return |
| } |
| // Issue the certificate. |
| der, err := ca.leafCert(csr) |
| if err != nil { |
| ca.httpErrorf(w, http.StatusBadRequest, "new-cert response: ca.leafCert: %v", err) |
| return |
| } |
| o.leaf = der |
| o.CertURL = ca.serverURL("/issued-cert/%s", orderID) |
| o.Status = acme.StatusValid |
| if err := json.NewEncoder(w).Encode(o); err != nil { |
| panic(err) |
| } |
| |
| // Already issued cert download requests. |
| case strings.HasPrefix(r.URL.Path, "/issued-cert/"): |
| ca.mu.Lock() |
| defer ca.mu.Unlock() |
| o, err := ca.storedOrder(strings.TrimPrefix(r.URL.Path, "/issued-cert/")) |
| if err != nil { |
| ca.httpErrorf(w, http.StatusBadRequest, err.Error()) |
| return |
| } |
| if o.Status != acme.StatusValid { |
| ca.httpErrorf(w, http.StatusForbidden, "order status: %s", o.Status) |
| return |
| } |
| w.Header().Set("Content-Type", "application/pem-certificate-chain") |
| pem.Encode(w, &pem.Block{Type: "CERTIFICATE", Bytes: o.leaf}) |
| pem.Encode(w, &pem.Block{Type: "CERTIFICATE", Bytes: ca.rootCert}) |
| } |
| } |
| |
| // storedOrder retrieves a previously created order at index i. |
| // It requires ca.mu to be locked. |
| func (ca *CAServer) storedOrder(i string) (*order, error) { |
| idx, err := strconv.Atoi(i) |
| if err != nil { |
| return nil, fmt.Errorf("storedOrder: %v", err) |
| } |
| if idx < 0 { |
| return nil, fmt.Errorf("storedOrder: invalid order index %d", idx) |
| } |
| if idx > len(ca.orders)-1 { |
| return nil, fmt.Errorf("storedOrder: no such order %d", idx) |
| } |
| return ca.orders[idx], nil |
| } |
| |
| // storedAuthz retrieves a previously created authz at index i. |
| // It requires ca.mu to be locked. |
| func (ca *CAServer) storedAuthz(i string) (*authorization, error) { |
| idx, err := strconv.Atoi(i) |
| if err != nil { |
| return nil, fmt.Errorf("storedAuthz: %v", err) |
| } |
| if idx < 0 { |
| return nil, fmt.Errorf("storedAuthz: invalid authz index %d", idx) |
| } |
| if idx > len(ca.authorizations)-1 { |
| return nil, fmt.Errorf("storedAuthz: no such authz %d", idx) |
| } |
| return ca.authorizations[idx], nil |
| } |
| |
| // authz returns an existing valid authorization for the identifier or creates a |
| // new one. It requires ca.mu to be locked. |
| func (ca *CAServer) authz(identifier string) *authorization { |
| authz, ok := ca.validAuthz[identifier] |
| if !ok { |
| authzId := len(ca.authorizations) |
| authz = &authorization{ |
| id: authzId, |
| domain: identifier, |
| Status: acme.StatusPending, |
| } |
| for _, typ := range ca.challengeTypes { |
| authz.Challenges = append(authz.Challenges, challenge{ |
| Type: typ, |
| URI: ca.serverURL("/challenge/%s/%d", typ, authzId), |
| Token: challengeToken(authz.domain, typ, authzId), |
| }) |
| } |
| ca.authorizations = append(ca.authorizations, authz) |
| } |
| return authz |
| } |
| |
| // leafCert issues a new certificate. |
| // It requires ca.mu to be locked. |
| func (ca *CAServer) leafCert(csr *x509.CertificateRequest) (der []byte, err error) { |
| ca.certCount++ // next leaf cert serial number |
| leaf := &x509.Certificate{ |
| SerialNumber: big.NewInt(int64(ca.certCount)), |
| Subject: pkix.Name{Organization: []string{"Test Acme Co"}}, |
| NotBefore: time.Now(), |
| NotAfter: time.Now().Add(90 * 24 * time.Hour), |
| KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment, |
| ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}, |
| DNSNames: csr.DNSNames, |
| BasicConstraintsValid: true, |
| } |
| if len(csr.DNSNames) == 0 { |
| leaf.DNSNames = []string{csr.Subject.CommonName} |
| } |
| return x509.CreateCertificate(rand.Reader, leaf, ca.rootTemplate, csr.PublicKey, ca.rootKey) |
| } |
| |
| // LeafCert issues a leaf certificate. |
| func (ca *CAServer) LeafCert(name, keyType string, notBefore, notAfter time.Time) *tls.Certificate { |
| if ca.url == "" { |
| panic("LeafCert called before Start") |
| } |
| |
| ca.mu.Lock() |
| defer ca.mu.Unlock() |
| var pk crypto.Signer |
| switch keyType { |
| case "RSA": |
| var err error |
| pk, err = rsa.GenerateKey(rand.Reader, 1024) |
| if err != nil { |
| ca.t.Fatal(err) |
| } |
| case "ECDSA": |
| var err error |
| pk, err = ecdsa.GenerateKey(elliptic.P256(), rand.Reader) |
| if err != nil { |
| ca.t.Fatal(err) |
| } |
| default: |
| panic("LeafCert: unknown key type") |
| } |
| ca.certCount++ // next leaf cert serial number |
| leaf := &x509.Certificate{ |
| SerialNumber: big.NewInt(int64(ca.certCount)), |
| Subject: pkix.Name{Organization: []string{"Test Acme Co"}}, |
| NotBefore: notBefore, |
| NotAfter: notAfter, |
| KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment, |
| ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}, |
| DNSNames: []string{name}, |
| BasicConstraintsValid: true, |
| } |
| der, err := x509.CreateCertificate(rand.Reader, leaf, ca.rootTemplate, pk.Public(), ca.rootKey) |
| if err != nil { |
| ca.t.Fatal(err) |
| } |
| return &tls.Certificate{ |
| Certificate: [][]byte{der}, |
| PrivateKey: pk, |
| } |
| } |
| |
| func (ca *CAServer) validateChallenge(authz *authorization, typ string) { |
| var err error |
| switch typ { |
| case "tls-alpn-01": |
| err = ca.verifyALPNChallenge(authz) |
| case "http-01": |
| err = ca.verifyHTTPChallenge(authz) |
| default: |
| panic(fmt.Sprintf("validation of %q is not implemented", typ)) |
| } |
| ca.mu.Lock() |
| defer ca.mu.Unlock() |
| if err != nil { |
| authz.Status = "invalid" |
| } else { |
| authz.Status = "valid" |
| ca.validAuthz[authz.domain] = authz |
| } |
| ca.t.Logf("validated %q for %q, err: %v", typ, authz.domain, err) |
| ca.t.Logf("authz %d is now %s", authz.id, authz.Status) |
| // Update all pending orders. |
| // An order becomes "ready" if all authorizations are "valid". |
| // An order becomes "invalid" if any authorization is "invalid". |
| // Status changes: https://tools.ietf.org/html/rfc8555#section-7.1.6 |
| OrdersLoop: |
| for i, o := range ca.orders { |
| if o.Status != acme.StatusPending { |
| continue |
| } |
| var countValid int |
| for _, zurl := range o.AuthzURLs { |
| z, err := ca.storedAuthz(path.Base(zurl)) |
| if err != nil { |
| ca.t.Logf("no authz %q for order %d", zurl, i) |
| continue OrdersLoop |
| } |
| if z.Status == acme.StatusInvalid { |
| o.Status = acme.StatusInvalid |
| ca.t.Logf("order %d is now invalid", i) |
| continue OrdersLoop |
| } |
| if z.Status == acme.StatusValid { |
| countValid++ |
| } |
| } |
| if countValid == len(o.AuthzURLs) { |
| o.Status = acme.StatusReady |
| o.FinalizeURL = ca.serverURL("/new-cert/%d", i) |
| ca.t.Logf("order %d is now ready", i) |
| } |
| } |
| } |
| |
| func (ca *CAServer) verifyALPNChallenge(a *authorization) error { |
| const acmeALPNProto = "acme-tls/1" |
| |
| addr, haveAddr := ca.addr(a.domain) |
| getCert, haveGetCert := ca.getCert(a.domain) |
| if !haveAddr && !haveGetCert { |
| return fmt.Errorf("no resolution information for %q", a.domain) |
| } |
| if haveAddr && haveGetCert { |
| return fmt.Errorf("overlapping resolution information for %q", a.domain) |
| } |
| |
| var crt *x509.Certificate |
| switch { |
| case haveAddr: |
| conn, err := tls.Dial("tcp", addr, &tls.Config{ |
| ServerName: a.domain, |
| InsecureSkipVerify: true, |
| NextProtos: []string{acmeALPNProto}, |
| MinVersion: tls.VersionTLS12, |
| }) |
| if err != nil { |
| return err |
| } |
| if v := conn.ConnectionState().NegotiatedProtocol; v != acmeALPNProto { |
| return fmt.Errorf("CAServer: verifyALPNChallenge: negotiated proto is %q; want %q", v, acmeALPNProto) |
| } |
| if n := len(conn.ConnectionState().PeerCertificates); n != 1 { |
| return fmt.Errorf("len(PeerCertificates) = %d; want 1", n) |
| } |
| crt = conn.ConnectionState().PeerCertificates[0] |
| case haveGetCert: |
| hello := &tls.ClientHelloInfo{ |
| ServerName: a.domain, |
| // TODO: support selecting ECDSA. |
| CipherSuites: []uint16{tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305}, |
| SupportedProtos: []string{acme.ALPNProto}, |
| SupportedVersions: []uint16{tls.VersionTLS12}, |
| } |
| c, err := getCert(hello) |
| if err != nil { |
| return err |
| } |
| crt, err = x509.ParseCertificate(c.Certificate[0]) |
| if err != nil { |
| return err |
| } |
| } |
| |
| if err := crt.VerifyHostname(a.domain); err != nil { |
| return fmt.Errorf("verifyALPNChallenge: VerifyHostname: %v", err) |
| } |
| // See RFC 8737, Section 6.1. |
| oid := asn1.ObjectIdentifier{1, 3, 6, 1, 5, 5, 7, 1, 31} |
| for _, x := range crt.Extensions { |
| if x.Id.Equal(oid) { |
| // TODO: check the token. |
| return nil |
| } |
| } |
| return fmt.Errorf("verifyTokenCert: no id-pe-acmeIdentifier extension found") |
| } |
| |
| func (ca *CAServer) verifyHTTPChallenge(a *authorization) error { |
| addr, haveAddr := ca.addr(a.domain) |
| handler, haveHandler := ca.getHandler(a.domain) |
| if !haveAddr && !haveHandler { |
| return fmt.Errorf("no resolution information for %q", a.domain) |
| } |
| if haveAddr && haveHandler { |
| return fmt.Errorf("overlapping resolution information for %q", a.domain) |
| } |
| |
| token := challengeToken(a.domain, "http-01", a.id) |
| path := "/.well-known/acme-challenge/" + token |
| |
| var body string |
| switch { |
| case haveAddr: |
| t := &http.Transport{ |
| DialContext: func(ctx context.Context, network, _ string) (net.Conn, error) { |
| return (&net.Dialer{}).DialContext(ctx, network, addr) |
| }, |
| } |
| req, err := http.NewRequest("GET", "http://"+a.domain+path, nil) |
| if err != nil { |
| return err |
| } |
| res, err := t.RoundTrip(req) |
| if err != nil { |
| return err |
| } |
| if res.StatusCode != http.StatusOK { |
| return fmt.Errorf("http token: w.Code = %d; want %d", res.StatusCode, http.StatusOK) |
| } |
| b, err := io.ReadAll(res.Body) |
| if err != nil { |
| return err |
| } |
| body = string(b) |
| case haveHandler: |
| r := httptest.NewRequest("GET", path, nil) |
| r.Host = a.domain |
| w := httptest.NewRecorder() |
| handler.ServeHTTP(w, r) |
| if w.Code != http.StatusOK { |
| return fmt.Errorf("http token: w.Code = %d; want %d", w.Code, http.StatusOK) |
| } |
| body = w.Body.String() |
| } |
| |
| if !strings.HasPrefix(body, token) { |
| return fmt.Errorf("http token value = %q; want 'token-http-01.' prefix", body) |
| } |
| return nil |
| } |
| |
| func decodePayload(v interface{}, r io.Reader) error { |
| var req struct{ Payload string } |
| if err := json.NewDecoder(r).Decode(&req); err != nil { |
| return err |
| } |
| payload, err := base64.RawURLEncoding.DecodeString(req.Payload) |
| if err != nil { |
| return err |
| } |
| return json.Unmarshal(payload, v) |
| } |
| |
| func challengeToken(domain, challType string, authzID int) string { |
| return fmt.Sprintf("token-%s-%s-%d", domain, challType, authzID) |
| } |
| |
| func unique(a []string) []string { |
| seen := make(map[string]bool) |
| var res []string |
| for _, s := range a { |
| if s != "" && !seen[s] { |
| seen[s] = true |
| res = append(res, s) |
| } |
| } |
| return res |
| } |