mirror of
https://github.com/mjl-/mox.git
synced 2025-06-28 02:28:15 +03:00

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