1package store
2
3import (
4 "context"
5 "crypto/ecdsa"
6 "crypto/ed25519"
7 "crypto/rsa"
8 "crypto/sha256"
9 "crypto/x509"
10 "encoding/base64"
11 "fmt"
12 "os"
13 "path/filepath"
14 "strings"
15 "time"
16
17 "github.com/mjl-/bstore"
18
19 "github.com/mjl-/mox/mlog"
20 "github.com/mjl-/mox/mox-"
21 "github.com/mjl-/mox/smtp"
22)
23
24// TLSPublicKey is a public key for use with TLS client authentication based on the
25// public key of the certificate.
26type TLSPublicKey struct {
27 // Raw-url-base64-encoded Subject Public Key Info of certificate.
28 Fingerprint string
29 Created time.Time `bstore:"nonzero,default now"`
30 Type string // E.g. "rsa-2048", "ecdsa-p256", "ed25519"
31
32 // Descriptive name to identify the key, e.g. the device where key is used.
33 Name string `bstore:"nonzero"`
34
35 // If set, new immediate authenticated TLS connections are not moved to
36 // "authenticated" state. For clients that don't understand it, and will try an
37 // authenticate command anyway.
38 NoIMAPPreauth bool
39
40 CertDER []byte `bstore:"nonzero"`
41 Account string `bstore:"nonzero"` // Key authenticates this account.
42 LoginAddress string `bstore:"nonzero"` // Must belong to account.
43}
44
45// AuthDB and AuthDBTypes are exported for ../backup.go.
46var AuthDB *bstore.DB
47var AuthDBTypes = []any{TLSPublicKey{}}
48
49// Init opens auth.db.
50func Init(ctx context.Context) error {
51 if AuthDB != nil {
52 return fmt.Errorf("already initialized")
53 }
54 pkglog := mlog.New("store", nil)
55 p := mox.DataDirPath("auth.db")
56 os.MkdirAll(filepath.Dir(p), 0770)
57 opts := bstore.Options{Timeout: 5 * time.Second, Perm: 0660, RegisterLogger: pkglog.Logger}
58 var err error
59 AuthDB, err = bstore.Open(ctx, p, &opts, AuthDBTypes...)
60 return err
61}
62
63// Close closes auth.db.
64func Close() error {
65 if AuthDB == nil {
66 return fmt.Errorf("not open")
67 }
68 err := AuthDB.Close()
69 AuthDB = nil
70 return err
71}
72
73// ParseTLSPublicKeyCert parses a certificate, preparing a TLSPublicKey for
74// insertion into the database. Caller must set fields that are not in the
75// certificat, such as Account and LoginAddress.
76func ParseTLSPublicKeyCert(certDER []byte) (TLSPublicKey, error) {
77 cert, err := x509.ParseCertificate(certDER)
78 if err != nil {
79 return TLSPublicKey{}, fmt.Errorf("parsing certificate: %v", err)
80 }
81 name := cert.Subject.CommonName
82 if name == "" && cert.SerialNumber != nil {
83 name = fmt.Sprintf("serial %x", cert.SerialNumber.Bytes())
84 }
85
86 buf := sha256.Sum256(cert.RawSubjectPublicKeyInfo)
87 fp := base64.RawURLEncoding.EncodeToString(buf[:])
88 var typ string
89 switch k := cert.PublicKey.(type) {
90 case *rsa.PublicKey:
91 bits := k.N.BitLen()
92 if bits < 2048 {
93 return TLSPublicKey{}, fmt.Errorf("rsa keys smaller than 2048 bits not accepted")
94 }
95 typ = "rsa-" + fmt.Sprintf("%d", bits)
96 case *ecdsa.PublicKey:
97 typ = "ecdsa-" + strings.ReplaceAll(strings.ToLower(k.Params().Name), "-", "")
98 case ed25519.PublicKey:
99 typ = "ed25519"
100 default:
101 return TLSPublicKey{}, fmt.Errorf("public key type %T not implemented", cert.PublicKey)
102 }
103
104 return TLSPublicKey{Fingerprint: fp, Type: typ, Name: name, CertDER: certDER}, nil
105}
106
107// TLSPublicKeyList returns tls public keys. If accountOpt is empty, keys for all
108// accounts are returned.
109func TLSPublicKeyList(ctx context.Context, accountOpt string) ([]TLSPublicKey, error) {
110 q := bstore.QueryDB[TLSPublicKey](ctx, AuthDB)
111 if accountOpt != "" {
112 q.FilterNonzero(TLSPublicKey{Account: accountOpt})
113 }
114 return q.List()
115}
116
117// TLSPublicKeyGet retrieves a single tls public key by fingerprint.
118// If absent, bstore.ErrAbsent is returned.
119func TLSPublicKeyGet(ctx context.Context, fingerprint string) (TLSPublicKey, error) {
120 pubKey := TLSPublicKey{Fingerprint: fingerprint}
121 err := AuthDB.Get(ctx, &pubKey)
122 return pubKey, err
123}
124
125// TLSPublicKeyAdd adds a new tls public key.
126//
127// Caller is responsible for checking the account and email address are valid.
128func TLSPublicKeyAdd(ctx context.Context, pubKey *TLSPublicKey) error {
129 if err := checkTLSPublicKeyAddress(pubKey.LoginAddress); err != nil {
130 return err
131 }
132 return AuthDB.Insert(ctx, pubKey)
133}
134
135// TLSPublicKeyUpdate updates an existing tls public key.
136//
137// Caller is responsible for checking the account and email address are valid.
138func TLSPublicKeyUpdate(ctx context.Context, pubKey *TLSPublicKey) error {
139 if err := checkTLSPublicKeyAddress(pubKey.LoginAddress); err != nil {
140 return err
141 }
142 return AuthDB.Update(ctx, pubKey)
143}
144
145func checkTLSPublicKeyAddress(addr string) error {
146 a, err := smtp.ParseAddress(addr)
147 if err != nil {
148 return fmt.Errorf("parsing login address %q: %v", addr, err)
149 }
150 if a.String() != addr {
151 return fmt.Errorf("login address %q must be specified in canonical form %q", addr, a.String())
152 }
153 return nil
154}
155
156// TLSPublicKeyRemove removes a tls public key.
157func TLSPublicKeyRemove(ctx context.Context, fingerprint string) error {
158 k := TLSPublicKey{Fingerprint: fingerprint}
159 return AuthDB.Delete(ctx, &k)
160}
161
162// TLSPublicKeyRemoveForAccount removes all tls public keys for an account.
163func TLSPublicKeyRemoveForAccount(ctx context.Context, account string) error {
164 q := bstore.QueryDB[TLSPublicKey](ctx, AuthDB)
165 q.FilterNonzero(TLSPublicKey{Account: account})
166 _, err := q.Delete()
167 return err
168}
169