mirror of
https://github.com/mjl-/mox.git
synced 2025-06-27 22:28:16 +03:00

The intent to remove the account is stored in the database. At startup, if there are any such referenes, they are applied by removing the account directories and the entry in the database. This ensures the account directory is properly removed, even on incomplete shutdown. Don't add an account when its directory already exits.
137 lines
4.3 KiB
Go
137 lines
4.3 KiB
Go
package store
|
|
|
|
import (
|
|
"context"
|
|
"crypto/ecdsa"
|
|
"crypto/ed25519"
|
|
"crypto/rsa"
|
|
"crypto/sha256"
|
|
"crypto/x509"
|
|
"encoding/base64"
|
|
"fmt"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/mjl-/bstore"
|
|
|
|
"github.com/mjl-/mox/smtp"
|
|
)
|
|
|
|
// TLSPublicKey is a public key for use with TLS client authentication based on the
|
|
// public key of the certificate.
|
|
type TLSPublicKey struct {
|
|
// Raw-url-base64-encoded Subject Public Key Info of certificate.
|
|
Fingerprint string
|
|
Created time.Time `bstore:"nonzero,default now"`
|
|
Type string // E.g. "rsa-2048", "ecdsa-p256", "ed25519"
|
|
|
|
// Descriptive name to identify the key, e.g. the device where key is used.
|
|
Name string `bstore:"nonzero"`
|
|
|
|
// If set, new immediate authenticated TLS connections are not moved to
|
|
// "authenticated" state. For clients that don't understand it, and will try an
|
|
// authenticate command anyway.
|
|
NoIMAPPreauth bool
|
|
|
|
CertDER []byte `bstore:"nonzero"`
|
|
Account string `bstore:"nonzero"` // Key authenticates this account.
|
|
LoginAddress string `bstore:"nonzero"` // Must belong to account.
|
|
}
|
|
|
|
// ParseTLSPublicKeyCert parses a certificate, preparing a TLSPublicKey for
|
|
// insertion into the database. Caller must set fields that are not in the
|
|
// certificat, such as Account and LoginAddress.
|
|
func ParseTLSPublicKeyCert(certDER []byte) (TLSPublicKey, error) {
|
|
cert, err := x509.ParseCertificate(certDER)
|
|
if err != nil {
|
|
return TLSPublicKey{}, fmt.Errorf("parsing certificate: %v", err)
|
|
}
|
|
name := cert.Subject.CommonName
|
|
if name == "" && cert.SerialNumber != nil {
|
|
name = fmt.Sprintf("serial %x", cert.SerialNumber.Bytes())
|
|
}
|
|
|
|
buf := sha256.Sum256(cert.RawSubjectPublicKeyInfo)
|
|
fp := base64.RawURLEncoding.EncodeToString(buf[:])
|
|
var typ string
|
|
switch k := cert.PublicKey.(type) {
|
|
case *rsa.PublicKey:
|
|
bits := k.N.BitLen()
|
|
if bits < 2048 {
|
|
return TLSPublicKey{}, fmt.Errorf("rsa keys smaller than 2048 bits not accepted")
|
|
}
|
|
typ = "rsa-" + fmt.Sprintf("%d", bits)
|
|
case *ecdsa.PublicKey:
|
|
typ = "ecdsa-" + strings.ReplaceAll(strings.ToLower(k.Params().Name), "-", "")
|
|
case ed25519.PublicKey:
|
|
typ = "ed25519"
|
|
default:
|
|
return TLSPublicKey{}, fmt.Errorf("public key type %T not implemented", cert.PublicKey)
|
|
}
|
|
|
|
return TLSPublicKey{Fingerprint: fp, Type: typ, Name: name, CertDER: certDER}, nil
|
|
}
|
|
|
|
// TLSPublicKeyList returns tls public keys. If accountOpt is empty, keys for all
|
|
// accounts are returned.
|
|
func TLSPublicKeyList(ctx context.Context, accountOpt string) ([]TLSPublicKey, error) {
|
|
q := bstore.QueryDB[TLSPublicKey](ctx, AuthDB)
|
|
if accountOpt != "" {
|
|
q.FilterNonzero(TLSPublicKey{Account: accountOpt})
|
|
}
|
|
return q.List()
|
|
}
|
|
|
|
// TLSPublicKeyGet retrieves a single tls public key by fingerprint.
|
|
// If absent, bstore.ErrAbsent is returned.
|
|
func TLSPublicKeyGet(ctx context.Context, fingerprint string) (TLSPublicKey, error) {
|
|
pubKey := TLSPublicKey{Fingerprint: fingerprint}
|
|
err := AuthDB.Get(ctx, &pubKey)
|
|
return pubKey, err
|
|
}
|
|
|
|
// TLSPublicKeyAdd adds a new tls public key.
|
|
//
|
|
// Caller is responsible for checking the account and email address are valid.
|
|
func TLSPublicKeyAdd(ctx context.Context, pubKey *TLSPublicKey) error {
|
|
if err := checkTLSPublicKeyAddress(pubKey.LoginAddress); err != nil {
|
|
return err
|
|
}
|
|
return AuthDB.Insert(ctx, pubKey)
|
|
}
|
|
|
|
// TLSPublicKeyUpdate updates an existing tls public key.
|
|
//
|
|
// Caller is responsible for checking the account and email address are valid.
|
|
func TLSPublicKeyUpdate(ctx context.Context, pubKey *TLSPublicKey) error {
|
|
if err := checkTLSPublicKeyAddress(pubKey.LoginAddress); err != nil {
|
|
return err
|
|
}
|
|
return AuthDB.Update(ctx, pubKey)
|
|
}
|
|
|
|
func checkTLSPublicKeyAddress(addr string) error {
|
|
a, err := smtp.ParseAddress(addr)
|
|
if err != nil {
|
|
return fmt.Errorf("parsing login address %q: %v", addr, err)
|
|
}
|
|
if a.String() != addr {
|
|
return fmt.Errorf("login address %q must be specified in canonical form %q", addr, a.String())
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// TLSPublicKeyRemove removes a tls public key.
|
|
func TLSPublicKeyRemove(ctx context.Context, fingerprint string) error {
|
|
k := TLSPublicKey{Fingerprint: fingerprint}
|
|
return AuthDB.Delete(ctx, &k)
|
|
}
|
|
|
|
// tlsPublicKeyRemoveForAccount removes all tls public keys for an account.
|
|
func tlsPublicKeyRemoveForAccount(tx *bstore.Tx, account string) error {
|
|
q := bstore.QueryTx[TLSPublicKey](tx)
|
|
q.FilterNonzero(TLSPublicKey{Account: account})
|
|
_, err := q.Delete()
|
|
return err
|
|
}
|