Commit a68b0108 authored by Matthew Holt's avatar Matthew Holt

vendor: Update dependencies; add certmagic, update lego

parent e0f1a02c
The MIT License (MIT)
Copyright (c) 2014 Coda Hale
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
// func HasAESNI() bool
TEXT ·HasAESNI(SB),$0
XORQ AX, AX
INCL AX
CPUID
SHRQ $25, CX
ANDQ $1, CX
MOVB CX, ret+0(FP)
RET
// +build amd64
package aesnicheck
// HasAESNI returns whether AES-NI is supported by the CPU.
func HasAESNI() bool
// +build !amd64
package aesnicheck
// HasAESNI returns whether AES-NI is supported by the CPU.
func HasAESNI() bool {
return false
}
// Command aesnicheck queries the CPU for AES-NI support. If AES-NI is supported,
// aesnicheck will print "supported" and exit with a status of 0. If AES-NI is
// not supported, aesnicheck will print "unsupported" and exit with a status of
// -1.
package main
import (
"fmt"
"os"
"github.com/codahale/aesnicheck"
)
func main() {
if aesnicheck.HasAESNI() {
fmt.Println("supported")
os.Exit(0)
} else {
fmt.Println("unsupported")
os.Exit(-1)
}
}
// Package aesnicheck provides a simple check to see if crypto/aes is using
// AES-NI instructions or if the AES transform is being done in software. AES-NI
// is constant-time, which makes it impervious to cache-level timing attacks. For
// security-conscious deployments on public cloud infrastructure (Amazon EC2,
// Google Compute Engine, Microsoft Azure, etc.) this may be critical.
//
// See http://eprint.iacr.org/2014/248 for details on cross-VM timing attacks on
// AES keys.
package aesnicheck
This diff is collapsed.
// Copyright 2015 Matthew Holt
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package certmagic
import (
"fmt"
"sync"
"time"
)
// Cache is a structure that stores certificates in memory.
// Generally, there should only be one per process. However,
// complex applications that virtualize the concept of a
// "process" (such as Caddy, which virtualizes processes as
// "instances" so it can do graceful, in-memory reloads of
// its configuration) may use more of these per OS process.
//
// Using just one cache per process avoids duplication of
// certificates across multiple configurations and makes
// maintenance easier.
//
// An empty cache is INVALID and must not be used.
// Be sure to call NewCertificateCache to get one.
//
// These should be very long-lived values, and must not be
// copied. Before all references leave scope to be garbage
// collected, ensure you call Stop() to stop maintenance
// maintenance on the certificates stored in this cache.
type Cache struct {
// How often to check certificates for renewal
RenewInterval time.Duration
// How often to check if OCSP stapling needs updating
OCSPInterval time.Duration
// The storage implementation
storage Storage
// The cache is keyed by certificate hash
cache map[string]Certificate
// Protects the cache map
mu sync.RWMutex
// Close this channel to cancel asset maintenance
stopChan chan struct{}
}
// NewCache returns a new, valid Cache backed by the
// given storage implementation. It also begins a
// maintenance goroutine for any managed certificates
// stored in this cache.
//
// See the godoc for Cache to use it properly.
//
// Note that all processes running in a cluster
// configuration must use the same storage value
// in order to share certificates. (A single storage
// value may be shared by multiple clusters as well.)
func NewCache(storage Storage) *Cache {
c := &Cache{
RenewInterval: DefaultRenewInterval,
OCSPInterval: DefaultOCSPInterval,
storage: storage,
cache: make(map[string]Certificate),
stopChan: make(chan struct{}),
}
go c.maintainAssets()
return c
}
// Stop stops the maintenance goroutine for
// certificates in certCache.
func (certCache *Cache) Stop() {
close(certCache.stopChan)
}
// replaceCertificate replaces oldCert with newCert in the cache, and
// updates all configs that are pointing to the old certificate to
// point to the new one instead. newCert must already be loaded into
// the cache (this method does NOT load it into the cache).
//
// Note that all the names on the old certificate will be deleted
// from the name lookup maps of each config, then all the names on
// the new certificate will be added to the lookup maps as long as
// they do not overwrite any entries.
//
// The newCert may be modified and its cache entry updated.
//
// This method is safe for concurrent use.
func (certCache *Cache) replaceCertificate(oldCert, newCert Certificate) error {
certCache.mu.Lock()
defer certCache.mu.Unlock()
// have all the configs that are pointing to the old
// certificate point to the new certificate instead
for _, cfg := range oldCert.configs {
// first delete all the name lookup entries that
// pointed to the old certificate
for name, certKey := range cfg.certificates {
if certKey == oldCert.Hash {
delete(cfg.certificates, name)
}
}
// then add name lookup entries for the names
// on the new certificate, but don't overwrite
// entries that may already exist, not only as
// a courtesy, but importantly: because if we
// overwrote a value here, and this config no
// longer pointed to a certain certificate in
// the cache, that certificate's list of configs
// referring to it would be incorrect; so just
// insert entries, don't overwrite any
for _, name := range newCert.Names {
if _, ok := cfg.certificates[name]; !ok {
cfg.certificates[name] = newCert.Hash
}
}
}
// since caching a new certificate attaches only the config
// that loaded it, the new certificate needs to be given the
// list of all the configs that use it, so copy the list
// over from the old certificate to the new certificate
// in the cache
newCert.configs = oldCert.configs
certCache.cache[newCert.Hash] = newCert
// finally, delete the old certificate from the cache
delete(certCache.cache, oldCert.Hash)
return nil
}
// reloadManagedCertificate reloads the certificate corresponding to the name(s)
// on oldCert into the cache, from storage. This also replaces the old certificate
// with the new one, so that all configurations that used the old cert now point
// to the new cert.
func (certCache *Cache) reloadManagedCertificate(oldCert Certificate) error {
// get the certificate from storage and cache it
newCert, err := oldCert.configs[0].CacheManagedCertificate(oldCert.Names[0])
if err != nil {
return fmt.Errorf("unable to reload certificate for %v into cache: %v", oldCert.Names, err)
}
// and replace the old certificate with the new one
err = certCache.replaceCertificate(oldCert, newCert)
if err != nil {
return fmt.Errorf("replacing certificate %v: %v", oldCert.Names, err)
}
return nil
}
// defaultCache is a convenient, default certificate cache for
// use by this process when no other certificate cache is provided.
var defaultCache = NewCache(DefaultStorage)
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
// Copyright 2015 Matthew Holt
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package certmagic
import (
"crypto"
"crypto/ecdsa"
"crypto/rsa"
"crypto/sha256"
"crypto/x509"
"encoding/json"
"encoding/pem"
"fmt"
"hash/fnv"
"github.com/xenolf/lego/certificate"
)
// encodePrivateKey marshals a EC or RSA private key into a PEM-encoded array of bytes.
func encodePrivateKey(key crypto.PrivateKey) ([]byte, error) {
var pemType string
var keyBytes []byte
switch key := key.(type) {
case *ecdsa.PrivateKey:
var err error
pemType = "EC"
keyBytes, err = x509.MarshalECPrivateKey(key)
if err != nil {
return nil, err
}
case *rsa.PrivateKey:
pemType = "RSA"
keyBytes = x509.MarshalPKCS1PrivateKey(key)
}
pemKey := pem.Block{Type: pemType + " PRIVATE KEY", Bytes: keyBytes}
return pem.EncodeToMemory(&pemKey), nil
}
// decodePrivateKey loads a PEM-encoded ECC/RSA private key from an array of bytes.
func decodePrivateKey(keyPEMBytes []byte) (crypto.PrivateKey, error) {
keyBlock, _ := pem.Decode(keyPEMBytes)
switch keyBlock.Type {
case "RSA PRIVATE KEY":
return x509.ParsePKCS1PrivateKey(keyBlock.Bytes)
case "EC PRIVATE KEY":
return x509.ParseECPrivateKey(keyBlock.Bytes)
}
return nil, fmt.Errorf("unknown private key type")
}
// parseCertsFromPEMBundle 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 parseCertsFromPEMBundle(bundle []byte) ([]*x509.Certificate, error) {
var certificates []*x509.Certificate
var certDERBlock *pem.Block
for {
certDERBlock, bundle = pem.Decode(bundle)
if certDERBlock == nil {
break
}
if certDERBlock.Type == "CERTIFICATE" {
cert, err := x509.ParseCertificate(certDERBlock.Bytes)
if err != nil {
return nil, err
}
certificates = append(certificates, cert)
}
}
if len(certificates) == 0 {
return nil, fmt.Errorf("no certificates found in bundle")
}
return certificates, nil
}
// fastHash hashes input using a hashing algorithm that
// is fast, and returns the hash as a hex-encoded string.
// Do not use this for cryptographic purposes.
func fastHash(input []byte) string {
h := fnv.New32a()
h.Write(input)
return fmt.Sprintf("%x", h.Sum32())
}
// saveCertResource saves the certificate resource to disk. This
// includes the certificate file itself, the private key, and the
// metadata file.
func (cfg *Config) saveCertResource(cert *certificate.Resource) error {
metaBytes, err := json.MarshalIndent(&cert, "", "\t")
if err != nil {
return fmt.Errorf("encoding certificate metadata: %v", err)
}
all := []keyValue{
{
key: prefixSiteCert(cfg.CA, cert.Domain),
value: cert.Certificate,
},
{
key: prefixSiteKey(cfg.CA, cert.Domain),
value: cert.PrivateKey,
},
{
key: prefixSiteMeta(cfg.CA, cert.Domain),
value: metaBytes,
},
}
return storeTx(cfg.certCache.storage, all)
}
func (cfg *Config) loadCertResource(domain string) (certificate.Resource, error) {
var certRes certificate.Resource
certBytes, err := cfg.certCache.storage.Load(prefixSiteCert(cfg.CA, domain))
if err != nil {
return certRes, err
}
keyBytes, err := cfg.certCache.storage.Load(prefixSiteKey(cfg.CA, domain))
if err != nil {
return certRes, err
}
metaBytes, err := cfg.certCache.storage.Load(prefixSiteMeta(cfg.CA, domain))
if err != nil {
return certRes, err
}
err = json.Unmarshal(metaBytes, &certRes)
if err != nil {
return certRes, fmt.Errorf("decoding certificate metadata: %v", err)
}
certRes.Certificate = certBytes
certRes.PrivateKey = keyBytes
return certRes, nil
}
// hashCertificateChain computes the unique hash of certChain,
// which is the chain of DER-encoded bytes. It returns the
// hex encoding of the hash.
func hashCertificateChain(certChain [][]byte) string {
h := sha256.New()
for _, certInChain := range certChain {
h.Write(certInChain)
}
return fmt.Sprintf("%x", h.Sum(nil))
}
// Copyright 2015 Matthew Holt
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package certmagic
import (
"io/ioutil"
"os"
"path/filepath"
"runtime"
)
// FileStorage facilitates forming file paths derived from a root
// directory. It is used to get file paths in a consistent,
// cross-platform way or persisting ACME assets on the file system.
type FileStorage struct {
Path string
}
// Exists returns true if key exists in fs.
func (fs FileStorage) Exists(key string) bool {
_, err := os.Stat(fs.filename(key))
return !os.IsNotExist(err)
}
// Store saves value at key.
func (fs FileStorage) Store(key string, value []byte) error {
filename := fs.filename(key)
err := os.MkdirAll(filepath.Dir(filename), 0700)
if err != nil {
return err
}
return ioutil.WriteFile(filename, value, 0600)
}
// Load retrieves the value at key.
func (fs FileStorage) Load(key string) ([]byte, error) {
contents, err := ioutil.ReadFile(fs.filename(key))
if os.IsNotExist(err) {
return nil, ErrNotExist(err)
}
return contents, nil
}
// Delete deletes the value at key.
// TODO: Delete any empty folders caused by this operation
func (fs FileStorage) Delete(key string) error {
err := os.Remove(fs.filename(key))
if os.IsNotExist(err) {
return ErrNotExist(err)
}
return err
}
// List returns all keys that match prefix.
func (fs FileStorage) List(prefix string) ([]string, error) {
d, err := os.Open(fs.filename(prefix))
if os.IsNotExist(err) {
return nil, ErrNotExist(err)
}
if err != nil {
return nil, err
}
defer d.Close()
return d.Readdirnames(-1)
}
// Stat returns information about key.
func (fs FileStorage) Stat(key string) (KeyInfo, error) {
fi, err := os.Stat(fs.filename(key))
if os.IsNotExist(err) {
return KeyInfo{}, ErrNotExist(err)
}
if err != nil {
return KeyInfo{}, err
}
return KeyInfo{
Key: key,
Modified: fi.ModTime(),
Size: fi.Size(),
}, nil
}
func (fs FileStorage) filename(key string) string {
return filepath.Join(fs.Path, filepath.FromSlash(key))
}
// homeDir returns the best guess of the current user's home
// directory from environment variables. If unknown, "." (the
// current directory) is returned instead.
func homeDir() string {
home := os.Getenv("HOME")
if home == "" && runtime.GOOS == "windows" {
drive := os.Getenv("HOMEDRIVE")
path := os.Getenv("HOMEPATH")
home = drive + path
if drive == "" || path == "" {
home = os.Getenv("USERPROFILE")
}
}
if home == "" {
home = "."
}
return home
}
func dataDir() string {
baseDir := filepath.Join(homeDir(), ".local", "share")
if xdgData := os.Getenv("XDG_DATA_HOME"); xdgData != "" {
baseDir = xdgData
}
return filepath.Join(baseDir, "certmagic")
}
var _ Storage = FileStorage{}
// Copyright 2015 Matthew Holt
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package certmagic
import (
"fmt"
"os"
"path/filepath"
"sync"
"time"
)
// FileStorageLocker implements the Locker interface
// using the file system. An empty value is NOT VALID,
// so you must use NewFileStorageLocker() to get one.
type FileStorageLocker struct {
fs FileStorage
}
// NewFileStorageLocker returns a valid Locker backed by fs.
func NewFileStorageLocker(fs FileStorage) *FileStorageLocker {
return &FileStorageLocker{fs: fs}
}
// TryLock attempts to get a lock for name, otherwise it returns
// a Waiter value to wait until the other process is finished.
func (l *FileStorageLocker) TryLock(name string) (Waiter, error) {
fileStorageNameLocksMu.Lock()
defer fileStorageNameLocksMu.Unlock()
// see if lock already exists within this process
fw, ok := fileStorageNameLocks[name]
if ok {
// lock already created within process, let caller wait on it
return fw, nil
}
// attempt to persist lock to disk by creating lock file
// parent dir must exist
lockDir := l.lockDir()
if err := os.MkdirAll(lockDir, 0700); err != nil {
return nil, err
}
fw = &FileStorageWaiter{
filename: filepath.Join(lockDir, safeKey(name)+".lock"),
wg: new(sync.WaitGroup),
}
// create the file in a special mode such that an
// error is returned if it already exists
lf, err := os.OpenFile(fw.filename, os.O_CREATE|os.O_EXCL, 0644)
if err != nil {
if os.IsExist(err) {
// another process has the lock; use it to wait
return fw, nil
}
// otherwise, this was some unexpected error
return nil, err
}
lf.Close()
// looks like we get the lock
fw.wg.Add(1)
fileStorageNameLocks[name] = fw
return nil, nil
}
// Unlock releases the lock for name.
func (l *FileStorageLocker) Unlock(name string) error {
fileStorageNameLocksMu.Lock()
defer fileStorageNameLocksMu.Unlock()
fw, ok := fileStorageNameLocks[name]
if !ok {
return fmt.Errorf("FileStorageLocker: no lock to release for %s", name)
}
// remove lock file
os.Remove(fw.filename)
// if parent folder is now empty, remove it too to keep it tidy
dir, err := os.Open(l.lockDir()) // OK to ignore error here
if err == nil {
items, _ := dir.Readdirnames(3) // OK to ignore error here
if len(items) == 0 {
os.Remove(dir.Name())
}
dir.Close()
}
// clean up in memory
fw.wg.Done()
delete(fileStorageNameLocks, name)
return nil
}
func (l *FileStorageLocker) lockDir() string {
return filepath.Join(l.fs.Path, "locks")
}
// FileStorageWaiter waits for a file to disappear; it
// polls the file system to check for the existence of
// a file. It also uses a WaitGroup to optimize the
// polling in the case when this process is the only
// one waiting. (Other processes that are waiting
// for the lock will still block, but must wait
// for the poll intervals to get their answer.)
type FileStorageWaiter struct {
filename string
wg *sync.WaitGroup
}
// Wait waits until the lock is released.
func (fw *FileStorageWaiter) Wait() {
start := time.Now()
fw.wg.Wait()
for time.Since(start) < 1*time.Hour {
_, err := os.Stat(fw.filename)
if os.IsNotExist(err) {
return
}
time.Sleep(1 * time.Second)
}
}
var fileStorageNameLocks = make(map[string]*FileStorageWaiter)
var fileStorageNameLocksMu sync.Mutex
var _ Locker = &FileStorageLocker{}
var _ Waiter = &FileStorageWaiter{}
This diff is collapsed.
// Copyright 2015 Matthew Holt
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package certmagic
import (
"encoding/json"
"log"
"net/http"
"strings"
"github.com/xenolf/lego/challenge/http01"
)
// HTTPChallengeHandler wraps h in a handler that can solve the ACME
// HTTP challenge. cfg is required, and it must have a certificate
// cache backed by a functional storage facility, since that is where
// the challenge state is stored between initiation and solution.
//
// If a request is not an ACME HTTP challenge, h willl be invoked.
func (cfg *Config) HTTPChallengeHandler(h http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if cfg.HandleHTTPChallenge(w, r) {
return
}
h.ServeHTTP(w, r)
})
}
// HandleHTTPChallenge uses cfg to solve challenge requests from an ACME
// server that were initiated by this instance or any other instance in
// this cluster (being, any instances using the same storage cfg does).
//
// If the HTTP challenge is disabled, this function is a no-op.
//
// If cfg is nil or if cfg does not have a certificate cache backed by
// usable storage, solving the HTTP challenge will fail.
//
// It returns true if it handled the request; if so, the response has
// already been written. If false is returned, this call was a no-op and
// the request has not been handled.
func (cfg *Config) HandleHTTPChallenge(w http.ResponseWriter, r *http.Request) bool {
if cfg == nil {
return false
}
if cfg.DisableHTTPChallenge {
return false
}
if !strings.HasPrefix(r.URL.Path, challengeBasePath) {
return false
}
return cfg.distributedHTTPChallengeSolver(w, r)
}
// distributedHTTPChallengeSolver checks to see if this challenge
// request was initiated by this or another instance which uses the
// same storage as cfg does, and attempts to complete the challenge for
// it. It returns true if the request was handled; false otherwise.
func (cfg *Config) distributedHTTPChallengeSolver(w http.ResponseWriter, r *http.Request) bool {
if cfg == nil {
return false
}
tokenKey := distributedSolver{config: cfg}.challengeTokensKey(r.Host)
chalInfoBytes, err := cfg.certCache.storage.Load(tokenKey)
if err != nil {
if _, ok := err.(ErrNotExist); !ok {
log.Printf("[ERROR][%s] Opening distributed HTTP challenge token file: %v", r.Host, err)
}
return false
}
var chalInfo challengeInfo
err = json.Unmarshal(chalInfoBytes, &chalInfo)
if err != nil {
log.Printf("[ERROR][%s] Decoding challenge token file %s (corrupted?): %v", r.Host, tokenKey, err)
return false
}
return answerHTTPChallenge(w, r, chalInfo)
}
// answerHTTPChallenge solves the challenge with chalInfo.
// Most of this code borrowed from xenolf/lego's built-in HTTP-01
// challenge solver in March 2018.
func answerHTTPChallenge(w http.ResponseWriter, r *http.Request, chalInfo challengeInfo) bool {
challengeReqPath := http01.ChallengePath(chalInfo.Token)
if r.URL.Path == challengeReqPath &&
strings.HasPrefix(r.Host, chalInfo.Domain) &&
r.Method == "GET" {
w.Header().Add("Content-Type", "text/plain")
w.Write([]byte(chalInfo.KeyAuth))
r.Close = true
log.Printf("[INFO][%s] Served key authentication (distributed)", chalInfo.Domain)
return true
}
return false
}
const challengeBasePath = "/.well-known/acme-challenge"
This diff is collapsed.
// Copyright 2015 Matthew Holt
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package certmagic
import (
"fmt"
"sync"
)
// MemoryLocker implements the Locker interface
// using memory. An empty value is NOT VALID,
// so you must use NewMemoryLocker() to get one.
type MemoryLocker struct {
nameLocks map[string]*MemoryWaiter
nameLocksMu *sync.Mutex
}
// NewMemoryLocker returns a valid Locker backed by fs.
func NewMemoryLocker() *MemoryLocker {
return &MemoryLocker{
nameLocks: make(map[string]*MemoryWaiter),
nameLocksMu: new(sync.Mutex),
}
}
// TryLock attempts to get a lock for name, otherwise it returns
// a Waiter value to wait until the other process is finished.
func (l *MemoryLocker) TryLock(name string) (Waiter, error) {
l.nameLocksMu.Lock()
defer l.nameLocksMu.Unlock()
// see if lock already exists within this process
w, ok := l.nameLocks[name]
if ok {
return w, nil
}
// we got the lock, so create it
w = &MemoryWaiter{wg: new(sync.WaitGroup)}
w.wg.Add(1)
l.nameLocks[name] = w
return nil, nil
}
// Unlock releases the lock for name.
func (l *MemoryLocker) Unlock(name string) error {
l.nameLocksMu.Lock()
defer l.nameLocksMu.Unlock()
w, ok := l.nameLocks[name]
if !ok {
return fmt.Errorf("MemoryLocker: no lock to release for %s", name)
}
w.wg.Done()
delete(l.nameLocks, name)
return nil
}
// MemoryWaiter implements Waiter in memory.
type MemoryWaiter struct {
wg *sync.WaitGroup
}
// Wait waits until w.wg is done.
func (w *MemoryWaiter) Wait() {
w.Wait()
}
var _ Locker = &MemoryLocker{}
var _ Waiter = &MemoryWaiter{}
// Copyright 2015 Matthew Holt
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package certmagic
import (
"bytes"
"crypto/x509"
"encoding/pem"
"fmt"
"io"
"io/ioutil"
"log"
"net/http"
"time"
"golang.org/x/crypto/ocsp"
)
// stapleOCSP staples OCSP information to cert for hostname name.
// If you have it handy, you should pass in the PEM-encoded certificate
// bundle; otherwise the DER-encoded cert will have to be PEM-encoded.
// If you don't have the PEM blocks already, just pass in nil.
//
// Errors here are not necessarily fatal, it could just be that the
// certificate doesn't have an issuer URL.
func (certCache *Cache) stapleOCSP(cert *Certificate, pemBundle []byte) error {
if pemBundle == nil {
// we need a PEM encoding only for some function calls below
bundle := new(bytes.Buffer)
for _, derBytes := range cert.Certificate.Certificate {
pem.Encode(bundle, &pem.Block{Type: "CERTIFICATE", Bytes: derBytes})
}
pemBundle = bundle.Bytes()
}
var ocspBytes []byte
var ocspResp *ocsp.Response
var ocspErr error
var gotNewOCSP bool
// First try to load OCSP staple from storage and see if
// we can still use it.
ocspStapleKey := prefixOCSPStaple(cert, pemBundle)
cachedOCSP, err := certCache.storage.Load(ocspStapleKey)
if err == nil {
resp, err := ocsp.ParseResponse(cachedOCSP, nil)
if err == nil {
if freshOCSP(resp) {
// staple is still fresh; use it
ocspBytes = cachedOCSP
ocspResp = resp
}
} else {
// invalid contents; delete the file
// (we do this independently of the maintenance routine because
// in this case we know for sure this should be a staple file
// because we loaded it by name, whereas the maintenance routine
// just iterates the list of files, even if somehow a non-staple
// file gets in the folder. in this case we are sure it is corrupt.)
err := certCache.storage.Delete(ocspStapleKey)
if err != nil {
log.Printf("[WARNING] Unable to delete invalid OCSP staple file: %v", err)
}
}
}
// If we couldn't get a fresh staple by reading the cache,
// then we need to request it from the OCSP responder
if ocspResp == nil || len(ocspBytes) == 0 {
ocspBytes, ocspResp, ocspErr = getOCSPForCert(pemBundle)
if ocspErr != nil {
// An error here is not a problem because a certificate may simply
// not contain a link to an OCSP server. But we should log it anyway.
// There's nothing else we can do to get OCSP for this certificate,
// so we can return here with the error.
return fmt.Errorf("no OCSP stapling for %v: %v", cert.Names, ocspErr)
}
gotNewOCSP = true
}
// By now, we should have a response. If good, staple it to
// the certificate. If the OCSP response was not loaded from
// storage, we persist it for next time.
if ocspResp.Status == ocsp.Good {
if ocspResp.NextUpdate.After(cert.NotAfter) {
// uh oh, this OCSP response expires AFTER the certificate does, that's kinda bogus.
// it was the reason a lot of Symantec-validated sites (not Caddy) went down
// in October 2017. https://twitter.com/mattiasgeniar/status/919432824708648961
return fmt.Errorf("invalid: OCSP response for %v valid after certificate expiration (%s)",
cert.Names, cert.NotAfter.Sub(ocspResp.NextUpdate))
}
cert.Certificate.OCSPStaple = ocspBytes
cert.OCSP = ocspResp
if gotNewOCSP {
err := certCache.storage.Store(ocspStapleKey, ocspBytes)
if err != nil {
return fmt.Errorf("unable to write OCSP staple file for %v: %v", cert.Names, err)
}
}
}
return nil
}
// getOCSPForCert 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.
//
// Borrowed from github.com/xenolf/lego
func getOCSPForCert(bundle []byte) ([]byte, *ocsp.Response, error) {
// TODO: Perhaps this should be synchronized too, with a Locker?
certificates, err := parseCertsFromPEMBundle(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, fmt.Errorf("no OCSP server specified in certificate")
}
if len(certificates) == 1 {
if len(issuedCert.IssuingCertificateURL) == 0 {
return nil, nil, fmt.Errorf("no URL to issuing certificate")
}
resp, err := http.Get(issuedCert.IssuingCertificateURL[0])
if err != nil {
return nil, nil, fmt.Errorf("getting issuer certificate: %v", err)
}
defer resp.Body.Close()
issuerBytes, err := ioutil.ReadAll(io.LimitReader(resp.Body, 1024*1024))
if err != nil {
return nil, nil, fmt.Errorf("reading issuer certificate: %v", err)
}
issuerCert, err := x509.ParseCertificate(issuerBytes)
if err != nil {
return nil, nil, fmt.Errorf("parsing issuer certificate: %v", err)
}
// insert it into the slice on position 0;
// we want it ordered right SRV CRT -> CA
certificates = append(certificates, issuerCert)
}
issuerCert := certificates[1]
ocspReq, err := ocsp.CreateRequest(issuedCert, issuerCert, nil)
if err != nil {
return nil, nil, fmt.Errorf("creating OCSP request: %v", err)
}
reader := bytes.NewReader(ocspReq)
req, err := http.Post(issuedCert.OCSPServer[0], "application/ocsp-request", reader)
if err != nil {
return nil, nil, fmt.Errorf("making OCSP request: %v", err)
}
defer req.Body.Close()
ocspResBytes, err := ioutil.ReadAll(io.LimitReader(req.Body, 1024*1024))
if err != nil {
return nil, nil, fmt.Errorf("reading OCSP response: %v", err)
}
ocspRes, err := ocsp.ParseResponse(ocspResBytes, issuerCert)
if err != nil {
return nil, nil, fmt.Errorf("parsing OCSP response: %v", err)
}
return ocspResBytes, ocspRes, nil
}
// freshOCSP returns true if resp is still fresh,
// meaning that it is not expedient to get an
// updated response from the OCSP server.
func freshOCSP(resp *ocsp.Response) bool {
nextUpdate := resp.NextUpdate
// If there is an OCSP responder certificate, and it expires before the
// OCSP response, use its expiration date as the end of the OCSP
// response's validity period.
if resp.Certificate != nil && resp.Certificate.NotAfter.Before(nextUpdate) {
nextUpdate = resp.Certificate.NotAfter
}
// start checking OCSP staple about halfway through validity period for good measure
refreshTime := resp.ThisUpdate.Add(nextUpdate.Sub(resp.ThisUpdate) / 2)
return time.Now().Before(refreshTime)
}
// Copyright 2015 Matthew Holt
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package certmagic
import (
"encoding/json"
"fmt"
"log"
"path/filepath"
"github.com/xenolf/lego/challenge"
"github.com/xenolf/lego/challenge/tlsalpn01"
)
// tlsALPNSolver is a type that can solve TLS-ALPN challenges using
// an existing listener and our custom, in-memory certificate cache.
type tlsALPNSolver struct {
certCache *Cache
}
// Present adds the challenge certificate to the cache.
func (s tlsALPNSolver) Present(domain, token, keyAuth string) error {
cert, err := tlsalpn01.ChallengeCert(domain, keyAuth)
if err != nil {
return err
}
certHash := hashCertificateChain(cert.Certificate)
s.certCache.mu.Lock()
s.certCache.cache[tlsALPNCertKeyName(domain)] = Certificate{
Certificate: *cert,
Names: []string{domain},
Hash: certHash, // perhaps not necesssary
}
s.certCache.mu.Unlock()
return nil
}
// CleanUp removes the challenge certificate from the cache.
func (s tlsALPNSolver) CleanUp(domain, token, keyAuth string) error {
s.certCache.mu.Lock()
delete(s.certCache.cache, domain)
s.certCache.mu.Unlock()
return nil
}
// tlsALPNCertKeyName returns the key to use when caching a cert
// for use with the TLS-ALPN ACME challenge. It is simply to help
// avoid conflicts (although at time of writing, there shouldn't
// be, since the cert cache is keyed by hash of certificate chain).
func tlsALPNCertKeyName(sniName string) string {
return sniName + ":acme-tls-alpn"
}
// distributedSolver allows the ACME HTTP-01 and TLS-ALPN challenges
// to be solved by an instance other than the one which initiated it.
// This is useful behind load balancers or in other cluster/fleet
// configurations. The only requirement is that the instance which
// initiates the challenge shares the same storage and locker with
// the others in the cluster. The storage backing the certificate
// cache in distributedSolver.config is crucial.
//
// Obviously, the instance which completes the challenge must be
// serving on the HTTPChallengePort for the HTTP-01 challenge or the
// TLSALPNChallengePort for the TLS-ALPN-01 challenge (or have all
// the packets port-forwarded) to receive and handle the request. The
// server which receives the challenge must handle it by checking to
// see if the challenge token exists in storage, and if so, decode it
// and use it to serve up the correct response. HTTPChallengeHandler
// in this package as well as the GetCertificate method implemented
// by a Config support and even require this behavior.
//
// In short: the only two requirements for cluster operation are
// sharing sync and storage, and using the facilities provided by
// this package for solving the challenges.
type distributedSolver struct {
// The config with a certificate cache
// with a reference to the storage to
// use which is shared among all the
// instances in the cluster - REQUIRED.
config *Config
// Since the distributedSolver is only a
// wrapper over an actual solver, place
// the actual solver here.
providerServer challenge.Provider
}
// Present invokes the underlying solver's Present method
// and also stores domain, token, and keyAuth to the storage
// backing the certificate cache of dhs.config.
func (dhs distributedSolver) Present(domain, token, keyAuth string) error {
if dhs.providerServer != nil {
err := dhs.providerServer.Present(domain, token, keyAuth)
if err != nil {
return fmt.Errorf("presenting with standard provider server: %v", err)
}
}
infoBytes, err := json.Marshal(challengeInfo{
Domain: domain,
Token: token,
KeyAuth: keyAuth,
})
if err != nil {
return err
}
return dhs.config.certCache.storage.Store(dhs.challengeTokensKey(domain), infoBytes)
}
// CleanUp invokes the underlying solver's CleanUp method
// and also cleans up any assets saved to storage.
func (dhs distributedSolver) CleanUp(domain, token, keyAuth string) error {
if dhs.providerServer != nil {
err := dhs.providerServer.CleanUp(domain, token, keyAuth)
if err != nil {
log.Printf("[ERROR] Cleaning up standard provider server: %v", err)
}
}
return dhs.config.certCache.storage.Delete(dhs.challengeTokensKey(domain))
}
// challengeTokensPrefix returns the key prefix for challenge info.
func (dhs distributedSolver) challengeTokensPrefix() string {
return filepath.Join(prefixCA(dhs.config.CA), "challenge_tokens")
}
// challengeTokensKey returns the key to use to store and access
// challenge info for domain.
func (dhs distributedSolver) challengeTokensKey(domain string) string {
return filepath.Join(dhs.challengeTokensPrefix(), safeKey(domain)+".json")
}
type challengeInfo struct {
Domain, Token, KeyAuth string
}
// Copyright 2015 Matthew Holt
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package certmagic
import (
"net/url"
"path"
"regexp"
"strings"
"time"
)
// Storage is a type that implements a key-value store.
// Keys are prefix-based, with forward slash '/' as separators
// and without a leading slash.
//
// Processes running in a cluster will wish to use the
// same Storage value (its implementation and configuration)
// in order to share certificates and other TLS resources
// with the cluster.
type Storage interface {
// Exists returns true if the key exists
// and there was no error checking.
Exists(key string) bool
// Store puts value at key.
Store(key string, value []byte) error
// Load retrieves the value at key.
Load(key string) ([]byte, error)
// Delete deletes key.
Delete(key string) error
// List returns all keys that match prefix.
List(prefix string) ([]string, error)
// Stat returns information about key.
Stat(key string) (KeyInfo, error)
}
// KeyInfo holds information about a key in storage.
type KeyInfo struct {
Key string
Modified time.Time
Size int64
}
// storeTx stores all the values or none at all.
func storeTx(s Storage, all []keyValue) error {
for i, kv := range all {
err := s.Store(kv.key, kv.value)
if err != nil {
for j := i - 1; j >= 0; j-- {
s.Delete(all[j].key)
}
return err
}
}
return nil
}
// keyValue pairs a key and a value.
type keyValue struct {
key string
value []byte
}
const (
prefixACME = "acme"
prefixOCSP = "ocsp"
)
func prefixCA(ca string) string {
caURL, err := url.Parse(ca)
if err != nil {
caURL = &url.URL{Host: ca}
}
return path.Join(prefixACME, safeKey(caURL.Host))
}
func prefixSite(ca, domain string) string {
return path.Join(prefixCA(ca), "sites", safeKey(domain))
}
// prefixSiteCert returns the path to the certificate file for domain.
func prefixSiteCert(ca, domain string) string {
return path.Join(prefixSite(ca, domain), safeKey(domain)+".crt")
}
// prefixSiteKey returns the path to domain's private key file.
func prefixSiteKey(ca, domain string) string {
return path.Join(prefixSite(ca, domain), safeKey(domain)+".key")
}
// prefixSiteMeta returns the path to the domain's asset metadata file.
func prefixSiteMeta(ca, domain string) string {
return path.Join(prefixSite(ca, domain), safeKey(domain)+".json")
}
func prefixUsers(ca string) string {
return path.Join(prefixCA(ca), "users")
}
// prefixUser gets the account folder for the user with email
func prefixUser(ca, email string) string {
if email == "" {
email = emptyEmail
}
return path.Join(prefixUsers(ca), safeKey(email))
}
// prefixUserReg gets the path to the registration file for the user with the
// given email address.
func prefixUserReg(ca, email string) string {
return safeUserKey(ca, email, "registration", ".json")
}
// prefixUserKey gets the path to the private key file for the user with the
// given email address.
func prefixUserKey(ca, email string) string {
return safeUserKey(ca, email, "private", ".key")
}
func prefixOCSPStaple(cert *Certificate, pemBundle []byte) string {
var ocspFileName string
if len(cert.Names) > 0 {
firstName := safeKey(cert.Names[0])
ocspFileName = firstName + "-"
}
ocspFileName += fastHash(pemBundle)
return path.Join(prefixOCSP, ocspFileName)
}
// safeUserKey returns a key for the given email,
// with the default filename, and the filename
// ending in the given extension.
func safeUserKey(ca, email, defaultFilename, extension string) string {
if email == "" {
email = emptyEmail
}
email = strings.ToLower(email)
filename := emailUsername(email)
if filename == "" {
filename = defaultFilename
}
filename = safeKey(filename)
return path.Join(prefixUser(ca, email), filename+extension)
}
// emailUsername returns the username portion of an email address (part before
// '@') or the original input if it can't find the "@" symbol.
func emailUsername(email string) string {
at := strings.Index(email, "@")
if at == -1 {
return email
} else if at == 0 {
return email[1:]
}
return email[:at]
}
// safeKey standardizes and sanitizes str for use in a file path.
func safeKey(str string) string {
str = strings.ToLower(str)
str = strings.TrimSpace(str)
// replace a few specific characters
repl := strings.NewReplacer(
" ", "_",
"+", "_plus_",
"*", "wildcard_",
"..", "", // prevent directory traversal (regex allows single dots)
)
str = repl.Replace(str)
// finally remove all non-word characters
return safeKeyRE.ReplaceAllLiteralString(str, "")
}
// safeKeyRE matches any undesirable characters in storage keys.
// Note that this allows dots, so you'll have to strip ".." manually.
var safeKeyRE = regexp.MustCompile(`[^\w@.-]`)
// ErrNotExist is returned by Storage implementations when
// a resource is not found. It is similar to os.IsNotExist
// except this is a type, not a variable.
type ErrNotExist interface {
error
}
// defaultFileStorage is a convenient, default storage
// implementation using the local file system.
var defaultFileStorage = FileStorage{Path: dataDir()}
// DefaultStorage is the default Storage implementation.
var DefaultStorage Storage = defaultFileStorage
// DefaultSync is a default sync to use.
var DefaultSync Locker
// Copyright 2015 Matthew Holt
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package certmagic
import (
"bufio"
"crypto"
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"encoding/json"
"fmt"
"io"
"os"
"sort"
"strings"
"github.com/xenolf/lego/lego"
"github.com/xenolf/lego/registration"
)
// user represents a Let's Encrypt user account.
type user struct {
Email string
Registration *registration.Resource
key crypto.PrivateKey
}
// GetEmail gets u's email.
func (u user) GetEmail() string {
return u.Email
}
// GetRegistration gets u's registration resource.
func (u user) GetRegistration() *registration.Resource {
return u.Registration
}
// GetPrivateKey gets u's private key.
func (u user) GetPrivateKey() crypto.PrivateKey {
return u.key
}
// newUser creates a new User for the given email address
// with a new private key. This function does NOT save the
// user to disk or register it via ACME. If you want to use
// a user account that might already exist, call getUser
// instead. It does NOT prompt the user.
func (cfg *Config) newUser(email string) (user, error) {
user := user{Email: email}
privateKey, err := ecdsa.GenerateKey(elliptic.P384(), rand.Reader)
if err != nil {
return user, fmt.Errorf("generating private key: %v", err)
}
user.key = privateKey
return user, nil
}
// getEmail does everything it can to obtain an email address
// from the user within the scope of memory and storage to use
// for ACME TLS. If it cannot get an email address, it returns
// empty string. (If user is present, it will warn the user of
// the consequences of an empty email.) This function MAY prompt
// the user for input. If userPresent is false, the operator
// will NOT be prompted and an empty email may be returned.
// If the user is prompted, a new User will be created and
// stored in storage according to the email address they
// provided (which might be blank).
func (cfg *Config) getEmail(userPresent bool) (string, error) {
// First try memory
leEmail := cfg.Email
if leEmail == "" {
leEmail = Email
}
// Then try to get most recent user email from storage
if leEmail == "" {
leEmail = cfg.mostRecentUserEmail()
cfg.Email = leEmail // save for next time
}
// Looks like there is no email address readily available,
// so we will have to ask the user if we can.
if leEmail == "" && userPresent {
// evidently, no User data was present in storage;
// thus we must make a new User so that we can get
// the Terms of Service URL via our ACME client, phew!
user, err := cfg.newUser("")
if err != nil {
return "", err
}
// get the agreement URL
agreementURL := agreementTestURL
if agreementURL == "" {
// we call acme.NewClient directly because newACMEClient
// would require that we already know the user's email
caURL := CA
if cfg.CA != "" {
caURL = cfg.CA
}
legoConfig := lego.NewConfig(user)
legoConfig.CADirURL = caURL
legoConfig.UserAgent = UserAgent
tempClient, err := lego.NewClient(legoConfig)
if err != nil {
return "", fmt.Errorf("making ACME client to get ToS URL: %v", err)
}
agreementURL = tempClient.GetToSURL()
}
// prompt the user for an email address and terms agreement
reader := bufio.NewReader(stdin)
cfg.promptUserAgreement(agreementURL)
fmt.Println("Please enter your email address to signify agreement and to be notified")
fmt.Println("in case of issues. You can leave it blank, but we don't recommend it.")
fmt.Print(" Email address: ")
leEmail, err = reader.ReadString('\n')
if err != nil && err != io.EOF {
return "", fmt.Errorf("reading email address: %v", err)
}
leEmail = strings.TrimSpace(leEmail)
cfg.Email = leEmail
cfg.Agreed = true
// save the new user to preserve this for next time
user.Email = leEmail
err = cfg.saveUser(user)
if err != nil {
return "", err
}
}
// lower-casing the email is important for consistency
return strings.ToLower(leEmail), nil
}
// getUser loads the user with the given email from disk
// using the provided storage. If the user does not exist,
// it will create a new one, but it does NOT save new
// users to the disk or register them via ACME. It does
// NOT prompt the user.
func (cfg *Config) getUser(email string) (user, error) {
var user user
regBytes, err := cfg.certCache.storage.Load(prefixUserReg(cfg.CA, email))
if err != nil {
if _, ok := err.(ErrNotExist); ok {
// create a new user
return cfg.newUser(email)
}
return user, err
}
keyBytes, err := cfg.certCache.storage.Load(prefixUserKey(cfg.CA, email))
if err != nil {
if _, ok := err.(ErrNotExist); ok {
// create a new user
return cfg.newUser(email)
}
return user, err
}
err = json.Unmarshal(regBytes, &user)
if err != nil {
return user, err
}
user.key, err = decodePrivateKey(keyBytes)
return user, err
}
// saveUser persists a user's key and account registration
// to the file system. It does NOT register the user via ACME
// or prompt the user. You must also pass in the storage
// wherein the user should be saved. It should be the storage
// for the CA with which user has an account.
func (cfg *Config) saveUser(user user) error {
regBytes, err := json.MarshalIndent(&user, "", "\t")
if err != nil {
return err
}
keyBytes, err := encodePrivateKey(user.key)
if err != nil {
return err
}
all := []keyValue{
{
key: prefixUserReg(cfg.CA, user.Email),
value: regBytes,
},
{
key: prefixUserKey(cfg.CA, user.Email),
value: keyBytes,
},
}
return storeTx(cfg.certCache.storage, all)
}
// promptUserAgreement simply outputs the standard user
// agreement prompt with the given agreement URL.
// It outputs a newline after the message.
func (cfg *Config) promptUserAgreement(agreementURL string) {
const userAgreementPrompt = `Your sites will be served over HTTPS automatically using Let's Encrypt.
By continuing, you agree to the Let's Encrypt Subscriber Agreement at:`
fmt.Printf("\n\n%s\n %s\n", userAgreementPrompt, agreementURL)
}
// askUserAgreement prompts the user to agree to the agreement
// at the given agreement URL via stdin. It returns whether the
// user agreed or not.
func (cfg *Config) askUserAgreement(agreementURL string) bool {
cfg.promptUserAgreement(agreementURL)
fmt.Print("Do you agree to the terms? (y/n): ")
reader := bufio.NewReader(stdin)
answer, err := reader.ReadString('\n')
if err != nil {
return false
}
answer = strings.ToLower(strings.TrimSpace(answer))
return answer == "y" || answer == "yes"
}
// mostRecentUserEmail finds the most recently-written user file
// in s. Since this is part of a complex sequence to get a user
// account, errors here are discarded to simplify code flow in
// the caller, and errors are not important here anyway.
func (cfg *Config) mostRecentUserEmail() string {
userList, err := cfg.certCache.storage.List(prefixUsers(cfg.CA))
if err != nil || len(userList) == 0 {
return ""
}
sort.Slice(userList, func(i, j int) bool {
iInfo, _ := cfg.certCache.storage.Stat(prefixUser(cfg.CA, userList[i]))
jInfo, _ := cfg.certCache.storage.Stat(prefixUser(cfg.CA, userList[j]))
return jInfo.Modified.Before(iInfo.Modified)
})
user, err := cfg.getUser(userList[0])
if err != nil {
return ""
}
return user.Email
}
// agreementTestURL is set during tests to skip requiring
// setting up an entire ACME CA endpoint.
var agreementTestURL string
// stdin is used to read the user's input if prompted;
// this is changed by tests during tests.
var stdin = io.ReadWriter(os.Stdin)
// The name of the folder for accounts where the email
// address was not provided; default 'username' if you will,
// but only for local/storage use, not with the CA.
const emptyEmail = "default"
package api
import (
"encoding/base64"
"errors"
"fmt"
"github.com/xenolf/lego/acme"
)
type AccountService service
// New Creates a new account.
func (a *AccountService) New(req acme.Account) (acme.ExtendedAccount, error) {
var account acme.Account
resp, err := a.core.post(a.core.GetDirectory().NewAccountURL, req, &account)
location := getLocation(resp)
if len(location) > 0 {
a.core.jws.SetKid(location)
}
if err != nil {
return acme.ExtendedAccount{Location: location}, err
}
return acme.ExtendedAccount{Account: account, Location: location}, nil
}
// NewEAB Creates a new account with an External Account Binding.
func (a *AccountService) NewEAB(accMsg acme.Account, kid string, hmacEncoded string) (acme.ExtendedAccount, error) {
hmac, err := base64.RawURLEncoding.DecodeString(hmacEncoded)
if err != nil {
return acme.ExtendedAccount{}, fmt.Errorf("acme: could not decode hmac key: %v", err)
}
eabJWS, err := a.core.signEABContent(a.core.GetDirectory().NewAccountURL, kid, hmac)
if err != nil {
return acme.ExtendedAccount{}, fmt.Errorf("acme: error signing eab content: %v", err)
}
accMsg.ExternalAccountBinding = eabJWS
return a.New(accMsg)
}
// Get Retrieves an account.
func (a *AccountService) Get(accountURL string) (acme.Account, error) {
if len(accountURL) == 0 {
return acme.Account{}, errors.New("account[get]: empty URL")
}
var account acme.Account
_, err := a.core.post(accountURL, acme.Account{}, &account)
if err != nil {
return acme.Account{}, err
}
return account, nil
}
// Deactivate Deactivates an account.
func (a *AccountService) Deactivate(accountURL string) error {
if len(accountURL) == 0 {
return errors.New("account[deactivate]: empty URL")
}
req := acme.Account{Status: acme.StatusDeactivated}
_, err := a.core.post(accountURL, req, nil)
return err
}
package api
import (
"bytes"
"crypto"
"encoding/json"
"errors"
"fmt"
"net/http"
"github.com/xenolf/lego/acme"
"github.com/xenolf/lego/acme/api/internal/nonces"
"github.com/xenolf/lego/acme/api/internal/secure"
"github.com/xenolf/lego/acme/api/internal/sender"
"github.com/xenolf/lego/log"
)
// Core ACME/LE core API.
type Core struct {
doer *sender.Doer
nonceManager *nonces.Manager
jws *secure.JWS
directory acme.Directory
HTTPClient *http.Client
common service // Reuse a single struct instead of allocating one for each service on the heap.
Accounts *AccountService
Authorizations *AuthorizationService
Certificates *CertificateService
Challenges *ChallengeService
Orders *OrderService
}
// New Creates a new Core.
func New(httpClient *http.Client, userAgent string, caDirURL, kid string, privateKey crypto.PrivateKey) (*Core, error) {
doer := sender.NewDoer(httpClient, userAgent)
dir, err := getDirectory(doer, caDirURL)
if err != nil {
return nil, err
}
nonceManager := nonces.NewManager(doer, dir.NewNonceURL)
jws := secure.NewJWS(privateKey, kid, nonceManager)
c := &Core{doer: doer, nonceManager: nonceManager, jws: jws, directory: dir}
c.common.core = c
c.Accounts = (*AccountService)(&c.common)
c.Authorizations = (*AuthorizationService)(&c.common)
c.Certificates = (*CertificateService)(&c.common)
c.Challenges = (*ChallengeService)(&c.common)
c.Orders = (*OrderService)(&c.common)
return c, nil
}
// post performs an HTTP POST request and parses the response body as JSON,
// into the provided respBody object.
func (a *Core) post(uri string, reqBody, response interface{}) (*http.Response, error) {
content, err := json.Marshal(reqBody)
if err != nil {
return nil, errors.New("failed to marshal message")
}
return a.retrievablePost(uri, content, response, 0)
}
// postAsGet performs an HTTP POST ("POST-as-GET") request.
// https://tools.ietf.org/html/draft-ietf-acme-acme-16#section-6.3
func (a *Core) postAsGet(uri string, response interface{}) (*http.Response, error) {
return a.retrievablePost(uri, []byte{}, response, 0)
}
func (a *Core) retrievablePost(uri string, content []byte, response interface{}, retry int) (*http.Response, error) {
resp, err := a.signedPost(uri, content, response)
if err != nil {
// during tests, 5 retries allow to support ~50% of bad nonce.
if retry >= 5 {
log.Infof("too many retry on a nonce error, retry count: %d", retry)
return resp, err
}
switch err.(type) {
// Retry once if the nonce was invalidated
case *acme.NonceError:
log.Infof("nonce error retry: %s", err)
resp, err = a.retrievablePost(uri, content, response, retry+1)
if err != nil {
return resp, err
}
default:
return resp, err
}
}
return resp, nil
}
func (a *Core) signedPost(uri string, content []byte, response interface{}) (*http.Response, error) {
signedContent, err := a.jws.SignContent(uri, content)
if err != nil {
return nil, fmt.Errorf("failed to post JWS message -> failed to sign content -> %v", err)
}
signedBody := bytes.NewBuffer([]byte(signedContent.FullSerialize()))
resp, err := a.doer.Post(uri, signedBody, "application/jose+json", response)
// nonceErr is ignored to keep the root error.
nonce, nonceErr := nonces.GetFromResponse(resp)
if nonceErr == nil {
a.nonceManager.Push(nonce)
}
return resp, err
}
func (a *Core) signEABContent(newAccountURL, kid string, hmac []byte) ([]byte, error) {
eabJWS, err := a.jws.SignEABContent(newAccountURL, kid, hmac)
if err != nil {
return nil, err
}
return []byte(eabJWS.FullSerialize()), nil
}
// GetKeyAuthorization Gets the key authorization
func (a *Core) GetKeyAuthorization(token string) (string, error) {
return a.jws.GetKeyAuthorization(token)
}
func (a *Core) GetDirectory() acme.Directory {
return a.directory
}
func getDirectory(do *sender.Doer, caDirURL string) (acme.Directory, error) {
var dir acme.Directory
if _, err := do.Get(caDirURL, &dir); err != nil {
return dir, fmt.Errorf("get directory at '%s': %v", caDirURL, err)
}
if dir.NewAccountURL == "" {
return dir, errors.New("directory missing new registration URL")
}
if dir.NewOrderURL == "" {
return dir, errors.New("directory missing new order URL")
}
return dir, nil
}
package api
import (
"errors"
"github.com/xenolf/lego/acme"
)
type AuthorizationService service
// Get Gets an authorization.
func (c *AuthorizationService) Get(authzURL string) (acme.Authorization, error) {
if len(authzURL) == 0 {
return acme.Authorization{}, errors.New("authorization[get]: empty URL")
}
var authz acme.Authorization
_, err := c.core.postAsGet(authzURL, &authz)
if err != nil {
return acme.Authorization{}, err
}
return authz, nil
}
// Deactivate Deactivates an authorization.
func (c *AuthorizationService) Deactivate(authzURL string) error {
if len(authzURL) == 0 {
return errors.New("authorization[deactivate]: empty URL")
}
var disabledAuth acme.Authorization
_, err := c.core.post(authzURL, acme.Authorization{Status: acme.StatusDeactivated}, &disabledAuth)
return err
}
package api
import (
"crypto/x509"
"encoding/pem"
"errors"
"io/ioutil"
"net/http"
"github.com/xenolf/lego/acme"
"github.com/xenolf/lego/certcrypto"
"github.com/xenolf/lego/log"
)
// maxBodySize is the maximum size of body that we will read.
const maxBodySize = 1024 * 1024
type CertificateService service
// Get Returns the certificate and the issuer certificate.
// 'bundle' is only applied if the issuer is provided by the 'up' link.
func (c *CertificateService) Get(certURL string, bundle bool) ([]byte, []byte, error) {
cert, up, err := c.get(certURL)
if err != nil {
return nil, nil, err
}
// Get issuerCert from bundled response from Let's Encrypt
// See https://community.letsencrypt.org/t/acme-v2-no-up-link-in-response/64962
_, issuer := pem.Decode(cert)
if issuer != nil {
return cert, issuer, nil
}
issuer, err = c.getIssuerFromLink(up)
if err != nil {
// If we fail to acquire the issuer cert, return the issued certificate - do not fail.
log.Warnf("acme: Could not bundle issuer certificate [%s]: %v", certURL, err)
} else if len(issuer) > 0 {
// If bundle is true, we want to return a certificate bundle.
// To do this, we append the issuer cert to the issued cert.
if bundle {
cert = append(cert, issuer...)
}
}
return cert, issuer, nil
}
// Revoke Revokes a certificate.
func (c *CertificateService) Revoke(req acme.RevokeCertMessage) error {
_, err := c.core.post(c.core.GetDirectory().RevokeCertURL, req, nil)
return err
}
// get Returns the certificate and the "up" link.
func (c *CertificateService) get(certURL string) ([]byte, string, error) {
if len(certURL) == 0 {
return nil, "", errors.New("certificate[get]: empty URL")
}
resp, err := c.core.postAsGet(certURL, nil)
if err != nil {
return nil, "", err
}
cert, err := ioutil.ReadAll(http.MaxBytesReader(nil, resp.Body, maxBodySize))
if err != nil {
return nil, "", err
}
// The issuer certificate link may be supplied via an "up" link
// in the response headers of a new certificate.
// See https://tools.ietf.org/html/draft-ietf-acme-acme-12#section-7.4.2
up := getLink(resp.Header, "up")
return cert, up, err
}
// getIssuerFromLink requests the issuer certificate
func (c *CertificateService) getIssuerFromLink(up string) ([]byte, error) {
if len(up) == 0 {
return nil, nil
}
log.Infof("acme: Requesting issuer cert from %s", up)
cert, _, err := c.get(up)
if err != nil {
return nil, err
}
_, err = x509.ParseCertificate(cert)
if err != nil {
return nil, err
}
return certcrypto.PEMEncode(certcrypto.DERCertificateBytes(cert)), nil
}
package api
import (
"errors"
"github.com/xenolf/lego/acme"
)
type ChallengeService service
// New Creates a challenge.
func (c *ChallengeService) New(chlgURL string) (acme.ExtendedChallenge, error) {
if len(chlgURL) == 0 {
return acme.ExtendedChallenge{}, errors.New("challenge[new]: empty URL")
}
// Challenge initiation is done by sending a JWS payload containing the trivial JSON object `{}`.
// We use an empty struct instance as the postJSON payload here to achieve this result.
var chlng acme.ExtendedChallenge
resp, err := c.core.post(chlgURL, struct{}{}, &chlng)
if err != nil {
return acme.ExtendedChallenge{}, err
}
chlng.AuthorizationURL = getLink(resp.Header, "up")
chlng.RetryAfter = getRetryAfter(resp)
return chlng, nil
}
// Get Gets a challenge.
func (c *ChallengeService) Get(chlgURL string) (acme.ExtendedChallenge, error) {
if len(chlgURL) == 0 {
return acme.ExtendedChallenge{}, errors.New("challenge[get]: empty URL")
}
var chlng acme.ExtendedChallenge
resp, err := c.core.postAsGet(chlgURL, &chlng)
if err != nil {
return acme.ExtendedChallenge{}, err
}
chlng.AuthorizationURL = getLink(resp.Header, "up")
chlng.RetryAfter = getRetryAfter(resp)
return chlng, nil
}
package nonces
import (
"errors"
"fmt"
"net/http"
"sync"
"github.com/xenolf/lego/acme/api/internal/sender"
)
// Manager Manages nonces.
type Manager struct {
do *sender.Doer
nonceURL string
nonces []string
sync.Mutex
}
// NewManager Creates a new Manager.
func NewManager(do *sender.Doer, nonceURL string) *Manager {
return &Manager{
do: do,
nonceURL: nonceURL,
}
}
// Pop Pops a nonce.
func (n *Manager) Pop() (string, bool) {
n.Lock()
defer n.Unlock()
if len(n.nonces) == 0 {
return "", false
}
nonce := n.nonces[len(n.nonces)-1]
n.nonces = n.nonces[:len(n.nonces)-1]
return nonce, true
}
// Push Pushes a nonce.
func (n *Manager) Push(nonce string) {
n.Lock()
defer n.Unlock()
n.nonces = append(n.nonces, nonce)
}
// Nonce implement jose.NonceSource
func (n *Manager) Nonce() (string, error) {
if nonce, ok := n.Pop(); ok {
return nonce, nil
}
return n.getNonce()
}
func (n *Manager) getNonce() (string, error) {
resp, err := n.do.Head(n.nonceURL)
if err != nil {
return "", fmt.Errorf("failed to get nonce from HTTP HEAD -> %v", err)
}
return GetFromResponse(resp)
}
// GetFromResponse Extracts a nonce from a HTTP response.
func GetFromResponse(resp *http.Response) (string, error) {
if resp == nil {
return "", errors.New("nil response")
}
nonce := resp.Header.Get("Replay-Nonce")
if nonce == "" {
return "", fmt.Errorf("server did not respond with a proper nonce header")
}
return nonce, nil
}
package acme
package secure
import (
"bytes"
"crypto"
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rsa"
"encoding/base64"
"errors"
"fmt"
"net/http"
"sync"
"github.com/xenolf/lego/acme/api/internal/nonces"
"gopkg.in/square/go-jose.v2"
)
type jws struct {
getNonceURL string
privKey crypto.PrivateKey
kid string
nonces nonceManager
// JWS Represents a JWS.
type JWS struct {
privKey crypto.PrivateKey
kid string // Key identifier
nonces *nonces.Manager
}
// Posts a JWS signed message to the specified URL.
// It does NOT close the response body, so the caller must
// do that if no error was returned.
func (j *jws) post(url string, content []byte) (*http.Response, error) {
signedContent, err := j.signContent(url, content)
if err != nil {
return nil, fmt.Errorf("failed to sign content -> %s", err.Error())
}
data := bytes.NewBuffer([]byte(signedContent.FullSerialize()))
resp, err := httpPost(url, "application/jose+json", data)
if err != nil {
return nil, fmt.Errorf("failed to HTTP POST to %s -> %s", url, err.Error())
// NewJWS Create a new JWS.
func NewJWS(privateKey crypto.PrivateKey, kid string, nonceManager *nonces.Manager) *JWS {
return &JWS{
privKey: privateKey,
nonces: nonceManager,
kid: kid,
}
nonce, nonceErr := getNonceFromResponse(resp)
if nonceErr == nil {
j.nonces.Push(nonce)
}
return resp, nil
}
func (j *jws) signContent(url string, content []byte) (*jose.JSONWebSignature, error) {
// SetKid Sets a key identifier.
func (j *JWS) SetKid(kid string) {
j.kid = kid
}
// SignContent Signs a content with the JWS.
func (j *JWS) SignContent(url string, content []byte) (*jose.JSONWebSignature, error) {
var alg jose.SignatureAlgorithm
switch k := j.privKey.(type) {
case *rsa.PrivateKey:
......@@ -57,41 +48,40 @@ func (j *jws) signContent(url string, content []byte) (*jose.JSONWebSignature, e
}
}
jsonKey := jose.JSONWebKey{
Key: j.privKey,
KeyID: j.kid,
}
signKey := jose.SigningKey{
Algorithm: alg,
Key: jsonKey,
Key: jose.JSONWebKey{Key: j.privKey, KeyID: j.kid},
}
options := jose.SignerOptions{
NonceSource: j,
ExtraHeaders: make(map[jose.HeaderKey]interface{}),
NonceSource: j.nonces,
ExtraHeaders: map[jose.HeaderKey]interface{}{
"url": url,
},
}
options.ExtraHeaders["url"] = url
if j.kid == "" {
options.EmbedJWK = true
}
signer, err := jose.NewSigner(signKey, &options)
if err != nil {
return nil, fmt.Errorf("failed to create jose signer -> %s", err.Error())
return nil, fmt.Errorf("failed to create jose signer -> %v", err)
}
signed, err := signer.Sign(content)
if err != nil {
return nil, fmt.Errorf("failed to sign content -> %s", err.Error())
return nil, fmt.Errorf("failed to sign content -> %v", err)
}
return signed, nil
}
func (j *jws) signEABContent(url, kid string, hmac []byte) (*jose.JSONWebSignature, error) {
// SignEABContent Signs an external account binding content with the JWS.
func (j *JWS) SignEABContent(url, kid string, hmac []byte) (*jose.JSONWebSignature, error) {
jwk := jose.JSONWebKey{Key: j.privKey}
jwkJSON, err := jwk.Public().MarshalJSON()
if err != nil {
return nil, fmt.Errorf("acme: error encoding eab jwk key: %s", err.Error())
return nil, fmt.Errorf("acme: error encoding eab jwk key: %v", err)
}
signer, err := jose.NewSigner(
......@@ -105,63 +95,40 @@ func (j *jws) signEABContent(url, kid string, hmac []byte) (*jose.JSONWebSignatu
},
)
if err != nil {
return nil, fmt.Errorf("failed to create External Account Binding jose signer -> %s", err.Error())
return nil, fmt.Errorf("failed to create External Account Binding jose signer -> %v", err)
}
signed, err := signer.Sign(jwkJSON)
if err != nil {
return nil, fmt.Errorf("failed to External Account Binding sign content -> %s", err.Error())
return nil, fmt.Errorf("failed to External Account Binding sign content -> %v", err)
}
return signed, nil
}
func (j *jws) Nonce() (string, error) {
if nonce, ok := j.nonces.Pop(); ok {
return nonce, nil
// GetKeyAuthorization Gets the key authorization for a token.
func (j *JWS) GetKeyAuthorization(token string) (string, error) {
var publicKey crypto.PublicKey
switch k := j.privKey.(type) {
case *ecdsa.PrivateKey:
publicKey = k.Public()
case *rsa.PrivateKey:
publicKey = k.Public()
}
return getNonce(j.getNonceURL)
}
type nonceManager struct {
nonces []string
sync.Mutex
}
func (n *nonceManager) Pop() (string, bool) {
n.Lock()
defer n.Unlock()
if len(n.nonces) == 0 {
return "", false
// Generate the Key Authorization for the challenge
jwk := &jose.JSONWebKey{Key: publicKey}
if jwk == nil {
return "", errors.New("could not generate JWK from key")
}
nonce := n.nonces[len(n.nonces)-1]
n.nonces = n.nonces[:len(n.nonces)-1]
return nonce, true
}
func (n *nonceManager) Push(nonce string) {
n.Lock()
defer n.Unlock()
n.nonces = append(n.nonces, nonce)
}
func getNonce(url string) (string, error) {
resp, err := httpHead(url)
thumbBytes, err := jwk.Thumbprint(crypto.SHA256)
if err != nil {
return "", fmt.Errorf("failed to get nonce from HTTP HEAD -> %s", err.Error())
return "", err
}
return getNonceFromResponse(resp)
}
func getNonceFromResponse(resp *http.Response) (string, error) {
nonce := resp.Header.Get("Replay-Nonce")
if nonce == "" {
return "", fmt.Errorf("server did not respond with a proper nonce header")
}
// unpad the base64URL
keyThumb := base64.RawURLEncoding.EncodeToString(thumbBytes)
return nonce, nil
return token + "." + keyThumb, nil
}
package sender
// CODE GENERATED AUTOMATICALLY
// THIS FILE MUST NOT BE EDITED BY HAND
const (
// ourUserAgent is the User-Agent of this underlying library package.
ourUserAgent = "xenolf-acme/1.2.1"
// ourUserAgentComment is part of the UA comment linked to the version status of this underlying library package.
// values: detach|release
// NOTE: Update this with each tagged release.
ourUserAgentComment = "detach"
)
package api
import (
"encoding/base64"
"errors"
"github.com/xenolf/lego/acme"
)
type OrderService service
// New Creates a new order.
func (o *OrderService) New(domains []string) (acme.ExtendedOrder, error) {
var identifiers []acme.Identifier
for _, domain := range domains {
identifiers = append(identifiers, acme.Identifier{Type: "dns", Value: domain})
}
orderReq := acme.Order{Identifiers: identifiers}
var order acme.Order
resp, err := o.core.post(o.core.GetDirectory().NewOrderURL, orderReq, &order)
if err != nil {
return acme.ExtendedOrder{}, err
}
return acme.ExtendedOrder{
Location: resp.Header.Get("Location"),
Order: order,
}, nil
}
// Get Gets an order.
func (o *OrderService) Get(orderURL string) (acme.Order, error) {
if len(orderURL) == 0 {
return acme.Order{}, errors.New("order[get]: empty URL")
}
var order acme.Order
_, err := o.core.postAsGet(orderURL, &order)
if err != nil {
return acme.Order{}, err
}
return order, nil
}
// UpdateForCSR Updates an order for a CSR.
func (o *OrderService) UpdateForCSR(orderURL string, csr []byte) (acme.Order, error) {
csrMsg := acme.CSRMessage{
Csr: base64.RawURLEncoding.EncodeToString(csr),
}
var order acme.Order
_, err := o.core.post(orderURL, csrMsg, &order)
if err != nil {
return acme.Order{}, err
}
if order.Status == acme.StatusInvalid {
return acme.Order{}, order.Error
}
return order, nil
}
This diff is collapsed.
package acme
// Challenge is a string that identifies a particular type and version of ACME challenge.
type Challenge string
const (
// HTTP01 is the "http-01" ACME challenge https://github.com/ietf-wg-acme/acme/blob/master/draft-ietf-acme-acme.md#http
// Note: HTTP01ChallengePath returns the URL path to fulfill this challenge
HTTP01 = Challenge("http-01")
// DNS01 is the "dns-01" ACME challenge https://github.com/ietf-wg-acme/acme/blob/master/draft-ietf-acme-acme.md#dns
// Note: DNS01Record returns a DNS record which will fulfill this challenge
DNS01 = Challenge("dns-01")
// TLSALPN01 is the "tls-alpn-01" ACME challenge https://tools.ietf.org/html/draft-ietf-acme-tls-alpn-01
TLSALPN01 = Challenge("tls-alpn-01")
)
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment