Skip to content

Commit 27ab129

Browse files
authored
Use configured email to pin to specific account key in storage (#283)
* Use the `email` configuration in the ACME issuer to "pin" an account to a key When the issuer is configured with both an email and key material, these should match -- but that also means we can use the email information to predict the key-key, skipping the potentially expensive storage.List operation. * `continue` when we cannot load the private key for an account Not being able to load this might be caused by a storage problem, or it could have been something we did earlier. In either case we do not know whether this is the account we're looking for, and breaking out now will trigger expensive calls to the ACME server to lookup the account and then save that account again even though it was perfectly fine to begin with. * Add unit tests for the changed behaviors
1 parent f64401c commit 27ab129

File tree

2 files changed

+253
-5
lines changed

2 files changed

+253
-5
lines changed

account.go

+17-5
Original file line numberDiff line numberDiff line change
@@ -88,11 +88,18 @@ func (*ACMEIssuer) newAccount(email string) (acme.Account, error) {
8888
// If it does not exist in storage, it will be retrieved from the ACME server and added to storage.
8989
// The account must already exist; it does not create a new account.
9090
func (am *ACMEIssuer) GetAccount(ctx context.Context, privateKeyPEM []byte) (acme.Account, error) {
91-
account, err := am.loadAccountByKey(ctx, privateKeyPEM)
92-
if errors.Is(err, fs.ErrNotExist) {
93-
account, err = am.lookUpAccount(ctx, privateKeyPEM)
91+
email := am.getEmail()
92+
if email == "" {
93+
if account, err := am.loadAccountByKey(ctx, privateKeyPEM); err == nil {
94+
return account, nil
95+
}
96+
} else {
97+
keyBytes, err := am.config.Storage.Load(ctx, am.storageKeyUserPrivateKey(am.CA, email))
98+
if err == nil && bytes.Equal(bytes.TrimSpace(keyBytes), bytes.TrimSpace(privateKeyPEM)) {
99+
return am.loadAccount(ctx, am.CA, email)
100+
}
94101
}
95-
return account, err
102+
return am.lookUpAccount(ctx, privateKeyPEM)
96103
}
97104

98105
// loadAccountByKey loads the account with the given private key from storage, if it exists.
@@ -107,9 +114,14 @@ func (am *ACMEIssuer) loadAccountByKey(ctx context.Context, privateKeyPEM []byte
107114
email := path.Base(accountFolderKey)
108115
keyBytes, err := am.config.Storage.Load(ctx, am.storageKeyUserPrivateKey(am.CA, email))
109116
if err != nil {
110-
return acme.Account{}, err
117+
// Try the next account: This one is missing its private key, if it turns out to be the one we're looking
118+
// for we will try to save it again after confirming with the ACME server.
119+
continue
111120
}
112121
if bytes.Equal(bytes.TrimSpace(keyBytes), bytes.TrimSpace(privateKeyPEM)) {
122+
// Found the account with the correct private key, try loading it. If this fails we we will follow
123+
// the same procedure as if the private key was not found and confirm with the ACME server before saving
124+
// it again.
113125
return am.loadAccount(ctx, am.CA, email)
114126
}
115127
}

account_test.go

+236
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ package certmagic
1717
import (
1818
"bytes"
1919
"context"
20+
"io/fs"
2021
"os"
2122
"path/filepath"
2223
"reflect"
@@ -26,6 +27,131 @@ import (
2627
"time"
2728
)
2829

30+
// memoryStorage is an in-memory storage implementation with known contents *and* fixed iteration order for List.
31+
type memoryStorage struct {
32+
contents []memoryStorageItem
33+
}
34+
35+
type memoryStorageItem struct {
36+
key string
37+
data []byte
38+
}
39+
40+
func (m *memoryStorage) lookup(_ context.Context, key string) *memoryStorageItem {
41+
for _, item := range m.contents {
42+
if item.key == key {
43+
return &item
44+
}
45+
}
46+
return nil
47+
}
48+
func (m *memoryStorage) Delete(ctx context.Context, key string) error {
49+
for i, item := range m.contents {
50+
if item.key == key {
51+
m.contents = append(m.contents[:i], m.contents[i+1:]...)
52+
return nil
53+
}
54+
}
55+
return fs.ErrNotExist
56+
}
57+
func (m *memoryStorage) Store(ctx context.Context, key string, value []byte) error {
58+
m.contents = append(m.contents, memoryStorageItem{key: key, data: value})
59+
return nil
60+
}
61+
func (m *memoryStorage) Exists(ctx context.Context, key string) bool {
62+
return m.lookup(ctx, key) != nil
63+
}
64+
func (m *memoryStorage) List(ctx context.Context, path string, recursive bool) ([]string, error) {
65+
if recursive {
66+
panic("unimplemented")
67+
}
68+
69+
result := []string{}
70+
nextitem:
71+
for _, item := range m.contents {
72+
if !strings.HasPrefix(item.key, path+"/") {
73+
continue
74+
}
75+
name := strings.TrimPrefix(item.key, path+"/")
76+
if i := strings.Index(name, "/"); i >= 0 {
77+
name = name[:i]
78+
}
79+
80+
for _, existing := range result {
81+
if existing == name {
82+
continue nextitem
83+
}
84+
}
85+
result = append(result, name)
86+
}
87+
return result, nil
88+
}
89+
func (m *memoryStorage) Load(ctx context.Context, key string) ([]byte, error) {
90+
if item := m.lookup(ctx, key); item != nil {
91+
return item.data, nil
92+
}
93+
return nil, fs.ErrNotExist
94+
}
95+
func (m *memoryStorage) Stat(ctx context.Context, key string) (KeyInfo, error) {
96+
if item := m.lookup(ctx, key); item != nil {
97+
return KeyInfo{Key: key, Size: int64(len(item.data))}, nil
98+
}
99+
return KeyInfo{}, fs.ErrNotExist
100+
}
101+
func (m *memoryStorage) Lock(ctx context.Context, name string) error { panic("unimplemented") }
102+
func (m *memoryStorage) Unlock(ctx context.Context, name string) error { panic("unimplemented") }
103+
104+
var _ Storage = (*memoryStorage)(nil)
105+
106+
type recordingStorage struct {
107+
Storage
108+
calls []recordedCall
109+
}
110+
111+
func (r *recordingStorage) Delete(ctx context.Context, key string) error {
112+
r.record("Delete", key)
113+
return r.Storage.Delete(ctx, key)
114+
}
115+
func (r *recordingStorage) Exists(ctx context.Context, key string) bool {
116+
r.record("Exists", key)
117+
return r.Storage.Exists(ctx, key)
118+
}
119+
func (r *recordingStorage) List(ctx context.Context, path string, recursive bool) ([]string, error) {
120+
r.record("List", path, recursive)
121+
return r.Storage.List(ctx, path, recursive)
122+
}
123+
func (r *recordingStorage) Load(ctx context.Context, key string) ([]byte, error) {
124+
r.record("Load", key)
125+
return r.Storage.Load(ctx, key)
126+
}
127+
func (r *recordingStorage) Lock(ctx context.Context, name string) error {
128+
r.record("Lock", name)
129+
return r.Storage.Lock(ctx, name)
130+
}
131+
func (r *recordingStorage) Stat(ctx context.Context, key string) (KeyInfo, error) {
132+
r.record("Stat", key)
133+
return r.Storage.Stat(ctx, key)
134+
}
135+
func (r *recordingStorage) Store(ctx context.Context, key string, value []byte) error {
136+
r.record("Store", key)
137+
return r.Storage.Store(ctx, key, value)
138+
}
139+
func (r *recordingStorage) Unlock(ctx context.Context, name string) error {
140+
r.record("Unlock", name)
141+
return r.Storage.Unlock(ctx, name)
142+
}
143+
144+
type recordedCall struct {
145+
name string
146+
args []interface{}
147+
}
148+
149+
func (r *recordingStorage) record(name string, args ...interface{}) {
150+
r.calls = append(r.calls, recordedCall{name: name, args: args})
151+
}
152+
153+
var _ Storage = (*recordingStorage)(nil)
154+
29155
func TestNewAccount(t *testing.T) {
30156
am := &ACMEIssuer{CA: dummyCA, mu: new(sync.Mutex)}
31157
testConfig := &Config{
@@ -159,6 +285,116 @@ func TestGetAccountAlreadyExists(t *testing.T) {
159285
}
160286
}
161287

288+
func TestGetAccountAlreadyExistsSkipsBroken(t *testing.T) {
289+
ctx := context.Background()
290+
291+
am := &ACMEIssuer{CA: dummyCA, mu: new(sync.Mutex)}
292+
testConfig := &Config{
293+
Issuers: []Issuer{am},
294+
Storage: &memoryStorage{},
295+
Logger: defaultTestLogger,
296+
certCache: new(Cache),
297+
}
298+
am.config = testConfig
299+
300+
email := "[email protected]"
301+
302+
// Create a "corrupted" account
303+
am.config.Storage.Store(ctx, am.storageKeyUserReg(am.CA, "[email protected]"), []byte("this is not a valid account"))
304+
305+
// Create the actual account
306+
account, err := am.newAccount(email)
307+
if err != nil {
308+
t.Fatalf("Error creating account: %v", err)
309+
}
310+
err = am.saveAccount(ctx, am.CA, account)
311+
if err != nil {
312+
t.Fatalf("Error saving account: %v", err)
313+
}
314+
315+
// Expect to load account from disk
316+
keyBytes, err := PEMEncodePrivateKey(account.PrivateKey)
317+
if err != nil {
318+
t.Fatalf("Error encoding private key: %v", err)
319+
}
320+
321+
loadedAccount, err := am.GetAccount(ctx, keyBytes)
322+
if err != nil {
323+
t.Fatalf("Error getting account: %v", err)
324+
}
325+
326+
// Assert keys are the same
327+
if !privateKeysSame(account.PrivateKey, loadedAccount.PrivateKey) {
328+
t.Error("Expected private key to be the same after loading, but it wasn't")
329+
}
330+
331+
// Assert emails are the same
332+
if !reflect.DeepEqual(account.Contact, loadedAccount.Contact) {
333+
t.Errorf("Expected contacts to be equal, but was '%s' before and '%s' after loading", account.Contact, loadedAccount.Contact)
334+
}
335+
}
336+
337+
func TestGetAccountWithEmailAlreadyExists(t *testing.T) {
338+
ctx := context.Background()
339+
340+
am := &ACMEIssuer{CA: dummyCA, mu: new(sync.Mutex)}
341+
testConfig := &Config{
342+
Issuers: []Issuer{am},
343+
Storage: &recordingStorage{Storage: &memoryStorage{}},
344+
Logger: defaultTestLogger,
345+
certCache: new(Cache),
346+
}
347+
am.config = testConfig
348+
349+
email := "[email protected]"
350+
351+
// Set up test
352+
account, err := am.newAccount(email)
353+
if err != nil {
354+
t.Fatalf("Error creating account: %v", err)
355+
}
356+
err = am.saveAccount(ctx, am.CA, account)
357+
if err != nil {
358+
t.Fatalf("Error saving account: %v", err)
359+
}
360+
361+
// Set the expected email:
362+
am.Email = email
363+
err = am.setEmail(ctx, true)
364+
if err != nil {
365+
t.Fatalf("setEmail error: %v", err)
366+
}
367+
368+
// Expect to load account from disk
369+
keyBytes, err := PEMEncodePrivateKey(account.PrivateKey)
370+
if err != nil {
371+
t.Fatalf("Error encoding private key: %v", err)
372+
}
373+
374+
loadedAccount, err := am.GetAccount(ctx, keyBytes)
375+
if err != nil {
376+
t.Fatalf("Error getting account: %v", err)
377+
}
378+
379+
// Assert keys are the same
380+
if !privateKeysSame(account.PrivateKey, loadedAccount.PrivateKey) {
381+
t.Error("Expected private key to be the same after loading, but it wasn't")
382+
}
383+
384+
// Assert emails are the same
385+
if !reflect.DeepEqual(account.Contact, loadedAccount.Contact) {
386+
t.Errorf("Expected contacts to be equal, but was '%s' before and '%s' after loading", account.Contact, loadedAccount.Contact)
387+
}
388+
389+
// Assert that this was found without listing all accounts
390+
rs := testConfig.Storage.(*recordingStorage)
391+
for _, call := range rs.calls {
392+
if call.name == "List" {
393+
t.Error("Unexpected List call")
394+
}
395+
}
396+
}
397+
162398
func TestGetEmailFromPackageDefault(t *testing.T) {
163399
ctx := context.Background()
164400

0 commit comments

Comments
 (0)