mirror of
				https://codeberg.org/forgejo/forgejo.git
				synced 2025-10-30 22:11:07 +00:00 
			
		
		
		
	* use certmagic for more extensible/robust ACME cert handling * accept TOS based on config option Signed-off-by: Andrew Thornton <art27@cantab.net> Co-authored-by: zeripath <art27@cantab.net> Co-authored-by: Lauris BH <lauris@nix.lv>
		
			
				
	
	
		
			165 lines
		
	
	
	
		
			5.6 KiB
		
	
	
	
		
			Go
		
	
	
	
		
			Vendored
		
	
	
	
			
		
		
	
	
			165 lines
		
	
	
	
		
			5.6 KiB
		
	
	
	
		
			Go
		
	
	
	
		
			Vendored
		
	
	
	
| // Copyright 2020 Matthew Holt
 | |
| //
 | |
| // Licensed under the Apache License, Version 2.0 (the "License");
 | |
| // you may not use this file except in compliance with the License.
 | |
| // You may obtain a copy of the License at
 | |
| //
 | |
| //     http://www.apache.org/licenses/LICENSE-2.0
 | |
| //
 | |
| // Unless required by applicable law or agreed to in writing, software
 | |
| // distributed under the License is distributed on an "AS IS" BASIS,
 | |
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | |
| // See the License for the specific language governing permissions and
 | |
| // limitations under the License.
 | |
| 
 | |
| package acme
 | |
| 
 | |
| import (
 | |
| 	"bytes"
 | |
| 	"context"
 | |
| 	"crypto"
 | |
| 	"crypto/x509"
 | |
| 	"encoding/base64"
 | |
| 	"fmt"
 | |
| 	"net/http"
 | |
| )
 | |
| 
 | |
| // Certificate represents a certificate chain, which we usually refer
 | |
| // to as "a certificate" because in practice an end-entity certificate
 | |
| // is seldom useful/practical without a chain.
 | |
| type Certificate struct {
 | |
| 	// The certificate resource URL as provisioned by
 | |
| 	// the ACME server. Some ACME servers may split
 | |
| 	// the chain into multiple URLs that are Linked
 | |
| 	// together, in which case this URL represents the
 | |
| 	// starting point.
 | |
| 	URL string `json:"url"`
 | |
| 
 | |
| 	// The PEM-encoded certificate chain, end-entity first.
 | |
| 	ChainPEM []byte `json:"-"`
 | |
| }
 | |
| 
 | |
| // GetCertificateChain downloads all available certificate chains originating from
 | |
| // the given certURL. This is to be done after an order is finalized.
 | |
| //
 | |
| // "To download the issued certificate, the client simply sends a POST-
 | |
| // as-GET request to the certificate URL."
 | |
| //
 | |
| // "The server MAY provide one or more link relation header fields
 | |
| // [RFC8288] with relation 'alternate'.  Each such field SHOULD express
 | |
| // an alternative certificate chain starting with the same end-entity
 | |
| // certificate.  This can be used to express paths to various trust
 | |
| // anchors.  Clients can fetch these alternates and use their own
 | |
| // heuristics to decide which is optimal." §7.4.2
 | |
| func (c *Client) GetCertificateChain(ctx context.Context, account Account, certURL string) ([]Certificate, error) {
 | |
| 	if err := c.provision(ctx); err != nil {
 | |
| 		return nil, err
 | |
| 	}
 | |
| 
 | |
| 	var chains []Certificate
 | |
| 
 | |
| 	addChain := func(certURL string) (*http.Response, error) {
 | |
| 		// can't pool this buffer; bytes escape scope
 | |
| 		buf := new(bytes.Buffer)
 | |
| 
 | |
| 		// TODO: set the Accept header? ("application/pem-certificate-chain") See end of §7.4.2
 | |
| 		resp, err := c.httpPostJWS(ctx, account.PrivateKey, account.Location, certURL, nil, buf)
 | |
| 		if err != nil {
 | |
| 			return resp, err
 | |
| 		}
 | |
| 		contentType := parseMediaType(resp)
 | |
| 
 | |
| 		switch contentType {
 | |
| 		case "application/pem-certificate-chain":
 | |
| 			chains = append(chains, Certificate{
 | |
| 				URL:      certURL,
 | |
| 				ChainPEM: buf.Bytes(),
 | |
| 			})
 | |
| 		default:
 | |
| 			return resp, fmt.Errorf("unrecognized Content-Type from server: %s", contentType)
 | |
| 		}
 | |
| 
 | |
| 		// "For formats that can only express a single certificate, the server SHOULD
 | |
| 		// provide one or more "Link: rel="up"" header fields pointing to an
 | |
| 		// issuer or issuers so that ACME clients can build a certificate chain
 | |
| 		// as defined in TLS (see Section 4.4.2 of [RFC8446])." (end of §7.4.2)
 | |
| 		allUp := extractLinks(resp, "up")
 | |
| 		for _, upURL := range allUp {
 | |
| 			upCerts, err := c.GetCertificateChain(ctx, account, upURL)
 | |
| 			if err != nil {
 | |
| 				return resp, fmt.Errorf("retrieving next certificate in chain: %s: %w", upURL, err)
 | |
| 			}
 | |
| 			for _, upCert := range upCerts {
 | |
| 				chains[len(chains)-1].ChainPEM = append(chains[len(chains)-1].ChainPEM, upCert.ChainPEM...)
 | |
| 			}
 | |
| 		}
 | |
| 
 | |
| 		return resp, nil
 | |
| 	}
 | |
| 
 | |
| 	// always add preferred/first certificate chain
 | |
| 	resp, err := addChain(certURL)
 | |
| 	if err != nil {
 | |
| 		return chains, err
 | |
| 	}
 | |
| 
 | |
| 	// "The server MAY provide one or more link relation header fields
 | |
| 	// [RFC8288] with relation 'alternate'.  Each such field SHOULD express
 | |
| 	// an alternative certificate chain starting with the same end-entity
 | |
| 	// certificate.  This can be used to express paths to various trust
 | |
| 	// anchors.  Clients can fetch these alternates and use their own
 | |
| 	// heuristics to decide which is optimal." §7.4.2
 | |
| 	alternates := extractLinks(resp, "alternate")
 | |
| 	for _, altURL := range alternates {
 | |
| 		resp, err = addChain(altURL)
 | |
| 		if err != nil {
 | |
| 			return nil, fmt.Errorf("retrieving alternate certificate chain at %s: %w", altURL, err)
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	return chains, nil
 | |
| }
 | |
| 
 | |
| // RevokeCertificate revokes the given certificate. If the certificate key is not
 | |
| // provided, then the account key is used instead. See §7.6.
 | |
| func (c *Client) RevokeCertificate(ctx context.Context, account Account, cert *x509.Certificate, certKey crypto.Signer, reason int) error {
 | |
| 	if err := c.provision(ctx); err != nil {
 | |
| 		return err
 | |
| 	}
 | |
| 
 | |
| 	body := struct {
 | |
| 		Certificate string `json:"certificate"`
 | |
| 		Reason      int    `json:"reason"`
 | |
| 	}{
 | |
| 		Certificate: base64.RawURLEncoding.EncodeToString(cert.Raw),
 | |
| 		Reason:      reason,
 | |
| 	}
 | |
| 
 | |
| 	// "Revocation requests are different from other ACME requests in that
 | |
| 	// they can be signed with either an account key pair or the key pair in
 | |
| 	// the certificate." §7.6
 | |
| 	kid := ""
 | |
| 	if certKey == account.PrivateKey {
 | |
| 		kid = account.Location
 | |
| 	}
 | |
| 
 | |
| 	_, err := c.httpPostJWS(ctx, certKey, kid, c.dir.RevokeCert, body, nil)
 | |
| 	return err
 | |
| }
 | |
| 
 | |
| // Reasons for revoking a certificate, as defined
 | |
| // by RFC 5280 §5.3.1.
 | |
| // https://tools.ietf.org/html/rfc5280#section-5.3.1
 | |
| const (
 | |
| 	ReasonUnspecified          = iota // 0
 | |
| 	ReasonKeyCompromise               // 1
 | |
| 	ReasonCACompromise                // 2
 | |
| 	ReasonAffiliationChanged          // 3
 | |
| 	ReasonSuperseded                  // 4
 | |
| 	ReasonCessationOfOperation        // 5
 | |
| 	ReasonCertificateHold             // 6
 | |
| 	_                                 // 7 (unused)
 | |
| 	ReasonRemoveFromCRL               // 8
 | |
| 	ReasonPrivilegeWithdrawn          // 9
 | |
| 	ReasonAACompromise                // 10
 | |
| )
 |