From 4b725e7d3c2ed3dd49b5f708966c73dc411022a0 Mon Sep 17 00:00:00 2001 From: violog <51th.apprent1ce.f0rce@gmail.com> Date: Wed, 8 May 2024 19:19:49 +0300 Subject: [PATCH 1/2] Add protection from DDoS (multiaccount) attacks --- config.yaml | 2 + internal/assets/migrations/003_banned.sql | 5 ++ internal/config/verifier.go | 6 +++ internal/data/claims.go | 6 ++- internal/data/pg/claims.go | 50 ++++++++++++++----- .../service/api/handlers/create_identity.go | 39 +++++++++++++++ 6 files changed, 94 insertions(+), 14 deletions(-) create mode 100644 internal/assets/migrations/003_banned.sql diff --git a/config.yaml b/config.yaml index 1a527e5..c434350 100644 --- a/config.yaml +++ b/config.yaml @@ -12,6 +12,8 @@ verifier: sha256: "./sha256_verification_key.json" master_certs_path: "./masterList.dev.pem" allowed_age: 18 + multi_acc_min_limit: 10 + multi_acc_max_limit: 30 registration_timeout: 1h issuer: diff --git a/internal/assets/migrations/003_banned.sql b/internal/assets/migrations/003_banned.sql new file mode 100644 index 0000000..de2565a --- /dev/null +++ b/internal/assets/migrations/003_banned.sql @@ -0,0 +1,5 @@ +-- +migrate Up +ALTER TABLE claims ADD COLUMN is_banned BOOLEAN NOT NULL DEFAULT FALSE; + +-- -migrate Down +ALTER TABLE claims DROP COLUMN is_banned; diff --git a/internal/config/verifier.go b/internal/config/verifier.go index c38e0a4..5281b08 100644 --- a/internal/config/verifier.go +++ b/internal/config/verifier.go @@ -18,6 +18,8 @@ type VerifierConfig struct { MasterCerts []byte AllowedAge int RegistrationTimeout time.Duration + MultiAccMinLimit int + MultiAccMaxLimit int } type verifier struct { @@ -37,6 +39,8 @@ func (v *verifier) VerifierConfig() *VerifierConfig { VerificationKeysPaths map[string]string `fig:"verification_keys_paths,required"` MasterCertsPath string `fig:"master_certs_path,required"` AllowedAge int `fig:"allowed_age,required"` + MultiAccMinLimit int `fig:"multi_acc_min_limit,required"` + MultiAccMaxLimit int `fig:"multi_acc_max_limit,required"` RegistrationTimeout time.Duration `fig:"registration_timeout"` }{} @@ -68,6 +72,8 @@ func (v *verifier) VerifierConfig() *VerifierConfig { VerificationKeys: verificationKeys, MasterCerts: masterCerts, AllowedAge: newCfg.AllowedAge, + MultiAccMinLimit: newCfg.MultiAccMinLimit, + MultiAccMaxLimit: newCfg.MultiAccMaxLimit, RegistrationTimeout: newCfg.RegistrationTimeout, } }).(*VerifierConfig) diff --git a/internal/data/claims.go b/internal/data/claims.go index 47b68a3..9515839 100644 --- a/internal/data/claims.go +++ b/internal/data/claims.go @@ -1,16 +1,19 @@ package data import ( - "github.com/google/uuid" "time" + + "github.com/google/uuid" ) type ClaimQ interface { New() ClaimQ Insert(value Claim) error + Update(value Claim) error FilterBy(column string, value any) ClaimQ Get() (*Claim, error) Select() ([]Claim, error) + Count() (int, error) DeleteByID(id uuid.UUID) error ForUpdate() ClaimQ ResetFilter() ClaimQ @@ -24,4 +27,5 @@ type Claim struct { Salt string `db:"salt" structs:"salt"` DocumentHash string `db:"document_hash" structs:"document_hash"` CreatedAt time.Time `db:"created_at" structs:"-"` + IsBanned bool `db:"is_banned" structs:"is_banned"` } diff --git a/internal/data/pg/claims.go b/internal/data/pg/claims.go index 199283b..4c4345a 100644 --- a/internal/data/pg/claims.go +++ b/internal/data/pg/claims.go @@ -2,6 +2,8 @@ package pg import ( "database/sql" + "errors" + sq "github.com/Masterminds/squirrel" "github.com/fatih/structs" "github.com/google/uuid" @@ -14,20 +16,23 @@ const claimsTableName = "claims" var ( claimsSelector = sq.Select("*").From(claimsTableName) claimsUpdate = sq.Update(claimsTableName) + claimsCounter = sq.Select("COUNT(*) AS count").From(claimsTableName) ) func NewClaimsQ(db *pgdb.DB) data.ClaimQ { return &claimsQ{ - db: db, - sql: claimsSelector, - upd: claimsUpdate, + db: db, + sel: claimsSelector, + upd: claimsUpdate, + count: claimsCounter, } } type claimsQ struct { - db *pgdb.DB - sql sq.SelectBuilder - upd sq.UpdateBuilder + db *pgdb.DB + sel sq.SelectBuilder + upd sq.UpdateBuilder + count sq.SelectBuilder } func (q *claimsQ) New() data.ClaimQ { @@ -41,15 +46,25 @@ func (q *claimsQ) Insert(value data.Claim) error { return err } +func (q *claimsQ) Update(value data.Claim) error { + clauses := structs.Map(value) + stmt := q.upd.SetMap(clauses) + err := q.db.Exec(stmt) + return err +} + func (q *claimsQ) FilterBy(column string, value any) data.ClaimQ { - q.sql = q.sql.Where(sq.Eq{column: value}) + eq := sq.Eq{column: value} + q.sel = q.sel.Where(eq) + q.upd = q.upd.Where(eq) + q.count = q.count.Where(eq) return q } func (q *claimsQ) Get() (*data.Claim, error) { var result data.Claim - err := q.db.Get(&result, q.sql) - if err == sql.ErrNoRows { + err := q.db.Get(&result, q.sel) + if errors.Is(err, sql.ErrNoRows) { return nil, nil } return &result, err @@ -57,10 +72,18 @@ func (q *claimsQ) Get() (*data.Claim, error) { func (q *claimsQ) Select() ([]data.Claim, error) { var result []data.Claim - err := q.db.Select(&result, q.sql) + err := q.db.Select(&result, q.sel) return result, err } +func (q *claimsQ) Count() (int, error) { + var result struct { + Count int `db:"count"` + } + err := q.db.Select(&result, q.count) + return result.Count, err +} + func (q *claimsQ) DeleteByID(id uuid.UUID) error { if err := q.db.Exec(sq.Delete(claimsTableName).Where(sq.Eq{"id": id})); err != nil { return err @@ -69,12 +92,13 @@ func (q *claimsQ) DeleteByID(id uuid.UUID) error { } func (q *claimsQ) ForUpdate() data.ClaimQ { - q.sql = q.sql.Suffix("FOR UPDATE") + q.sel = q.sel.Suffix("FOR UPDATE") return q } func (q *claimsQ) ResetFilter() data.ClaimQ { - q.sql = sq.Select("*").From(claimsTableName) - q.upd = sq.Update(claimsTableName) + q.sel = claimsSelector + q.upd = claimsUpdate + q.count = claimsCounter return q } diff --git a/internal/service/api/handlers/create_identity.go b/internal/service/api/handlers/create_identity.go index 4c39ce3..ed6473d 100644 --- a/internal/service/api/handlers/create_identity.go +++ b/internal/service/api/handlers/create_identity.go @@ -12,6 +12,7 @@ import ( "encoding/pem" "fmt" "math/big" + "math/rand/v2" "net/http" "strconv" "strings" @@ -210,6 +211,44 @@ func CreateIdentity(w http.ResponseWriter, r *http.Request) { return } + existing, err := masterQ.Claim().FilterBy("document_hash", documentHash).Get() + if err != nil { + Log(r).WithError(err).Error("failed to get claim by document hash") + ape.RenderErr(w, problems.InternalError()) + return + } + if existing != nil { + log := Log(r).WithField("document_hash", documentHash) + if existing.IsBanned { + log.Info("user of the provided document is banned") + ape.RenderErr(w, problems.InternalError()) + return + } + + count, err := masterQ.Claim().FilterBy("document_hash", documentHash).Count() + if err != nil { + Log(r).WithError(err).Error("failed to count claims by document hash") + ape.RenderErr(w, problems.InternalError()) + return + } + + if count > 0 { + allowed := rand.IntN(cfg.MultiAccMaxLimit-cfg.MultiAccMinLimit+1) + cfg.MultiAccMinLimit + if count >= allowed { + err = masterQ.Claim().FilterBy("document_hash", documentHash).Update(data.Claim{IsBanned: true}) + + if err != nil { + Log(r).WithError(err).Error("failed to ban user") + } else { + log.Infof("user of the provided document was banned for registering %d accounts, allowed is %d", count, allowed) + } + + ape.RenderErr(w, problems.InternalError()) + return + } + } + } + if err := masterQ.Transaction(func(db data.MasterQ) error { claimID, err = iss.IssueVotingClaim( req.Data.ID.String(), int64(issuingAuthority), true, identityExpiration, nullifier, From b6d2234fe41391d08c24c961e3b6163dab904e6d Mon Sep 17 00:00:00 2001 From: violog <51th.apprent1ce.f0rce@gmail.com> Date: Wed, 8 May 2024 19:21:59 +0300 Subject: [PATCH 2/2] use log with document hash field --- internal/service/api/handlers/create_identity.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/internal/service/api/handlers/create_identity.go b/internal/service/api/handlers/create_identity.go index ed6473d..df4732e 100644 --- a/internal/service/api/handlers/create_identity.go +++ b/internal/service/api/handlers/create_identity.go @@ -227,7 +227,7 @@ func CreateIdentity(w http.ResponseWriter, r *http.Request) { count, err := masterQ.Claim().FilterBy("document_hash", documentHash).Count() if err != nil { - Log(r).WithError(err).Error("failed to count claims by document hash") + log.WithError(err).Error("failed to count claims by document hash") ape.RenderErr(w, problems.InternalError()) return } @@ -238,7 +238,7 @@ func CreateIdentity(w http.ResponseWriter, r *http.Request) { err = masterQ.Claim().FilterBy("document_hash", documentHash).Update(data.Claim{IsBanned: true}) if err != nil { - Log(r).WithError(err).Error("failed to ban user") + log.WithError(err).Error("failed to ban user") } else { log.Infof("user of the provided document was banned for registering %d accounts, allowed is %d", count, allowed) }