1// Copyright 2018 The Go Authors. All rights reserved. 2// Use of this source code is governed by a BSD-style 3// license that can be found in the LICENSE file. 4 5// Package acmetest provides types for testing acme and autocert packages. 6// 7// TODO: Consider moving this to x/crypto/acme/internal/acmetest for acme tests as well. 8package acmetest 9 10import ( 11 "crypto" 12 "crypto/ecdsa" 13 "crypto/elliptic" 14 "crypto/rand" 15 "crypto/tls" 16 "crypto/x509" 17 "crypto/x509/pkix" 18 "encoding/base64" 19 "encoding/json" 20 "encoding/pem" 21 "fmt" 22 "io" 23 "log" 24 "math/big" 25 "net/http" 26 "net/http/httptest" 27 "path" 28 "sort" 29 "strconv" 30 "strings" 31 "sync" 32 "time" 33 34 "golang.org/x/crypto/acme" 35) 36 37// CAServer is a simple test server which implements ACME spec bits needed for testing. 38type CAServer struct { 39 URL string // server URL after it has been started 40 Roots *x509.CertPool // CA root certificates; initialized in NewCAServer 41 42 rootKey crypto.Signer 43 rootCert []byte // DER encoding 44 rootTemplate *x509.Certificate 45 46 server *httptest.Server 47 challengeTypes []string // supported challenge types 48 domainsWhitelist []string // only these domains are valid for issuing, unless empty 49 50 mu sync.Mutex 51 certCount int // number of issued certs 52 domainAddr map[string]string // domain name to addr:port resolution 53 authorizations map[string]*authorization // keyed by domain name 54 orders []*order // index is used as order ID 55 errors []error // encountered client errors 56} 57 58// NewCAServer creates a new ACME test server and starts serving requests. 59// The returned CAServer issues certs signed with the CA roots 60// available in the Roots field. 61// 62// The challengeTypes argument defines the supported ACME challenge types 63// sent to a client in a response for a domain authorization. 64// If domainsWhitelist is non-empty, the certs will be issued only for the specified 65// list of domains. Otherwise, any domain name is allowed. 66func NewCAServer(challengeTypes []string, domainsWhitelist []string) *CAServer { 67 var whitelist []string 68 for _, name := range domainsWhitelist { 69 whitelist = append(whitelist, name) 70 } 71 sort.Strings(whitelist) 72 ca := &CAServer{ 73 challengeTypes: challengeTypes, 74 domainsWhitelist: whitelist, 75 domainAddr: make(map[string]string), 76 authorizations: make(map[string]*authorization), 77 } 78 79 key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) 80 if err != nil { 81 panic(fmt.Sprintf("ecdsa.GenerateKey: %v", err)) 82 } 83 tmpl := &x509.Certificate{ 84 SerialNumber: big.NewInt(1), 85 Subject: pkix.Name{ 86 Organization: []string{"Test Acme Co"}, 87 CommonName: "Root CA", 88 }, 89 NotBefore: time.Now(), 90 NotAfter: time.Now().Add(365 * 24 * time.Hour), 91 KeyUsage: x509.KeyUsageCertSign, 92 BasicConstraintsValid: true, 93 IsCA: true, 94 } 95 der, err := x509.CreateCertificate(rand.Reader, tmpl, tmpl, &key.PublicKey, key) 96 if err != nil { 97 panic(fmt.Sprintf("x509.CreateCertificate: %v", err)) 98 } 99 cert, err := x509.ParseCertificate(der) 100 if err != nil { 101 panic(fmt.Sprintf("x509.ParseCertificate: %v", err)) 102 } 103 ca.Roots = x509.NewCertPool() 104 ca.Roots.AddCert(cert) 105 ca.rootKey = key 106 ca.rootCert = der 107 ca.rootTemplate = tmpl 108 109 ca.server = httptest.NewServer(http.HandlerFunc(ca.handle)) 110 ca.URL = ca.server.URL 111 return ca 112} 113 114// Close shuts down the server and blocks until all outstanding 115// requests on this server have completed. 116func (ca *CAServer) Close() { 117 ca.server.Close() 118} 119 120func (ca *CAServer) serverURL(format string, arg ...interface{}) string { 121 return ca.server.URL + fmt.Sprintf(format, arg...) 122} 123 124func (ca *CAServer) addr(domain string) (string, error) { 125 ca.mu.Lock() 126 defer ca.mu.Unlock() 127 addr, ok := ca.domainAddr[domain] 128 if !ok { 129 return "", fmt.Errorf("CAServer: no addr resolution for %q", domain) 130 } 131 return addr, nil 132} 133 134func (ca *CAServer) httpErrorf(w http.ResponseWriter, code int, format string, a ...interface{}) { 135 s := fmt.Sprintf(format, a...) 136 log.Println(s) 137 http.Error(w, s, code) 138} 139 140// Resolve adds a domain to address resolution for the ca to dial to 141// when validating challenges for the domain authorization. 142func (ca *CAServer) Resolve(domain, addr string) { 143 ca.mu.Lock() 144 defer ca.mu.Unlock() 145 ca.domainAddr[domain] = addr 146} 147 148type discovery struct { 149 NewNonce string `json:"newNonce"` 150 NewReg string `json:"newAccount"` 151 NewOrder string `json:"newOrder"` 152 NewAuthz string `json:"newAuthz"` 153} 154 155type challenge struct { 156 URI string `json:"uri"` 157 Type string `json:"type"` 158 Token string `json:"token"` 159} 160 161type authorization struct { 162 Status string `json:"status"` 163 Challenges []challenge `json:"challenges"` 164 165 domain string 166} 167 168type order struct { 169 Status string `json:"status"` 170 AuthzURLs []string `json:"authorizations"` 171 FinalizeURL string `json:"finalize"` // CSR submit URL 172 CertURL string `json:"certificate"` // already issued cert 173 174 leaf []byte // issued cert in DER format 175} 176 177func (ca *CAServer) handle(w http.ResponseWriter, r *http.Request) { 178 log.Printf("%s %s", r.Method, r.URL) 179 w.Header().Set("Replay-Nonce", "nonce") 180 // TODO: Verify nonce header for all POST requests. 181 182 switch { 183 default: 184 ca.httpErrorf(w, http.StatusBadRequest, "unrecognized r.URL.Path: %s", r.URL.Path) 185 186 // Discovery request. 187 case r.URL.Path == "/": 188 resp := &discovery{ 189 NewNonce: ca.serverURL("/new-nonce"), 190 NewReg: ca.serverURL("/new-reg"), 191 NewOrder: ca.serverURL("/new-order"), 192 NewAuthz: ca.serverURL("/new-authz"), 193 } 194 if err := json.NewEncoder(w).Encode(resp); err != nil { 195 panic(fmt.Sprintf("discovery response: %v", err)) 196 } 197 198 // Nonce requests. 199 case r.URL.Path == "/new-nonce": 200 // Nonce values are always set. Nothing else to do. 201 return 202 203 // Client key registration request. 204 case r.URL.Path == "/new-reg": 205 // TODO: Check the user account key against a ca.accountKeys? 206 w.Header().Set("Location", ca.serverURL("/accounts/1")) 207 w.WriteHeader(http.StatusCreated) 208 w.Write([]byte("{}")) 209 210 // New order request. 211 case r.URL.Path == "/new-order": 212 var req struct { 213 Identifiers []struct{ Value string } 214 } 215 if err := decodePayload(&req, r.Body); err != nil { 216 ca.httpErrorf(w, http.StatusBadRequest, err.Error()) 217 return 218 } 219 ca.mu.Lock() 220 defer ca.mu.Unlock() 221 o := &order{Status: acme.StatusPending} 222 for _, id := range req.Identifiers { 223 z := ca.authz(id.Value) 224 o.AuthzURLs = append(o.AuthzURLs, ca.serverURL("/authz/%s", z.domain)) 225 } 226 orderID := len(ca.orders) 227 ca.orders = append(ca.orders, o) 228 w.Header().Set("Location", ca.serverURL("/orders/%d", orderID)) 229 w.WriteHeader(http.StatusCreated) 230 if err := json.NewEncoder(w).Encode(o); err != nil { 231 panic(err) 232 } 233 234 // Existing order status requests. 235 case strings.HasPrefix(r.URL.Path, "/orders/"): 236 ca.mu.Lock() 237 defer ca.mu.Unlock() 238 o, err := ca.storedOrder(strings.TrimPrefix(r.URL.Path, "/orders/")) 239 if err != nil { 240 ca.httpErrorf(w, http.StatusBadRequest, err.Error()) 241 return 242 } 243 if err := json.NewEncoder(w).Encode(o); err != nil { 244 panic(err) 245 } 246 247 // Identifier authorization request. 248 case r.URL.Path == "/new-authz": 249 var req struct { 250 Identifier struct{ Value string } 251 } 252 if err := decodePayload(&req, r.Body); err != nil { 253 ca.httpErrorf(w, http.StatusBadRequest, err.Error()) 254 return 255 } 256 ca.mu.Lock() 257 defer ca.mu.Unlock() 258 z := ca.authz(req.Identifier.Value) 259 w.Header().Set("Location", ca.serverURL("/authz/%s", z.domain)) 260 w.WriteHeader(http.StatusCreated) 261 if err := json.NewEncoder(w).Encode(z); err != nil { 262 panic(fmt.Sprintf("new authz response: %v", err)) 263 } 264 265 // Accept tls-alpn-01 challenge type requests. 266 case strings.HasPrefix(r.URL.Path, "/challenge/tls-alpn-01/"): 267 domain := strings.TrimPrefix(r.URL.Path, "/challenge/tls-alpn-01/") 268 ca.mu.Lock() 269 _, exist := ca.authorizations[domain] 270 ca.mu.Unlock() 271 if !exist { 272 ca.httpErrorf(w, http.StatusBadRequest, "challenge accept: no authz for %q", domain) 273 return 274 } 275 go ca.validateChallenge("tls-alpn-01", domain) 276 w.Write([]byte("{}")) 277 278 // Get authorization status requests. 279 case strings.HasPrefix(r.URL.Path, "/authz/"): 280 domain := strings.TrimPrefix(r.URL.Path, "/authz/") 281 ca.mu.Lock() 282 defer ca.mu.Unlock() 283 authz, ok := ca.authorizations[domain] 284 if !ok { 285 ca.httpErrorf(w, http.StatusNotFound, "no authz for %q", domain) 286 return 287 } 288 if err := json.NewEncoder(w).Encode(authz); err != nil { 289 panic(fmt.Sprintf("get authz for %q response: %v", domain, err)) 290 } 291 292 // Certificate issuance request. 293 case strings.HasPrefix(r.URL.Path, "/new-cert/"): 294 ca.mu.Lock() 295 defer ca.mu.Unlock() 296 orderID := strings.TrimPrefix(r.URL.Path, "/new-cert/") 297 o, err := ca.storedOrder(orderID) 298 if err != nil { 299 ca.httpErrorf(w, http.StatusBadRequest, err.Error()) 300 return 301 } 302 if o.Status != acme.StatusReady { 303 ca.httpErrorf(w, http.StatusForbidden, "order status: %s", o.Status) 304 return 305 } 306 // Validate CSR request. 307 var req struct { 308 CSR string `json:"csr"` 309 } 310 decodePayload(&req, r.Body) 311 b, _ := base64.RawURLEncoding.DecodeString(req.CSR) 312 csr, err := x509.ParseCertificateRequest(b) 313 if err != nil { 314 ca.httpErrorf(w, http.StatusBadRequest, err.Error()) 315 return 316 } 317 names := unique(append(csr.DNSNames, csr.Subject.CommonName)) 318 if err := ca.matchWhitelist(names); err != nil { 319 ca.httpErrorf(w, http.StatusUnauthorized, err.Error()) 320 return 321 } 322 if err := ca.authorized(names); err != nil { 323 ca.httpErrorf(w, http.StatusUnauthorized, err.Error()) 324 return 325 } 326 // Issue the certificate. 327 der, err := ca.leafCert(csr) 328 if err != nil { 329 ca.httpErrorf(w, http.StatusBadRequest, "new-cert response: ca.leafCert: %v", err) 330 return 331 } 332 o.leaf = der 333 o.CertURL = ca.serverURL("/issued-cert/%s", orderID) 334 o.Status = acme.StatusValid 335 if err := json.NewEncoder(w).Encode(o); err != nil { 336 panic(err) 337 } 338 339 // Already issued cert download requests. 340 case strings.HasPrefix(r.URL.Path, "/issued-cert/"): 341 ca.mu.Lock() 342 defer ca.mu.Unlock() 343 o, err := ca.storedOrder(strings.TrimPrefix(r.URL.Path, "/issued-cert/")) 344 if err != nil { 345 ca.httpErrorf(w, http.StatusBadRequest, err.Error()) 346 return 347 } 348 if o.Status != acme.StatusValid { 349 ca.httpErrorf(w, http.StatusForbidden, "order status: %s", o.Status) 350 return 351 } 352 w.Header().Set("Content-Type", "application/pem-certificate-chain") 353 pem.Encode(w, &pem.Block{Type: "CERTIFICATE", Bytes: o.leaf}) 354 pem.Encode(w, &pem.Block{Type: "CERTIFICATE", Bytes: ca.rootCert}) 355 } 356} 357 358// matchWhitelist reports whether all dnsNames are whitelisted. 359// The whitelist is provided in NewCAServer. 360func (ca *CAServer) matchWhitelist(dnsNames []string) error { 361 if len(ca.domainsWhitelist) == 0 { 362 return nil 363 } 364 var nomatch []string 365 for _, name := range dnsNames { 366 i := sort.SearchStrings(ca.domainsWhitelist, name) 367 if i == len(ca.domainsWhitelist) || ca.domainsWhitelist[i] != name { 368 nomatch = append(nomatch, name) 369 } 370 } 371 if len(nomatch) > 0 { 372 return fmt.Errorf("matchWhitelist: some domains don't match: %q", nomatch) 373 } 374 return nil 375} 376 377// storedOrder retrieves a previously created order at index i. 378// It requires ca.mu to be locked. 379func (ca *CAServer) storedOrder(i string) (*order, error) { 380 idx, err := strconv.Atoi(i) 381 if err != nil { 382 return nil, fmt.Errorf("storedOrder: %v", err) 383 } 384 if idx < 0 { 385 return nil, fmt.Errorf("storedOrder: invalid order index %d", idx) 386 } 387 if idx > len(ca.orders)-1 { 388 return nil, fmt.Errorf("storedOrder: no such order %d", idx) 389 } 390 return ca.orders[idx], nil 391} 392 393// authz returns an existing authorization for the identifier or creates a new one. 394// It requires ca.mu to be locked. 395func (ca *CAServer) authz(identifier string) *authorization { 396 authz, ok := ca.authorizations[identifier] 397 if !ok { 398 authz = &authorization{ 399 domain: identifier, 400 Status: acme.StatusPending, 401 } 402 for _, typ := range ca.challengeTypes { 403 authz.Challenges = append(authz.Challenges, challenge{ 404 Type: typ, 405 URI: ca.serverURL("/challenge/%s/%s", typ, authz.domain), 406 Token: challengeToken(authz.domain, typ), 407 }) 408 } 409 ca.authorizations[authz.domain] = authz 410 } 411 return authz 412} 413 414// authorized reports whether all authorizations for dnsNames have been satisfied. 415// It requires ca.mu to be locked. 416func (ca *CAServer) authorized(dnsNames []string) error { 417 var noauthz []string 418 for _, name := range dnsNames { 419 authz, ok := ca.authorizations[name] 420 if !ok || authz.Status != acme.StatusValid { 421 noauthz = append(noauthz, name) 422 } 423 } 424 if len(noauthz) > 0 { 425 return fmt.Errorf("CAServer: no authz for %q", noauthz) 426 } 427 return nil 428} 429 430// leafCert issues a new certificate. 431// It requires ca.mu to be locked. 432func (ca *CAServer) leafCert(csr *x509.CertificateRequest) (der []byte, err error) { 433 ca.certCount++ // next leaf cert serial number 434 leaf := &x509.Certificate{ 435 SerialNumber: big.NewInt(int64(ca.certCount)), 436 Subject: pkix.Name{Organization: []string{"Test Acme Co"}}, 437 NotBefore: time.Now(), 438 NotAfter: time.Now().Add(90 * 24 * time.Hour), 439 KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment, 440 ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}, 441 DNSNames: csr.DNSNames, 442 BasicConstraintsValid: true, 443 } 444 if len(csr.DNSNames) == 0 { 445 leaf.DNSNames = []string{csr.Subject.CommonName} 446 } 447 return x509.CreateCertificate(rand.Reader, leaf, ca.rootTemplate, csr.PublicKey, ca.rootKey) 448} 449 450// TODO: Only tls-alpn-01 is currently supported: implement http-01 and dns-01. 451func (ca *CAServer) validateChallenge(typ, identifier string) { 452 var err error 453 switch typ { 454 case "tls-alpn-01": 455 err = ca.verifyALPNChallenge(identifier) 456 default: 457 panic(fmt.Sprintf("validation of %q is not implemented", typ)) 458 } 459 ca.mu.Lock() 460 defer ca.mu.Unlock() 461 authz := ca.authorizations[identifier] 462 if err != nil { 463 authz.Status = "invalid" 464 } else { 465 authz.Status = "valid" 466 } 467 log.Printf("validated %q for %q; authz status is now: %s", typ, identifier, authz.Status) 468 // Update all pending orders. 469 // An order becomes "ready" if all authorizations are "valid". 470 // An order becomes "invalid" if any authorization is "invalid". 471 // Status changes: https://tools.ietf.org/html/rfc8555#section-7.1.6 472OrdersLoop: 473 for i, o := range ca.orders { 474 if o.Status != acme.StatusPending { 475 continue 476 } 477 var countValid int 478 for _, zurl := range o.AuthzURLs { 479 z, ok := ca.authorizations[path.Base(zurl)] 480 if !ok { 481 log.Printf("no authz %q for order %d", zurl, i) 482 continue OrdersLoop 483 } 484 if z.Status == acme.StatusInvalid { 485 o.Status = acme.StatusInvalid 486 log.Printf("order %d is now invalid", i) 487 continue OrdersLoop 488 } 489 if z.Status == acme.StatusValid { 490 countValid++ 491 } 492 } 493 if countValid == len(o.AuthzURLs) { 494 o.Status = acme.StatusReady 495 o.FinalizeURL = ca.serverURL("/new-cert/%d", i) 496 log.Printf("order %d is now ready", i) 497 } 498 } 499} 500 501func (ca *CAServer) verifyALPNChallenge(domain string) error { 502 const acmeALPNProto = "acme-tls/1" 503 504 addr, err := ca.addr(domain) 505 if err != nil { 506 return err 507 } 508 conn, err := tls.Dial("tcp", addr, &tls.Config{ 509 ServerName: domain, 510 InsecureSkipVerify: true, 511 NextProtos: []string{acmeALPNProto}, 512 }) 513 if err != nil { 514 return err 515 } 516 if v := conn.ConnectionState().NegotiatedProtocol; v != acmeALPNProto { 517 return fmt.Errorf("CAServer: verifyALPNChallenge: negotiated proto is %q; want %q", v, acmeALPNProto) 518 } 519 if n := len(conn.ConnectionState().PeerCertificates); n != 1 { 520 return fmt.Errorf("len(PeerCertificates) = %d; want 1", n) 521 } 522 // TODO: verify conn.ConnectionState().PeerCertificates[0] 523 return nil 524} 525 526func decodePayload(v interface{}, r io.Reader) error { 527 var req struct{ Payload string } 528 if err := json.NewDecoder(r).Decode(&req); err != nil { 529 return err 530 } 531 payload, err := base64.RawURLEncoding.DecodeString(req.Payload) 532 if err != nil { 533 return err 534 } 535 return json.Unmarshal(payload, v) 536} 537 538func challengeToken(domain, challType string) string { 539 return fmt.Sprintf("token-%s-%s", domain, challType) 540} 541 542func unique(a []string) []string { 543 seen := make(map[string]bool) 544 var res []string 545 for _, s := range a { 546 if s != "" && !seen[s] { 547 seen[s] = true 548 res = append(res, s) 549 } 550 } 551 return res 552} 553