forked from TrueCloudLab/lego
Add initial support for certificate bundling
This commit is contained in:
parent
d6f4e42b13
commit
51a95ee548
4 changed files with 113 additions and 23 deletions
125
acme/client.go
125
acme/client.go
|
@ -44,12 +44,13 @@ type solver interface {
|
|||
|
||||
// Client is the user-friendy way to ACME
|
||||
type Client struct {
|
||||
directory directory
|
||||
user User
|
||||
jws *jws
|
||||
keyBits int
|
||||
devMode bool
|
||||
solvers map[string]solver
|
||||
directory directory
|
||||
user User
|
||||
jws *jws
|
||||
keyBits int
|
||||
devMode bool
|
||||
issuerCert []byte
|
||||
solvers map[string]solver
|
||||
}
|
||||
|
||||
// NewClient creates a new client for the set user.
|
||||
|
@ -161,7 +162,9 @@ func (c *Client) AgreeToTOS() error {
|
|||
// ObtainCertificates tries to obtain certificates from the CA server
|
||||
// using the challenges it has configured. The returned certificates are
|
||||
// PEM encoded byte slices.
|
||||
func (c *Client) ObtainCertificates(domains []string) ([]CertificateResource, error) {
|
||||
// If bundle is true, the []byte contains both the issuer certificate and
|
||||
// your issued certificate as a bundle.
|
||||
func (c *Client) ObtainCertificates(domains []string, bundle bool) ([]CertificateResource, error) {
|
||||
logger().Print("Obtaining certificates...")
|
||||
challenges := c.getChallenges(domains)
|
||||
err := c.solveChallenges(challenges)
|
||||
|
@ -171,17 +174,22 @@ func (c *Client) ObtainCertificates(domains []string) ([]CertificateResource, er
|
|||
|
||||
logger().Print("Validations succeeded. Getting certificates")
|
||||
|
||||
return c.requestCertificates(challenges)
|
||||
return c.requestCertificates(challenges, bundle)
|
||||
}
|
||||
|
||||
// RevokeCertificate takes a PEM encoded certificate and tries to revoke it at the CA.
|
||||
// RevokeCertificate takes a PEM encoded certificate or bundle and tries to revoke it at the CA.
|
||||
func (c *Client) RevokeCertificate(certificate []byte) error {
|
||||
certBlock, err := pemDecode(certificate)
|
||||
certificates, err := parsePEMBundle(certificate)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
encodedCert := base64.URLEncoding.EncodeToString(certBlock.Bytes)
|
||||
x509Cert := certificates[len(certificates)-1]
|
||||
if x509Cert.IsCA {
|
||||
return fmt.Errorf("Certificate bundle ends with a CA certificate")
|
||||
}
|
||||
|
||||
encodedCert := base64.URLEncoding.EncodeToString(x509Cert.Raw)
|
||||
|
||||
jsonBytes, err := json.Marshal(revokeCertMessage{Resource: "revoke-cert", Certificate: encodedCert})
|
||||
if err != nil {
|
||||
|
@ -207,14 +215,21 @@ func (c *Client) RevokeCertificate(certificate []byte) error {
|
|||
// 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.
|
||||
func (c *Client) RenewCertificate(cert CertificateResource, revokeOld bool) (CertificateResource, error) {
|
||||
// If bundle is true, the []byte contains both the issuer certificate and
|
||||
// your issued certificate as a bundle.
|
||||
func (c *Client) RenewCertificate(cert CertificateResource, revokeOld bool, bundle bool) (CertificateResource, error) {
|
||||
// Input certificate is PEM encoded. Decode it here as we may need the decoded
|
||||
// cert later on in the renewal process.
|
||||
x509Cert, err := pemDecodeTox509(cert.Certificate)
|
||||
// cert later on in the renewal process. The input may be a bundle or a single certificate.
|
||||
certificates, err := parsePEMBundle(cert.Certificate)
|
||||
if err != nil {
|
||||
return CertificateResource{}, err
|
||||
}
|
||||
|
||||
x509Cert := certificates[len(certificates)-1]
|
||||
if x509Cert.IsCA {
|
||||
return CertificateResource{}, fmt.Errorf("[%s] Certificate bundle ends with a CA certificate", cert.Domain)
|
||||
}
|
||||
|
||||
// This is just meant to be informal for the user.
|
||||
timeLeft := x509Cert.NotAfter.Sub(time.Now().UTC())
|
||||
logger().Printf("[%s] Trying to renew certificate with %d hours remaining.", cert.Domain, int(timeLeft.Hours()))
|
||||
|
@ -243,11 +258,31 @@ func (c *Client) RenewCertificate(cert CertificateResource, revokeOld bool) (Cer
|
|||
if revokeOld {
|
||||
c.RevokeCertificate(cert.Certificate)
|
||||
}
|
||||
cert.Certificate = pemEncode(derCertificateBytes(serverCertBytes))
|
||||
issuedCert := pemEncode(derCertificateBytes(serverCertBytes))
|
||||
// If bundle is true, we want to return a certificate bundle.
|
||||
// To do this, we need the issuer certificate.
|
||||
if bundle {
|
||||
// The issuer certificate link is always supplied via an "up" link
|
||||
// in the response headers of a new certificate.
|
||||
links := parseLinks(resp.Header["Link"])
|
||||
issuerCert, err := c.getIssuerCertificate(links["up"])
|
||||
if err != nil {
|
||||
// If we fail to aquire the issuer cert, return the issued certificate - do not fail.
|
||||
logger().Printf("[%s] Could not bundle issuer certificate.\n%v", cert.Domain, err)
|
||||
cert.Certificate = issuedCert
|
||||
} else {
|
||||
// Success - prepend the issuer cert to the issued cert.
|
||||
issuerCert = pemEncode(derCertificateBytes(issuerCert))
|
||||
issuerCert = append(issuerCert, issuedCert...)
|
||||
cert.Certificate = issuerCert
|
||||
}
|
||||
} else {
|
||||
cert.Certificate = issuedCert
|
||||
}
|
||||
return cert, nil
|
||||
}
|
||||
|
||||
newCerts, err := c.ObtainCertificates([]string{cert.Domain})
|
||||
newCerts, err := c.ObtainCertificates([]string{cert.Domain}, bundle)
|
||||
if err != nil {
|
||||
return CertificateResource{}, err
|
||||
}
|
||||
|
@ -361,10 +396,10 @@ func (c *Client) getChallenges(domains []string) []*authorizationResource {
|
|||
// requestCertificates iterates all granted authorizations, creates RSA private keys and CSRs.
|
||||
// It then uses these to request a certificate from the CA and returns the list of successfully
|
||||
// granted certificates.
|
||||
func (c *Client) requestCertificates(challenges []*authorizationResource) ([]CertificateResource, error) {
|
||||
func (c *Client) requestCertificates(challenges []*authorizationResource, bundle bool) ([]CertificateResource, error) {
|
||||
resc, errc := make(chan CertificateResource), make(chan error)
|
||||
for _, authz := range challenges {
|
||||
go c.requestCertificate(authz, resc, errc)
|
||||
go c.requestCertificate(authz, resc, errc, bundle)
|
||||
}
|
||||
|
||||
var certs []CertificateResource
|
||||
|
@ -383,13 +418,14 @@ func (c *Client) requestCertificates(challenges []*authorizationResource) ([]Cer
|
|||
return certs, nil
|
||||
}
|
||||
|
||||
func (c *Client) requestCertificate(authz *authorizationResource, result chan CertificateResource, errc chan error) {
|
||||
func (c *Client) requestCertificate(authz *authorizationResource, result chan CertificateResource, errc chan error, bundle bool) {
|
||||
privKey, err := generatePrivateKey(rsakey, c.keyBits)
|
||||
if err != nil {
|
||||
errc <- err
|
||||
return
|
||||
}
|
||||
|
||||
// TODO: should the CSR be customizable?
|
||||
csr, err := generateCsr(privKey.(*rsa.PrivateKey), authz.Domain)
|
||||
if err != nil {
|
||||
errc <- err
|
||||
|
@ -432,8 +468,30 @@ func (c *Client) requestCertificate(authz *authorizationResource, result chan Ce
|
|||
// certificate was not ready at the time this request completed.
|
||||
// Otherwise the body is the certificate.
|
||||
if len(cert) > 0 {
|
||||
|
||||
cerRes.CertStableURL = resp.Header.Get("Content-Location")
|
||||
cerRes.Certificate = pemEncode(derCertificateBytes(cert))
|
||||
|
||||
issuedCert := pemEncode(derCertificateBytes(cert))
|
||||
// If bundle is true, we want to return a certificate bundle.
|
||||
// To do this, we need the issuer certificate.
|
||||
if bundle {
|
||||
// The issuer certificate link is always supplied via an "up" link
|
||||
// in the response headers of a new certificate.
|
||||
links := parseLinks(resp.Header["Link"])
|
||||
issuerCert, err := c.getIssuerCertificate(links["up"])
|
||||
if err != nil {
|
||||
// If we fail to aquire the issuer cert, return the issued certificate - do not fail.
|
||||
logger().Printf("[%s] Could not bundle issuer certificate.\n%v", authz.Domain, err)
|
||||
cerRes.Certificate = issuedCert
|
||||
} else {
|
||||
// Success - prepend the issuer cert to the issued cert.
|
||||
issuerCert = pemEncode(derCertificateBytes(issuerCert))
|
||||
issuerCert = append(issuerCert, issuedCert...)
|
||||
cerRes.Certificate = issuerCert
|
||||
}
|
||||
} else {
|
||||
cerRes.Certificate = issuedCert
|
||||
}
|
||||
logger().Printf("[%s] Server responded with a certificate.", authz.Domain)
|
||||
result <- cerRes
|
||||
return
|
||||
|
@ -465,6 +523,33 @@ func (c *Client) requestCertificate(authz *authorizationResource, result chan Ce
|
|||
}
|
||||
}
|
||||
|
||||
// getIssuerCertificate requests the issuer certificate and caches it for
|
||||
// subsequent requests.
|
||||
func (c *Client) getIssuerCertificate(url string) ([]byte, error) {
|
||||
logger().Printf("Requesting issuer cert from: %s", url)
|
||||
if c.issuerCert != nil {
|
||||
return c.issuerCert, nil
|
||||
}
|
||||
|
||||
resp, err := http.Get(url)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
issuerBytes, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
_, err = x509.ParseCertificate(issuerBytes)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
c.issuerCert = issuerBytes
|
||||
return issuerBytes, err
|
||||
}
|
||||
|
||||
func logResponseHeaders(resp *http.Response) {
|
||||
logger().Println(resp.Status)
|
||||
for k, v := range resp.Header {
|
||||
|
|
|
@ -61,6 +61,7 @@ func GetOCSPForCert(bundle []byte) ([]byte, error) {
|
|||
}
|
||||
|
||||
// Insert it into the slice on position 0
|
||||
// We want it ordered right CA -> CRT
|
||||
certificates = append(certificates, nil)
|
||||
copy(certificates[1:], certificates[0:])
|
||||
certificates[0] = issuerCert
|
||||
|
@ -127,6 +128,8 @@ func performECDH(priv *ecdsa.PrivateKey, pub *ecdsa.PublicKey, outLen int, label
|
|||
return buffer
|
||||
}
|
||||
|
||||
// parsePEMBundle parses a certificate bundle from top to bottom and returns
|
||||
// a slice of x509 certificates. This function will error if no certificates are found.
|
||||
func parsePEMBundle(bundle []byte) ([]*x509.Certificate, error) {
|
||||
var certificates []*x509.Certificate
|
||||
|
||||
|
|
|
@ -94,7 +94,9 @@ type revokeCertMessage struct {
|
|||
|
||||
// CertificateResource represents a CA issued certificate.
|
||||
// PrivateKey and Certificate are both already PEM encoded
|
||||
// and can be directly written to disk.
|
||||
// and can be directly written to disk. Certificate may
|
||||
// be a certificate bundle, depending on the options supplied
|
||||
// to create it.
|
||||
type CertificateResource struct {
|
||||
Domain string `json:"domain"`
|
||||
CertURL string `json:"certUrl"`
|
||||
|
|
|
@ -120,7 +120,7 @@ func run(c *cli.Context) {
|
|||
logger().Fatal("Please specify --domains")
|
||||
}
|
||||
|
||||
certs, err := client.ObtainCertificates(c.GlobalStringSlice("domains"))
|
||||
certs, err := client.ObtainCertificates(c.GlobalStringSlice("domains"), true)
|
||||
if err != nil {
|
||||
logger().Fatalf("Could not obtain certificates\n\t%v", err)
|
||||
}
|
||||
|
@ -198,7 +198,7 @@ func renew(c *cli.Context) {
|
|||
certRes.PrivateKey = keyBytes
|
||||
certRes.Certificate = certBytes
|
||||
|
||||
newCert, err := client.RenewCertificate(certRes, true)
|
||||
newCert, err := client.RenewCertificate(certRes, true, true)
|
||||
if err != nil {
|
||||
logger().Printf("%v", err)
|
||||
return
|
||||
|
|
Loading…
Reference in a new issue