mox/store/tlspubkey.go
Mechiel Lukkien 1277d78cb1
keep track of login attempts, both successful and failures
and show them in the account and admin interfaces. this should help with
debugging, to find misconfigured clients, and potentially find attackers trying
to login.

we include details like login name, account name, protocol, authentication
mechanism, ip addresses, tls connection properties, user-agent. and of course
the result.

we group entries by their details. repeat connections don't cause new records
in the database, they just increase the count on the existing record.

we keep data for at most 30 days. and we keep at most 10k entries per account.
to prevent unbounded growth. for successful login attempts, we store them all
for 30d. if a bad user causes so many entries this becomes a problem, it will
be time to talk to the user...

there is no pagination/searching yet in the admin/account interfaces. so the
list may be long. we only show the 10 most recent login attempts by default.
the rest is only shown on a separate page.

there is no way yet to disable this. may come later, either as global setting
or per account.
2025-02-06 14:16:13 +01:00

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(ctx context.Context, account string) error {
q := bstore.QueryDB[TLSPublicKey](ctx, AuthDB)
q.FilterNonzero(TLSPublicKey{Account: account})
_, err := q.Delete()
return err
}