Skip to content

Commit e3826dd

Browse files
committed
Add ACME CA capabilities
1 parent 68ab03d commit e3826dd

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

54 files changed

+15687
-184
lines changed

.golangci.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ issues:
6060
- declaration of "err" shadows declaration at line
6161
- should have a package comment, unless it's in another file for this package
6262
- error strings should not be capitalized or end with punctuation or a newline
63+
- declaration of "authz" shadows declaration at line
6364
# golangci.com configuration
6465
# https://github.com/golangci/golangci/wiki/Configuration
6566
service:

.travis.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
language: go
22
go:
3-
- 1.12.x
3+
- 1.13.x
44
addons:
55
apt:
66
packages:

Gopkg.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

acme/account.go

Lines changed: 214 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,214 @@
1+
package acme
2+
3+
import (
4+
"encoding/json"
5+
"time"
6+
7+
"github.com/pkg/errors"
8+
"github.com/smallstep/certificates/authority/provisioner"
9+
"github.com/smallstep/cli/jose"
10+
"github.com/smallstep/nosql"
11+
)
12+
13+
// Account is a subset of the internal account type containing only those
14+
// attributes required for responses in the ACME protocol.
15+
type Account struct {
16+
Contact []string `json:"contact,omitempty"`
17+
Status string `json:"status"`
18+
Orders string `json:"orders"`
19+
ID string `json:"-"`
20+
Key *jose.JSONWebKey `json:"-"`
21+
}
22+
23+
// ToLog enables response logging.
24+
func (a *Account) ToLog() (interface{}, error) {
25+
b, err := json.Marshal(a)
26+
if err != nil {
27+
return nil, ServerInternalErr(errors.Wrap(err, "error marshaling account for logging"))
28+
}
29+
return string(b), nil
30+
}
31+
32+
// GetID returns the account ID.
33+
func (a *Account) GetID() string {
34+
return a.ID
35+
}
36+
37+
// GetKey returns the JWK associated with the account.
38+
func (a *Account) GetKey() *jose.JSONWebKey {
39+
return a.Key
40+
}
41+
42+
// IsValid returns true if the Account is valid.
43+
func (a *Account) IsValid() bool {
44+
return a.Status == StatusValid
45+
}
46+
47+
// AccountOptions are the options needed to create a new ACME account.
48+
type AccountOptions struct {
49+
Key *jose.JSONWebKey
50+
Contact []string
51+
}
52+
53+
// account represents an ACME account.
54+
type account struct {
55+
ID string `json:"id"`
56+
Created time.Time `json:"created"`
57+
Deactivated time.Time `json:"deactivated"`
58+
Key *jose.JSONWebKey `json:"key"`
59+
Contact []string `json:"contact,omitempty"`
60+
Status string `json:"status"`
61+
}
62+
63+
// newAccount returns a new acme account type.
64+
func newAccount(db nosql.DB, ops AccountOptions) (*account, error) {
65+
id, err := randID()
66+
if err != nil {
67+
return nil, err
68+
}
69+
70+
a := &account{
71+
ID: id,
72+
Key: ops.Key,
73+
Contact: ops.Contact,
74+
Status: "valid",
75+
Created: clock.Now(),
76+
}
77+
return a, a.saveNew(db)
78+
}
79+
80+
// toACME converts the internal Account type into the public acmeAccount
81+
// type for presentation in the ACME protocol.
82+
func (a *account) toACME(db nosql.DB, dir *directory, p provisioner.Interface) (*Account, error) {
83+
return &Account{
84+
Status: a.Status,
85+
Contact: a.Contact,
86+
Orders: dir.getLink(OrdersByAccountLink, URLSafeProvisionerName(p), true, a.ID),
87+
Key: a.Key,
88+
ID: a.ID,
89+
}, nil
90+
}
91+
92+
// save writes the Account to the DB.
93+
// If the account is new then the necessary indices will be created.
94+
// Else, the account in the DB will be updated.
95+
func (a *account) saveNew(db nosql.DB) error {
96+
kid, err := keyToID(a.Key)
97+
if err != nil {
98+
return err
99+
}
100+
kidB := []byte(kid)
101+
102+
// Set the jwkID -> acme account ID index
103+
_, swapped, err := db.CmpAndSwap(accountByKeyIDTable, kidB, nil, []byte(a.ID))
104+
switch {
105+
case err != nil:
106+
return ServerInternalErr(errors.Wrap(err, "error setting key-id to account-id index"))
107+
case !swapped:
108+
return ServerInternalErr(errors.Errorf("key-id to account-id index already exists"))
109+
default:
110+
if err = a.save(db, nil); err != nil {
111+
db.Del(accountByKeyIDTable, kidB)
112+
return err
113+
}
114+
return nil
115+
}
116+
}
117+
118+
func (a *account) save(db nosql.DB, old *account) error {
119+
var (
120+
err error
121+
oldB []byte
122+
)
123+
if old == nil {
124+
oldB = nil
125+
} else {
126+
if oldB, err = json.Marshal(old); err != nil {
127+
return ServerInternalErr(errors.Wrap(err, "error marshaling old acme order"))
128+
}
129+
}
130+
131+
b, err := json.Marshal(*a)
132+
if err != nil {
133+
return errors.Wrap(err, "error marshaling new account object")
134+
}
135+
// Set the Account
136+
_, swapped, err := db.CmpAndSwap(accountTable, []byte(a.ID), oldB, b)
137+
switch {
138+
case err != nil:
139+
return ServerInternalErr(errors.Wrap(err, "error storing account"))
140+
case !swapped:
141+
return ServerInternalErr(errors.New("error storing account; " +
142+
"value has changed since last read"))
143+
default:
144+
return nil
145+
}
146+
}
147+
148+
// update updates the acme account object stored in the database if,
149+
// and only if, the account has not changed since the last read.
150+
func (a *account) update(db nosql.DB, contact []string) (*account, error) {
151+
b := *a
152+
b.Contact = contact
153+
if err := b.save(db, a); err != nil {
154+
return nil, err
155+
}
156+
return &b, nil
157+
}
158+
159+
// deactivate deactivates the acme account.
160+
func (a *account) deactivate(db nosql.DB) (*account, error) {
161+
b := *a
162+
b.Status = StatusDeactivated
163+
b.Deactivated = clock.Now()
164+
if err := b.save(db, a); err != nil {
165+
return nil, err
166+
}
167+
return &b, nil
168+
}
169+
170+
// getAccountByID retrieves the account with the given ID.
171+
func getAccountByID(db nosql.DB, id string) (*account, error) {
172+
ab, err := db.Get(accountTable, []byte(id))
173+
if err != nil {
174+
if nosql.IsErrNotFound(err) {
175+
return nil, MalformedErr(errors.Wrapf(err, "account %s not found", id))
176+
}
177+
return nil, ServerInternalErr(errors.Wrapf(err, "error loading account %s", id))
178+
}
179+
180+
a := new(account)
181+
if err = json.Unmarshal(ab, a); err != nil {
182+
return nil, ServerInternalErr(errors.Wrap(err, "error unmarshaling account"))
183+
}
184+
return a, nil
185+
}
186+
187+
// getAccountByKeyID retrieves Id associated with the given Kid.
188+
func getAccountByKeyID(db nosql.DB, kid string) (*account, error) {
189+
id, err := db.Get(accountByKeyIDTable, []byte(kid))
190+
if err != nil {
191+
if nosql.IsErrNotFound(err) {
192+
return nil, MalformedErr(errors.Wrapf(err, "account with key id %s not found", kid))
193+
}
194+
return nil, ServerInternalErr(errors.Wrapf(err, "error loading key-account index"))
195+
}
196+
return getAccountByID(db, string(id))
197+
}
198+
199+
// getOrderIDsByAccount retrieves a list of Order IDs that were created by the
200+
// account.
201+
func getOrderIDsByAccount(db nosql.DB, id string) ([]string, error) {
202+
b, err := db.Get(ordersByAccountIDTable, []byte(id))
203+
if err != nil {
204+
if nosql.IsErrNotFound(err) {
205+
return []string{}, nil
206+
}
207+
return nil, ServerInternalErr(errors.Wrapf(err, "error loading orderIDs for account %s", id))
208+
}
209+
var orderIDs []string
210+
if err := json.Unmarshal(b, &orderIDs); err != nil {
211+
return nil, ServerInternalErr(errors.Wrapf(err, "error unmarshaling orderIDs for account %s", id))
212+
}
213+
return orderIDs, nil
214+
}

0 commit comments

Comments
 (0)