Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Start propagating antispam denial reasons to a couple high impact RPCs #131

Merged
merged 1 commit into from
May 23, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ require (
firebase.google.com/go/v4 v4.8.0
github.com/aws/aws-sdk-go-v2 v0.17.0
github.com/bits-and-blooms/bloom/v3 v3.1.0
github.com/code-payments/code-protobuf-api v1.16.2
github.com/code-payments/code-protobuf-api v1.16.5
github.com/emirpasic/gods v1.12.0
github.com/envoyproxy/protoc-gen-validate v1.0.4
github.com/golang-jwt/jwt/v5 v5.0.0
Expand Down
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -121,8 +121,8 @@ github.com/cncf/xds/go v0.0.0-20210922020428-25de7278fc84/go.mod h1:eXthEFrGJvWH
github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
github.com/cockroachdb/apd v1.1.0 h1:3LFP3629v+1aKXU5Q37mxmRxX/pIu1nijXydLShEq5I=
github.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMeY4+DwBQ=
github.com/code-payments/code-protobuf-api v1.16.2 h1:SH8I+JOYOnoARJ5h3jDX92vcoHeqm8UCQBlqP5516rI=
github.com/code-payments/code-protobuf-api v1.16.2/go.mod h1:pHQm75vydD6Cm2qHAzlimW6drysm489Z4tVxC2zHSsU=
github.com/code-payments/code-protobuf-api v1.16.5 h1:mQYhFbpqiXdecrSo9cbmP3zoUT37qWZAYOMcrWGyo78=
github.com/code-payments/code-protobuf-api v1.16.5/go.mod h1:pHQm75vydD6Cm2qHAzlimW6drysm489Z4tVxC2zHSsU=
github.com/containerd/continuity v0.0.0-20190827140505-75bee3e2ccb6 h1:NmTXa/uVnDyp0TY5MKi197+3HWcnYWfnHGyaFthlnGw=
github.com/containerd/continuity v0.0.0-20190827140505-75bee3e2ccb6/go.mod h1:GL3xCUCBDV3CZiTSEKksMWbLE66hEyuu9qyDOOqM47Y=
github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk=
Expand Down
10 changes: 5 additions & 5 deletions pkg/code/antispam/airdrop.go
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ func (g *Guard) AllowWelcomeBonus(ctx context.Context, owner *common.Account) (b
}

// Deny abusers from known phone ranges
if hasBannedPhoneNumberPrefix(verification.PhoneNumber) {
if isSanctionedPhoneNumber(verification.PhoneNumber) {
log.Info("denying phone prefix")
recordDenialEvent(ctx, actionWelcomeBonus, "phone prefix banned")
return false, nil
Expand Down Expand Up @@ -170,10 +170,10 @@ func (g *Guard) AllowReferralBonus(

log := log.WithField("phone", verification.PhoneNumber)

// Deny abusers from known phone ranges
if hasBannedPhoneNumberPrefix(verification.PhoneNumber) {
log.Info("denying phone prefix")
recordDenialEvent(ctx, actionReferralBonus, "phone prefix banned")
// Deny users from sanctioned countries
if isSanctionedPhoneNumber(verification.PhoneNumber) {
log.Info("denying sanctioned country")
recordDenialEvent(ctx, actionReferralBonus, "sanctioned country")
return false, nil
}

Expand Down
38 changes: 22 additions & 16 deletions pkg/code/antispam/guard_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -414,7 +414,7 @@ func TestAllowOpenAccounts_HappyPath(t *testing.T) {

// Account isn't phone verified, so it cannot be created
for i := 0; i < 5; i++ {
allow, _, err := env.guard.AllowOpenAccounts(env.ctx, ownerAccount1, pointer.String(memory_device_verifier.ValidDeviceToken))
allow, _, _, err := env.guard.AllowOpenAccounts(env.ctx, ownerAccount1, pointer.String(memory_device_verifier.ValidDeviceToken))
require.NoError(t, err)
assert.False(t, allow)
}
Expand All @@ -431,17 +431,19 @@ func TestAllowOpenAccounts_HappyPath(t *testing.T) {

// New accounts are always denied when using a fake or unverifiable device.
for i := 0; i < 5; i++ {
allow, _, err := env.guard.AllowOpenAccounts(env.ctx, ownerAccount1, pointer.String(memory_device_verifier.InvalidDeviceToken))
allow, reason, _, err := env.guard.AllowOpenAccounts(env.ctx, ownerAccount1, pointer.String(memory_device_verifier.InvalidDeviceToken))
require.NoError(t, err)
assert.False(t, allow)
assert.Equal(t, ReasonUnsupportedDevice, reason)

allow, _, err = env.guard.AllowOpenAccounts(env.ctx, ownerAccount1, nil)
allow, reason, _, err = env.guard.AllowOpenAccounts(env.ctx, ownerAccount1, nil)
require.NoError(t, err)
assert.False(t, allow)
assert.Equal(t, ReasonUnsupportedDevice, reason)
}

// The first account creation should always be successful
allow, successCallback, err := env.guard.AllowOpenAccounts(env.ctx, ownerAccount1, pointer.String(memory_device_verifier.ValidDeviceToken))
allow, _, successCallback, err := env.guard.AllowOpenAccounts(env.ctx, ownerAccount1, pointer.String(memory_device_verifier.ValidDeviceToken))
require.NoError(t, err)
assert.True(t, allow)
require.NotNil(t, successCallback)
Expand All @@ -454,7 +456,7 @@ func TestAllowOpenAccounts_HappyPath(t *testing.T) {

// Account creation is unaffected by previous creations that didn't
// result in success
allow, _, err = env.guard.AllowOpenAccounts(env.ctx, ownerAccount1, pointer.String(memory_device_verifier.ValidDeviceToken))
allow, _, _, err = env.guard.AllowOpenAccounts(env.ctx, ownerAccount1, pointer.String(memory_device_verifier.ValidDeviceToken))
require.NoError(t, err)
assert.True(t, allow)

Expand All @@ -464,9 +466,10 @@ func TestAllowOpenAccounts_HappyPath(t *testing.T) {
// Account creations are denied after breaching the daily count limit regardless
// of owner account associated with the phone number
for i := 0; i < 5; i++ {
allow, _, err := env.guard.AllowOpenAccounts(env.ctx, ownerAccount2, pointer.String(memory_device_verifier.ValidDeviceToken))
allow, reason, _, err := env.guard.AllowOpenAccounts(env.ctx, ownerAccount2, pointer.String(memory_device_verifier.ValidDeviceToken))
require.NoError(t, err)
assert.False(t, allow)
assert.Equal(t, ReasonTooManyFreeAccountsForPhoneNumber, reason)
}

// New accounts are always denied within the same device
Expand All @@ -482,9 +485,10 @@ func TestAllowOpenAccounts_HappyPath(t *testing.T) {
require.NoError(t, env.guard.data.SavePhoneVerification(env.ctx, verification))

for i := 0; i < 5; i++ {
allow, _, err := env.guard.AllowOpenAccounts(env.ctx, ownerAccount2, pointer.String(memory_device_verifier.ValidDeviceToken))
allow, reason, _, err := env.guard.AllowOpenAccounts(env.ctx, ownerAccount2, pointer.String(memory_device_verifier.ValidDeviceToken))
require.NoError(t, err)
assert.False(t, allow)
assert.Equal(t, ReasonTooManyFreeAccountsForDevice, reason)
}
}
}
Expand Down Expand Up @@ -519,15 +523,15 @@ func TestAllowOpenAccounts_StaffUser(t *testing.T) {
}

// First account creation is always successful, regardless of user status
allow, _, err := env.guard.AllowOpenAccounts(env.ctx, ownerAccount1, pointer.String(memory_device_verifier.ValidDeviceToken))
allow, _, _, err := env.guard.AllowOpenAccounts(env.ctx, ownerAccount1, pointer.String(memory_device_verifier.ValidDeviceToken))
require.NoError(t, err)
assert.True(t, allow)

// Consume the remaining daily limit of account creations
simulateAccountCreation(t, env, ownerAccount1, intent.StateConfirmed, time.Now())

// Staff users should not be subject to any denials
allow, _, err = env.guard.AllowOpenAccounts(env.ctx, ownerAccount2, pointer.String(memory_device_verifier.ValidDeviceToken))
allow, _, _, err = env.guard.AllowOpenAccounts(env.ctx, ownerAccount2, pointer.String(memory_device_verifier.ValidDeviceToken))
require.NoError(t, err)
assert.Equal(t, isStaffUser, allow)
}
Expand Down Expand Up @@ -643,20 +647,22 @@ func TestAllowNewPhoneVerification_HappyPath(t *testing.T) {
// New verifications are always allowed when the phone has never started one
// and has a valid device token
for i := 0; i < 5; i++ {
allow, err := env.guard.AllowNewPhoneVerification(env.ctx, phoneNumber, pointer.String(memory_device_verifier.ValidDeviceToken))
allow, _, err := env.guard.AllowNewPhoneVerification(env.ctx, phoneNumber, pointer.String(memory_device_verifier.ValidDeviceToken))
require.NoError(t, err)
assert.True(t, allow)
}

// New verifications are always denied when using a fake or unverifiable device.
for i := 0; i < 5; i++ {
allow, err := env.guard.AllowNewPhoneVerification(env.ctx, phoneNumber, pointer.String(memory_device_verifier.InvalidDeviceToken))
allow, reason, err := env.guard.AllowNewPhoneVerification(env.ctx, phoneNumber, pointer.String(memory_device_verifier.InvalidDeviceToken))
require.NoError(t, err)
assert.False(t, allow)
assert.Equal(t, ReasonUnsupportedDevice, reason)

allow, err = env.guard.AllowNewPhoneVerification(env.ctx, phoneNumber, nil)
allow, reason, err = env.guard.AllowNewPhoneVerification(env.ctx, phoneNumber, nil)
require.NoError(t, err)
assert.False(t, allow)
assert.Equal(t, ReasonUnsupportedDevice, reason)
}

// New verifications are allowed when we're under the time interval limit,
Expand All @@ -665,7 +671,7 @@ func TestAllowNewPhoneVerification_HappyPath(t *testing.T) {
for j := 0; j < 3; j++ {
simulateSmsCodeSent(t, env, phoneNumber, fmt.Sprintf("verification%d", i))

allow, err := env.guard.AllowNewPhoneVerification(env.ctx, phoneNumber, pointer.String(memory_device_verifier.ValidDeviceToken))
allow, _, err := env.guard.AllowNewPhoneVerification(env.ctx, phoneNumber, pointer.String(memory_device_verifier.ValidDeviceToken))
require.NoError(t, err)
assert.True(t, allow)
}
Expand All @@ -675,13 +681,13 @@ func TestAllowNewPhoneVerification_HappyPath(t *testing.T) {
// interval limit.
simulateSmsCodeSent(t, env, phoneNumber, "last_allowed_verification")
for i := 0; i < 5; i++ {
allow, err := env.guard.AllowNewPhoneVerification(env.ctx, phoneNumber, pointer.String(memory_device_verifier.ValidDeviceToken))
allow, _, err := env.guard.AllowNewPhoneVerification(env.ctx, phoneNumber, pointer.String(memory_device_verifier.ValidDeviceToken))
require.NoError(t, err)
assert.False(t, allow)
}

// Phone numbers are not affected by limits enforced on other phone numbers
allow, err := env.guard.AllowNewPhoneVerification(env.ctx, otherPhoneNumber, pointer.String(memory_device_verifier.ValidDeviceToken))
allow, _, err := env.guard.AllowNewPhoneVerification(env.ctx, otherPhoneNumber, pointer.String(memory_device_verifier.ValidDeviceToken))
require.NoError(t, err)
assert.True(t, allow)
}
Expand All @@ -705,7 +711,7 @@ func TestAllowNewPhoneVerification_StaffUser(t *testing.T) {
for j := 0; j < 3; j++ {
simulateSmsCodeSent(t, env, phoneNumber, fmt.Sprintf("verification%d", i))

allow, err := env.guard.AllowNewPhoneVerification(env.ctx, phoneNumber, nil)
allow, _, err := env.guard.AllowNewPhoneVerification(env.ctx, phoneNumber, nil)
require.NoError(t, err)
assert.True(t, allow)
}
Expand Down
64 changes: 32 additions & 32 deletions pkg/code/antispam/intent.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import (
// AllowOpenAccounts determines whether a phone-verified owner account can create
// a Code account via an open accounts intent. The objective here is to limit attacks
// against our Subsidizer's SOL balance.
func (g *Guard) AllowOpenAccounts(ctx context.Context, owner *common.Account, deviceToken *string) (bool, func() error, error) {
func (g *Guard) AllowOpenAccounts(ctx context.Context, owner *common.Account, deviceToken *string) (bool, Reason, func() error, error) {
tracer := metrics.TraceMethodCall(ctx, metricsStructName, "AllowOpenAccounts")
defer tracer.End()

Expand All @@ -31,28 +31,28 @@ func (g *Guard) AllowOpenAccounts(ctx context.Context, owner *common.Account, de
if isIpBanned(ctx) {
log.Info("ip is banned")
recordDenialEvent(ctx, actionOpenAccounts, "ip banned")
return false, nil, nil
return false, ReasonUnspecified, nil, nil
}

verification, err := g.data.GetLatestPhoneVerificationForAccount(ctx, owner.PublicKey().ToBase58())
if err == phone.ErrVerificationNotFound {
// Owner account was never phone verified, so deny the action.
log.Info("owner account is not phone verified")
recordDenialEvent(ctx, actionOpenAccounts, "not phone verified")
return false, nil, nil
return false, ReasonUnspecified, nil, nil
} else if err != nil {
tracer.OnError(err)
log.WithError(err).Warn("failure getting phone verification record")
return false, nil, err
return false, ReasonUnspecified, nil, err
}

log = log.WithField("phone", verification.PhoneNumber)

// Deny abusers from known phone ranges
if hasBannedPhoneNumberPrefix(verification.PhoneNumber) {
log.Info("denying phone prefix")
recordDenialEvent(ctx, actionOpenAccounts, "phone prefix banned")
return false, nil, nil
// Deny users from sanctioned countries
if isSanctionedPhoneNumber(verification.PhoneNumber) {
log.Info("denying sanctioned country")
recordDenialEvent(ctx, actionOpenAccounts, "sanctioned country")
return false, ReasonUnsupportedCountry, nil, nil
}

user, err := g.data.GetUserByPhoneView(ctx, verification.PhoneNumber)
Expand All @@ -62,18 +62,18 @@ func (g *Guard) AllowOpenAccounts(ctx context.Context, owner *common.Account, de
if user.IsBanned {
log.Info("denying banned user")
recordDenialEvent(ctx, actionOpenAccounts, "user banned")
return false, nil, nil
return false, ReasonUnspecified, nil, nil
}

// Staff users have unlimited access to enable testing and demoing.
if user.IsStaffUser {
return true, func() error { return nil }, nil
return true, ReasonUnspecified, func() error { return nil }, nil
}
case identity.ErrNotFound:
default:
tracer.OnError(err)
log.WithError(err).Warn("failure getting user identity by phone view")
return false, nil, err
return false, ReasonUnspecified, nil, err
}

// Account creation limit since the beginning of time
Expand All @@ -87,7 +87,7 @@ func (g *Guard) AllowOpenAccounts(ctx context.Context, owner *common.Account, de
if err != nil {
tracer.OnError(err)
log.WithError(err).Warn("failure getting intent count")
return false, nil, err
return false, ReasonUnspecified, nil, err
}

// Device-based restrictions guaranteeing 1 free account per valid device
Expand All @@ -97,27 +97,27 @@ func (g *Guard) AllowOpenAccounts(ctx context.Context, owner *common.Account, de
if deviceToken == nil {
log.Info("denying attempt without device token")
recordDenialEvent(ctx, actionOpenAccounts, "device token missing")
return false, nil, nil
return false, ReasonUnsupportedDevice, nil, nil
}

isValidDeviceToken, reason, err := g.deviceVerifier.IsValid(ctx, *deviceToken)
if err != nil {
log.WithError(err).Warn("failure performing device validation check")
return false, nil, err
return false, ReasonUnspecified, nil, err
} else if !isValidDeviceToken {
log.WithField("reason", reason).Info("denying fake device")
recordDenialEvent(ctx, actionOpenAccounts, "fake device")
return false, nil, nil
return false, ReasonUnsupportedDevice, nil, nil
}

hasCreatedFreeAccount, err := g.deviceVerifier.HasCreatedFreeAccount(ctx, *deviceToken)
if err != nil {
log.WithError(err).Warn("failure performing free account check for device")
return false, nil, err
return false, ReasonUnspecified, nil, err
} else if hasCreatedFreeAccount {
log.Info("denying duplicate device")
recordDenialEvent(ctx, actionOpenAccounts, "duplicate device")
return false, nil, nil
return false, ReasonTooManyFreeAccountsForDevice, nil, nil
}
}

Expand All @@ -126,7 +126,7 @@ func (g *Guard) AllowOpenAccounts(ctx context.Context, owner *common.Account, de
if int(count) >= 1 {
log.Info("phone is rate limited by lifetime account creation count")
recordDenialEvent(ctx, actionOpenAccounts, "lifetime limit exceeded")
return false, nil, nil
return false, ReasonTooManyFreeAccountsForPhoneNumber, nil, nil
}

onFreeAccountCreated := func() error {
Expand All @@ -135,7 +135,7 @@ func (g *Guard) AllowOpenAccounts(ctx context.Context, owner *common.Account, de
}
return g.deviceVerifier.MarkCreatedFreeAccount(ctx, *deviceToken)
}
return true, onFreeAccountCreated, nil
return true, ReasonUnspecified, onFreeAccountCreated, nil
}

// AllowSendPayment determines whether a phone-verified owner account is allowed to
Expand Down Expand Up @@ -178,10 +178,10 @@ func (g *Guard) AllowSendPayment(ctx context.Context, owner *common.Account, isP

log = log.WithField("phone", verification.PhoneNumber)

// Deny abusers from known phone ranges
if hasBannedPhoneNumberPrefix(verification.PhoneNumber) {
log.Info("denying phone prefix")
recordDenialEvent(ctx, actionSendPayment, "phone prefix banned")
// Deny users from sanctioned countries
if isSanctionedPhoneNumber(verification.PhoneNumber) {
log.Info("denying sanctioned country")
recordDenialEvent(ctx, actionSendPayment, "sanctioned country")
return false, nil
}

Expand Down Expand Up @@ -322,10 +322,10 @@ func (g *Guard) AllowReceivePayments(ctx context.Context, owner *common.Account,

log = log.WithField("phone", verification.PhoneNumber)

// Deny abusers from known phone ranges
if hasBannedPhoneNumberPrefix(verification.PhoneNumber) {
log.Info("denying phone prefix")
recordDenialEvent(ctx, actionReceivePayments, "phone prefix banned")
// Deny users from sanctioned countries
if isSanctionedPhoneNumber(verification.PhoneNumber) {
log.Info("denying sanctioned country")
recordDenialEvent(ctx, actionReceivePayments, "sanctioned country")
return false, nil
}

Expand Down Expand Up @@ -466,10 +466,10 @@ func (g *Guard) AllowEstablishNewRelationship(ctx context.Context, owner *common

log = log.WithField("phone", verification.PhoneNumber)

// Deny abusers from known phone ranges
if hasBannedPhoneNumberPrefix(verification.PhoneNumber) {
log.Info("denying phone prefix")
recordDenialEvent(ctx, actionEstablishNewRelationship, "phone prefix banned")
// Deny users from sanctioned countries
if isSanctionedPhoneNumber(verification.PhoneNumber) {
log.Info("denying sanctioned country")
recordDenialEvent(ctx, actionEstablishNewRelationship, "sanctioned country")
return false, nil
}

Expand Down
2 changes: 1 addition & 1 deletion pkg/code/antispam/phone.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import "strings"

// todo: Put in a DB somehwere? Or make configurable?
// todo: Needs tests
func hasBannedPhoneNumberPrefix(phoneNumber string) bool {
func isSanctionedPhoneNumber(phoneNumber string) bool {
// Check that a +7 phone number is not a mobile number from Kazakhstan
if strings.HasPrefix(phoneNumber, "+76") || strings.HasPrefix(phoneNumber, "+77") {
return false
Expand Down
Loading
Loading