Skip to content

Commit

Permalink
Merge pull request #8 from rarimo/feature/ddos-guard
Browse files Browse the repository at this point in the history
Add protection from DDoS (multiaccount) attacks
  • Loading branch information
artemskriabin authored May 8, 2024
2 parents 374461c + b6d2234 commit 64faac6
Show file tree
Hide file tree
Showing 6 changed files with 94 additions and 14 deletions.
2 changes: 2 additions & 0 deletions config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
5 changes: 5 additions & 0 deletions internal/assets/migrations/003_banned.sql
Original file line number Diff line number Diff line change
@@ -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;
6 changes: 6 additions & 0 deletions internal/config/verifier.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ type VerifierConfig struct {
MasterCerts []byte
AllowedAge int
RegistrationTimeout time.Duration
MultiAccMinLimit int
MultiAccMaxLimit int
}

type verifier struct {
Expand All @@ -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"`
}{}

Expand Down Expand Up @@ -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)
Expand Down
6 changes: 5 additions & 1 deletion internal/data/claims.go
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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"`
}
50 changes: 37 additions & 13 deletions internal/data/pg/claims.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ package pg

import (
"database/sql"
"errors"

sq "github.com/Masterminds/squirrel"
"github.com/fatih/structs"
"github.com/google/uuid"
Expand All @@ -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 {
Expand All @@ -41,26 +46,44 @@ 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
}

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
Expand All @@ -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
}
39 changes: 39 additions & 0 deletions internal/service/api/handlers/create_identity.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import (
"encoding/pem"
"fmt"
"math/big"
"math/rand/v2"
"net/http"
"strconv"
"strings"
Expand Down Expand Up @@ -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.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.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,
Expand Down

0 comments on commit 64faac6

Please sign in to comment.