Skip to content

Commit

Permalink
New v2 API for more customizable orders (#23)
Browse files Browse the repository at this point in the history
* New v2 API for more customizable orders

* Minor tweaks and improve docs

* Fix comment
  • Loading branch information
mholt committed Mar 8, 2024
1 parent 10d0fd9 commit c0dcf7b
Show file tree
Hide file tree
Showing 9 changed files with 262 additions and 156 deletions.
16 changes: 12 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
acmez - ACME client library for Go
==================================

[![godoc](https://pkg.go.dev/badge/github.com/mholt/acmez)](https://pkg.go.dev/github.com/mholt/acmez)
[![godoc](https://pkg.go.dev/badge/github.com/mholt/acmez/v2)](https://pkg.go.dev/github.com/mholt/acmez/v2)

ACMEz ("ack-measy" or "acme-zee", whichever you prefer) is a fully-compliant [RFC 8555](https://tools.ietf.org/html/rfc8555) (ACME) implementation in pure Go. It is lightweight, has an elegant Go API, and its retry logic is highly robust against external errors. ACMEz is suitable for large-scale enterprise deployments.
ACMEz ("ack-measy" or "acme-zee", whichever you prefer) is a fully-compliant [RFC 8555](https://tools.ietf.org/html/rfc8555) (ACME) implementation in pure Go. It is lightweight, has an elegant Go API, and its retry logic is highly robust against external errors. ACMEz is suitable for large-scale enterprise deployments. It also supports common IETF-standardized ACME extensions.

**NOTE:** This module is for _getting_ certificates, not _managing_ certificates. Most users probably want certificate _management_ (keeping certificates renewed) rather than to interface directly with ACME. Developers who want to use certificates in their long-running Go programs should use [CertMagic](https://github.com/caddyserver/certmagic) instead; or, if their program is not written in Go, [Caddy](https://caddyserver.com/) can be used to manage certificates (even without running an HTTP or TLS server).
**NOTE:** This module is for _getting_ certificates, not _managing_ certificates. Most users probably want certificate _management_ (keeping certificates renewed) rather than to interface directly with ACME. Developers who want to use certificates in their long-running Go programs should use [CertMagic](https://github.com/caddyserver/certmagic) instead; or, if their program is not written in Go, [Caddy](https://caddyserver.com/) can be used to manage certificates (even without running an HTTP or TLS server if needed).

This module has two primary packages:

Expand All @@ -27,11 +27,19 @@ In other words, the `acmez` package is **porcelain** while the `acme` package is
- Highly flexible and customizable
- External Account Binding (EAB) support
- Tested with multiple ACME CAs (more than just Let's Encrypt)
- Supports niche aspects of RFC 8555 (such as alt cert chains and account key rollover)
- Implements niche aspects of RFC 8555 (such as alt cert chains and account key rollover)
- Efficient solving of large SAN lists (e.g. for slow DNS record propagation)
- Utility functions for solving challenges
- [Device attestation challenges](https://datatracker.ietf.org/doc/draft-acme-device-attest/)
- RFC 8737 (tls-alpn-01 challenge)
- ACME Renewal Information (ARI) support (draft-ietf-acme-ari-03)


## Install

```
go get github.com/mholt/acmez/v2
```


## Examples
Expand Down
9 changes: 9 additions & 0 deletions acme/account.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,12 +25,19 @@ import (
// Account represents a set of metadata associated with an account
// as defined by the ACME spec §7.1.2:
// https://tools.ietf.org/html/rfc8555#section-7.1.2
//
// Users of this Go package should generally set Contact,
// TermsOfServiceAgreed, ExternalAccountBinding if relevant,
// and PrivateKey fields when creating a new account. Other
// fields are populated by the ACME server.
type Account struct {
// status (required, string): The status of this account. Possible
// values are "valid", "deactivated", and "revoked". The value
// "deactivated" should be used to indicate client-initiated
// deactivation whereas "revoked" should be used to indicate server-
// initiated deactivation. See Section 7.1.6.
//
// The client need NOT set this field when creating a new account.
Status string `json:"status"`

// contact (optional, array of string): An array of URLs that the
Expand Down Expand Up @@ -70,6 +77,8 @@ type Account struct {
// The private key to the account. Because it is secret, it is
// not serialized as JSON and must be stored separately (usually
// a PEM-encoded file).
//
// This is a required field when creating a new account.
PrivateKey crypto.Signer `json:"-"`
}

Expand Down
13 changes: 9 additions & 4 deletions acme/ari.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ import (
"context"
"crypto/x509"
"encoding/base64"
"fmt"
"net/http"
"time"

Expand Down Expand Up @@ -83,9 +82,15 @@ func (c *Client) ariEndpoint(leafCert *x509.Certificate) string {
if leafCert == nil || leafCert.SerialNumber == nil {
return ""
}
return fmt.Sprintf("%s/%s.%s", c.dir.RenewalInfo,
b64NoPad.EncodeToString(leafCert.AuthorityKeyId),
b64NoPad.EncodeToString(leafCert.SerialNumber.Bytes()))
return c.dir.RenewalInfo + "/" + ARIUniqueIdentifier(leafCert)
}

// ARIUniqueIdentifier returns the unique identifier for the certificate
// as used by ACME Renewal Information.
// EXPERIMENTAL: ARI is a draft RFC spec: draft-ietf-acme-ari-03
func ARIUniqueIdentifier(leafCert *x509.Certificate) string {
return b64NoPad.EncodeToString(leafCert.AuthorityKeyId) + "." +
b64NoPad.EncodeToString(leafCert.SerialNumber.Bytes())
}

var b64NoPad = base64.URLEncoding.WithPadding(base64.NoPadding)
14 changes: 4 additions & 10 deletions acme/jws.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,16 +46,10 @@ type keyID string
// See jwsEncodeJSON for details.
const noKeyID = keyID("")

// // noPayload indicates jwsEncodeJSON will encode zero-length octet string
// // in a JWS request. This is called POST-as-GET in RFC 8555 and is used to make
// // authenticated GET requests via POSTing with an empty payload.
// // See https://tools.ietf.org/html/rfc8555#section-6.3 for more details.
// const noPayload = ""

// jwsEncodeEAB creates a JWS payload for External Account Binding according to RFC 8555 §7.3.4.
func jwsEncodeEAB(accountKey crypto.PublicKey, hmacKey []byte, kid keyID, url string) ([]byte, error) {
// §7.3.4: "The 'alg' field MUST indicate a MAC-based algorithm"
alg, sha := "HS256", crypto.SHA256
const alg = "HS256"

// §7.3.4: "The 'nonce' field MUST NOT be present"
phead, err := jwsHead(alg, "", url, kid, nil)
Expand All @@ -75,7 +69,7 @@ func jwsEncodeEAB(accountKey crypto.PublicKey, hmacKey []byte, kid keyID, url st
h.Write(payloadToSign)
sig := h.Sum(nil)

return jwsFinal(sha, sig, phead, payload)
return jwsFinal(sig, phead, payload)
}

// jwsEncodeJSON signs claimset using provided key and a nonce.
Expand Down Expand Up @@ -119,7 +113,7 @@ func jwsEncodeJSON(claimset any, key crypto.Signer, kid keyID, nonce, url string
return nil, err
}

return jwsFinal(sha, sig, phead, payload)
return jwsFinal(sig, phead, payload)
}

// jwkEncode encodes public part of an RSA or ECDSA key into a JWK.
Expand Down Expand Up @@ -186,7 +180,7 @@ func jwsHead(alg, nonce, url string, kid keyID, key crypto.Signer) (string, erro
}

// jwsFinal constructs the final JWS object.
func jwsFinal(sha crypto.Hash, sig []byte, phead, payload string) ([]byte, error) {
func jwsFinal(sig []byte, phead, payload string) ([]byte, error) {
enc := struct {
Protected string `json:"protected"`
Payload string `json:"payload"`
Expand Down
3 changes: 1 addition & 2 deletions acme/order.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,8 +51,7 @@ type Order struct {
// this field in New Order requests if there is a clear predecessor
// certificate, as is the case for most certificate renewals.
//
// ACME EXTENSION. REFER TO:
// ACME Renewal Information (ARI) spec, draft-ietf-acme-ari-03.
// EXPERIMENTAL: Draft ACME extension ARI: draft-ietf-acme-ari-03
Replaces string `json:"replaces,omitempty"`

// notBefore (optional, string): The requested value of the notBefore
Expand Down
Loading

0 comments on commit c0dcf7b

Please sign in to comment.