Skip to content

Commit

Permalink
Feature/kleros iden3 v1
Browse files Browse the repository at this point in the history
* add Kleros identity provider

* rework identity providers credential subject

* fix Kleros config

* fix Kleros metadata

* add Kleros to the config

* fix CI

* return 401 Unauthorized if user is not registered in Kleros

* minor fix

* fix errors lib
  • Loading branch information
freigeistig committed Dec 21, 2023
1 parent a2262f1 commit e82d58f
Show file tree
Hide file tree
Showing 19 changed files with 4,063 additions and 30 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/tag.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ name: Build KYC service docker image
on:
push:
tags:
- 'v[0-9]+.[0-9]+.[0-9]+'
- 'v[0-9]+.[0-9]+.[0-9]+*'

jobs:
converge:
Expand Down
4 changes: 4 additions & 0 deletions config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,10 @@ gitcoin_passport:
get_score_max_retries: 3
skip_sig_check: false

kleros:
eth_rpc_url:
proof_of_humanity_contract:

kyc_service:
nonce_life_time: 30m

Expand Down
19 changes: 19 additions & 0 deletions docs/spec/components/schemas/KlerosData.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
type: object
description: Kleros provider's data
required:
- address
- signature
properties:
address:
type: string
format: string
description: The user's address
example: '0x1234567890123456789012345678901234567890'
signature:
type: string
format: string
description: >-
The signature of the requested nonce to validate if the user owns the
address
example: >-
0x1234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890
50 changes: 50 additions & 0 deletions internal/config/kleros.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package config

import (
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/ethclient"
"gitlab.com/distributed_lab/figure"
"gitlab.com/distributed_lab/kit/kv"
"gitlab.com/distributed_lab/logan/v3/errors"
)

type Kleros struct {
EthereumRpc *ethclient.Client
ProofOfHumanityContract common.Address
}

type kleros struct {
EthRpcURL string `fig:"eth_rpc_url,required"`
ProofOfHumanityContract string `fig:"proof_of_humanity_contract,required"`
}

func (c *config) Kleros() *Kleros {
return c.kleros.Do(func() interface{} {
cfg := kleros{}
err := figure.
Out(&cfg).
From(kv.MustGetStringMap(c.getter, "kleros")).
Please()
if err != nil {
panic(errors.Wrap(err, "failed to figure out"))
}

return parseKlerosConfig(&cfg)
}).(*Kleros)
}

func parseKlerosConfig(cfg *kleros) *Kleros {
ethClient, err := ethclient.Dial(cfg.EthRpcURL)
if err != nil {
panic(errors.Wrap(err, "failed to create an Ethereum client"))
}

if !common.IsHexAddress(cfg.ProofOfHumanityContract) {
panic(errors.New("failed to parse gateway token contract address"))
}

return &Kleros{
EthereumRpc: ethClient,
ProofOfHumanityContract: common.HexToAddress(cfg.ProofOfHumanityContract),
}
}
2 changes: 2 additions & 0 deletions internal/config/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ type Config interface {
WorldcoinSettings() *WorldcoinSettings
UnstoppableDomains() *UnstoppableDomains
GitcoinPassportSettings() *GitcoinPassportSettings
Kleros() *Kleros
Issuer() *Issuer
KYCService() *KYCService
}
Expand All @@ -33,6 +34,7 @@ type config struct {
unstoppableDomains comfig.Once
gitcoinPassportSettings comfig.Once
worldcoinSettings comfig.Once
kleros comfig.Once
issuer comfig.Once
kycService comfig.Once
}
Expand Down
4 changes: 3 additions & 1 deletion internal/service/api/handlers/verify.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
providers "github.com/rarimo/kyc-service/internal/service/core/identity_providers"
"github.com/rarimo/kyc-service/internal/service/core/identity_providers/civic"
gcpsp "github.com/rarimo/kyc-service/internal/service/core/identity_providers/gitcoin_passport"
"github.com/rarimo/kyc-service/internal/service/core/identity_providers/kleros"
"github.com/rarimo/kyc-service/internal/service/core/identity_providers/worldcoin"

"github.com/rarimo/kyc-service/internal/data"
Expand Down Expand Up @@ -69,5 +70,6 @@ func isUnauthorizedError(err error) bool {
errors.Is(err, providers.ErrNonceNotFound) ||
errors.Is(err, worldcoin.ErrNotLikelyHuman) ||
errors.Is(err, gcpsp.ErrScoreIsTooLow) ||
errors.Is(err, civic.ErrInvalidGatewayToken)
errors.Is(err, civic.ErrInvalidGatewayToken) ||
errors.Is(err, kleros.ErrIsNotRegistered)
}
3 changes: 3 additions & 0 deletions internal/service/api/responses/verify.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
providers "github.com/rarimo/kyc-service/internal/service/core/identity_providers"
"github.com/rarimo/kyc-service/internal/service/core/identity_providers/civic"
gcpsp "github.com/rarimo/kyc-service/internal/service/core/identity_providers/gitcoin_passport"
"github.com/rarimo/kyc-service/internal/service/core/identity_providers/kleros"
"github.com/rarimo/kyc-service/internal/service/core/identity_providers/worldcoin"

"github.com/rarimo/kyc-service/resources"
Expand All @@ -22,6 +23,7 @@ const (
NotLikelyHuman
ScoreIsTooLow
InvalidGatewayToken
IsNotRegistered
)

const (
Expand All @@ -37,6 +39,7 @@ var unauthorizedErrorCodes = map[string]int{
worldcoin.ErrNotLikelyHuman.Error(): NotLikelyHuman,
gcpsp.ErrScoreIsTooLow.Error(): ScoreIsTooLow,
civic.ErrInvalidGatewayToken.Error(): InvalidGatewayToken,
kleros.ErrIsNotRegistered.Error(): IsNotRegistered,
}

var conflictErrorCodes = map[string]int{
Expand Down
9 changes: 8 additions & 1 deletion internal/service/core/identity_providers/civic/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,14 @@ func (c *Civic) Verify(
credentialSubject := issuer.NewEmptyIdentityProvidersCredentialSubject()
credentialSubject.Provider = issuer.CivicProviderName
credentialSubject.Address = verifyData.Address.String()
credentialSubject.CivicGatekeeperNetworkID = token.Int64()

marshalled, err := json.Marshal(issuer.IdentityProviderMetadata{
CivicGatekeeperNetworkID: token.Int64(),
})
if err != nil {
return nil, nil, errors.Wrap(err, "failed to marshal")
}
credentialSubject.ProviderMetadata = string(marshalled)

return credentialSubject, cryptoPkg.Keccak256(
verifyData.Address.Bytes(),
Expand Down
2 changes: 1 addition & 1 deletion internal/service/core/identity_providers/civic/models.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import (
)

var (
// Interanl errors
// Internal errors
ErrVerifierNotFound = errors.New("verifier not found")

// Unauthorized errors
Expand Down
26 changes: 22 additions & 4 deletions internal/service/core/identity_providers/gitcoin_passport/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -118,8 +118,17 @@ func (g *GitcoinPassport) Verify(

credentialSubject.Provider = issuer.GitcoinProviderName
credentialSubject.Address = userAddr.String()
credentialSubject.GitcoinPassportScore = response.Score
credentialSubject.KYCAdditionalData = string(providerDataRaw)

marshalled, err := json.Marshal(issuer.IdentityProviderMetadata{
GitcoinPassportData: issuer.GitcoinPassportData{
Score: response.Score,
AdditionalData: string(providerDataRaw),
},
})
if err != nil {
return nil, nil, errors.Wrap(err, "failed to marshal")
}
credentialSubject.ProviderMetadata = string(marshalled)

case statusProcessing:
g.scoreReqChan <- *user
Expand Down Expand Up @@ -189,8 +198,17 @@ func (g *GitcoinPassport) watchNewCheckScoreRequest(ctx context.Context) {
credentialSubject.IsNatural = 1
credentialSubject.Provider = issuer.GitcoinProviderName
credentialSubject.Address = user.EthAddress.String()
credentialSubject.GitcoinPassportScore = score
credentialSubject.KYCAdditionalData = string(user.ProviderData)

marshalled, err := json.Marshal(issuer.IdentityProviderMetadata{
GitcoinPassportData: issuer.GitcoinPassportData{
Score: score,
AdditionalData: string(user.ProviderData),
},
})
if err != nil {
g.logger.WithError(err).Error("failed to marshal")
}
credentialSubject.ProviderMetadata = string(marshalled)

sigProof := true

Expand Down
3,729 changes: 3,729 additions & 0 deletions internal/service/core/identity_providers/kleros/contracts/proof_of_humanity.go

Large diffs are not rendered by default.

119 changes: 119 additions & 0 deletions internal/service/core/identity_providers/kleros/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
package kleros

import (
"encoding/json"
"time"

"github.com/ethereum/go-ethereum/common"
cryptoPkg "github.com/ethereum/go-ethereum/crypto"
"github.com/pkg/errors"
"gitlab.com/distributed_lab/logan/v3"

"github.com/rarimo/kyc-service/internal/config"
"github.com/rarimo/kyc-service/internal/crypto"
"github.com/rarimo/kyc-service/internal/data"
providers "github.com/rarimo/kyc-service/internal/service/core/identity_providers"
"github.com/rarimo/kyc-service/internal/service/core/identity_providers/kleros/contracts"
"github.com/rarimo/kyc-service/internal/service/core/issuer"
)

type Kleros struct {
logger *logan.Entry
masterQ data.MasterQ
proofOfHumanityContract *contracts.ProofOfHumanity
}

func NewIdentityProvider(
logger *logan.Entry, masterQ data.MasterQ, config *config.Kleros,
) (*Kleros, error) {
pohContract, err := contracts.NewProofOfHumanity(config.ProofOfHumanityContract, config.EthereumRpc)
if err != nil {
return nil, errors.Wrap(err, "failed to create proof of humanity contract")
}

return &Kleros{
logger: logger,
masterQ: masterQ,
proofOfHumanityContract: pohContract,
}, nil
}

func (k *Kleros) Verify(
user *data.User, verifyDataRaw []byte,
) (*issuer.IdentityProvidersCredentialSubject, []byte, error) {
var verifyData VerificationData
if err := json.Unmarshal(verifyDataRaw, &verifyData); err != nil {
return nil, nil, errors.Wrap(err, "failed to unmarshal verification data")
}

if err := verifyData.Validate(); err != nil {
return nil, nil, providers.ErrInvalidVerificationData
}

userAddr := common.HexToAddress(verifyData.Address)

if err := k.verifySignature(verifyData.Signature, userAddr); err != nil {
return nil, nil, errors.Wrap(err, "failed to verify signature")
}

if err := k.checkIfIsRegistered(userAddr); err != nil {
return nil, nil, errors.Wrap(err, "failed to check if user is registered")
}

providerDataRaw, err := json.Marshal(ProviderData{
Address: userAddr,
})
if err != nil {
return nil, nil, errors.Wrap(err, "failed to marshal provider data")
}

user.EthAddress = &userAddr
user.Status = data.UserStatusVerified
user.ProviderData = providerDataRaw

credentialSubject := issuer.NewEmptyIdentityProvidersCredentialSubject()
credentialSubject.Provider = issuer.KlerosProviderName
credentialSubject.Address = userAddr.String()
credentialSubject.ProviderMetadata = "none"

return credentialSubject, cryptoPkg.Keccak256(
userAddr.Bytes(),
providers.KlerosIdentityProvider.Bytes(),
), nil
}

// verifySignature verifies user's signature
func (k *Kleros) verifySignature(signature string, userAddr common.Address) error {
nonce, err := k.masterQ.NonceQ().
WhereEthAddress(userAddr).
WhereExpiresAtGt(time.Now()).
Get()
if err != nil {
return errors.Wrap(err, "failed to get nonce")
}
if nonce == nil {
return providers.ErrNonceNotFound
}

valid, err := crypto.VerifyEIP191Signature(
signature,
crypto.NonceToSignMessage(nonce.Nonce),
userAddr,
)
if err != nil || !valid {
return providers.ErrInvalidUsersSignature
}

return errors.Wrap(k.masterQ.NonceQ().WhereEthAddress(userAddr).Delete(), "failed to delete nonce")
}

func (k *Kleros) checkIfIsRegistered(userAddress common.Address) error {
isRegistered, err := k.proofOfHumanityContract.IsRegistered(nil, userAddress)
if err != nil {
return errors.Wrap(err, "failed to call isRegistered contract method")
}
if !isRegistered {
return ErrIsNotRegistered
}
return nil
}
29 changes: 29 additions & 0 deletions internal/service/core/identity_providers/kleros/models.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package kleros

import (
"github.com/ethereum/go-ethereum/common"
validation "github.com/go-ozzo/ozzo-validation/v4"
"github.com/pkg/errors"

"github.com/rarimo/kyc-service/internal/service/api/requests"
"github.com/rarimo/kyc-service/resources"
)

var ErrIsNotRegistered = errors.New("user is not registered")

type (
// VerificationData is a data that is required by Kleros to verify a user
VerificationData resources.KlerosData
)

type ProviderData struct {
Address common.Address `json:"address"`
}

// Validate is a method that validates VerificationData
func (v VerificationData) Validate() error {
return validation.Errors{
"signature": validation.Validate(v.Signature, validation.Required),
"address": validation.Validate(v.Address, validation.Required, validation.By(requests.MustBeEthAddress)),
}.Filter()
}
2 changes: 2 additions & 0 deletions internal/service/core/identity_providers/models.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,11 +27,13 @@ const (
CivicIdentityProvider IdentityProviderName = "civic"
GitCoinPassportIdentityProvider IdentityProviderName = "gitcoin_passport"
WorldCoinIdentityProvider IdentityProviderName = "worldcoin"
KlerosIdentityProvider IdentityProviderName = "kleros"
)

var IdentityProviderNames = map[string]IdentityProviderName{
UnstoppableDomainsIdentityProvider.String(): UnstoppableDomainsIdentityProvider,
CivicIdentityProvider.String(): CivicIdentityProvider,
GitCoinPassportIdentityProvider.String(): GitCoinPassportIdentityProvider,
WorldCoinIdentityProvider.String(): WorldCoinIdentityProvider,
KlerosIdentityProvider.String(): KlerosIdentityProvider,
}
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,14 @@ func (u *UnstoppableDomains) Verify(
credentialSubject := issuer.NewEmptyIdentityProvidersCredentialSubject()
credentialSubject.Provider = issuer.UnstoppableDomainsProviderName
credentialSubject.Address = userInfo.WalletAddress
credentialSubject.UnstoppableDomain = userInfo.Domain

marshalled, err := json.Marshal(issuer.IdentityProviderMetadata{
UnstoppableDomain: userInfo.Domain,
})
if err != nil {
return nil, nil, errors.Wrap(err, "failed to marshal")
}
credentialSubject.ProviderMetadata = string(marshalled)

return credentialSubject, cryptoPkg.Keccak256(
[]byte(userInfo.Domain),
Expand Down
Loading

0 comments on commit e82d58f

Please sign in to comment.