Skip to content
Permalink
Browse files

Initial commit.

©! I, Hugo Landau <hlandau@devever.net>, hereby licence these changes under the
©! licence with SHA256 hash
©! fd80a26fbb3f644af1fa994134446702932968519797227e07a1368dea80f0bc.
  • Loading branch information
hlandau committed Mar 14, 2018
0 parents commit b8372fa18dee8789f17eedb9b05e0a54940b4d87
Showing with 3,414 additions and 0 deletions.
  1. +15 −0 README.md
  2. +91 −0 acmeendpoints/endpoint.go
  3. +47 −0 acmeendpoints/endpoint_test.go
  4. +33 −0 acmeendpoints/endpoints.go
  5. +176 −0 acmeendpoints/url.go
  6. +259 −0 acmeendpoints/url_test.go
  7. +33 −0 acmeutils/hostname.go
  8. +127 −0 acmeutils/keyauth.go
  9. +56 −0 acmeutils/keyauth_test.go
  10. +145 −0 acmeutils/load.go
  11. +540 −0 acmeutils/load_test.go
  12. +370 −0 api-res.go
  13. +511 −0 api.go
  14. +98 −0 nonce.go
  15. +37 −0 nonce_test.go
  16. +66 −0 ocsp.go
  17. +86 −0 ocsp_test.go
  18. +465 −0 types.go
  19. +21 −0 types_test.go
  20. +54 −0 util-errors.go
  21. +73 −0 util-retry.go
  22. +111 −0 util-retry_test.go
@@ -0,0 +1,15 @@
acmeapi
=======

WORK IN PROGRESS.

This is an ACME protocol client library written in Go.

It targets the final version of the ACME specification.

It deprecates [hlandau/acme/acmeapi](https://github.com/hlandau/acme), which
implemented the `-02` version of the specification initially implemented by
Let's Encrypt.

© 2017-2018 Hugo Landau <hlandau@devever.net> MIT License

@@ -0,0 +1,91 @@
// Package acmeendpoints provides information on known ACME servers.
package acmeendpoints

import (
"fmt"
"regexp"
"sync"
"text/template"
)

// Provides information on a known ACME endpoint.
type Endpoint struct {
// Friendly name for the provider. Should be a short, single-line, title case
// human readable description of the endpoint.
Title string

// Short unique endpoint identifier. Must match ^[a-zA-Z][a-zA-Z0-9_]*$ and
// should use CamelCase.
Code string

// The ACME directory URL. Must be an HTTPS URL and typically ends in
// "/directory".
DirectoryURL string

// If this is not "", this is a regexp which must be matched iff an OCSP
// endpoint URL as found in a certificate implies that a certificate was
// issued by this endpoint.
OCSPURLRegexp string
ocspURLRegexp *regexp.Regexp

// If this is not "", this is a regexp which must be matched iff an URL
// appears to be an ACME certificate URL for this endpoint.
CertificateURLRegexp string
certificateURLRegexp *regexp.Regexp

// If this is not "", it is a Go template used to construct a certificate URL
// from an *x509.Certificate. The certificate is passed as variable
// "Certificate".
CertificateURLTemplate string
certificateURLTemplate *template.Template

// Whether the endpoint gives live certificates.
Live bool

initOnce sync.Once
}

func (e *Endpoint) String() string {
return fmt.Sprintf("Endpoint(%v)", e.DirectoryURL)
}

func (e *Endpoint) init() {
e.initOnce.Do(func() {
if e.OCSPURLRegexp != "" {
e.ocspURLRegexp = regexp.MustCompile(e.OCSPURLRegexp)
}

if e.CertificateURLRegexp != "" {
e.certificateURLRegexp = regexp.MustCompile(e.CertificateURLRegexp)
}

if e.CertificateURLTemplate != "" {
e.certificateURLTemplate = template.Must(template.New("certificate-url").Parse(e.CertificateURLTemplate))
}
})
}

var endpoints []*Endpoint

// Visit all registered endpoints.
func Visit(f func(p *Endpoint) error) error {
for _, p := range endpoints {
err := f(p)
if err != nil {
return err
}
}

return nil
}

// Register a new endpoint.
func RegisterEndpoint(p *Endpoint) {
endpoints = append(endpoints, p)
}

func init() {
for _, p := range builtinEndpoints {
RegisterEndpoint(p)
}
}
@@ -0,0 +1,47 @@
package acmeendpoints

import (
"fmt"
"testing"
)

func TestVisit(t *testing.T) {
ep := map[*Endpoint]struct{}{}
err := Visit(func(e *Endpoint) error {
ep[e] = struct{}{}
return nil
})
if err != nil {
t.Fail()
}
_, ok := ep[&LetsEncryptLiveV2]
if !ok {
t.Fail()
}
_, ok = ep[&LetsEncryptStagingV2]
if !ok {
t.Fail()
}

ep = map[*Endpoint]struct{}{}
e1 := fmt.Errorf("e1")
i := 0
err = Visit(func(e *Endpoint) error {
if i == 1 {
return e1
}
i++
ep[e] = struct{}{}
return nil
})
if err != e1 {
t.Fail()
}
if len(ep) != 1 {
t.Fail()
}
_, ok = ep[&LetsEncryptLiveV2]
if !ok {
t.Fail()
}
}
@@ -0,0 +1,33 @@
package acmeendpoints

var (
// Let's Encrypt (Live v2)
LetsEncryptLiveV2 = Endpoint{
Code: "LetsEncryptLiveV2",
Title: "Let's Encrypt (Live v2)",
DirectoryURL: "https://acme-v02.api.letsencrypt.org/directory",
OCSPURLRegexp: `^http://ocsp\.int-[^.]+\.letsencrypt\.org\.?/.*$`,
CertificateURLRegexp: `^https://acme-v02\.api\.letsencrypt\.org\.?/acme/cert/.*$`,
CertificateURLTemplate: `https://acme-v02.api.letsencrypt.org/acme/cert/{{.Certificate.SerialNumber|printf "%036x"}}`,
Live: true,
}

// Let's Encrypt (Staging v2)
LetsEncryptStagingV2 = Endpoint{
Code: "LetsEncryptStagingV2",
Title: "Let's Encrypt (Staging v2)",
DirectoryURL: "https://acme-staging-v02.api.letsencrypt.org/directory",
OCSPURLRegexp: `^http://ocsp\.(staging|stg-int)-[^.]+\.letsencrypt\.org\.?/.*$`,
CertificateURLRegexp: `^https://acme-staging-v02\.api\.letsencrypt\.org\.?/acme/cert/.*$`,
CertificateURLTemplate: `https://acme-staging-v02.api.letsencrypt.org/acme/cert/{{.Certificate.SerialNumber|printf "%036x"}}`,
Live: false,
}
)

// Suggested default endpoint.
var DefaultEndpoint = &LetsEncryptLiveV2

var builtinEndpoints = []*Endpoint{
&LetsEncryptLiveV2,
&LetsEncryptStagingV2,
}
@@ -0,0 +1,176 @@
package acmeendpoints

import (
"bytes"
"crypto/sha256"
"crypto/x509"
"errors"
"fmt"
"github.com/hlandau/acme/acmeapi"
"github.com/hlandau/xlog"
"golang.org/x/net/context"
"net/url"
"regexp"
)

var log, Log = xlog.New("acme.endpoints")

// Returned when no matching endpoint can be found.
var ErrNotFound = errors.New("no corresponding endpoint found")

// Finds an endpoint with the given directory URL. If no such endpoint is
// found, returns ErrNotFound.
func ByDirectoryURL(directoryURL string) (*Endpoint, error) {
for _, e := range endpoints {
if directoryURL == e.DirectoryURL {
return e, nil
}
}

return nil, ErrNotFound
}

// If an endpoint exists with the given directory URL, returns it.
//
// Otherwise, tries to create a new endpoint for the directory URL. Where
// possible, endpoint parameters are guessed. Currently boulder is supported.
// Non-boulder based endpoints will not have any parameters set other than the
// directory URL, which means some operations on the endpoint will not succeed.
//
// It is acceptable to change the fields of the returned endpoint.
// By default, the title of the endpoint is the directory URL.
func CreateByDirectoryURL(directoryURL string) (*Endpoint, error) {
e, err := ByDirectoryURL(directoryURL)
if err == nil {
return e, nil
}

// Make a code for the endpoint by hashing the directory URL...
h := sha256.New()
h.Write([]byte(directoryURL))
code := fmt.Sprintf("Temp%08x", h.Sum(nil)[0:4])

e = &Endpoint{
Title: directoryURL,
DirectoryURL: directoryURL,
Code: code,
}

guessParameters(e)

return e, nil
}

func guessParameters(e *Endpoint) {
u, err := url.Parse(e.DirectoryURL)
if err != nil {
return
}

// not boulder
if u.Path != "/directory" {
return
}

if e.CertificateURLRegexp == "" {
e.CertificateURLRegexp = "^https://" + regexp.QuoteMeta(u.Host) + "/acme/cert/.*$"
}

if e.CertificateURLTemplate == "" {
e.CertificateURLTemplate = "https://" + u.Host + "/acme/cert/{{.Certificate.SerialNumber|printf \"%036x\"}}"
}
}

// Given an URL to a certificate, tries to determine the directory URL.
func CertificateURLToDirectoryURL(certificateURL string) (string, error) {
for _, e := range endpoints {
e.init()

if e.certificateURLRegexp != nil && e.certificateURLRegexp.MatchString(certificateURL) {
return e.DirectoryURL, nil
}
}

return "", ErrNotFound
}

// Given a certificate in DER form, tries to determine the set of endpoints
// which may have issued the certificate. certain is true if the returned
// endpoint definitely issued the certificate, in which case len(endpoints) ==
// 1 (but len(endpoints) == 1 does not necessarily imply certainty).
func CertificateToEndpoints(cert *x509.Certificate) (endp []*Endpoint, certain bool, err error) {
var unknownEndpoints []*Endpoint

for _, e := range endpoints {
e.init()

if e.ocspURLRegexp == nil {
unknownEndpoints = append(unknownEndpoints, e)
}

log.Debugf("cert has OCSP %v", cert.OCSPServer)
for _, ocspServer := range cert.OCSPServer {
log.Debugf("%v %v", e, ocspServer)
if e.ocspURLRegexp != nil && e.ocspURLRegexp.MatchString(ocspServer) {
return []*Endpoint{e}, true, nil
}
}
}

if len(unknownEndpoints) > 0 {
return unknownEndpoints, false, nil
}

log.Debugf("cannot find any endpoints for certificate")
return nil, false, ErrNotFound
}

// Given a certificate, tries to determine the certificate URL and definite endpoint.
func CertificateToEndpointURL(cl *acmeapi.Client, cert *x509.Certificate, ctx context.Context) (*Endpoint, string, error) {
es, certain, err := CertificateToEndpoints(cert)
if err != nil {
return nil, "", err
}

for _, e := range es {
if e.certificateURLTemplate == nil {
continue
}

var b bytes.Buffer
err = e.certificateURLTemplate.Execute(&b, map[string]interface{}{
"Certificate": cert,
})
if err != nil {
return nil, "", err
}

u := b.String()
if !certain {
// Check that this is the right endpoint via an HTTP request.
acrt := acmeapi.Certificate{
URI: u,
}

err := cl.LoadCertificate(&acrt, ctx)
if err != nil {
continue
}

// check that the certificate DER matches
if !bytes.Equal(acrt.Certificate, cert.Raw) {
continue
}
}

return e, u, nil
}

return nil, "", ErrNotFound
}

// Given a certificate, tries to determine the definite endpoint.
func CertificateToEndpoint(cl *acmeapi.Client, cert *x509.Certificate, ctx context.Context) (*Endpoint, error) {
e, _, err := CertificateToEndpointURL(cl, cert, ctx)
return e, err
}

0 comments on commit b8372fa

Please sign in to comment.
You can’t perform that action at this time.