2020-11-25 20:36:07 +01:00
package certificate
import (
"bytes"
"crypto"
"crypto/x509"
"encoding/base64"
"errors"
"fmt"
"io/ioutil"
"net/http"
"strings"
"time"
"github.com/go-acme/lego/v4/acme"
"github.com/go-acme/lego/v4/acme/api"
"github.com/go-acme/lego/v4/certcrypto"
"github.com/go-acme/lego/v4/challenge"
"github.com/go-acme/lego/v4/log"
"github.com/go-acme/lego/v4/platform/wait"
"golang.org/x/crypto/ocsp"
"golang.org/x/net/idna"
)
// maxBodySize is the maximum size of body that we will read.
const maxBodySize = 1024 * 1024
// Resource represents a CA issued certificate.
// PrivateKey, Certificate and IssuerCertificate are all
// already PEM encoded and can be directly written to disk.
// Certificate may be a certificate bundle,
// depending on the options supplied to create it.
type Resource struct {
Domain string ` json:"domain" `
CertURL string ` json:"certUrl" `
CertStableURL string ` json:"certStableUrl" `
PrivateKey [ ] byte ` json:"-" `
Certificate [ ] byte ` json:"-" `
IssuerCertificate [ ] byte ` json:"-" `
CSR [ ] byte ` json:"-" `
}
// ObtainRequest The request to obtain certificate.
//
// The first domain in domains is used for the CommonName field of the certificate,
// all other domains are added using the Subject Alternate Names extension.
//
// A new private key is generated for every invocation of the function Obtain.
// If you do not want that you can supply your own private key in the privateKey parameter.
// If this parameter is non-nil it will be used instead of generating a new one.
//
// If bundle is true, the []byte contains both the issuer certificate and your issued certificate as a bundle.
type ObtainRequest struct {
Domains [ ] string
Bundle bool
PrivateKey crypto . PrivateKey
MustStaple bool
PreferredChain string
}
// ObtainForCSRRequest The request to obtain a certificate matching the CSR passed into it.
//
// If bundle is true, the []byte contains both the issuer certificate and your issued certificate as a bundle.
type ObtainForCSRRequest struct {
CSR * x509 . CertificateRequest
Bundle bool
PreferredChain string
}
type resolver interface {
Solve ( authorizations [ ] acme . Authorization ) error
}
type CertifierOptions struct {
KeyType certcrypto . KeyType
Timeout time . Duration
}
// Certifier A service to obtain/renew/revoke certificates.
type Certifier struct {
core * api . Core
resolver resolver
options CertifierOptions
}
// NewCertifier creates a Certifier.
func NewCertifier ( core * api . Core , resolver resolver , options CertifierOptions ) * Certifier {
return & Certifier {
core : core ,
resolver : resolver ,
options : options ,
}
}
// Obtain tries to obtain a single certificate using all domains passed into it.
//
// This function will never return a partial certificate.
// If one domain in the list fails, the whole certificate will fail.
func ( c * Certifier ) Obtain ( request ObtainRequest ) ( * Resource , error ) {
if len ( request . Domains ) == 0 {
return nil , errors . New ( "no domains to obtain a certificate for" )
}
domains := sanitizeDomain ( request . Domains )
if request . Bundle {
log . Infof ( "[%s] acme: Obtaining bundled SAN certificate" , strings . Join ( domains , ", " ) )
} else {
log . Infof ( "[%s] acme: Obtaining SAN certificate" , strings . Join ( domains , ", " ) )
}
order , err := c . core . Orders . New ( domains )
if err != nil {
return nil , err
}
authz , err := c . getAuthorizations ( order )
if err != nil {
// If any challenge fails, return. Do not generate partial SAN certificates.
c . deactivateAuthorizations ( order )
return nil , err
}
err = c . resolver . Solve ( authz )
if err != nil {
// If any challenge fails, return. Do not generate partial SAN certificates.
c . deactivateAuthorizations ( order )
return nil , err
}
log . Infof ( "[%s] acme: Validations succeeded; requesting certificates" , strings . Join ( domains , ", " ) )
failures := make ( obtainError )
cert , err := c . getForOrder ( domains , order , request . Bundle , request . PrivateKey , request . MustStaple , request . PreferredChain )
if err != nil {
for _ , auth := range authz {
failures [ challenge . GetTargetedDomain ( auth ) ] = err
}
}
// Do not return an empty failures map, because
// it would still be a non-nil error value
if len ( failures ) > 0 {
return cert , failures
}
return cert , nil
}
// ObtainForCSR tries to obtain a certificate matching the CSR passed into it.
//
// The domains are inferred from the CommonName and SubjectAltNames, if any.
// The private key for this CSR is not required.
//
// If bundle is true, the []byte contains both the issuer certificate and your issued certificate as a bundle.
//
// This function will never return a partial certificate.
// If one domain in the list fails, the whole certificate will fail.
func ( c * Certifier ) ObtainForCSR ( request ObtainForCSRRequest ) ( * Resource , error ) {
if request . CSR == nil {
return nil , errors . New ( "cannot obtain resource for CSR: CSR is missing" )
}
// figure out what domains it concerns
// start with the common name
domains := certcrypto . ExtractDomainsCSR ( request . CSR )
if request . Bundle {
log . Infof ( "[%s] acme: Obtaining bundled SAN certificate given a CSR" , strings . Join ( domains , ", " ) )
} else {
log . Infof ( "[%s] acme: Obtaining SAN certificate given a CSR" , strings . Join ( domains , ", " ) )
}
order , err := c . core . Orders . New ( domains )
if err != nil {
return nil , err
}
authz , err := c . getAuthorizations ( order )
if err != nil {
// If any challenge fails, return. Do not generate partial SAN certificates.
c . deactivateAuthorizations ( order )
return nil , err
}
err = c . resolver . Solve ( authz )
if err != nil {
// If any challenge fails, return. Do not generate partial SAN certificates.
c . deactivateAuthorizations ( order )
return nil , err
}
log . Infof ( "[%s] acme: Validations succeeded; requesting certificates" , strings . Join ( domains , ", " ) )
failures := make ( obtainError )
cert , err := c . getForCSR ( domains , order , request . Bundle , request . CSR . Raw , nil , request . PreferredChain )
if err != nil {
for _ , auth := range authz {
failures [ challenge . GetTargetedDomain ( auth ) ] = err
}
}
if cert != nil {
// Add the CSR to the certificate so that it can be used for renewals.
cert . CSR = certcrypto . PEMEncode ( request . CSR )
}
// Do not return an empty failures map,
// because it would still be a non-nil error value
if len ( failures ) > 0 {
return cert , failures
}
return cert , nil
}
func ( c * Certifier ) getForOrder ( domains [ ] string , order acme . ExtendedOrder , bundle bool , privateKey crypto . PrivateKey , mustStaple bool , preferredChain string ) ( * Resource , error ) {
if privateKey == nil {
var err error
privateKey , err = certcrypto . GeneratePrivateKey ( c . options . KeyType )
if err != nil {
return nil , err
}
}
// Determine certificate name(s) based on the authorization resources
commonName := domains [ 0 ]
// RFC8555 Section 7.4 "Applying for Certificate Issuance"
// https://tools.ietf.org/html/rfc8555#section-7.4
// says:
// Clients SHOULD NOT make any assumptions about the sort order of
// "identifiers" or "authorizations" elements in the returned order
// object.
san := [ ] string { commonName }
for _ , auth := range order . Identifiers {
if auth . Value != commonName {
san = append ( san , auth . Value )
}
}
// TODO: should the CSR be customizable?
csr , err := certcrypto . GenerateCSR ( privateKey , commonName , san , mustStaple )
if err != nil {
return nil , err
}
return c . getForCSR ( domains , order , bundle , csr , certcrypto . PEMEncode ( privateKey ) , preferredChain )
}
func ( c * Certifier ) getForCSR ( domains [ ] string , order acme . ExtendedOrder , bundle bool , csr , privateKeyPem [ ] byte , preferredChain string ) ( * Resource , error ) {
respOrder , err := c . core . Orders . UpdateForCSR ( order . Finalize , csr )
if err != nil {
return nil , err
}
commonName := domains [ 0 ]
certRes := & Resource {
Domain : commonName ,
CertURL : respOrder . Certificate ,
PrivateKey : privateKeyPem ,
}
if respOrder . Status == acme . StatusValid {
// if the certificate is available right away, short cut!
ok , errR := c . checkResponse ( respOrder , certRes , bundle , preferredChain )
if errR != nil {
return nil , errR
}
if ok {
return certRes , nil
}
}
timeout := c . options . Timeout
if c . options . Timeout <= 0 {
timeout = 30 * time . Second
}
err = wait . For ( "certificate" , timeout , timeout / 60 , func ( ) ( bool , error ) {
ord , errW := c . core . Orders . Get ( order . Location )
if errW != nil {
return false , errW
}
done , errW := c . checkResponse ( ord , certRes , bundle , preferredChain )
if errW != nil {
return false , errW
}
return done , nil
} )
return certRes , err
}
// checkResponse checks to see if the certificate is ready and a link is contained in the response.
//
// If so, loads it into certRes and returns true.
// If the cert is not yet ready, it returns false.
//
// The certRes input should already have the Domain (common name) field populated.
//
// If bundle is true, the certificate will be bundled with the issuer's cert.
func ( c * Certifier ) checkResponse ( order acme . ExtendedOrder , certRes * Resource , bundle bool , preferredChain string ) ( bool , error ) {
valid , err := checkOrderStatus ( order )
if err != nil || ! valid {
return valid , err
}
2020-12-04 20:40:08 +01:00
certs , err := c . core . Certificates . GetAll ( order . Certificate , bundle )
if err != nil {
return false , err
}
2020-11-25 20:36:07 +01:00
2020-12-04 20:40:08 +01:00
// Set the default certificate
certRes . IssuerCertificate = certs [ order . Certificate ] . Issuer
certRes . Certificate = certs [ order . Certificate ] . Cert
certRes . CertURL = order . Certificate
certRes . CertStableURL = order . Certificate
2020-11-25 20:36:07 +01:00
2020-12-04 20:40:08 +01:00
if preferredChain == "" {
log . Infof ( "[%s] Server responded with a certificate." , certRes . Domain )
2020-11-25 20:36:07 +01:00
2020-12-04 20:40:08 +01:00
return true , nil
}
2020-11-25 20:36:07 +01:00
2020-12-04 20:40:08 +01:00
for link , cert := range certs {
ok , err := hasPreferredChain ( cert . Issuer , preferredChain )
2020-11-25 20:36:07 +01:00
if err != nil {
return false , err
}
if ok {
log . Infof ( "[%s] Server responded with a certificate for the preferred certificate chains %q." , certRes . Domain , preferredChain )
2020-12-04 20:40:08 +01:00
certRes . IssuerCertificate = cert . Issuer
certRes . Certificate = cert . Cert
2020-11-25 20:36:07 +01:00
certRes . CertURL = link
certRes . CertStableURL = link
return true , nil
}
}
log . Infof ( "lego has been configured to prefer certificate chains with issuer %q, but no chain from the CA matched this issuer. Using the default certificate chain instead." , preferredChain )
return true , nil
}
// Revoke takes a PEM encoded certificate or bundle and tries to revoke it at the CA.
func ( c * Certifier ) Revoke ( cert [ ] byte ) error {
certificates , err := certcrypto . ParsePEMBundle ( cert )
if err != nil {
return err
}
x509Cert := certificates [ 0 ]
if x509Cert . IsCA {
return errors . New ( "certificate bundle starts with a CA certificate" )
}
revokeMsg := acme . RevokeCertMessage {
Certificate : base64 . RawURLEncoding . EncodeToString ( x509Cert . Raw ) ,
}
return c . core . Certificates . Revoke ( revokeMsg )
}
// Renew takes a Resource and tries to renew the certificate.
//
// If the renewal process succeeds, the new certificate will be returned in a new CertResource.
// Please be aware that this function will return a new certificate in ANY case that is not an error.
// If the server does not provide us with a new cert on a GET request to the CertURL
// this function will start a new-cert flow where a new certificate gets generated.
//
// If bundle is true, the []byte contains both the issuer certificate and your issued certificate as a bundle.
//
// For private key reuse the PrivateKey property of the passed in Resource should be non-nil.
func ( c * Certifier ) Renew ( certRes Resource , bundle , mustStaple bool , preferredChain string ) ( * Resource , error ) {
// Input certificate is PEM encoded.
// Decode it here as we may need the decoded cert later on in the renewal process.
// The input may be a bundle or a single certificate.
certificates , err := certcrypto . ParsePEMBundle ( certRes . Certificate )
if err != nil {
return nil , err
}
x509Cert := certificates [ 0 ]
if x509Cert . IsCA {
return nil , fmt . Errorf ( "[%s] Certificate bundle starts with a CA certificate" , certRes . Domain )
}
// This is just meant to be informal for the user.
timeLeft := x509Cert . NotAfter . Sub ( time . Now ( ) . UTC ( ) )
log . Infof ( "[%s] acme: Trying renewal with %d hours remaining" , certRes . Domain , int ( timeLeft . Hours ( ) ) )
// We always need to request a new certificate to renew.
// Start by checking to see if the certificate was based off a CSR,
// and use that if it's defined.
if len ( certRes . CSR ) > 0 {
csr , errP := certcrypto . PemDecodeTox509CSR ( certRes . CSR )
if errP != nil {
return nil , errP
}
return c . ObtainForCSR ( ObtainForCSRRequest {
CSR : csr ,
Bundle : bundle ,
PreferredChain : preferredChain ,
} )
}
var privateKey crypto . PrivateKey
if certRes . PrivateKey != nil {
privateKey , err = certcrypto . ParsePEMPrivateKey ( certRes . PrivateKey )
if err != nil {
return nil , err
}
}
query := ObtainRequest {
Domains : certcrypto . ExtractDomains ( x509Cert ) ,
Bundle : bundle ,
PrivateKey : privateKey ,
MustStaple : mustStaple ,
}
return c . Obtain ( query )
}
// GetOCSP takes a PEM encoded cert or cert bundle returning the raw OCSP response,
// the parsed response, and an error, if any.
//
// The returned []byte can be passed directly into the OCSPStaple property of a tls.Certificate.
// If the bundle only contains the issued certificate,
// this function will try to get the issuer certificate from the IssuingCertificateURL in the certificate.
//
// If the []byte and/or ocsp.Response return values are nil, the OCSP status may be assumed OCSPUnknown.
func ( c * Certifier ) GetOCSP ( bundle [ ] byte ) ( [ ] byte , * ocsp . Response , error ) {
certificates , err := certcrypto . ParsePEMBundle ( bundle )
if err != nil {
return nil , nil , err
}
// We expect the certificate slice to be ordered downwards the chain.
// SRV CRT -> CA. We need to pull the leaf and issuer certs out of it,
// which should always be the first two certificates.
// If there's no OCSP server listed in the leaf cert, there's nothing to do.
// And if we have only one certificate so far, we need to get the issuer cert.
issuedCert := certificates [ 0 ]
if len ( issuedCert . OCSPServer ) == 0 {
return nil , nil , errors . New ( "no OCSP server specified in cert" )
}
if len ( certificates ) == 1 {
// TODO: build fallback. If this fails, check the remaining array entries.
if len ( issuedCert . IssuingCertificateURL ) == 0 {
return nil , nil , errors . New ( "no issuing certificate URL" )
}
resp , errC := c . core . HTTPClient . Get ( issuedCert . IssuingCertificateURL [ 0 ] )
if errC != nil {
return nil , nil , errC
}
defer resp . Body . Close ( )
issuerBytes , errC := ioutil . ReadAll ( http . MaxBytesReader ( nil , resp . Body , maxBodySize ) )
if errC != nil {
return nil , nil , errC
}
issuerCert , errC := x509 . ParseCertificate ( issuerBytes )
if errC != nil {
return nil , nil , errC
}
// Insert it into the slice on position 0
// We want it ordered right SRV CRT -> CA
certificates = append ( certificates , issuerCert )
}
issuerCert := certificates [ 1 ]
// Finally kick off the OCSP request.
ocspReq , err := ocsp . CreateRequest ( issuedCert , issuerCert , nil )
if err != nil {
return nil , nil , err
}
resp , err := c . core . HTTPClient . Post ( issuedCert . OCSPServer [ 0 ] , "application/ocsp-request" , bytes . NewReader ( ocspReq ) )
if err != nil {
return nil , nil , err
}
defer resp . Body . Close ( )
ocspResBytes , err := ioutil . ReadAll ( http . MaxBytesReader ( nil , resp . Body , maxBodySize ) )
if err != nil {
return nil , nil , err
}
ocspRes , err := ocsp . ParseResponse ( ocspResBytes , issuerCert )
if err != nil {
return nil , nil , err
}
return ocspResBytes , ocspRes , nil
}
// Get attempts to fetch the certificate at the supplied URL.
// The URL is the same as what would normally be supplied at the Resource's CertURL.
//
// The returned Resource will not have the PrivateKey and CSR fields populated as these will not be available.
//
// If bundle is true, the Certificate field in the returned Resource includes the issuer certificate.
func ( c * Certifier ) Get ( url string , bundle bool ) ( * Resource , error ) {
cert , issuer , err := c . core . Certificates . Get ( url , bundle )
if err != nil {
return nil , err
}
// Parse the returned cert bundle so that we can grab the domain from the common name.
x509Certs , err := certcrypto . ParsePEMBundle ( cert )
if err != nil {
return nil , err
}
return & Resource {
Domain : x509Certs [ 0 ] . Subject . CommonName ,
Certificate : cert ,
IssuerCertificate : issuer ,
CertURL : url ,
CertStableURL : url ,
} , nil
}
func hasPreferredChain ( issuer [ ] byte , preferredChain string ) ( bool , error ) {
certs , err := certcrypto . ParsePEMBundle ( issuer )
if err != nil {
return false , err
}
2021-04-07 10:58:21 +02:00
topCert := certs [ len ( certs ) - 1 ]
if topCert . Issuer . CommonName == preferredChain {
return true , nil
2020-11-25 20:36:07 +01:00
}
return false , nil
}
func checkOrderStatus ( order acme . ExtendedOrder ) ( bool , error ) {
switch order . Status {
case acme . StatusValid :
return true , nil
case acme . StatusInvalid :
return false , order . Error
default :
return false , nil
}
}
// https://tools.ietf.org/html/rfc8555#section-7.1.4
2020-12-04 20:40:08 +01:00
// The domain name MUST be encoded in the form in which it would appear in a certificate.
// That is, it MUST be encoded according to the rules in Section 7 of [RFC5280].
2020-11-25 20:36:07 +01:00
//
// https://tools.ietf.org/html/rfc5280#section-7
func sanitizeDomain ( domains [ ] string ) [ ] string {
var sanitizedDomains [ ] string
for _ , domain := range domains {
sanitizedDomain , err := idna . ToASCII ( domain )
if err != nil {
log . Infof ( "skip domain %q: unable to sanitize (punnycode): %v" , domain , err )
} else {
sanitizedDomains = append ( sanitizedDomains , sanitizedDomain )
}
}
return sanitizedDomains
}