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
|
// Client is the user-friendy way to ACME
|
||||||
type Client struct {
|
type Client struct {
|
||||||
directory directory
|
directory directory
|
||||||
user User
|
user User
|
||||||
jws *jws
|
jws *jws
|
||||||
keyBits int
|
keyBits int
|
||||||
devMode bool
|
devMode bool
|
||||||
solvers map[string]solver
|
issuerCert []byte
|
||||||
|
solvers map[string]solver
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewClient creates a new client for the set user.
|
// 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
|
// ObtainCertificates tries to obtain certificates from the CA server
|
||||||
// using the challenges it has configured. The returned certificates are
|
// using the challenges it has configured. The returned certificates are
|
||||||
// PEM encoded byte slices.
|
// 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...")
|
logger().Print("Obtaining certificates...")
|
||||||
challenges := c.getChallenges(domains)
|
challenges := c.getChallenges(domains)
|
||||||
err := c.solveChallenges(challenges)
|
err := c.solveChallenges(challenges)
|
||||||
|
@ -171,17 +174,22 @@ func (c *Client) ObtainCertificates(domains []string) ([]CertificateResource, er
|
||||||
|
|
||||||
logger().Print("Validations succeeded. Getting certificates")
|
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 {
|
func (c *Client) RevokeCertificate(certificate []byte) error {
|
||||||
certBlock, err := pemDecode(certificate)
|
certificates, err := parsePEMBundle(certificate)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
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})
|
jsonBytes, err := json.Marshal(revokeCertMessage{Resource: "revoke-cert", Certificate: encodedCert})
|
||||||
if err != nil {
|
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.
|
// 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
|
// 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.
|
// 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
|
// Input certificate is PEM encoded. Decode it here as we may need the decoded
|
||||||
// cert later on in the renewal process.
|
// cert later on in the renewal process. The input may be a bundle or a single certificate.
|
||||||
x509Cert, err := pemDecodeTox509(cert.Certificate)
|
certificates, err := parsePEMBundle(cert.Certificate)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return CertificateResource{}, err
|
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.
|
// This is just meant to be informal for the user.
|
||||||
timeLeft := x509Cert.NotAfter.Sub(time.Now().UTC())
|
timeLeft := x509Cert.NotAfter.Sub(time.Now().UTC())
|
||||||
logger().Printf("[%s] Trying to renew certificate with %d hours remaining.", cert.Domain, int(timeLeft.Hours()))
|
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 {
|
if revokeOld {
|
||||||
c.RevokeCertificate(cert.Certificate)
|
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
|
return cert, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
newCerts, err := c.ObtainCertificates([]string{cert.Domain})
|
newCerts, err := c.ObtainCertificates([]string{cert.Domain}, bundle)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return CertificateResource{}, err
|
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.
|
// 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
|
// It then uses these to request a certificate from the CA and returns the list of successfully
|
||||||
// granted certificates.
|
// 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)
|
resc, errc := make(chan CertificateResource), make(chan error)
|
||||||
for _, authz := range challenges {
|
for _, authz := range challenges {
|
||||||
go c.requestCertificate(authz, resc, errc)
|
go c.requestCertificate(authz, resc, errc, bundle)
|
||||||
}
|
}
|
||||||
|
|
||||||
var certs []CertificateResource
|
var certs []CertificateResource
|
||||||
|
@ -383,13 +418,14 @@ func (c *Client) requestCertificates(challenges []*authorizationResource) ([]Cer
|
||||||
return certs, nil
|
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)
|
privKey, err := generatePrivateKey(rsakey, c.keyBits)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
errc <- err
|
errc <- err
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: should the CSR be customizable?
|
||||||
csr, err := generateCsr(privKey.(*rsa.PrivateKey), authz.Domain)
|
csr, err := generateCsr(privKey.(*rsa.PrivateKey), authz.Domain)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
errc <- err
|
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.
|
// certificate was not ready at the time this request completed.
|
||||||
// Otherwise the body is the certificate.
|
// Otherwise the body is the certificate.
|
||||||
if len(cert) > 0 {
|
if len(cert) > 0 {
|
||||||
|
|
||||||
cerRes.CertStableURL = resp.Header.Get("Content-Location")
|
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)
|
logger().Printf("[%s] Server responded with a certificate.", authz.Domain)
|
||||||
result <- cerRes
|
result <- cerRes
|
||||||
return
|
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) {
|
func logResponseHeaders(resp *http.Response) {
|
||||||
logger().Println(resp.Status)
|
logger().Println(resp.Status)
|
||||||
for k, v := range resp.Header {
|
for k, v := range resp.Header {
|
||||||
|
|
|
@ -61,6 +61,7 @@ func GetOCSPForCert(bundle []byte) ([]byte, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Insert it into the slice on position 0
|
// Insert it into the slice on position 0
|
||||||
|
// We want it ordered right CA -> CRT
|
||||||
certificates = append(certificates, nil)
|
certificates = append(certificates, nil)
|
||||||
copy(certificates[1:], certificates[0:])
|
copy(certificates[1:], certificates[0:])
|
||||||
certificates[0] = issuerCert
|
certificates[0] = issuerCert
|
||||||
|
@ -127,6 +128,8 @@ func performECDH(priv *ecdsa.PrivateKey, pub *ecdsa.PublicKey, outLen int, label
|
||||||
return buffer
|
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) {
|
func parsePEMBundle(bundle []byte) ([]*x509.Certificate, error) {
|
||||||
var certificates []*x509.Certificate
|
var certificates []*x509.Certificate
|
||||||
|
|
||||||
|
|
|
@ -94,7 +94,9 @@ type revokeCertMessage struct {
|
||||||
|
|
||||||
// CertificateResource represents a CA issued certificate.
|
// CertificateResource represents a CA issued certificate.
|
||||||
// PrivateKey and Certificate are both already PEM encoded
|
// 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 {
|
type CertificateResource struct {
|
||||||
Domain string `json:"domain"`
|
Domain string `json:"domain"`
|
||||||
CertURL string `json:"certUrl"`
|
CertURL string `json:"certUrl"`
|
||||||
|
|
|
@ -120,7 +120,7 @@ func run(c *cli.Context) {
|
||||||
logger().Fatal("Please specify --domains")
|
logger().Fatal("Please specify --domains")
|
||||||
}
|
}
|
||||||
|
|
||||||
certs, err := client.ObtainCertificates(c.GlobalStringSlice("domains"))
|
certs, err := client.ObtainCertificates(c.GlobalStringSlice("domains"), true)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger().Fatalf("Could not obtain certificates\n\t%v", err)
|
logger().Fatalf("Could not obtain certificates\n\t%v", err)
|
||||||
}
|
}
|
||||||
|
@ -198,7 +198,7 @@ func renew(c *cli.Context) {
|
||||||
certRes.PrivateKey = keyBytes
|
certRes.PrivateKey = keyBytes
|
||||||
certRes.Certificate = certBytes
|
certRes.Certificate = certBytes
|
||||||
|
|
||||||
newCert, err := client.RenewCertificate(certRes, true)
|
newCert, err := client.RenewCertificate(certRes, true, true)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger().Printf("%v", err)
|
logger().Printf("%v", err)
|
||||||
return
|
return
|
||||||
|
|
Loading…
Reference in a new issue