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 "strings"
13 "time"
14
15 "github.com/mjl-/bstore"
16
17 "github.com/mjl-/mox/smtp"
18)
19
20// TLSPublicKey is a public key for use with TLS client authentication based on the
21// public key of the certificate.
22type TLSPublicKey struct {
23 // Raw-url-base64-encoded Subject Public Key Info of certificate.
24 Fingerprint string
25 Created time.Time `bstore:"nonzero,default now"`
26 Type string // E.g. "rsa-2048", "ecdsa-p256", "ed25519"
27
28 // Descriptive name to identify the key, e.g. the device where key is used.
29 Name string `bstore:"nonzero"`
30
31 // If set, new immediate authenticated TLS connections are not moved to
32 // "authenticated" state. For clients that don't understand it, and will try an
33 // authenticate command anyway.
34 NoIMAPPreauth bool
35
36 CertDER []byte `bstore:"nonzero"`
37 Account string `bstore:"nonzero"` // Key authenticates this account.
38 LoginAddress string `bstore:"nonzero"` // Must belong to account.
39}
40
41// ParseTLSPublicKeyCert parses a certificate, preparing a TLSPublicKey for
42// insertion into the database. Caller must set fields that are not in the
43// certificat, such as Account and LoginAddress.
44func ParseTLSPublicKeyCert(certDER []byte) (TLSPublicKey, error) {
45 cert, err := x509.ParseCertificate(certDER)
46 if err != nil {
47 return TLSPublicKey{}, fmt.Errorf("parsing certificate: %v", err)
48 }
49 name := cert.Subject.CommonName
50 if name == "" && cert.SerialNumber != nil {
51 name = fmt.Sprintf("serial %x", cert.SerialNumber.Bytes())
52 }
53
54 buf := sha256.Sum256(cert.RawSubjectPublicKeyInfo)
55 fp := base64.RawURLEncoding.EncodeToString(buf[:])
56 var typ string
57 switch k := cert.PublicKey.(type) {
58 case *rsa.PublicKey:
59 bits := k.N.BitLen()
60 if bits < 2048 {
61 return TLSPublicKey{}, fmt.Errorf("rsa keys smaller than 2048 bits not accepted")
62 }
63 typ = "rsa-" + fmt.Sprintf("%d", bits)
64 case *ecdsa.PublicKey:
65 typ = "ecdsa-" + strings.ReplaceAll(strings.ToLower(k.Params().Name), "-", "")
66 case ed25519.PublicKey:
67 typ = "ed25519"
68 default:
69 return TLSPublicKey{}, fmt.Errorf("public key type %T not implemented", cert.PublicKey)
70 }
71
72 return TLSPublicKey{Fingerprint: fp, Type: typ, Name: name, CertDER: certDER}, nil
73}
74
75// TLSPublicKeyList returns tls public keys. If accountOpt is empty, keys for all
76// accounts are returned.
77func TLSPublicKeyList(ctx context.Context, accountOpt string) ([]TLSPublicKey, error) {
78 q := bstore.QueryDB[TLSPublicKey](ctx, AuthDB)
79 if accountOpt != "" {
80 q.FilterNonzero(TLSPublicKey{Account: accountOpt})
81 }
82 return q.List()
83}
84
85// TLSPublicKeyGet retrieves a single tls public key by fingerprint.
86// If absent, bstore.ErrAbsent is returned.
87func TLSPublicKeyGet(ctx context.Context, fingerprint string) (TLSPublicKey, error) {
88 pubKey := TLSPublicKey{Fingerprint: fingerprint}
89 err := AuthDB.Get(ctx, &pubKey)
90 return pubKey, err
91}
92
93// TLSPublicKeyAdd adds a new tls public key.
94//
95// Caller is responsible for checking the account and email address are valid.
96func TLSPublicKeyAdd(ctx context.Context, pubKey *TLSPublicKey) error {
97 if err := checkTLSPublicKeyAddress(pubKey.LoginAddress); err != nil {
98 return err
99 }
100 return AuthDB.Insert(ctx, pubKey)
101}
102
103// TLSPublicKeyUpdate updates an existing tls public key.
104//
105// Caller is responsible for checking the account and email address are valid.
106func TLSPublicKeyUpdate(ctx context.Context, pubKey *TLSPublicKey) error {
107 if err := checkTLSPublicKeyAddress(pubKey.LoginAddress); err != nil {
108 return err
109 }
110 return AuthDB.Update(ctx, pubKey)
111}
112
113func checkTLSPublicKeyAddress(addr string) error {
114 a, err := smtp.ParseAddress(addr)
115 if err != nil {
116 return fmt.Errorf("parsing login address %q: %v", addr, err)
117 }
118 if a.String() != addr {
119 return fmt.Errorf("login address %q must be specified in canonical form %q", addr, a.String())
120 }
121 return nil
122}
123
124// TLSPublicKeyRemove removes a tls public key.
125func TLSPublicKeyRemove(ctx context.Context, fingerprint string) error {
126 k := TLSPublicKey{Fingerprint: fingerprint}
127 return AuthDB.Delete(ctx, &k)
128}
129
130// TLSPublicKeyRemoveForAccount removes all tls public keys for an account.
131func TLSPublicKeyRemoveForAccount(ctx context.Context, account string) error {
132 q := bstore.QueryDB[TLSPublicKey](ctx, AuthDB)
133 q.FilterNonzero(TLSPublicKey{Account: account})
134 _, err := q.Delete()
135 return err
136}
137